mirror of
https://github.com/tailscale/tailscale.git
synced 2025-04-21 06:01:42 +00:00
cmd/tailscale/cli, ipn/ipnlocal: [funnel] add stream mode
Adds ability to start Funnel in the foreground and stream incoming connections. When foreground process is stopped, Funnel is turned back off for the port. Exampe usage: ``` TAILSCALE_FUNNEL_V2=on tailscale funnel 8080 ``` Updates #8489 Signed-off-by: Marwan Sulaiman <marwan@tailscale.com>
This commit is contained in:
parent
cb4a61f951
commit
35ff5bf5a6
@ -1057,6 +1057,29 @@ func (lc *LocalClient) NetworkLockDisable(ctx context.Context, secret []byte) er
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StreamServe returns an io.ReadCloser that streams serve/Funnel
|
||||||
|
// connections made to the provided HostPort.
|
||||||
|
//
|
||||||
|
// If Serve and Funnel were not already enabled for the HostPort in the ServeConfig,
|
||||||
|
// the backend enables it for the duration of the context's lifespan and
|
||||||
|
// then turns it back off once the context is closed. If either are already enabled,
|
||||||
|
// then they remain that way but logs are still streamed
|
||||||
|
func (lc *LocalClient) StreamServe(ctx context.Context, hp ipn.ServeStreamRequest) (io.ReadCloser, error) {
|
||||||
|
req, err := http.NewRequestWithContext(ctx, "POST", "http://"+apitype.LocalAPIHost+"/localapi/v0/stream-serve", jsonBody(hp))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res, err := lc.doLocalRequestNiceError(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
res.Body.Close()
|
||||||
|
return nil, errors.New(res.Status)
|
||||||
|
}
|
||||||
|
return res.Body, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetServeConfig return the current serve config.
|
// GetServeConfig return the current serve config.
|
||||||
//
|
//
|
||||||
// If the serve config is empty, it returns (nil, nil).
|
// If the serve config is empty, it returns (nil, nil).
|
||||||
|
@ -120,7 +120,7 @@ change in the future.
|
|||||||
pingCmd,
|
pingCmd,
|
||||||
ncCmd,
|
ncCmd,
|
||||||
sshCmd,
|
sshCmd,
|
||||||
funnelCmd,
|
funnelCmd(),
|
||||||
serveCmd,
|
serveCmd,
|
||||||
versionCmd,
|
versionCmd,
|
||||||
webCmd,
|
webCmd,
|
||||||
|
@ -20,7 +20,16 @@ import (
|
|||||||
"tailscale.com/util/mak"
|
"tailscale.com/util/mak"
|
||||||
)
|
)
|
||||||
|
|
||||||
var funnelCmd = newFunnelCommand(&serveEnv{lc: &localClient})
|
var funnelCmd = func() *ffcli.Command {
|
||||||
|
se := &serveEnv{lc: &localClient}
|
||||||
|
// This flag is used to switch to an in-development
|
||||||
|
// implementation of the tailscale funnel command.
|
||||||
|
// See https://github.com/tailscale/tailscale/issues/7844
|
||||||
|
if os.Getenv("TAILSCALE_FUNNEL_DEV") == "on" {
|
||||||
|
return newFunnelDevCommand(se)
|
||||||
|
}
|
||||||
|
return newFunnelCommand(se)
|
||||||
|
}
|
||||||
|
|
||||||
// newFunnelCommand returns a new "funnel" subcommand using e as its environment.
|
// newFunnelCommand returns a new "funnel" subcommand using e as its environment.
|
||||||
// The funnel subcommand is used to turn on/off the Funnel service.
|
// The funnel subcommand is used to turn on/off the Funnel service.
|
||||||
|
112
cmd/tailscale/cli/funnel_dev.go
Normal file
112
cmd/tailscale/cli/funnel_dev.go
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/peterbourgon/ff/v3/ffcli"
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newFunnelDevCommand returns a new "funnel" subcommand using e as its environment.
|
||||||
|
// The funnel subcommand is used to turn on/off the Funnel service.
|
||||||
|
// Funnel is off by default.
|
||||||
|
// Funnel allows you to publish a 'tailscale serve' server publicly,
|
||||||
|
// open to the entire internet.
|
||||||
|
// newFunnelCommand shares the same serveEnv as the "serve" subcommand.
|
||||||
|
// See newServeCommand and serve.go for more details.
|
||||||
|
func newFunnelDevCommand(e *serveEnv) *ffcli.Command {
|
||||||
|
return &ffcli.Command{
|
||||||
|
Name: "funnel",
|
||||||
|
ShortHelp: "Turn on/off Funnel service",
|
||||||
|
ShortUsage: strings.Join([]string{
|
||||||
|
"funnel <port>",
|
||||||
|
"funnel status [--json]",
|
||||||
|
}, "\n "),
|
||||||
|
LongHelp: strings.Join([]string{
|
||||||
|
"Funnel allows you to expose your local",
|
||||||
|
"server publicly to the entire internet.",
|
||||||
|
"Note that it only supports https servers at this point.",
|
||||||
|
"This command is in development and is unsupported",
|
||||||
|
}, "\n"),
|
||||||
|
Exec: e.runFunnelDev,
|
||||||
|
UsageFunc: usageFunc,
|
||||||
|
Subcommands: []*ffcli.Command{
|
||||||
|
{
|
||||||
|
Name: "status",
|
||||||
|
Exec: e.runServeStatus,
|
||||||
|
ShortHelp: "show current serve/Funnel status",
|
||||||
|
FlagSet: e.newFlags("funnel-status", func(fs *flag.FlagSet) {
|
||||||
|
fs.BoolVar(&e.json, "json", false, "output JSON")
|
||||||
|
}),
|
||||||
|
UsageFunc: usageFunc,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runFunnelDev is the entry point for the "tailscale funnel" subcommand and
|
||||||
|
// manages turning on/off Funnel. Funnel is off by default.
|
||||||
|
//
|
||||||
|
// Note: funnel is only supported on single DNS name for now. (2023-08-18)
|
||||||
|
func (e *serveEnv) runFunnelDev(ctx context.Context, args []string) error {
|
||||||
|
if len(args) != 1 {
|
||||||
|
return flag.ErrHelp
|
||||||
|
}
|
||||||
|
var source string
|
||||||
|
port64, err := strconv.ParseUint(args[0], 10, 16)
|
||||||
|
if err == nil {
|
||||||
|
source = fmt.Sprintf("http://127.0.0.1:%d", port64)
|
||||||
|
} else {
|
||||||
|
source, err = expandProxyTarget(args[0])
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
st, err := e.getLocalClientStatusWithoutPeers(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting client status: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := e.verifyFunnelEnabled(ctx, st, 443); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||||
|
hp := ipn.HostPort(dnsName + ":443") // TODO(marwan-at-work): support the 2 other ports
|
||||||
|
|
||||||
|
// In the streaming case, the process stays running in the
|
||||||
|
// foreground and prints out connections to the HostPort.
|
||||||
|
//
|
||||||
|
// The local backend handles updating the ServeConfig as
|
||||||
|
// necessary, then restores it to its original state once
|
||||||
|
// the process's context is closed or the client turns off
|
||||||
|
// Tailscale.
|
||||||
|
return e.streamServe(ctx, ipn.ServeStreamRequest{
|
||||||
|
HostPort: hp,
|
||||||
|
Source: source,
|
||||||
|
MountPoint: "/", // TODO(marwan-at-work): support multiple mount points
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *serveEnv) streamServe(ctx context.Context, req ipn.ServeStreamRequest) error {
|
||||||
|
stream, err := e.lc.StreamServe(ctx, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer stream.Close()
|
||||||
|
|
||||||
|
fmt.Fprintf(os.Stderr, "Funnel started on \"https://%s\".\n", strings.TrimSuffix(string(req.HostPort), ":443"))
|
||||||
|
fmt.Fprintf(os.Stderr, "Press Ctrl-C to stop Funnel.\n\n")
|
||||||
|
_, err = io.Copy(os.Stdout, stream)
|
||||||
|
return err
|
||||||
|
}
|
@ -135,6 +135,7 @@ type localServeClient interface {
|
|||||||
QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error)
|
QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error)
|
||||||
WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*tailscale.IPNBusWatcher, error)
|
WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*tailscale.IPNBusWatcher, error)
|
||||||
IncrementCounter(ctx context.Context, name string, delta int) error
|
IncrementCounter(ctx context.Context, name string, delta int) error
|
||||||
|
StreamServe(ctx context.Context, req ipn.ServeStreamRequest) (io.ReadCloser, error) // TODO: testing :)
|
||||||
}
|
}
|
||||||
|
|
||||||
// serveEnv is the environment the serve command runs within. All I/O should be
|
// serveEnv is the environment the serve command runs within. All I/O should be
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@ -900,6 +901,11 @@ func (lc *fakeLocalServeClient) IncrementCounter(ctx context.Context, name strin
|
|||||||
return nil // unused in tests
|
return nil // unused in tests
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (lc *fakeLocalServeClient) StreamServe(ctx context.Context, req ipn.ServeStreamRequest) (io.ReadCloser, error) {
|
||||||
|
// TODO: testing :)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
// exactError returns an error checker that wants exactly the provided want error.
|
// exactError returns an error checker that wants exactly the provided want error.
|
||||||
// If optName is non-empty, it's used in the error message.
|
// If optName is non-empty, it's used in the error message.
|
||||||
func exactErr(want error, optName ...string) func(error) string {
|
func exactErr(want error, optName ...string) func(error) string {
|
||||||
|
@ -93,6 +93,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
L github.com/google/nftables/expr from github.com/google/nftables+
|
L github.com/google/nftables/expr from github.com/google/nftables+
|
||||||
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
|
||||||
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
L github.com/google/nftables/xt from github.com/google/nftables/expr+
|
||||||
|
github.com/google/uuid from tailscale.com/ipn/ipnlocal
|
||||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||||
L 💣 github.com/illarion/gonotify from tailscale.com/net/dns
|
L 💣 github.com/illarion/gonotify from tailscale.com/net/dns
|
||||||
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
|
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
|
||||||
@ -438,6 +439,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
crypto/tls from github.com/tcnksm/go-httpstat+
|
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||||
crypto/x509 from crypto/tls+
|
crypto/x509 from crypto/tls+
|
||||||
crypto/x509/pkix from crypto/x509+
|
crypto/x509/pkix from crypto/x509+
|
||||||
|
database/sql/driver from github.com/google/uuid
|
||||||
W debug/dwarf from debug/pe
|
W debug/dwarf from debug/pe
|
||||||
W debug/pe from github.com/dblohm7/wingoes/pe
|
W debug/pe from github.com/dblohm7/wingoes/pe
|
||||||
embed from tailscale.com+
|
embed from tailscale.com+
|
||||||
|
@ -244,6 +244,9 @@ type LocalBackend struct {
|
|||||||
|
|
||||||
serveListeners map[netip.AddrPort]*serveListener // addrPort => serveListener
|
serveListeners map[netip.AddrPort]*serveListener // addrPort => serveListener
|
||||||
serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *httputil.ReverseProxy
|
serveProxyHandlers sync.Map // string (HTTPHandler.Proxy) => *httputil.ReverseProxy
|
||||||
|
// serveStreamers is a map for those running Funnel in the foreground
|
||||||
|
// and streaming incoming requests.
|
||||||
|
serveStreamers map[uint16]map[uint32]func(ipn.FunnelRequestLog) // serve port => map of stream loggers (key is UUID)
|
||||||
|
|
||||||
// statusLock must be held before calling statusChanged.Wait() or
|
// statusLock must be held before calling statusChanged.Wait() or
|
||||||
// statusChanged.Broadcast().
|
// statusChanged.Broadcast().
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/logtail/backoff"
|
"tailscale.com/logtail/backoff"
|
||||||
"tailscale.com/net/netutil"
|
"tailscale.com/net/netutil"
|
||||||
@ -257,6 +258,165 @@ func (b *LocalBackend) ServeConfig() ipn.ServeConfigView {
|
|||||||
return b.serveConfig
|
return b.serveConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StreamServe opens a stream to write any incoming connections made
|
||||||
|
// to the given HostPort out to the listening io.Writer.
|
||||||
|
//
|
||||||
|
// If Serve and Funnel were not already enabled for the HostPort in the ServeConfig,
|
||||||
|
// the backend enables it for the duration of the context's lifespan and
|
||||||
|
// then turns it back off once the context is closed. If either are already enabled,
|
||||||
|
// then they remain that way but logs are still streamed
|
||||||
|
func (b *LocalBackend) StreamServe(ctx context.Context, w io.Writer, req ipn.ServeStreamRequest) (err error) {
|
||||||
|
f, ok := w.(http.Flusher)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("writer not a flusher")
|
||||||
|
}
|
||||||
|
f.Flush()
|
||||||
|
|
||||||
|
port, err := req.HostPort.Port()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turn on Funnel for the given HostPort.
|
||||||
|
sc := b.ServeConfig().AsStruct()
|
||||||
|
if sc == nil {
|
||||||
|
sc = &ipn.ServeConfig{}
|
||||||
|
}
|
||||||
|
setHandler(sc, req)
|
||||||
|
if err := b.SetServeConfig(sc); err != nil {
|
||||||
|
return fmt.Errorf("errro setting serve config: %w", err)
|
||||||
|
}
|
||||||
|
// Defer turning off Funnel once stream ends.
|
||||||
|
defer func() {
|
||||||
|
sc := b.ServeConfig().AsStruct()
|
||||||
|
deleteHandler(sc, req, port)
|
||||||
|
err = errors.Join(err, b.SetServeConfig(sc))
|
||||||
|
}()
|
||||||
|
|
||||||
|
var writeErrs []error
|
||||||
|
writeToStream := func(log ipn.FunnelRequestLog) {
|
||||||
|
jsonLog, err := json.Marshal(log)
|
||||||
|
if err != nil {
|
||||||
|
writeErrs = append(writeErrs, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprintf(w, "%s\n", jsonLog); err != nil {
|
||||||
|
writeErrs = append(writeErrs, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
f.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook up connections stream.
|
||||||
|
b.mu.Lock()
|
||||||
|
mak.NonNilMapForJSON(&b.serveStreamers)
|
||||||
|
if b.serveStreamers[port] == nil {
|
||||||
|
b.serveStreamers[port] = make(map[uint32]func(ipn.FunnelRequestLog))
|
||||||
|
}
|
||||||
|
id := uuid.New().ID()
|
||||||
|
b.serveStreamers[port][id] = writeToStream
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
// Clean up streamer when done.
|
||||||
|
defer func() {
|
||||||
|
b.mu.Lock()
|
||||||
|
mak.NonNilMapForJSON(&b.serveStreamers)
|
||||||
|
delete(b.serveStreamers[port], id)
|
||||||
|
b.mu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
// Triggered by foreground `tailscale funnel` process
|
||||||
|
// (the streamer) getting closed, or by turning off Tailscale.
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.Join(writeErrs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHandler(sc *ipn.ServeConfig, req ipn.ServeStreamRequest) {
|
||||||
|
if sc.TCP == nil {
|
||||||
|
sc.TCP = make(map[uint16]*ipn.TCPPortHandler)
|
||||||
|
}
|
||||||
|
if _, ok := sc.TCP[443]; !ok {
|
||||||
|
sc.TCP[443] = &ipn.TCPPortHandler{
|
||||||
|
HTTPS: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sc.Web == nil {
|
||||||
|
sc.Web = make(map[ipn.HostPort]*ipn.WebServerConfig)
|
||||||
|
}
|
||||||
|
wsc, ok := sc.Web[req.HostPort]
|
||||||
|
if !ok {
|
||||||
|
wsc = &ipn.WebServerConfig{}
|
||||||
|
sc.Web[req.HostPort] = wsc
|
||||||
|
}
|
||||||
|
if wsc.Handlers == nil {
|
||||||
|
wsc.Handlers = make(map[string]*ipn.HTTPHandler)
|
||||||
|
}
|
||||||
|
wsc.Handlers[req.MountPoint] = &ipn.HTTPHandler{
|
||||||
|
Proxy: req.Source,
|
||||||
|
}
|
||||||
|
if sc.AllowFunnel == nil {
|
||||||
|
sc.AllowFunnel = make(map[ipn.HostPort]bool)
|
||||||
|
}
|
||||||
|
sc.AllowFunnel[req.HostPort] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteHandler(sc *ipn.ServeConfig, req ipn.ServeStreamRequest, port uint16) {
|
||||||
|
delete(sc.AllowFunnel, req.HostPort)
|
||||||
|
if sc.TCP != nil {
|
||||||
|
delete(sc.TCP, port)
|
||||||
|
}
|
||||||
|
if sc.Web == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if sc.Web[req.HostPort] == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wsc, ok := sc.Web[req.HostPort]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if wsc.Handlers == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, ok := wsc.Handlers[req.MountPoint]; !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(wsc.Handlers, req.MountPoint)
|
||||||
|
if len(wsc.Handlers) == 0 {
|
||||||
|
delete(sc.Web, req.HostPort)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) maybeLogServeConnection(destPort uint16, srcAddr netip.AddrPort) {
|
||||||
|
b.mu.Lock()
|
||||||
|
streamers := b.serveStreamers[destPort]
|
||||||
|
b.mu.Unlock()
|
||||||
|
if len(streamers) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var log ipn.FunnelRequestLog
|
||||||
|
log.SrcAddr = srcAddr
|
||||||
|
log.Time = time.Now() // TODO: use a different clock somewhere?
|
||||||
|
|
||||||
|
if node, user, ok := b.WhoIs(srcAddr); ok {
|
||||||
|
log.NodeName = node.ComputedName()
|
||||||
|
if node.IsTagged() {
|
||||||
|
log.NodeTags = node.Tags().AsSlice()
|
||||||
|
} else {
|
||||||
|
log.UserLoginName = user.LoginName
|
||||||
|
log.UserDisplayName = user.DisplayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, stream := range streamers {
|
||||||
|
stream(log)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) {
|
func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) {
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
sc := b.serveConfig
|
sc := b.serveConfig
|
||||||
@ -359,6 +519,7 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
|
|||||||
if backDst := tcph.TCPForward(); backDst != "" {
|
if backDst := tcph.TCPForward(); backDst != "" {
|
||||||
return func(conn net.Conn) error {
|
return func(conn net.Conn) error {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
b.maybeLogServeConnection(dport, srcAddr)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
|
backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
|
||||||
cancel()
|
cancel()
|
||||||
@ -527,6 +688,9 @@ func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if c, ok := getServeHTTPContext(r); ok {
|
||||||
|
b.maybeLogServeConnection(c.DestPort, c.SrcAddr)
|
||||||
|
}
|
||||||
if s := h.Text(); s != "" {
|
if s := h.Text(); s != "" {
|
||||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
io.WriteString(w, s)
|
io.WriteString(w, s)
|
||||||
|
@ -99,6 +99,7 @@ var handler = map[string]localAPIHandler{
|
|||||||
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
|
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
|
||||||
"start": (*Handler).serveStart,
|
"start": (*Handler).serveStart,
|
||||||
"status": (*Handler).serveStatus,
|
"status": (*Handler).serveStatus,
|
||||||
|
"stream-serve": (*Handler).serveStreamServe,
|
||||||
"tka/init": (*Handler).serveTKAInit,
|
"tka/init": (*Handler).serveTKAInit,
|
||||||
"tka/log": (*Handler).serveTKALog,
|
"tka/log": (*Handler).serveTKALog,
|
||||||
"tka/modify": (*Handler).serveTKAModify,
|
"tka/modify": (*Handler).serveTKAModify,
|
||||||
@ -857,6 +858,31 @@ func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// serveStreamServe handles foreground serve and funnel streams. This is
|
||||||
|
// currently in development per https://github.com/tailscale/tailscale/issues/8489
|
||||||
|
func (h *Handler) serveStreamServe(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.PermitWrite {
|
||||||
|
// Write permission required because we modify the ServeConfig.
|
||||||
|
http.Error(w, "serve stream denied", http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != "POST" {
|
||||||
|
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req ipn.ServeStreamRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeErrorJSON(w, fmt.Errorf("decoding HostPort: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if err := h.b.StreamServe(r.Context(), w, req); err != nil {
|
||||||
|
writeErrorJSON(w, fmt.Errorf("streaming serve: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) {
|
||||||
if !h.PermitRead {
|
if !h.PermitRead {
|
||||||
http.Error(w, "IP forwarding check access denied", http.StatusForbidden)
|
http.Error(w, "IP forwarding check access denied", http.StatusForbidden)
|
||||||
|
51
ipn/serve.go
51
ipn/serve.go
@ -12,6 +12,7 @@ import (
|
|||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
)
|
)
|
||||||
@ -42,6 +43,21 @@ type ServeConfig struct {
|
|||||||
// There is no implicit port 443. It must contain a colon.
|
// There is no implicit port 443. It must contain a colon.
|
||||||
type HostPort string
|
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
|
// A FunnelConn wraps a net.Conn that is coming over a
|
||||||
// Funnel connection. It can be used to determine further
|
// Funnel connection. It can be used to determine further
|
||||||
// information about the connection, like the source address
|
// information about the connection, like the source address
|
||||||
@ -62,6 +78,41 @@ type FunnelConn struct {
|
|||||||
Src netip.AddrPort
|
Src netip.AddrPort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServeStreamRequest defines the json request body
|
||||||
|
// for the serve stream endpoint
|
||||||
|
type ServeStreamRequest struct {
|
||||||
|
// HostPort is the DNS and port of the tailscale
|
||||||
|
// URL.
|
||||||
|
HostPort HostPort `json:",omitempty"`
|
||||||
|
|
||||||
|
// Source is the user's serve destination
|
||||||
|
// such as their localhost server.
|
||||||
|
Source string `json:",omitempty"`
|
||||||
|
|
||||||
|
// MountPoint is the path prefix for
|
||||||
|
// the given HostPort.
|
||||||
|
MountPoint string `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FunnelRequestLog is the JSON type written out to io.Writers
|
||||||
|
// watching funnel connections via ipnlocal.StreamServe.
|
||||||
|
//
|
||||||
|
// This structure is in development and subject to change.
|
||||||
|
type FunnelRequestLog struct {
|
||||||
|
Time time.Time `json:",omitempty"` // time of request forwarding
|
||||||
|
|
||||||
|
// SrcAddr is the address that initiated the Funnel request.
|
||||||
|
SrcAddr netip.AddrPort `json:",omitempty"`
|
||||||
|
|
||||||
|
// The following fields are only populated if the connection
|
||||||
|
// initiated from another node on the client's tailnet.
|
||||||
|
|
||||||
|
NodeName string `json:",omitempty"` // src node MagicDNS name
|
||||||
|
NodeTags []string `json:",omitempty"` // src node tags
|
||||||
|
UserLoginName string `json:",omitempty"` // src node's owner login (if not tagged)
|
||||||
|
UserDisplayName string `json:",omitempty"` // src node's owner name (if not tagged)
|
||||||
|
}
|
||||||
|
|
||||||
// WebServerConfig describes a web server's configuration.
|
// WebServerConfig describes a web server's configuration.
|
||||||
type WebServerConfig struct {
|
type WebServerConfig struct {
|
||||||
Handlers map[string]*HTTPHandler // mountPoint => handler
|
Handlers map[string]*HTTPHandler // mountPoint => handler
|
||||||
|
Loading…
x
Reference in New Issue
Block a user