mirror of
https://github.com/tailscale/tailscale.git
synced 2025-10-21 07:28:45 +00:00
all: add ts_omit_serve, start making tailscale serve/funnel be modular
tailscaled tailscale combined (linux/amd64) 29853147 17384418 31412596 omitting everything + 621570 + 219277 + 554256 .. add serve Updates #17128 Change-Id: I87c2c6c3d3fc2dc026c3de8ef7000a813b41d31c Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:

committed by
Brad Fitzpatrick

parent
5b5ae2b2ee
commit
4cca9f7c67
@@ -1217,20 +1217,6 @@ func (lc *Client) Ping(ctx context.Context, ip netip.Addr, pingtype tailcfg.Ping
|
||||
return lc.PingWithOpts(ctx, ip, pingtype, PingOpts{})
|
||||
}
|
||||
|
||||
// SetServeConfig sets or replaces the serving settings.
|
||||
// If config is nil, settings are cleared and serving is disabled.
|
||||
func (lc *Client) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
|
||||
h := make(http.Header)
|
||||
if config != nil {
|
||||
h.Set("If-Match", config.ETag)
|
||||
}
|
||||
_, _, err := lc.sendWithHeaders(ctx, "POST", "/localapi/v0/serve-config", 200, jsonBody(config), h)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending serve config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisconnectControl shuts down all connections to control, thus making control consider this node inactive. This can be
|
||||
// run on HA subnet router or app connector replicas before shutting them down to ensure peers get told to switch over
|
||||
// to another replica whilst there is still some grace period for the existing connections to terminate.
|
||||
@@ -1242,32 +1228,6 @@ func (lc *Client) DisconnectControl(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetServeConfig return the current serve config.
|
||||
//
|
||||
// If the serve config is empty, it returns (nil, nil).
|
||||
func (lc *Client) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
|
||||
body, h, err := lc.sendWithHeaders(ctx, "GET", "/localapi/v0/serve-config", 200, nil, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting serve config: %w", err)
|
||||
}
|
||||
sc, err := getServeConfigFromJSON(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
sc.ETag = h.Get("Etag")
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
func getServeConfigFromJSON(body []byte) (sc *ipn.ServeConfig, err error) {
|
||||
if err := json.Unmarshal(body, &sc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// tailscaledConnectHint gives a little thing about why tailscaled (or
|
||||
// platform equivalent) is not answering localapi connections.
|
||||
//
|
||||
|
55
client/local/serve.go
Normal file
55
client/local/serve.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_serve
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
// GetServeConfig return the current serve config.
|
||||
//
|
||||
// If the serve config is empty, it returns (nil, nil).
|
||||
func (lc *Client) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
|
||||
body, h, err := lc.sendWithHeaders(ctx, "GET", "/localapi/v0/serve-config", 200, nil, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting serve config: %w", err)
|
||||
}
|
||||
sc, err := getServeConfigFromJSON(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
sc.ETag = h.Get("Etag")
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
func getServeConfigFromJSON(body []byte) (sc *ipn.ServeConfig, err error) {
|
||||
if err := json.Unmarshal(body, &sc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// SetServeConfig sets or replaces the serving settings.
|
||||
// If config is nil, settings are cleared and serving is disabled.
|
||||
func (lc *Client) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
|
||||
h := make(http.Header)
|
||||
if config != nil {
|
||||
h.Set("If-Match", config.ETag)
|
||||
}
|
||||
_, _, err := lc.sendWithHeaders(ctx, "POST", "/localapi/v0/serve-config", 200, jsonBody(config), h)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sending serve config: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
@@ -798,6 +798,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
|
||||
tailscale.com/envknob from tailscale.com/client/local+
|
||||
tailscale.com/envknob/featureknob from tailscale.com/client/web+
|
||||
tailscale.com/feature from tailscale.com/ipn/ipnext+
|
||||
tailscale.com/feature/buildfeatures from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/feature/syspolicy from tailscale.com/logpolicy
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal+
|
||||
|
@@ -213,6 +213,8 @@ var (
|
||||
maybeWebCmd,
|
||||
maybeDriveCmd,
|
||||
maybeNetlockCmd,
|
||||
maybeFunnelCmd,
|
||||
maybeServeCmd,
|
||||
_ func() *ffcli.Command
|
||||
)
|
||||
|
||||
@@ -254,8 +256,8 @@ change in the future.
|
||||
pingCmd,
|
||||
ncCmd,
|
||||
sshCmd,
|
||||
funnelCmd(),
|
||||
serveCmd(),
|
||||
nilOrCall(maybeFunnelCmd),
|
||||
nilOrCall(maybeServeCmd),
|
||||
versionCmd,
|
||||
nilOrCall(maybeWebCmd),
|
||||
nilOrCall(fileCmd),
|
||||
|
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_serve
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
@@ -16,6 +18,10 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func init() {
|
||||
maybeFunnelCmd = funnelCmd
|
||||
}
|
||||
|
||||
var funnelCmd = func() *ffcli.Command {
|
||||
se := &serveEnv{lc: &localClient}
|
||||
// previously used to serve legacy newFunnelCommand unless useWIPCode is true
|
||||
@@ -174,3 +180,42 @@ func printFunnelWarning(sc *ipn.ServeConfig) {
|
||||
fmt.Fprintf(Stderr, " run: `tailscale serve --help` to see how to configure handlers\n")
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
hookPrintFunnelStatus.Set(printFunnelStatus)
|
||||
}
|
||||
|
||||
// printFunnelStatus prints the status of the funnel, if it's running.
|
||||
// It prints nothing if the funnel is not running.
|
||||
func printFunnelStatus(ctx context.Context) {
|
||||
sc, err := localClient.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
outln()
|
||||
printf("# Funnel:\n")
|
||||
printf("# - Unable to get Funnel status: %v\n", err)
|
||||
return
|
||||
}
|
||||
if !sc.IsFunnelOn() {
|
||||
return
|
||||
}
|
||||
outln()
|
||||
printf("# Funnel on:\n")
|
||||
for hp, on := range sc.AllowFunnel {
|
||||
if !on { // if present, should be on
|
||||
continue
|
||||
}
|
||||
sni, portStr, _ := net.SplitHostPort(string(hp))
|
||||
p, _ := strconv.ParseUint(portStr, 10, 16)
|
||||
isTCP := sc.IsTCPForwardingOnPort(uint16(p), noService)
|
||||
url := "https://"
|
||||
if isTCP {
|
||||
url = "tcp://"
|
||||
}
|
||||
url += sni
|
||||
if isTCP || p != 443 {
|
||||
url += ":" + portStr
|
||||
}
|
||||
printf("# - %s\n", url)
|
||||
}
|
||||
outln()
|
||||
}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_serve
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
@@ -31,6 +33,10 @@ import (
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func init() {
|
||||
maybeServeCmd = serveCmd
|
||||
}
|
||||
|
||||
var serveCmd = func() *ffcli.Command {
|
||||
se := &serveEnv{lc: &localClient}
|
||||
// previously used to serve legacy newFunnelCommand unless useWIPCode is true
|
||||
|
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_serve
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
|
@@ -15,12 +15,12 @@ import (
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/toqueteos/webbrowser"
|
||||
"golang.org/x/net/idna"
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/netmon"
|
||||
@@ -238,44 +238,13 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
outln()
|
||||
printHealth()
|
||||
}
|
||||
printFunnelStatus(ctx)
|
||||
if f, ok := hookPrintFunnelStatus.GetOk(); ok {
|
||||
f(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// printFunnelStatus prints the status of the funnel, if it's running.
|
||||
// It prints nothing if the funnel is not running.
|
||||
func printFunnelStatus(ctx context.Context) {
|
||||
sc, err := localClient.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
outln()
|
||||
printf("# Funnel:\n")
|
||||
printf("# - Unable to get Funnel status: %v\n", err)
|
||||
return
|
||||
}
|
||||
if !sc.IsFunnelOn() {
|
||||
return
|
||||
}
|
||||
outln()
|
||||
printf("# Funnel on:\n")
|
||||
for hp, on := range sc.AllowFunnel {
|
||||
if !on { // if present, should be on
|
||||
continue
|
||||
}
|
||||
sni, portStr, _ := net.SplitHostPort(string(hp))
|
||||
p, _ := strconv.ParseUint(portStr, 10, 16)
|
||||
isTCP := sc.IsTCPForwardingOnPort(uint16(p), noService)
|
||||
url := "https://"
|
||||
if isTCP {
|
||||
url = "tcp://"
|
||||
}
|
||||
url += sni
|
||||
if isTCP || p != 443 {
|
||||
url += ":" + portStr
|
||||
}
|
||||
printf("# - %s\n", url)
|
||||
}
|
||||
outln()
|
||||
}
|
||||
var hookPrintFunnelStatus feature.Hook[func(context.Context)]
|
||||
|
||||
// isRunningOrStarting reports whether st is in state Running or Starting.
|
||||
// It also returns a description of the status suitable to display to a user.
|
||||
|
@@ -104,7 +104,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/drive from tailscale.com/client/local+
|
||||
tailscale.com/envknob from tailscale.com/client/local+
|
||||
tailscale.com/envknob/featureknob from tailscale.com/client/web
|
||||
tailscale.com/feature from tailscale.com/tsweb
|
||||
tailscale.com/feature from tailscale.com/tsweb+
|
||||
tailscale.com/feature/capture/dissector from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/feature/syspolicy from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/health from tailscale.com/net/tlsdial+
|
||||
|
@@ -272,6 +272,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/envknob from tailscale.com/client/local+
|
||||
tailscale.com/envknob/featureknob from tailscale.com/client/web+
|
||||
tailscale.com/feature from tailscale.com/feature/wakeonlan+
|
||||
tailscale.com/feature/buildfeatures from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/feature/capture from tailscale.com/feature/condregister
|
||||
tailscale.com/feature/condregister from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/feature/drive from tailscale.com/feature/condregister
|
||||
|
@@ -239,6 +239,7 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar
|
||||
tailscale.com/envknob from tailscale.com/client/local+
|
||||
tailscale.com/envknob/featureknob from tailscale.com/client/web+
|
||||
tailscale.com/feature from tailscale.com/ipn/ipnext+
|
||||
tailscale.com/feature/buildfeatures from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/feature/syspolicy from tailscale.com/logpolicy
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal+
|
||||
|
@@ -72,9 +72,6 @@ var c2nHandlers = map[methodAndPath]c2nHandler{
|
||||
|
||||
// Linux netfilter.
|
||||
req("POST /netfilter-kind"): handleC2NSetNetfilterKind,
|
||||
|
||||
// VIP services.
|
||||
req("GET /vip-services"): handleC2NVIPServicesGet,
|
||||
}
|
||||
|
||||
// RegisterC2N registers a new c2n handler for the given pattern.
|
||||
@@ -280,16 +277,6 @@ func handleC2NSetNetfilterKind(b *LocalBackend, w http.ResponseWriter, r *http.R
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func handleC2NVIPServicesGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
b.logf("c2n: GET /vip-services received")
|
||||
var res tailcfg.C2NVIPServicesResponse
|
||||
res.VIPServices = b.VIPServices()
|
||||
res.ServicesHash = b.vipServiceHash(res.VIPServices)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
func handleC2NUpdateGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
b.logf("c2n: GET /update received")
|
||||
|
||||
|
@@ -18,7 +18,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"maps"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
@@ -53,6 +52,7 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/envknob/featureknob"
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/health/healthmsg"
|
||||
"tailscale.com/hostinfo"
|
||||
@@ -585,7 +585,6 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
b.e.SetJailedFilter(noneFilter)
|
||||
|
||||
b.setTCPPortsIntercepted(nil)
|
||||
b.setVIPServicesTCPPortsIntercepted(nil)
|
||||
|
||||
b.statusChanged = sync.NewCond(&b.statusLock)
|
||||
b.e.SetStatusCallback(b.setWgengineStatus)
|
||||
@@ -3759,46 +3758,6 @@ func generateInterceptVIPServicesTCPPortFunc(svcAddrPorts map[netip.Addr]func(ui
|
||||
}
|
||||
}
|
||||
|
||||
// setVIPServicesTCPPortsIntercepted populates b.shouldInterceptVIPServicesTCPPortAtomic with an
|
||||
// efficient func for ShouldInterceptTCPPort to use, which is called on every incoming packet.
|
||||
func (b *LocalBackend) setVIPServicesTCPPortsIntercepted(svcPorts map[tailcfg.ServiceName][]uint16) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.setVIPServicesTCPPortsInterceptedLocked(svcPorts)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) setVIPServicesTCPPortsInterceptedLocked(svcPorts map[tailcfg.ServiceName][]uint16) {
|
||||
if len(svcPorts) == 0 {
|
||||
b.shouldInterceptVIPServicesTCPPortAtomic.Store(func(netip.AddrPort) bool { return false })
|
||||
return
|
||||
}
|
||||
nm := b.currentNode().NetMap()
|
||||
if nm == nil {
|
||||
b.logf("can't set intercept function for Service TCP Ports, netMap is nil")
|
||||
return
|
||||
}
|
||||
vipServiceIPMap := nm.GetVIPServiceIPMap()
|
||||
if len(vipServiceIPMap) == 0 {
|
||||
// No approved VIP Services
|
||||
return
|
||||
}
|
||||
|
||||
svcAddrPorts := make(map[netip.Addr]func(uint16) bool)
|
||||
// Only set the intercept function if the service has been assigned a VIP.
|
||||
for svcName, ports := range svcPorts {
|
||||
addrs, ok := vipServiceIPMap[svcName]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
interceptFn := generateInterceptTCPPortFunc(ports)
|
||||
for _, addr := range addrs {
|
||||
svcAddrPorts[addr] = interceptFn
|
||||
}
|
||||
}
|
||||
|
||||
b.shouldInterceptVIPServicesTCPPortAtomic.Store(generateInterceptVIPServicesTCPPortFunc(svcAddrPorts))
|
||||
}
|
||||
|
||||
// setAtomicValuesFromPrefsLocked populates sshAtomicBool, containsViaIPFuncAtomic,
|
||||
// shouldInterceptTCPPortAtomic, and exposeRemoteWebClientAtomicBool from the prefs p,
|
||||
// which may be !Valid().
|
||||
@@ -3809,7 +3768,9 @@ func (b *LocalBackend) setAtomicValuesFromPrefsLocked(p ipn.PrefsView) {
|
||||
if !p.Valid() {
|
||||
b.containsViaIPFuncAtomic.Store(ipset.FalseContainsIPFunc())
|
||||
b.setTCPPortsIntercepted(nil)
|
||||
b.setVIPServicesTCPPortsInterceptedLocked(nil)
|
||||
if f, ok := hookServeClearVIPServicesTCPPortsInterceptedLocked.GetOk(); ok {
|
||||
f(b)
|
||||
}
|
||||
b.lastServeConfJSON = mem.B(nil)
|
||||
b.serveConfig = ipn.ServeConfigView{}
|
||||
} else {
|
||||
@@ -4738,32 +4699,6 @@ func (b *LocalBackend) checkProfileNameLocked(p *ipn.Prefs) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// wantIngressLocked reports whether this node has ingress configured. This bool
|
||||
// is sent to the coordination server (in Hostinfo.WireIngress) as an
|
||||
// optimization hint to know primarily which nodes are NOT using ingress, to
|
||||
// avoid doing work for regular nodes.
|
||||
//
|
||||
// Even if the user's ServeConfig.AllowFunnel map was manually edited in raw
|
||||
// mode and contains map entries with false values, sending true (from Len > 0)
|
||||
// is still fine. This is only an optimization hint for the control plane and
|
||||
// doesn't affect security or correctness. And we also don't expect people to
|
||||
// modify their ServeConfig in raw mode.
|
||||
func (b *LocalBackend) wantIngressLocked() bool {
|
||||
return b.serveConfig.Valid() && b.serveConfig.HasAllowFunnel()
|
||||
}
|
||||
|
||||
// hasIngressEnabledLocked reports whether the node has any funnel endpoint enabled. This bool is sent to control (in
|
||||
// Hostinfo.IngressEnabled) to determine whether 'Funnel' badge should be displayed on this node in the admin panel.
|
||||
func (b *LocalBackend) hasIngressEnabledLocked() bool {
|
||||
return b.serveConfig.Valid() && b.serveConfig.IsFunnelOn()
|
||||
}
|
||||
|
||||
// shouldWireInactiveIngressLocked reports whether the node is in a state where funnel is not actively enabled, but it
|
||||
// seems that it is intended to be used with funnel.
|
||||
func (b *LocalBackend) shouldWireInactiveIngressLocked() bool {
|
||||
return b.serveConfig.Valid() && !b.hasIngressEnabledLocked() && b.wantIngressLocked()
|
||||
}
|
||||
|
||||
// setPrefsLockedOnEntry requires b.mu be held to call it, but it
|
||||
// unlocks b.mu when done. newp ownership passes to this function.
|
||||
// It returns a read-only copy of the new prefs.
|
||||
@@ -4907,6 +4842,16 @@ var (
|
||||
magicDNSIPv6 = tsaddr.TailscaleServiceIPv6()
|
||||
)
|
||||
|
||||
// Hook exclusively for serve.
|
||||
var (
|
||||
hookServeTCPHandlerForVIPService feature.Hook[func(b *LocalBackend, dst netip.AddrPort, src netip.AddrPort) (handler func(c net.Conn) error)]
|
||||
hookTCPHandlerForServe feature.Hook[func(b *LocalBackend, dport uint16, srcAddr netip.AddrPort, f *funnelFlow) (handler func(net.Conn) error)]
|
||||
hookServeUpdateServeTCPPortNetMapAddrListenersLocked feature.Hook[func(b *LocalBackend, ports []uint16)]
|
||||
|
||||
hookServeSetTCPPortsInterceptedFromNetmapAndPrefsLocked feature.Hook[func(b *LocalBackend, prefs ipn.PrefsView) (handlePorts []uint16)]
|
||||
hookServeClearVIPServicesTCPPortsInterceptedLocked feature.Hook[func(*LocalBackend)]
|
||||
)
|
||||
|
||||
// TCPHandlerForDst returns a TCP handler for connections to dst, or nil if
|
||||
// no handler is needed. It also returns a list of TCP socket options to
|
||||
// apply to the socket before calling the handler.
|
||||
@@ -4929,10 +4874,10 @@ func (b *LocalBackend) TCPHandlerForDst(src, dst netip.AddrPort) (handler func(c
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(tailscale/corp#26001): Get handler for VIP services and Local IPs using
|
||||
// the same function.
|
||||
if handler := b.tcpHandlerForVIPService(dst, src); handler != nil {
|
||||
return handler, opts
|
||||
if f, ok := hookServeTCPHandlerForVIPService.GetOk(); ok {
|
||||
if handler := f(b, dst, src); handler != nil {
|
||||
return handler, opts
|
||||
}
|
||||
}
|
||||
// Then handle external connections to the local IP.
|
||||
if !b.isLocalIP(dst.Addr()) {
|
||||
@@ -4958,8 +4903,10 @@ func (b *LocalBackend) TCPHandlerForDst(src, dst netip.AddrPort) (handler func(c
|
||||
return nil
|
||||
}, opts
|
||||
}
|
||||
if handler := b.tcpHandlerForServe(dst.Port(), src, nil); handler != nil {
|
||||
return handler, opts
|
||||
if f, ok := hookTCPHandlerForServe.GetOk(); ok {
|
||||
if handler := f(b, dst.Port(), src, nil); handler != nil {
|
||||
return handler, opts
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
@@ -6341,7 +6288,9 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
netns.SetDisableBindConnToInterface(nm.HasCap(tailcfg.CapabilityDebugDisableBindConnToInterface))
|
||||
|
||||
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs())
|
||||
b.ipVIPServiceMap = nm.GetIPVIPServiceMap()
|
||||
if buildfeatures.HasServe {
|
||||
b.ipVIPServiceMap = nm.GetIPVIPServiceMap()
|
||||
}
|
||||
|
||||
if !oldSelf.Equal(nm.SelfNodeOrZero()) {
|
||||
for _, f := range b.extHost.Hooks().OnSelfChange {
|
||||
@@ -6411,55 +6360,12 @@ func (b *LocalBackend) setDebugLogsByCapabilityLocked(nm *netmap.NetworkMap) {
|
||||
}
|
||||
}
|
||||
|
||||
// reloadServeConfigLocked reloads the serve config from the store or resets the
|
||||
// serve config to nil if not logged in. The "changed" parameter, when false, instructs
|
||||
// the method to only run the reset-logic and not reload the store from memory to ensure
|
||||
// foreground sessions are not removed if they are not saved on disk.
|
||||
func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) {
|
||||
if !b.currentNode().Self().Valid() || !prefs.Valid() || b.pm.CurrentProfile().ID() == "" {
|
||||
// We're not logged in, so we don't have a profile.
|
||||
// Don't try to load the serve config.
|
||||
b.lastServeConfJSON = mem.B(nil)
|
||||
b.serveConfig = ipn.ServeConfigView{}
|
||||
return
|
||||
}
|
||||
|
||||
confKey := ipn.ServeConfigKey(b.pm.CurrentProfile().ID())
|
||||
// TODO(maisem,bradfitz): prevent reading the config from disk
|
||||
// if the profile has not changed.
|
||||
confj, err := b.store.ReadState(confKey)
|
||||
if err != nil {
|
||||
b.lastServeConfJSON = mem.B(nil)
|
||||
b.serveConfig = ipn.ServeConfigView{}
|
||||
return
|
||||
}
|
||||
if b.lastServeConfJSON.Equal(mem.B(confj)) {
|
||||
return
|
||||
}
|
||||
b.lastServeConfJSON = mem.B(confj)
|
||||
var conf ipn.ServeConfig
|
||||
if err := json.Unmarshal(confj, &conf); err != nil {
|
||||
b.logf("invalid ServeConfig %q in StateStore: %v", confKey, err)
|
||||
b.serveConfig = ipn.ServeConfigView{}
|
||||
return
|
||||
}
|
||||
|
||||
// remove inactive sessions
|
||||
maps.DeleteFunc(conf.Foreground, func(sessionID string, sc *ipn.ServeConfig) bool {
|
||||
_, ok := b.notifyWatchers[sessionID]
|
||||
return !ok
|
||||
})
|
||||
|
||||
b.serveConfig = conf.View()
|
||||
}
|
||||
|
||||
// setTCPPortsInterceptedFromNetmapAndPrefsLocked calls setTCPPortsIntercepted with
|
||||
// the ports that tailscaled should handle as a function of b.netMap and b.prefs.
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.PrefsView) {
|
||||
handlePorts := make([]uint16, 0, 4)
|
||||
var vipServicesPorts map[tailcfg.ServiceName][]uint16
|
||||
|
||||
if prefs.Valid() && prefs.RunSSH() && envknob.CanSSHD() {
|
||||
handlePorts = append(handlePorts, 22)
|
||||
@@ -6473,42 +6379,14 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.
|
||||
}
|
||||
}
|
||||
|
||||
b.reloadServeConfigLocked(prefs)
|
||||
if b.serveConfig.Valid() {
|
||||
servePorts := make([]uint16, 0, 3)
|
||||
for port := range b.serveConfig.TCPs() {
|
||||
if port > 0 {
|
||||
servePorts = append(servePorts, uint16(port))
|
||||
}
|
||||
}
|
||||
handlePorts = append(handlePorts, servePorts...)
|
||||
|
||||
for svc, cfg := range b.serveConfig.Services().All() {
|
||||
servicePorts := make([]uint16, 0, 3)
|
||||
for port := range cfg.TCP().All() {
|
||||
if port > 0 {
|
||||
servicePorts = append(servicePorts, uint16(port))
|
||||
}
|
||||
}
|
||||
if _, ok := vipServicesPorts[svc]; !ok {
|
||||
mak.Set(&vipServicesPorts, svc, servicePorts)
|
||||
} else {
|
||||
mak.Set(&vipServicesPorts, svc, append(vipServicesPorts[svc], servicePorts...))
|
||||
}
|
||||
}
|
||||
|
||||
b.setServeProxyHandlersLocked()
|
||||
|
||||
// don't listen on netmap addresses if we're in userspace mode
|
||||
if !b.sys.IsNetstack() {
|
||||
b.updateServeTCPPortNetMapAddrListenersLocked(servePorts)
|
||||
}
|
||||
if f, ok := hookServeSetTCPPortsInterceptedFromNetmapAndPrefsLocked.GetOk(); ok {
|
||||
v := f(b, prefs)
|
||||
handlePorts = append(handlePorts, v...)
|
||||
}
|
||||
|
||||
// Update funnel and service hash info in hostinfo and kick off control update if needed.
|
||||
b.updateIngressAndServiceHashLocked(prefs)
|
||||
b.setTCPPortsIntercepted(handlePorts)
|
||||
b.setVIPServicesTCPPortsInterceptedLocked(vipServicesPorts)
|
||||
}
|
||||
|
||||
// updateIngressAndServiceHashLocked updates the hostinfo.ServicesHash, hostinfo.WireIngress and
|
||||
@@ -6541,51 +6419,6 @@ func (b *LocalBackend) updateIngressAndServiceHashLocked(prefs ipn.PrefsView) {
|
||||
}
|
||||
}
|
||||
|
||||
// setServeProxyHandlersLocked ensures there is an http proxy handler for each
|
||||
// backend specified in serveConfig. It expects serveConfig to be valid and
|
||||
// up-to-date, so should be called after reloadServeConfigLocked.
|
||||
func (b *LocalBackend) setServeProxyHandlersLocked() {
|
||||
if !b.serveConfig.Valid() {
|
||||
return
|
||||
}
|
||||
var backends map[string]bool
|
||||
for _, conf := range b.serveConfig.Webs() {
|
||||
for _, h := range conf.Handlers().All() {
|
||||
backend := h.Proxy()
|
||||
if backend == "" {
|
||||
// Only create proxy handlers for servers with a proxy backend.
|
||||
continue
|
||||
}
|
||||
mak.Set(&backends, backend, true)
|
||||
if _, ok := b.serveProxyHandlers.Load(backend); ok {
|
||||
continue
|
||||
}
|
||||
|
||||
b.logf("serve: creating a new proxy handler for %s", backend)
|
||||
p, err := b.proxyHandlerForBackend(backend)
|
||||
if err != nil {
|
||||
// The backend endpoint (h.Proxy) should have been validated by expandProxyTarget
|
||||
// in the CLI, so just log the error here.
|
||||
b.logf("[unexpected] could not create proxy for %v: %s", backend, err)
|
||||
continue
|
||||
}
|
||||
b.serveProxyHandlers.Store(backend, p)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up handlers for proxy backends that are no longer present
|
||||
// in configuration.
|
||||
b.serveProxyHandlers.Range(func(key, value any) bool {
|
||||
backend := key.(string)
|
||||
if !backends[backend] {
|
||||
b.logf("serve: closing idle connections to %s", backend)
|
||||
b.serveProxyHandlers.Delete(backend)
|
||||
value.(*reverseProxy).close()
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// operatorUserName returns the current pref's OperatorUser's name, or the
|
||||
// empty string if none.
|
||||
func (b *LocalBackend) operatorUserName() string {
|
||||
@@ -7196,7 +7029,14 @@ func (b *LocalBackend) ShouldInterceptTCPPort(port uint16) bool {
|
||||
// ShouldInterceptVIPServiceTCPPort reports whether the given TCP port number
|
||||
// to a VIP service should be intercepted by Tailscaled and handled in-process.
|
||||
func (b *LocalBackend) ShouldInterceptVIPServiceTCPPort(ap netip.AddrPort) bool {
|
||||
return b.shouldInterceptVIPServicesTCPPortAtomic.Load()(ap)
|
||||
if !buildfeatures.HasServe {
|
||||
return false
|
||||
}
|
||||
f := b.shouldInterceptVIPServicesTCPPortAtomic.Load()
|
||||
if f == nil {
|
||||
return false
|
||||
}
|
||||
return f(ap)
|
||||
}
|
||||
|
||||
// SwitchProfile switches to the profile with the given id.
|
||||
@@ -8131,15 +7971,6 @@ func maybeUsernameOf(actor ipnauth.Actor) string {
|
||||
return username
|
||||
}
|
||||
|
||||
// VIPServices returns the list of tailnet services that this node
|
||||
// is serving as a destination for.
|
||||
// The returned memory is owned by the caller.
|
||||
func (b *LocalBackend) VIPServices() []*tailcfg.VIPService {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.vipServicesFromPrefsLocked(b.pm.CurrentPrefs())
|
||||
}
|
||||
|
||||
func (b *LocalBackend) vipServiceHash(services []*tailcfg.VIPService) string {
|
||||
if len(services) == 0 {
|
||||
return ""
|
||||
@@ -8153,39 +7984,9 @@ func (b *LocalBackend) vipServiceHash(services []*tailcfg.VIPService) string {
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
func (b *LocalBackend) vipServicesFromPrefsLocked(prefs ipn.PrefsView) []*tailcfg.VIPService {
|
||||
// keyed by service name
|
||||
var services map[tailcfg.ServiceName]*tailcfg.VIPService
|
||||
if b.serveConfig.Valid() {
|
||||
for svc, config := range b.serveConfig.Services().All() {
|
||||
mak.Set(&services, svc, &tailcfg.VIPService{
|
||||
Name: svc,
|
||||
Ports: config.ServicePortRange(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range prefs.AdvertiseServices().All() {
|
||||
sn := tailcfg.ServiceName(s)
|
||||
if services == nil || services[sn] == nil {
|
||||
mak.Set(&services, sn, &tailcfg.VIPService{
|
||||
Name: sn,
|
||||
})
|
||||
}
|
||||
services[sn].Active = true
|
||||
}
|
||||
|
||||
servicesList := slicesx.MapValues(services)
|
||||
// [slicesx.MapValues] provides the values in an indeterminate order, but since we'll
|
||||
// be hashing a representation of this list later we want it to be in a consistent
|
||||
// order.
|
||||
slices.SortFunc(servicesList, func(a, b *tailcfg.VIPService) int {
|
||||
return strings.Compare(a.Name.String(), b.Name.String())
|
||||
})
|
||||
return servicesList
|
||||
}
|
||||
|
||||
var metricCurrentWatchIPNBus = clientmetric.NewGauge("localbackend_current_watch_ipn_bus")
|
||||
var (
|
||||
metricCurrentWatchIPNBus = clientmetric.NewGauge("localbackend_current_watch_ipn_bus")
|
||||
)
|
||||
|
||||
func (b *LocalBackend) stateEncrypted() opt.Bool {
|
||||
switch runtime.GOOS {
|
||||
|
@@ -28,7 +28,6 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netutil"
|
||||
@@ -387,10 +386,6 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case "/v0/sockstats":
|
||||
h.handleServeSockStats(w, r)
|
||||
return
|
||||
case "/v0/ingress":
|
||||
metricIngressCalls.Add(1)
|
||||
h.handleServeIngress(w, r)
|
||||
return
|
||||
}
|
||||
if ph, ok := peerAPIHandlers[r.URL.Path]; ok {
|
||||
ph(h, w, r)
|
||||
@@ -413,67 +408,6 @@ This is my Tailscale device. Your device is %v.
|
||||
}
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeIngress(w http.ResponseWriter, r *http.Request) {
|
||||
// http.Errors only useful if hitting endpoint manually
|
||||
// otherwise rely on log lines when debugging ingress connections
|
||||
// as connection is hijacked for bidi and is encrypted tls
|
||||
if !h.canIngress() {
|
||||
h.logf("ingress: denied; no ingress cap from %v", h.remoteAddr)
|
||||
http.Error(w, "denied; no ingress cap", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
logAndError := func(code int, publicMsg string) {
|
||||
h.logf("ingress: bad request from %v: %s", h.remoteAddr, publicMsg)
|
||||
http.Error(w, publicMsg, code)
|
||||
}
|
||||
bad := func(publicMsg string) {
|
||||
logAndError(http.StatusBadRequest, publicMsg)
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
logAndError(http.StatusMethodNotAllowed, "only POST allowed")
|
||||
return
|
||||
}
|
||||
srcAddrStr := r.Header.Get("Tailscale-Ingress-Src")
|
||||
if srcAddrStr == "" {
|
||||
bad("Tailscale-Ingress-Src header not set")
|
||||
return
|
||||
}
|
||||
srcAddr, err := netip.ParseAddrPort(srcAddrStr)
|
||||
if err != nil {
|
||||
bad("Tailscale-Ingress-Src header invalid; want ip:port")
|
||||
return
|
||||
}
|
||||
target := ipn.HostPort(r.Header.Get("Tailscale-Ingress-Target"))
|
||||
if target == "" {
|
||||
bad("Tailscale-Ingress-Target header not set")
|
||||
return
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(string(target)); err != nil {
|
||||
bad("Tailscale-Ingress-Target header invalid; want host:port")
|
||||
return
|
||||
}
|
||||
|
||||
getConnOrReset := func() (net.Conn, bool) {
|
||||
conn, _, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
h.logf("ingress: failed hijacking conn")
|
||||
http.Error(w, "failed hijacking conn", http.StatusInternalServerError)
|
||||
return nil, false
|
||||
}
|
||||
io.WriteString(conn, "HTTP/1.1 101 Switching Protocols\r\n\r\n")
|
||||
return &ipn.FunnelConn{
|
||||
Conn: conn,
|
||||
Src: srcAddr,
|
||||
Target: target,
|
||||
}, true
|
||||
}
|
||||
sendRST := func() {
|
||||
http.Error(w, "denied", http.StatusForbidden)
|
||||
}
|
||||
|
||||
h.ps.b.HandleIngressTCPConn(h.peerNode, target, srcAddr, getConnOrReset, sendRST)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.canDebug() {
|
||||
http.Error(w, "denied; no debug access", http.StatusForbidden)
|
||||
@@ -1099,6 +1033,5 @@ var (
|
||||
metricInvalidRequests = clientmetric.NewCounter("peerapi_invalid_requests")
|
||||
|
||||
// Non-debug PeerAPI endpoints.
|
||||
metricDNSCalls = clientmetric.NewCounter("peerapi_dns")
|
||||
metricIngressCalls = clientmetric.NewCounter("peerapi_ingress")
|
||||
metricDNSCalls = clientmetric.NewCounter("peerapi_dns")
|
||||
)
|
||||
|
@@ -1,6 +1,10 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_serve
|
||||
|
||||
// TODO: move this whole file to its own package, out of ipnlocal.
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
@@ -12,6 +16,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -28,6 +33,7 @@ import (
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/net/http2"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/logtail/backoff"
|
||||
@@ -36,11 +42,26 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/lazy"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/ctxkey"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/slicesx"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func init() {
|
||||
hookServeTCPHandlerForVIPService.Set((*LocalBackend).tcpHandlerForVIPService)
|
||||
hookTCPHandlerForServe.Set((*LocalBackend).tcpHandlerForServe)
|
||||
hookServeUpdateServeTCPPortNetMapAddrListenersLocked.Set((*LocalBackend).updateServeTCPPortNetMapAddrListenersLocked)
|
||||
|
||||
hookServeSetTCPPortsInterceptedFromNetmapAndPrefsLocked.Set(serveSetTCPPortsInterceptedFromNetmapAndPrefsLocked)
|
||||
hookServeClearVIPServicesTCPPortsInterceptedLocked.Set(func(b *LocalBackend) {
|
||||
b.setVIPServicesTCPPortsInterceptedLocked(nil)
|
||||
})
|
||||
|
||||
RegisterC2N("GET /vip-services", handleC2NVIPServicesGet)
|
||||
}
|
||||
|
||||
const (
|
||||
contentTypeHeader = "Content-Type"
|
||||
grpcBaseContentType = "application/grpc"
|
||||
@@ -222,6 +243,10 @@ func (s *localListener) handleListenersAccept(ln net.Listener) error {
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) updateServeTCPPortNetMapAddrListenersLocked(ports []uint16) {
|
||||
if b.sys.IsNetstack() {
|
||||
// don't listen on netmap addresses if we're in userspace mode
|
||||
return
|
||||
}
|
||||
// close existing listeners where port
|
||||
// is no longer in incoming ports list
|
||||
for ap, sl := range b.serveListeners {
|
||||
@@ -439,6 +464,38 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target
|
||||
handler(c)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) vipServicesFromPrefsLocked(prefs ipn.PrefsView) []*tailcfg.VIPService {
|
||||
// keyed by service name
|
||||
var services map[tailcfg.ServiceName]*tailcfg.VIPService
|
||||
if b.serveConfig.Valid() {
|
||||
for svc, config := range b.serveConfig.Services().All() {
|
||||
mak.Set(&services, svc, &tailcfg.VIPService{
|
||||
Name: svc,
|
||||
Ports: config.ServicePortRange(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range prefs.AdvertiseServices().All() {
|
||||
sn := tailcfg.ServiceName(s)
|
||||
if services == nil || services[sn] == nil {
|
||||
mak.Set(&services, sn, &tailcfg.VIPService{
|
||||
Name: sn,
|
||||
})
|
||||
}
|
||||
services[sn].Active = true
|
||||
}
|
||||
|
||||
servicesList := slicesx.MapValues(services)
|
||||
// [slicesx.MapValues] provides the values in an indeterminate order, but since we'll
|
||||
// be hashing a representation of this list later we want it to be in a consistent
|
||||
// order.
|
||||
slices.SortFunc(servicesList, func(a, b *tailcfg.VIPService) int {
|
||||
return strings.Compare(a.Name.String(), b.Name.String())
|
||||
})
|
||||
return servicesList
|
||||
}
|
||||
|
||||
// tcpHandlerForVIPService returns a handler for a TCP connection to a VIP service
|
||||
// that is being served via the ipn.ServeConfig. It returns nil if the destination
|
||||
// address is not a VIP service or if the VIP service does not have a TCP handler set.
|
||||
@@ -1046,3 +1103,278 @@ func (b *LocalBackend) getTLSServeCertForPort(port uint16, forVIPService tailcfg
|
||||
return &cert, nil
|
||||
}
|
||||
}
|
||||
|
||||
// setServeProxyHandlersLocked ensures there is an http proxy handler for each
|
||||
// backend specified in serveConfig. It expects serveConfig to be valid and
|
||||
// up-to-date, so should be called after reloadServeConfigLocked.
|
||||
func (b *LocalBackend) setServeProxyHandlersLocked() {
|
||||
if !b.serveConfig.Valid() {
|
||||
return
|
||||
}
|
||||
var backends map[string]bool
|
||||
for _, conf := range b.serveConfig.Webs() {
|
||||
for _, h := range conf.Handlers().All() {
|
||||
backend := h.Proxy()
|
||||
if backend == "" {
|
||||
// Only create proxy handlers for servers with a proxy backend.
|
||||
continue
|
||||
}
|
||||
mak.Set(&backends, backend, true)
|
||||
if _, ok := b.serveProxyHandlers.Load(backend); ok {
|
||||
continue
|
||||
}
|
||||
|
||||
b.logf("serve: creating a new proxy handler for %s", backend)
|
||||
p, err := b.proxyHandlerForBackend(backend)
|
||||
if err != nil {
|
||||
// The backend endpoint (h.Proxy) should have been validated by expandProxyTarget
|
||||
// in the CLI, so just log the error here.
|
||||
b.logf("[unexpected] could not create proxy for %v: %s", backend, err)
|
||||
continue
|
||||
}
|
||||
b.serveProxyHandlers.Store(backend, p)
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up handlers for proxy backends that are no longer present
|
||||
// in configuration.
|
||||
b.serveProxyHandlers.Range(func(key, value any) bool {
|
||||
backend := key.(string)
|
||||
if !backends[backend] {
|
||||
b.logf("serve: closing idle connections to %s", backend)
|
||||
b.serveProxyHandlers.Delete(backend)
|
||||
value.(*reverseProxy).close()
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// VIPServices returns the list of tailnet services that this node
|
||||
// is serving as a destination for.
|
||||
// The returned memory is owned by the caller.
|
||||
func (b *LocalBackend) VIPServices() []*tailcfg.VIPService {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.vipServicesFromPrefsLocked(b.pm.CurrentPrefs())
|
||||
}
|
||||
|
||||
func handleC2NVIPServicesGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
|
||||
b.logf("c2n: GET /vip-services received")
|
||||
var res tailcfg.C2NVIPServicesResponse
|
||||
res.VIPServices = b.VIPServices()
|
||||
res.ServicesHash = b.vipServiceHash(res.VIPServices)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
var metricIngressCalls = clientmetric.NewCounter("peerapi_ingress")
|
||||
|
||||
func init() {
|
||||
RegisterPeerAPIHandler("/v0/ingress", handleServeIngress)
|
||||
|
||||
}
|
||||
|
||||
func handleServeIngress(ph PeerAPIHandler, w http.ResponseWriter, r *http.Request) {
|
||||
h := ph.(*peerAPIHandler)
|
||||
metricIngressCalls.Add(1)
|
||||
|
||||
// http.Errors only useful if hitting endpoint manually
|
||||
// otherwise rely on log lines when debugging ingress connections
|
||||
// as connection is hijacked for bidi and is encrypted tls
|
||||
if !h.canIngress() {
|
||||
h.logf("ingress: denied; no ingress cap from %v", h.remoteAddr)
|
||||
http.Error(w, "denied; no ingress cap", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
logAndError := func(code int, publicMsg string) {
|
||||
h.logf("ingress: bad request from %v: %s", h.remoteAddr, publicMsg)
|
||||
http.Error(w, publicMsg, code)
|
||||
}
|
||||
bad := func(publicMsg string) {
|
||||
logAndError(http.StatusBadRequest, publicMsg)
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
logAndError(http.StatusMethodNotAllowed, "only POST allowed")
|
||||
return
|
||||
}
|
||||
srcAddrStr := r.Header.Get("Tailscale-Ingress-Src")
|
||||
if srcAddrStr == "" {
|
||||
bad("Tailscale-Ingress-Src header not set")
|
||||
return
|
||||
}
|
||||
srcAddr, err := netip.ParseAddrPort(srcAddrStr)
|
||||
if err != nil {
|
||||
bad("Tailscale-Ingress-Src header invalid; want ip:port")
|
||||
return
|
||||
}
|
||||
target := ipn.HostPort(r.Header.Get("Tailscale-Ingress-Target"))
|
||||
if target == "" {
|
||||
bad("Tailscale-Ingress-Target header not set")
|
||||
return
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(string(target)); err != nil {
|
||||
bad("Tailscale-Ingress-Target header invalid; want host:port")
|
||||
return
|
||||
}
|
||||
|
||||
getConnOrReset := func() (net.Conn, bool) {
|
||||
conn, _, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
h.logf("ingress: failed hijacking conn")
|
||||
http.Error(w, "failed hijacking conn", http.StatusInternalServerError)
|
||||
return nil, false
|
||||
}
|
||||
io.WriteString(conn, "HTTP/1.1 101 Switching Protocols\r\n\r\n")
|
||||
return &ipn.FunnelConn{
|
||||
Conn: conn,
|
||||
Src: srcAddr,
|
||||
Target: target,
|
||||
}, true
|
||||
}
|
||||
sendRST := func() {
|
||||
http.Error(w, "denied", http.StatusForbidden)
|
||||
}
|
||||
|
||||
h.ps.b.HandleIngressTCPConn(h.peerNode, target, srcAddr, getConnOrReset, sendRST)
|
||||
}
|
||||
|
||||
// wantIngressLocked reports whether this node has ingress configured. This bool
|
||||
// is sent to the coordination server (in Hostinfo.WireIngress) as an
|
||||
// optimization hint to know primarily which nodes are NOT using ingress, to
|
||||
// avoid doing work for regular nodes.
|
||||
//
|
||||
// Even if the user's ServeConfig.AllowFunnel map was manually edited in raw
|
||||
// mode and contains map entries with false values, sending true (from Len > 0)
|
||||
// is still fine. This is only an optimization hint for the control plane and
|
||||
// doesn't affect security or correctness. And we also don't expect people to
|
||||
// modify their ServeConfig in raw mode.
|
||||
func (b *LocalBackend) wantIngressLocked() bool {
|
||||
return b.serveConfig.Valid() && b.serveConfig.HasAllowFunnel()
|
||||
}
|
||||
|
||||
// hasIngressEnabledLocked reports whether the node has any funnel endpoint enabled. This bool is sent to control (in
|
||||
// Hostinfo.IngressEnabled) to determine whether 'Funnel' badge should be displayed on this node in the admin panel.
|
||||
func (b *LocalBackend) hasIngressEnabledLocked() bool {
|
||||
return b.serveConfig.Valid() && b.serveConfig.IsFunnelOn()
|
||||
}
|
||||
|
||||
// shouldWireInactiveIngressLocked reports whether the node is in a state where funnel is not actively enabled, but it
|
||||
// seems that it is intended to be used with funnel.
|
||||
func (b *LocalBackend) shouldWireInactiveIngressLocked() bool {
|
||||
return b.serveConfig.Valid() && !b.hasIngressEnabledLocked() && b.wantIngressLocked()
|
||||
}
|
||||
|
||||
func serveSetTCPPortsInterceptedFromNetmapAndPrefsLocked(b *LocalBackend, prefs ipn.PrefsView) (handlePorts []uint16) {
|
||||
var vipServicesPorts map[tailcfg.ServiceName][]uint16
|
||||
|
||||
b.reloadServeConfigLocked(prefs)
|
||||
if b.serveConfig.Valid() {
|
||||
servePorts := make([]uint16, 0, 3)
|
||||
for port := range b.serveConfig.TCPs() {
|
||||
if port > 0 {
|
||||
servePorts = append(servePorts, uint16(port))
|
||||
}
|
||||
}
|
||||
handlePorts = append(handlePorts, servePorts...)
|
||||
|
||||
for svc, cfg := range b.serveConfig.Services().All() {
|
||||
servicePorts := make([]uint16, 0, 3)
|
||||
for port := range cfg.TCP().All() {
|
||||
if port > 0 {
|
||||
servicePorts = append(servicePorts, uint16(port))
|
||||
}
|
||||
}
|
||||
if _, ok := vipServicesPorts[svc]; !ok {
|
||||
mak.Set(&vipServicesPorts, svc, servicePorts)
|
||||
} else {
|
||||
mak.Set(&vipServicesPorts, svc, append(vipServicesPorts[svc], servicePorts...))
|
||||
}
|
||||
}
|
||||
|
||||
b.setServeProxyHandlersLocked()
|
||||
|
||||
// don't listen on netmap addresses if we're in userspace mode
|
||||
if !b.sys.IsNetstack() {
|
||||
b.updateServeTCPPortNetMapAddrListenersLocked(servePorts)
|
||||
}
|
||||
}
|
||||
|
||||
b.setVIPServicesTCPPortsInterceptedLocked(vipServicesPorts)
|
||||
|
||||
return handlePorts
|
||||
}
|
||||
|
||||
// reloadServeConfigLocked reloads the serve config from the store or resets the
|
||||
// serve config to nil if not logged in. The "changed" parameter, when false, instructs
|
||||
// the method to only run the reset-logic and not reload the store from memory to ensure
|
||||
// foreground sessions are not removed if they are not saved on disk.
|
||||
func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) {
|
||||
if !b.currentNode().Self().Valid() || !prefs.Valid() || b.pm.CurrentProfile().ID() == "" {
|
||||
// We're not logged in, so we don't have a profile.
|
||||
// Don't try to load the serve config.
|
||||
b.lastServeConfJSON = mem.B(nil)
|
||||
b.serveConfig = ipn.ServeConfigView{}
|
||||
return
|
||||
}
|
||||
|
||||
confKey := ipn.ServeConfigKey(b.pm.CurrentProfile().ID())
|
||||
// TODO(maisem,bradfitz): prevent reading the config from disk
|
||||
// if the profile has not changed.
|
||||
confj, err := b.store.ReadState(confKey)
|
||||
if err != nil {
|
||||
b.lastServeConfJSON = mem.B(nil)
|
||||
b.serveConfig = ipn.ServeConfigView{}
|
||||
return
|
||||
}
|
||||
if b.lastServeConfJSON.Equal(mem.B(confj)) {
|
||||
return
|
||||
}
|
||||
b.lastServeConfJSON = mem.B(confj)
|
||||
var conf ipn.ServeConfig
|
||||
if err := json.Unmarshal(confj, &conf); err != nil {
|
||||
b.logf("invalid ServeConfig %q in StateStore: %v", confKey, err)
|
||||
b.serveConfig = ipn.ServeConfigView{}
|
||||
return
|
||||
}
|
||||
|
||||
// remove inactive sessions
|
||||
maps.DeleteFunc(conf.Foreground, func(sessionID string, sc *ipn.ServeConfig) bool {
|
||||
_, ok := b.notifyWatchers[sessionID]
|
||||
return !ok
|
||||
})
|
||||
|
||||
b.serveConfig = conf.View()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) setVIPServicesTCPPortsInterceptedLocked(svcPorts map[tailcfg.ServiceName][]uint16) {
|
||||
if len(svcPorts) == 0 {
|
||||
b.shouldInterceptVIPServicesTCPPortAtomic.Store(func(netip.AddrPort) bool { return false })
|
||||
return
|
||||
}
|
||||
nm := b.currentNode().NetMap()
|
||||
if nm == nil {
|
||||
b.logf("can't set intercept function for Service TCP Ports, netMap is nil")
|
||||
return
|
||||
}
|
||||
vipServiceIPMap := nm.GetVIPServiceIPMap()
|
||||
if len(vipServiceIPMap) == 0 {
|
||||
// No approved VIP Services
|
||||
return
|
||||
}
|
||||
|
||||
svcAddrPorts := make(map[netip.Addr]func(uint16) bool)
|
||||
// Only set the intercept function if the service has been assigned a VIP.
|
||||
for svcName, ports := range svcPorts {
|
||||
addrs, ok := vipServiceIPMap[svcName]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
interceptFn := generateInterceptTCPPortFunc(ports)
|
||||
for _, addr := range addrs {
|
||||
svcAddrPorts[addr] = interceptFn
|
||||
}
|
||||
}
|
||||
|
||||
b.shouldInterceptVIPServicesTCPPortAtomic.Store(generateInterceptVIPServicesTCPPortFunc(svcAddrPorts))
|
||||
}
|
||||
|
34
ipn/ipnlocal/serve_disabled.go
Normal file
34
ipn/ipnlocal/serve_disabled.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_serve
|
||||
|
||||
// These are temporary (2025-09-13) stubs for when tailscaled is built with the
|
||||
// ts_omit_serve build tag, disabling serve.
|
||||
//
|
||||
// TODO: move serve to a separate package, out of ipnlocal, and delete this
|
||||
// file. One step at a time.
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
const serveEnabled = false
|
||||
|
||||
type localListener = struct{}
|
||||
|
||||
func (b *LocalBackend) DeleteForegroundSession(sessionID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type funnelFlow = struct{}
|
||||
|
||||
func (*LocalBackend) hasIngressEnabledLocked() bool { return false }
|
||||
func (*LocalBackend) shouldWireInactiveIngressLocked() bool { return false }
|
||||
|
||||
func (b *LocalBackend) vipServicesFromPrefsLocked(prefs ipn.PrefsView) []*tailcfg.VIPService {
|
||||
return nil
|
||||
}
|
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_serve
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
|
@@ -8,8 +8,6 @@ import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -112,7 +110,6 @@ var handler = map[string]LocalAPIHandler{
|
||||
"query-feature": (*Handler).serveQueryFeature,
|
||||
"reload-config": (*Handler).reloadConfig,
|
||||
"reset-auth": (*Handler).serveResetAuth,
|
||||
"serve-config": (*Handler).serveServeConfig,
|
||||
"set-dns": (*Handler).serveSetDNS,
|
||||
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
|
||||
"set-gui-visible": (*Handler).serveSetGUIVisible,
|
||||
@@ -1209,89 +1206,6 @@ func (h *Handler) serveResetAuth(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case httpm.GET:
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "serve config denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
config := h.b.ServeConfig()
|
||||
bts, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
http.Error(w, "error encoding config: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
sum := sha256.Sum256(bts)
|
||||
etag := hex.EncodeToString(sum[:])
|
||||
w.Header().Set("Etag", etag)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(bts)
|
||||
case httpm.POST:
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "serve config denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
configIn := new(ipn.ServeConfig)
|
||||
if err := json.NewDecoder(r.Body).Decode(configIn); err != nil {
|
||||
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 err := authorizeServeConfigForGOOSAndUserContext(runtime.GOOS, configIn, h); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
etag := r.Header.Get("If-Match")
|
||||
if err := h.b.SetServeConfig(configIn, etag); err != nil {
|
||||
if errors.Is(err, ipnlocal.ErrETagMismatch) {
|
||||
http.Error(w, err.Error(), http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
WriteErrorJSON(w, fmt.Errorf("updating config: %w", err))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
default:
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func authorizeServeConfigForGOOSAndUserContext(goos string, configIn *ipn.ServeConfig, h *Handler) error {
|
||||
switch goos {
|
||||
case "windows", "linux", "darwin", "illumos", "solaris":
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
// Only check for local admin on tailscaled-on-mac (based on "sudo"
|
||||
// permissions). On sandboxed variants (MacSys and AppStore), tailscaled
|
||||
// cannot serve files outside of the sandbox and this check is not
|
||||
// relevant.
|
||||
if goos == "darwin" && version.IsSandboxedMacOS() {
|
||||
return nil
|
||||
}
|
||||
if !configIn.HasPathHandler() {
|
||||
return nil
|
||||
}
|
||||
if h.Actor.IsLocalAdmin(h.b.OperatorUserID()) {
|
||||
return nil
|
||||
}
|
||||
switch goos {
|
||||
case "windows":
|
||||
return errors.New("must be a Windows local admin to serve a path")
|
||||
case "linux", "darwin", "illumos", "solaris":
|
||||
return errors.New("must be root, or be an operator and able to run 'sudo tailscale' to serve a path")
|
||||
default:
|
||||
// We filter goos at the start of the func, this default case
|
||||
// should never happen.
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "IP forwarding check access denied", http.StatusForbidden)
|
||||
|
108
ipn/localapi/serve.go
Normal file
108
ipn/localapi/serve.go
Normal file
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_serve
|
||||
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func init() {
|
||||
Register("serve-config", (*Handler).serveServeConfig)
|
||||
}
|
||||
|
||||
func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case httpm.GET:
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "serve config denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
config := h.b.ServeConfig()
|
||||
bts, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
http.Error(w, "error encoding config: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
sum := sha256.Sum256(bts)
|
||||
etag := hex.EncodeToString(sum[:])
|
||||
w.Header().Set("Etag", etag)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(bts)
|
||||
case httpm.POST:
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "serve config denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
configIn := new(ipn.ServeConfig)
|
||||
if err := json.NewDecoder(r.Body).Decode(configIn); err != nil {
|
||||
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 err := authorizeServeConfigForGOOSAndUserContext(runtime.GOOS, configIn, h); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
etag := r.Header.Get("If-Match")
|
||||
if err := h.b.SetServeConfig(configIn, etag); err != nil {
|
||||
if errors.Is(err, ipnlocal.ErrETagMismatch) {
|
||||
http.Error(w, err.Error(), http.StatusPreconditionFailed)
|
||||
return
|
||||
}
|
||||
WriteErrorJSON(w, fmt.Errorf("updating config: %w", err))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
default:
|
||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func authorizeServeConfigForGOOSAndUserContext(goos string, configIn *ipn.ServeConfig, h *Handler) error {
|
||||
switch goos {
|
||||
case "windows", "linux", "darwin", "illumos", "solaris":
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
// Only check for local admin on tailscaled-on-mac (based on "sudo"
|
||||
// permissions). On sandboxed variants (MacSys and AppStore), tailscaled
|
||||
// cannot serve files outside of the sandbox and this check is not
|
||||
// relevant.
|
||||
if goos == "darwin" && version.IsSandboxedMacOS() {
|
||||
return nil
|
||||
}
|
||||
if !configIn.HasPathHandler() {
|
||||
return nil
|
||||
}
|
||||
if h.Actor.IsLocalAdmin(h.b.OperatorUserID()) {
|
||||
return nil
|
||||
}
|
||||
switch goos {
|
||||
case "windows":
|
||||
return errors.New("must be a Windows local admin to serve a path")
|
||||
case "linux", "darwin", "illumos", "solaris":
|
||||
return errors.New("must be root, or be an operator and able to run 'sudo tailscale' to serve a path")
|
||||
default:
|
||||
// We filter goos at the start of the func, this default case
|
||||
// should never happen.
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
@@ -235,6 +235,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
|
||||
tailscale.com/envknob from tailscale.com/client/local+
|
||||
tailscale.com/envknob/featureknob from tailscale.com/client/web+
|
||||
tailscale.com/feature from tailscale.com/ipn/ipnext+
|
||||
tailscale.com/feature/buildfeatures from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/feature/syspolicy from tailscale.com/logpolicy
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/health/healthmsg from tailscale.com/ipn/ipnlocal+
|
||||
|
@@ -33,6 +33,7 @@ import (
|
||||
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
|
||||
"gvisor.dev/gvisor/pkg/waiter"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/feature/buildfeatures"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/net/dns"
|
||||
@@ -643,13 +644,15 @@ func (ns *Impl) UpdateNetstackIPs(nm *netmap.NetworkMap) {
|
||||
var selfNode tailcfg.NodeView
|
||||
var serviceAddrSet set.Set[netip.Addr]
|
||||
if nm != nil {
|
||||
vipServiceIPMap := nm.GetVIPServiceIPMap()
|
||||
serviceAddrSet = make(set.Set[netip.Addr], len(vipServiceIPMap)*2)
|
||||
for _, addrs := range vipServiceIPMap {
|
||||
serviceAddrSet.AddSlice(addrs)
|
||||
}
|
||||
ns.atomicIsLocalIPFunc.Store(ipset.NewContainsIPFunc(nm.GetAddresses()))
|
||||
ns.atomicIsVIPServiceIPFunc.Store(serviceAddrSet.Contains)
|
||||
if buildfeatures.HasServe {
|
||||
vipServiceIPMap := nm.GetVIPServiceIPMap()
|
||||
serviceAddrSet = make(set.Set[netip.Addr], len(vipServiceIPMap)*2)
|
||||
for _, addrs := range vipServiceIPMap {
|
||||
serviceAddrSet.AddSlice(addrs)
|
||||
}
|
||||
ns.atomicIsVIPServiceIPFunc.Store(serviceAddrSet.Contains)
|
||||
}
|
||||
selfNode = nm.SelfNode
|
||||
} else {
|
||||
ns.atomicIsLocalIPFunc.Store(ipset.FalseContainsIPFunc())
|
||||
@@ -1032,6 +1035,9 @@ func (ns *Impl) isLocalIP(ip netip.Addr) bool {
|
||||
// isVIPServiceIP reports whether ip is an IP address that's
|
||||
// assigned to a VIP service.
|
||||
func (ns *Impl) isVIPServiceIP(ip netip.Addr) bool {
|
||||
if !buildfeatures.HasServe {
|
||||
return false
|
||||
}
|
||||
return ns.atomicIsVIPServiceIPFunc.Load()(ip)
|
||||
}
|
||||
|
||||
@@ -1074,7 +1080,7 @@ func (ns *Impl) shouldProcessInbound(p *packet.Parsed, t *tstun.Wrapper) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if isService {
|
||||
if buildfeatures.HasServe && isService {
|
||||
if p.IsEchoRequest() {
|
||||
return true
|
||||
}
|
||||
|
Reference in New Issue
Block a user