mirror of
https://github.com/tailscale/tailscale.git
synced 2025-05-01 13:11:01 +00:00
ipn/ipnlocal: Support TCP and Web VIP services
This commit intend to provide support for TCP and Web VIP services and also allow user to use Tun for VIP services if they want to. The commit includes: 1.Setting TCP intercept function for VIP Services. 2.Update netstack to send packet written from WG to netStack handler for VIP service. 3.Return correct TCP hander for VIP services when netstack acceptTCP. This commit also includes unit tests for if the local backend setServeConfig would set correct TCP intercept function and test if a hander gets returned when getting TCPHandlerForDst. The shouldProcessInbound check is not unit tested since the test result just depends on mocked functions. There should be an integration test to cover shouldProcessInbound and if the returned TCP handler actually does what the serveConfig says. Updates tailscale/corp#24604 Signed-off-by: KevinLiang10 <37811973+KevinLiang10@users.noreply.github.com>
This commit is contained in:
parent
cb3b1a1dcf
commit
8c8750f1b3
@ -228,10 +228,11 @@ type LocalBackend struct {
|
|||||||
// is never called.
|
// is never called.
|
||||||
getTCPHandlerForFunnelFlow func(srcAddr netip.AddrPort, dstPort uint16) (handler func(net.Conn))
|
getTCPHandlerForFunnelFlow func(srcAddr netip.AddrPort, dstPort uint16) (handler func(net.Conn))
|
||||||
|
|
||||||
filterAtomic atomic.Pointer[filter.Filter]
|
filterAtomic atomic.Pointer[filter.Filter]
|
||||||
containsViaIPFuncAtomic syncs.AtomicValue[func(netip.Addr) bool]
|
containsViaIPFuncAtomic syncs.AtomicValue[func(netip.Addr) bool]
|
||||||
shouldInterceptTCPPortAtomic syncs.AtomicValue[func(uint16) bool]
|
shouldInterceptTCPPortAtomic syncs.AtomicValue[func(uint16) bool]
|
||||||
numClientStatusCalls atomic.Uint32
|
shouldInterceptVIPServicesTCPPortAtomic syncs.AtomicValue[func(netip.AddrPort) bool]
|
||||||
|
numClientStatusCalls atomic.Uint32
|
||||||
|
|
||||||
// goTracker accounts for all goroutines started by LocalBacked, primarily
|
// goTracker accounts for all goroutines started by LocalBacked, primarily
|
||||||
// for testing and graceful shutdown purposes.
|
// for testing and graceful shutdown purposes.
|
||||||
@ -317,8 +318,9 @@ type LocalBackend struct {
|
|||||||
offlineAutoUpdateCancel func()
|
offlineAutoUpdateCancel func()
|
||||||
|
|
||||||
// ServeConfig fields. (also guarded by mu)
|
// ServeConfig fields. (also guarded by mu)
|
||||||
lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig
|
lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig
|
||||||
serveConfig ipn.ServeConfigView // or !Valid if none
|
serveConfig ipn.ServeConfigView // or !Valid if none
|
||||||
|
ipVIPServiceMap netmap.IPServiceMappings // map of VIPService IPs to their corresponding service names
|
||||||
|
|
||||||
webClient webClient
|
webClient webClient
|
||||||
webClientListeners map[netip.AddrPort]*localListener // listeners for local web client traffic
|
webClientListeners map[netip.AddrPort]*localListener // listeners for local web client traffic
|
||||||
@ -523,6 +525,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
|||||||
b.e.SetJailedFilter(noneFilter)
|
b.e.SetJailedFilter(noneFilter)
|
||||||
|
|
||||||
b.setTCPPortsIntercepted(nil)
|
b.setTCPPortsIntercepted(nil)
|
||||||
|
b.setVIPServicesTCPPortsIntercepted(nil)
|
||||||
|
|
||||||
b.statusChanged = sync.NewCond(&b.statusLock)
|
b.statusChanged = sync.NewCond(&b.statusLock)
|
||||||
b.e.SetStatusCallback(b.setWgengineStatus)
|
b.e.SetStatusCallback(b.setWgengineStatus)
|
||||||
@ -3362,10 +3365,7 @@ func (b *LocalBackend) clearMachineKeyLocked() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// setTCPPortsIntercepted populates b.shouldInterceptTCPPortAtomic with an
|
func generateInterceptTCPPortFunc(ports []uint16) func(uint16) bool {
|
||||||
// efficient func for ShouldInterceptTCPPort to use, which is called on every
|
|
||||||
// incoming packet.
|
|
||||||
func (b *LocalBackend) setTCPPortsIntercepted(ports []uint16) {
|
|
||||||
slices.Sort(ports)
|
slices.Sort(ports)
|
||||||
ports = slices.Compact(ports)
|
ports = slices.Compact(ports)
|
||||||
var f func(uint16) bool
|
var f func(uint16) bool
|
||||||
@ -3396,7 +3396,61 @@ func (b *LocalBackend) setTCPPortsIntercepted(ports []uint16) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b.shouldInterceptTCPPortAtomic.Store(f)
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// setTCPPortsIntercepted populates b.shouldInterceptTCPPortAtomic with an
|
||||||
|
// efficient func for ShouldInterceptTCPPort to use, which is called on every
|
||||||
|
// incoming packet.
|
||||||
|
func (b *LocalBackend) setTCPPortsIntercepted(ports []uint16) {
|
||||||
|
b.shouldInterceptTCPPortAtomic.Store(generateInterceptTCPPortFunc(ports))
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateInterceptVIPServicesTCPPortFunc(svcAddrPorts map[netip.Addr]func(uint16) bool) func(netip.AddrPort) bool {
|
||||||
|
return func(ap netip.AddrPort) bool {
|
||||||
|
if f, ok := svcAddrPorts[ap.Addr()]; ok {
|
||||||
|
return f(ap.Port())
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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[string][]uint16) {
|
||||||
|
b.mu.Lock()
|
||||||
|
defer b.mu.Unlock()
|
||||||
|
b.setVIPServicesTCPPortsInterceptedLocked(svcPorts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *LocalBackend) setVIPServicesTCPPortsInterceptedLocked(svcPorts map[string][]uint16) {
|
||||||
|
if len(svcPorts) == 0 {
|
||||||
|
b.shouldInterceptVIPServicesTCPPortAtomic.Store(func(netip.AddrPort) bool { return false })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
nm := b.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 {
|
||||||
|
if addrs, ok := vipServiceIPMap[svcName]; ok {
|
||||||
|
interceptFn := generateInterceptTCPPortFunc(ports)
|
||||||
|
for _, addr := range addrs {
|
||||||
|
svcAddrPorts[addr] = interceptFn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.shouldInterceptVIPServicesTCPPortAtomic.Store(generateInterceptVIPServicesTCPPortFunc(svcAddrPorts))
|
||||||
}
|
}
|
||||||
|
|
||||||
// setAtomicValuesFromPrefsLocked populates sshAtomicBool, containsViaIPFuncAtomic,
|
// setAtomicValuesFromPrefsLocked populates sshAtomicBool, containsViaIPFuncAtomic,
|
||||||
@ -3409,6 +3463,7 @@ func (b *LocalBackend) setAtomicValuesFromPrefsLocked(p ipn.PrefsView) {
|
|||||||
if !p.Valid() {
|
if !p.Valid() {
|
||||||
b.containsViaIPFuncAtomic.Store(ipset.FalseContainsIPFunc())
|
b.containsViaIPFuncAtomic.Store(ipset.FalseContainsIPFunc())
|
||||||
b.setTCPPortsIntercepted(nil)
|
b.setTCPPortsIntercepted(nil)
|
||||||
|
b.setVIPServicesTCPPortsInterceptedLocked(nil)
|
||||||
b.lastServeConfJSON = mem.B(nil)
|
b.lastServeConfJSON = mem.B(nil)
|
||||||
b.serveConfig = ipn.ServeConfigView{}
|
b.serveConfig = ipn.ServeConfigView{}
|
||||||
} else {
|
} else {
|
||||||
@ -4159,6 +4214,11 @@ func (b *LocalBackend) TCPHandlerForDst(src, dst netip.AddrPort) (handler func(c
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(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
|
||||||
|
}
|
||||||
// Then handle external connections to the local IP.
|
// Then handle external connections to the local IP.
|
||||||
if !b.isLocalIP(dst.Addr()) {
|
if !b.isLocalIP(dst.Addr()) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -5676,6 +5736,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
|||||||
netns.SetDisableBindConnToInterface(nm.HasCap(tailcfg.CapabilityDebugDisableBindConnToInterface))
|
netns.SetDisableBindConnToInterface(nm.HasCap(tailcfg.CapabilityDebugDisableBindConnToInterface))
|
||||||
|
|
||||||
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs())
|
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs())
|
||||||
|
b.ipVIPServiceMap = nm.GetIPVIPServiceMap()
|
||||||
if nm == nil {
|
if nm == nil {
|
||||||
b.nodeByAddr = nil
|
b.nodeByAddr = nil
|
||||||
|
|
||||||
@ -5962,6 +6023,7 @@ func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) {
|
|||||||
// b.mu must be held.
|
// b.mu must be held.
|
||||||
func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.PrefsView) {
|
func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.PrefsView) {
|
||||||
handlePorts := make([]uint16, 0, 4)
|
handlePorts := make([]uint16, 0, 4)
|
||||||
|
vipServicesPorts := make(map[string][]uint16)
|
||||||
|
|
||||||
if prefs.Valid() && prefs.RunSSH() && envknob.CanSSHD() {
|
if prefs.Valid() && prefs.RunSSH() && envknob.CanSSHD() {
|
||||||
handlePorts = append(handlePorts, 22)
|
handlePorts = append(handlePorts, 22)
|
||||||
@ -5985,6 +6047,20 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.
|
|||||||
}
|
}
|
||||||
handlePorts = append(handlePorts, servePorts...)
|
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 {
|
||||||
|
vipServicesPorts[svc] = servicePorts
|
||||||
|
} else {
|
||||||
|
vipServicesPorts[svc] = append(vipServicesPorts[svc], servicePorts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
b.setServeProxyHandlersLocked()
|
b.setServeProxyHandlersLocked()
|
||||||
|
|
||||||
// don't listen on netmap addresses if we're in userspace mode
|
// don't listen on netmap addresses if we're in userspace mode
|
||||||
@ -5996,6 +6072,7 @@ func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked(prefs ipn.
|
|||||||
// Update funnel info in hostinfo and kick off control update if needed.
|
// Update funnel info in hostinfo and kick off control update if needed.
|
||||||
b.updateIngressLocked()
|
b.updateIngressLocked()
|
||||||
b.setTCPPortsIntercepted(handlePorts)
|
b.setTCPPortsIntercepted(handlePorts)
|
||||||
|
b.setVIPServicesTCPPortsInterceptedLocked(vipServicesPorts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateIngressLocked updates the hostinfo.WireIngress and hostinfo.IngressEnabled fields and kicks off a Hostinfo
|
// updateIngressLocked updates the hostinfo.WireIngress and hostinfo.IngressEnabled fields and kicks off a Hostinfo
|
||||||
@ -6854,6 +6931,12 @@ func (b *LocalBackend) ShouldInterceptTCPPort(port uint16) bool {
|
|||||||
return b.shouldInterceptTCPPortAtomic.Load()(port)
|
return b.shouldInterceptTCPPortAtomic.Load()(port)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
// SwitchProfile switches to the profile with the given id.
|
// SwitchProfile switches to the profile with the given id.
|
||||||
// It will restart the backend on success.
|
// It will restart the backend on success.
|
||||||
// If the profile is not known, it returns an errProfileNotFound.
|
// If the profile is not known, it returns an errProfileNotFound.
|
||||||
|
@ -2615,6 +2615,150 @@ func TestOnTailnetDefaultAutoUpdate(t *testing.T) {
|
|||||||
|
|
||||||
func TestTCPHandlerForDst(t *testing.T) {
|
func TestTCPHandlerForDst(t *testing.T) {
|
||||||
b := newTestBackend(t)
|
b := newTestBackend(t)
|
||||||
|
tests := []struct {
|
||||||
|
desc string
|
||||||
|
dst string
|
||||||
|
intercept bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
desc: "intercept port 80 (Web UI) on quad100 IPv4",
|
||||||
|
dst: "100.100.100.100:80",
|
||||||
|
intercept: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "intercept port 80 (Web UI) on quad100 IPv6",
|
||||||
|
dst: "[fd7a:115c:a1e0::53]:80",
|
||||||
|
intercept: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "don't intercept port 80 on local ip",
|
||||||
|
dst: "100.100.103.100:80",
|
||||||
|
intercept: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "intercept port 8080 (Taildrive) on quad100 IPv4",
|
||||||
|
dst: "[fd7a:115c:a1e0::53]:8080",
|
||||||
|
intercept: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "don't intercept port 8080 on local ip",
|
||||||
|
dst: "100.100.103.100:8080",
|
||||||
|
intercept: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "don't intercept port 9080 on quad100 IPv4",
|
||||||
|
dst: "100.100.100.100:9080",
|
||||||
|
intercept: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "don't intercept port 9080 on quad100 IPv6",
|
||||||
|
dst: "[fd7a:115c:a1e0::53]:9080",
|
||||||
|
intercept: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "don't intercept port 9080 on local ip",
|
||||||
|
dst: "100.100.103.100:9080",
|
||||||
|
intercept: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.dst, func(t *testing.T) {
|
||||||
|
t.Log(tt.desc)
|
||||||
|
src := netip.MustParseAddrPort("100.100.102.100:51234")
|
||||||
|
h, _ := b.TCPHandlerForDst(src, netip.MustParseAddrPort(tt.dst))
|
||||||
|
if !tt.intercept && h != nil {
|
||||||
|
t.Error("intercepted traffic we shouldn't have")
|
||||||
|
} else if tt.intercept && h == nil {
|
||||||
|
t.Error("failed to intercept traffic we should have")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTCPHandlerForDstWithVIPService(t *testing.T) {
|
||||||
|
b := newTestBackend(t)
|
||||||
|
svcIPMap := tailcfg.ServiceIPMappings{
|
||||||
|
"svc:foo": []netip.Addr{
|
||||||
|
netip.MustParseAddr("100.101.101.101"),
|
||||||
|
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:cd96:6565:6565"),
|
||||||
|
},
|
||||||
|
"svc:bar": []netip.Addr{
|
||||||
|
netip.MustParseAddr("100.99.99.99"),
|
||||||
|
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:cd96:626b:628b"),
|
||||||
|
},
|
||||||
|
"svc:baz": []netip.Addr{
|
||||||
|
netip.MustParseAddr("100.133.133.133"),
|
||||||
|
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:cd96:8585:8585"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
svcIPMapJSON, err := json.Marshal(svcIPMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
b.setNetMapLocked(
|
||||||
|
&netmap.NetworkMap{
|
||||||
|
SelfNode: (&tailcfg.Node{
|
||||||
|
Name: "example.ts.net",
|
||||||
|
CapMap: tailcfg.NodeCapMap{
|
||||||
|
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)},
|
||||||
|
},
|
||||||
|
}).View(),
|
||||||
|
UserProfiles: map[tailcfg.UserID]tailcfg.UserProfile{
|
||||||
|
tailcfg.UserID(1): {
|
||||||
|
LoginName: "someone@example.com",
|
||||||
|
DisplayName: "Some One",
|
||||||
|
ProfilePicURL: "https://example.com/photo.jpg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
err = b.setServeConfigLocked(
|
||||||
|
&ipn.ServeConfig{
|
||||||
|
Services: map[string]*ipn.ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
882: {HTTP: true},
|
||||||
|
883: {HTTPS: true},
|
||||||
|
},
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"foo.example.ts.net:882": {
|
||||||
|
Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"foo.example.ts.net:883": {
|
||||||
|
Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/": {Text: "test"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"svc:bar": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
990: {TCPForward: "127.0.0.1:8443"},
|
||||||
|
991: {TCPForward: "127.0.0.1:5432", TerminateTLS: "bar.test.ts.net"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"svc:qux": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
600: {HTTPS: true},
|
||||||
|
},
|
||||||
|
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||||
|
"qux.example.ts.net:600": {
|
||||||
|
Handlers: map[string]*ipn.HTTPHandler{
|
||||||
|
"/": {Text: "qux"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
desc string
|
desc string
|
||||||
@ -2666,6 +2810,77 @@ func TestTCPHandlerForDst(t *testing.T) {
|
|||||||
dst: "100.100.103.100:9080",
|
dst: "100.100.103.100:9080",
|
||||||
intercept: false,
|
intercept: false,
|
||||||
},
|
},
|
||||||
|
// VIP service destinations
|
||||||
|
{
|
||||||
|
desc: "intercept port 882 (HTTP) on service foo IPv4",
|
||||||
|
dst: "100.101.101.101:882",
|
||||||
|
intercept: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "intercept port 882 (HTTP) on service foo IPv6",
|
||||||
|
dst: "[fd7a:115c:a1e0:ab12:4843:cd96:6565:6565]:882",
|
||||||
|
intercept: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "intercept port 883 (HTTPS) on service foo IPv4",
|
||||||
|
dst: "100.101.101.101:883",
|
||||||
|
intercept: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "intercept port 883 (HTTPS) on service foo IPv6",
|
||||||
|
dst: "[fd7a:115c:a1e0:ab12:4843:cd96:6565:6565]:883",
|
||||||
|
intercept: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "intercept port 990 (TCPForward) on service bar IPv4",
|
||||||
|
dst: "100.99.99.99:990",
|
||||||
|
intercept: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "intercept port 990 (TCPForward) on service bar IPv6",
|
||||||
|
dst: "[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:990",
|
||||||
|
intercept: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "intercept port 991 (TCPForward with TerminateTLS) on service bar IPv4",
|
||||||
|
dst: "100.99.99.99:990",
|
||||||
|
intercept: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "intercept port 991 (TCPForward with TerminateTLS) on service bar IPv6",
|
||||||
|
dst: "[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:990",
|
||||||
|
intercept: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "don't intercept port 4444 on service foo IPv4",
|
||||||
|
dst: "100.101.101.101:4444",
|
||||||
|
intercept: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "don't intercept port 4444 on service foo IPv6",
|
||||||
|
dst: "[fd7a:115c:a1e0:ab12:4843:cd96:6565:6565]:4444",
|
||||||
|
intercept: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "don't intercept port 600 on unknown service IPv4",
|
||||||
|
dst: "100.22.22.22:883",
|
||||||
|
intercept: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "don't intercept port 600 on unknown service IPv6",
|
||||||
|
dst: "[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:883",
|
||||||
|
intercept: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "don't intercept port 600 (HTTPS) on service baz IPv4",
|
||||||
|
dst: "100.133.133.133:600",
|
||||||
|
intercept: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "don't intercept port 600 (HTTPS) on service baz IPv6",
|
||||||
|
dst: "[fd7a:115c:a1e0:ab12:4843:cd96:8585:8585]:600",
|
||||||
|
intercept: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
@ -54,8 +54,9 @@ var ErrETagMismatch = errors.New("etag mismatch")
|
|||||||
var serveHTTPContextKey ctxkey.Key[*serveHTTPContext]
|
var serveHTTPContextKey ctxkey.Key[*serveHTTPContext]
|
||||||
|
|
||||||
type serveHTTPContext struct {
|
type serveHTTPContext struct {
|
||||||
SrcAddr netip.AddrPort
|
SrcAddr netip.AddrPort
|
||||||
DestPort uint16
|
ForVIPService bool
|
||||||
|
DestPort uint16
|
||||||
|
|
||||||
// provides funnel-specific context, nil if not funneled
|
// provides funnel-specific context, nil if not funneled
|
||||||
Funnel *funnelFlow
|
Funnel *funnelFlow
|
||||||
@ -275,6 +276,12 @@ func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string
|
|||||||
return errors.New("can't reconfigure tailscaled when using a config file; config file is locked")
|
return errors.New("can't reconfigure tailscaled when using a config file; config file is locked")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config != nil {
|
||||||
|
if err := config.CheckValidServicesConfig(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nm := b.netMap
|
nm := b.netMap
|
||||||
if nm == nil {
|
if nm == nil {
|
||||||
return errors.New("netMap is nil")
|
return errors.New("netMap is nil")
|
||||||
@ -432,6 +439,105 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target
|
|||||||
handler(c)
|
handler(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func (b *LocalBackend) tcpHandlerForVIPService(dstAddr, srcAddr netip.AddrPort) (handler func(net.Conn) error) {
|
||||||
|
b.mu.Lock()
|
||||||
|
sc := b.serveConfig
|
||||||
|
ipVIPServiceMap := b.ipVIPServiceMap
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
if !sc.Valid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dport := dstAddr.Port()
|
||||||
|
|
||||||
|
dstSvc, ok := ipVIPServiceMap[dstAddr.Addr()]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tcph, ok := sc.FindServiceTCP(dstSvc, dstAddr.Port())
|
||||||
|
if !ok {
|
||||||
|
b.logf("The destination service doesn't have a TCP handler set.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if tcph.HTTPS() || tcph.HTTP() {
|
||||||
|
hs := &http.Server{
|
||||||
|
Handler: http.HandlerFunc(b.serveWebHandler),
|
||||||
|
BaseContext: func(_ net.Listener) context.Context {
|
||||||
|
return serveHTTPContextKey.WithValue(context.Background(), &serveHTTPContext{
|
||||||
|
SrcAddr: srcAddr,
|
||||||
|
ForVIPService: true,
|
||||||
|
DestPort: dport,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if tcph.HTTPS() {
|
||||||
|
// TODO(kevinliang10): just leaving this TLS cert creation as if we don't have other
|
||||||
|
// hostnames, but for services this getTLSServeCetForPort will need a version that also take
|
||||||
|
// in the hostname. How to store the TLS cert is still being discussed.
|
||||||
|
hs.TLSConfig = &tls.Config{
|
||||||
|
GetCertificate: b.getTLSServeCertForPort(dport, true),
|
||||||
|
}
|
||||||
|
return func(c net.Conn) error {
|
||||||
|
return hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(c net.Conn) error {
|
||||||
|
return hs.Serve(netutil.NewOneConnListener(c, nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if backDst := tcph.TCPForward(); backDst != "" {
|
||||||
|
return func(conn net.Conn) error {
|
||||||
|
defer conn.Close()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
|
||||||
|
cancel()
|
||||||
|
if err != nil {
|
||||||
|
b.logf("localbackend: failed to TCP proxy port %v (from %v) to %s: %v", dport, srcAddr, backDst, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
defer backConn.Close()
|
||||||
|
if sni := tcph.TerminateTLS(); sni != "" {
|
||||||
|
conn = tls.Server(conn, &tls.Config{
|
||||||
|
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
pair, err := b.GetCertPEM(ctx, sni)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cert, err := tls.X509KeyPair(pair.CertPEM, pair.KeyPEM)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &cert, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
errc := make(chan error, 1)
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(backConn, conn)
|
||||||
|
errc <- err
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(conn, backConn)
|
||||||
|
errc <- err
|
||||||
|
}()
|
||||||
|
return <-errc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// tcpHandlerForServe returns a handler for a TCP connection to be served via
|
// tcpHandlerForServe returns a handler for a TCP connection to be served via
|
||||||
// the ipn.ServeConfig. The funnelFlow can be nil if this is not a funneled
|
// the ipn.ServeConfig. The funnelFlow can be nil if this is not a funneled
|
||||||
// connection.
|
// connection.
|
||||||
@ -462,7 +568,7 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort,
|
|||||||
}
|
}
|
||||||
if tcph.HTTPS() {
|
if tcph.HTTPS() {
|
||||||
hs.TLSConfig = &tls.Config{
|
hs.TLSConfig = &tls.Config{
|
||||||
GetCertificate: b.getTLSServeCertForPort(dport),
|
GetCertificate: b.getTLSServeCertForPort(dport, false),
|
||||||
}
|
}
|
||||||
return func(c net.Conn) error {
|
return func(c net.Conn) error {
|
||||||
return hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "")
|
return hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "")
|
||||||
@ -542,7 +648,7 @@ func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView,
|
|||||||
b.logf("[unexpected] localbackend: no serveHTTPContext in request")
|
b.logf("[unexpected] localbackend: no serveHTTPContext in request")
|
||||||
return z, "", false
|
return z, "", false
|
||||||
}
|
}
|
||||||
wsc, ok := b.webServerConfig(hostname, sctx.DestPort)
|
wsc, ok := b.webServerConfig(hostname, sctx.ForVIPService, sctx.DestPort)
|
||||||
if !ok {
|
if !ok {
|
||||||
return z, "", false
|
return z, "", false
|
||||||
}
|
}
|
||||||
@ -900,7 +1006,7 @@ func allNumeric(s string) bool {
|
|||||||
return s != ""
|
return s != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) webServerConfig(hostname string, port uint16) (c ipn.WebServerConfigView, ok bool) {
|
func (b *LocalBackend) webServerConfig(hostname string, forVIPService bool, port uint16) (c ipn.WebServerConfigView, ok bool) {
|
||||||
key := ipn.HostPort(fmt.Sprintf("%s:%v", hostname, port))
|
key := ipn.HostPort(fmt.Sprintf("%s:%v", hostname, port))
|
||||||
|
|
||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
@ -909,15 +1015,18 @@ func (b *LocalBackend) webServerConfig(hostname string, port uint16) (c ipn.WebS
|
|||||||
if !b.serveConfig.Valid() {
|
if !b.serveConfig.Valid() {
|
||||||
return c, false
|
return c, false
|
||||||
}
|
}
|
||||||
|
if forVIPService {
|
||||||
|
return b.serveConfig.FindServiceWeb(key)
|
||||||
|
}
|
||||||
return b.serveConfig.FindWeb(key)
|
return b.serveConfig.FindWeb(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *LocalBackend) getTLSServeCertForPort(port uint16) func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
func (b *LocalBackend) getTLSServeCertForPort(port uint16, forVIPService bool) func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
return func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
return func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
if hi == nil || hi.ServerName == "" {
|
if hi == nil || hi.ServerName == "" {
|
||||||
return nil, errors.New("no SNI ServerName")
|
return nil, errors.New("no SNI ServerName")
|
||||||
}
|
}
|
||||||
_, ok := b.webServerConfig(hi.ServerName, port)
|
_, ok := b.webServerConfig(hi.ServerName, forVIPService, port)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("no webserver configured for name/port")
|
return nil, errors.New("no webserver configured for name/port")
|
||||||
}
|
}
|
||||||
|
@ -296,6 +296,203 @@ func TestServeConfigForeground(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestServeConfigServices tests the side effects of setting the
|
||||||
|
// Services field in a ServeConfig. The Services field is a map
|
||||||
|
// of all services the current service host is serving. Unlike what we
|
||||||
|
// serve for node itself, there is no foreground and no local handlers
|
||||||
|
// for the services. So the only things we need to test are if the
|
||||||
|
// services configured are valid and if they correctly set intercept
|
||||||
|
// functions for netStack.
|
||||||
|
func TestServeConfigServices(t *testing.T) {
|
||||||
|
b := newTestBackend(t)
|
||||||
|
svcIPMap := tailcfg.ServiceIPMappings{
|
||||||
|
"svc:foo": []netip.Addr{
|
||||||
|
netip.MustParseAddr("100.101.101.101"),
|
||||||
|
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:cd96:6565:6565"),
|
||||||
|
},
|
||||||
|
"svc:bar": []netip.Addr{
|
||||||
|
netip.MustParseAddr("100.99.99.99"),
|
||||||
|
netip.MustParseAddr("fd7a:115c:a1e0:ab12:4843:cd96:626b:628b"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
svcIPMapJSON, err := json.Marshal(svcIPMap)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.netMap = &netmap.NetworkMap{
|
||||||
|
SelfNode: (&tailcfg.Node{
|
||||||
|
Name: "example.ts.net",
|
||||||
|
CapMap: tailcfg.NodeCapMap{
|
||||||
|
tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)},
|
||||||
|
},
|
||||||
|
}).View(),
|
||||||
|
UserProfiles: map[tailcfg.UserID]tailcfg.UserProfile{
|
||||||
|
tailcfg.UserID(1): {
|
||||||
|
LoginName: "someone@example.com",
|
||||||
|
DisplayName: "Some One",
|
||||||
|
ProfilePicURL: "https://example.com/photo.jpg",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
conf *ipn.ServeConfig
|
||||||
|
expectedErr error
|
||||||
|
packetDstAddrPort []netip.AddrPort
|
||||||
|
intercepted bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no-services",
|
||||||
|
conf: &ipn.ServeConfig{},
|
||||||
|
packetDstAddrPort: []netip.AddrPort{
|
||||||
|
netip.MustParseAddrPort("100.101.101.101:443"),
|
||||||
|
},
|
||||||
|
intercepted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "one-incorrectly-configured-service",
|
||||||
|
conf: &ipn.ServeConfig{
|
||||||
|
Services: map[string]*ipn.ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
80: {HTTP: true},
|
||||||
|
},
|
||||||
|
Tun: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedErr: ipn.ErrServiceConfigHasBothTCPAndTun,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// one correctly configured service with packet should be intercepted
|
||||||
|
name: "one-service-intercept-packet",
|
||||||
|
conf: &ipn.ServeConfig{
|
||||||
|
Services: map[string]*ipn.ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
80: {HTTP: true},
|
||||||
|
81: {HTTPS: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
packetDstAddrPort: []netip.AddrPort{
|
||||||
|
netip.MustParseAddrPort("100.101.101.101:80"),
|
||||||
|
netip.MustParseAddrPort("[fd7a:115c:a1e0:ab12:4843:cd96:6565:6565]:80"),
|
||||||
|
},
|
||||||
|
intercepted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// one correctly configured service with packet should not be intercepted
|
||||||
|
name: "one-service-not-intercept-packet",
|
||||||
|
conf: &ipn.ServeConfig{
|
||||||
|
Services: map[string]*ipn.ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
80: {HTTP: true},
|
||||||
|
81: {HTTPS: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
packetDstAddrPort: []netip.AddrPort{
|
||||||
|
netip.MustParseAddrPort("100.99.99.99:80"),
|
||||||
|
netip.MustParseAddrPort("[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80"),
|
||||||
|
netip.MustParseAddrPort("100.101.101.101:82"),
|
||||||
|
netip.MustParseAddrPort("[fd7a:115c:a1e0:ab12:4843:cd96:6565:6565]:82"),
|
||||||
|
},
|
||||||
|
intercepted: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
//multiple correctly configured service with packet should be intercepted
|
||||||
|
name: "multiple-service-intercept-packet",
|
||||||
|
conf: &ipn.ServeConfig{
|
||||||
|
Services: map[string]*ipn.ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
80: {HTTP: true},
|
||||||
|
81: {HTTPS: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"svc:bar": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
80: {HTTP: true},
|
||||||
|
81: {HTTPS: true},
|
||||||
|
82: {HTTPS: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
packetDstAddrPort: []netip.AddrPort{
|
||||||
|
netip.MustParseAddrPort("100.99.99.99:80"),
|
||||||
|
netip.MustParseAddrPort("[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80"),
|
||||||
|
netip.MustParseAddrPort("100.101.101.101:81"),
|
||||||
|
netip.MustParseAddrPort("[fd7a:115c:a1e0:ab12:4843:cd96:6565:6565]:81"),
|
||||||
|
},
|
||||||
|
intercepted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// multiple correctly configured service with packet should not be intercepted
|
||||||
|
name: "multiple-service-not-intercept-packet",
|
||||||
|
conf: &ipn.ServeConfig{
|
||||||
|
Services: map[string]*ipn.ServiceConfig{
|
||||||
|
"svc:foo": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
80: {HTTP: true},
|
||||||
|
81: {HTTPS: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"svc:bar": {
|
||||||
|
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||||
|
80: {HTTP: true},
|
||||||
|
81: {HTTPS: true},
|
||||||
|
82: {HTTPS: true},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
packetDstAddrPort: []netip.AddrPort{
|
||||||
|
// ips in capmap but port is not hosting service
|
||||||
|
netip.MustParseAddrPort("100.99.99.99:77"),
|
||||||
|
netip.MustParseAddrPort("[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:77"),
|
||||||
|
netip.MustParseAddrPort("100.101.101.101:85"),
|
||||||
|
netip.MustParseAddrPort("[fd7a:115c:a1e0:ab12:4843:cd96:6565:6565]:85"),
|
||||||
|
// ips not in capmap
|
||||||
|
netip.MustParseAddrPort("100.102.102.102:80"),
|
||||||
|
netip.MustParseAddrPort("[fd7a:115c:a1e0:ab12:4843:cd96:6666:6666]:80"),
|
||||||
|
},
|
||||||
|
intercepted: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := b.SetServeConfig(tt.conf, "")
|
||||||
|
if err != nil && tt.expectedErr != nil {
|
||||||
|
if !errors.Is(err, tt.expectedErr) {
|
||||||
|
t.Fatalf("expected error %v,\n got %v", tt.expectedErr, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, addrPort := range tt.packetDstAddrPort {
|
||||||
|
if tt.intercepted != b.ShouldInterceptVIPServiceTCPPort(addrPort) {
|
||||||
|
if tt.intercepted {
|
||||||
|
t.Fatalf("expected packet to be intercepted")
|
||||||
|
} else {
|
||||||
|
t.Fatalf("expected packet not to be intercepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestServeConfigETag(t *testing.T) {
|
func TestServeConfigETag(t *testing.T) {
|
||||||
b := newTestBackend(t)
|
b := newTestBackend(t)
|
||||||
|
|
||||||
|
54
ipn/serve.go
54
ipn/serve.go
@ -55,8 +55,8 @@ type ServeConfig struct {
|
|||||||
// keyed by mount point ("/", "/foo", etc)
|
// keyed by mount point ("/", "/foo", etc)
|
||||||
Web map[HostPort]*WebServerConfig `json:",omitempty"`
|
Web map[HostPort]*WebServerConfig `json:",omitempty"`
|
||||||
|
|
||||||
// Services maps from service name to a ServiceConfig. Which describes the
|
// Services maps from service name (in the form "svc:dns-label") to a ServiceConfig.
|
||||||
// L3, L4, and L7 forwarding information for the service.
|
// Which describes the L3, L4, and L7 forwarding information for the service.
|
||||||
Services map[string]*ServiceConfig `json:",omitempty"`
|
Services map[string]*ServiceConfig `json:",omitempty"`
|
||||||
|
|
||||||
// AllowFunnel is the set of SNI:port values for which funnel
|
// AllowFunnel is the set of SNI:port values for which funnel
|
||||||
@ -607,9 +607,34 @@ func (v ServeConfigView) Webs() iter.Seq2[HostPort, WebServerConfigView] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for _, service := range v.Services().All() {
|
||||||
|
for k, v := range service.Web().All() {
|
||||||
|
if !yield(k, v) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FindServiceTCP return the TCPPortHandlerView for the given service name and port.
|
||||||
|
func (v ServeConfigView) FindServiceTCP(svcName string, port uint16) (res TCPPortHandlerView, ok bool) {
|
||||||
|
svcCfg, ok := v.Services().GetOk(svcName)
|
||||||
|
if !ok {
|
||||||
|
return res, ok
|
||||||
|
}
|
||||||
|
return svcCfg.TCP().GetOk(port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v ServeConfigView) FindServiceWeb(hp HostPort) (res WebServerConfigView, ok bool) {
|
||||||
|
for _, service := range v.Services().All() {
|
||||||
|
if res, ok := service.Web().GetOk(hp); ok {
|
||||||
|
return res, ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res, ok
|
||||||
|
}
|
||||||
|
|
||||||
// FindTCP returns the first TCP that matches with the given port. It
|
// FindTCP returns the first TCP that matches with the given port. It
|
||||||
// prefers a foreground match first followed by a background search if none
|
// prefers a foreground match first followed by a background search if none
|
||||||
// existed.
|
// existed.
|
||||||
@ -662,6 +687,17 @@ func (v ServeConfigView) HasFunnelForTarget(target HostPort) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CheckValidServicesConfig reports whether the ServeConfig has
|
||||||
|
// invalid service configurations.
|
||||||
|
func (sc *ServeConfig) CheckValidServicesConfig() error {
|
||||||
|
for svcName, service := range sc.Services {
|
||||||
|
if err := service.checkValidConfig(); err != nil {
|
||||||
|
return fmt.Errorf("invalid service configuration for %q: %w", svcName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ServicePortRange returns the list of tailcfg.ProtoPortRange that represents
|
// ServicePortRange returns the list of tailcfg.ProtoPortRange that represents
|
||||||
// the proto/ports pairs that are being served by the service.
|
// the proto/ports pairs that are being served by the service.
|
||||||
//
|
//
|
||||||
@ -699,3 +735,17 @@ func (v ServiceConfigView) ServicePortRange() []tailcfg.ProtoPortRange {
|
|||||||
}
|
}
|
||||||
return ranges
|
return ranges
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrServiceConfigHasBothTCPAndTun signals that a service
|
||||||
|
// in Tun mode cannot also has TCP or Web handlers set.
|
||||||
|
var ErrServiceConfigHasBothTCPAndTun = errors.New("the VIP Service configuration can not set TUN at the same time as TCP or Web")
|
||||||
|
|
||||||
|
// checkValidConfig checks if the service configuration is valid.
|
||||||
|
// Currently, the only invalid configuration is when the service is in Tun mode
|
||||||
|
// and has TCP or Web handlers.
|
||||||
|
func (v *ServiceConfig) checkValidConfig() error {
|
||||||
|
if v.Tun && (len(v.TCP) > 0 || len(v.Web) > 0) {
|
||||||
|
return ErrServiceConfigHasBothTCPAndTun
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -2997,3 +2997,19 @@ const LBHeader = "Ts-Lb"
|
|||||||
// correspond to those IPs. Any services that don't correspond to a service
|
// correspond to those IPs. Any services that don't correspond to a service
|
||||||
// this client is hosting can be ignored.
|
// this client is hosting can be ignored.
|
||||||
type ServiceIPMappings map[string][]netip.Addr
|
type ServiceIPMappings map[string][]netip.Addr
|
||||||
|
|
||||||
|
// IPServiceMappings maps IP addresses to service names. This is the inverse of
|
||||||
|
// [ServiceIPMappings], and is used to inform clients which services is an VIP
|
||||||
|
// address associated with. This is set to b.ipVIPServiceMap every time the
|
||||||
|
// netmap is updated. This is used to reduce the cost for looking up the service
|
||||||
|
// name for the dst IP address in the netStack packet processing workflow.
|
||||||
|
//
|
||||||
|
// This is of the form:
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// "100.65.32.1": "svc:samba",
|
||||||
|
// "fd7a:115c:a1e0::1234": "svc:samba",
|
||||||
|
// "100.102.42.3": "svc:web",
|
||||||
|
// "fd7a:115c:a1e0::abcd": "svc:web",
|
||||||
|
// }
|
||||||
|
type IPServiceMappings map[netip.Addr]string
|
||||||
|
19
types/netmap/IPServiceMappings.go
Normal file
19
types/netmap/IPServiceMappings.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package netmap
|
||||||
|
|
||||||
|
import "net/netip"
|
||||||
|
|
||||||
|
// IPServiceMappings maps IP addresses to service names. This is the inverse of
|
||||||
|
// [ServiceIPMappings], and is used to inform clients which services is an VIP
|
||||||
|
// address associated with. This is set to b.ipVIPServiceMap every time the
|
||||||
|
// netmap is updated. This is used to reduce the cost for looking up the service
|
||||||
|
// name for the dst IP address in the netStack packet processing workflow.
|
||||||
|
//
|
||||||
|
// This is of the form:
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// "100.65.32.1": "svc:samba",
|
||||||
|
// "fd7a:115c:a1e0::1234": "svc:samba",
|
||||||
|
// "100.102.42.3": "svc:web",
|
||||||
|
// "fd7a:115c:a1e0::abcd": "svc:web",
|
||||||
|
// }
|
||||||
|
type IPServiceMappings map[netip.Addr]string
|
@ -101,6 +101,54 @@ func (nm *NetworkMap) GetAddresses() views.Slice[netip.Prefix] {
|
|||||||
return nm.SelfNode.Addresses()
|
return nm.SelfNode.Addresses()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetVIPServiceIPMap returns a map of service names to the slice of
|
||||||
|
// VIP addresses that correspond to the service. The service names are
|
||||||
|
// with the prefix "svc:".
|
||||||
|
//
|
||||||
|
// TODO(corp##25997): cache the result of decoding the capmap so that
|
||||||
|
// we don't have to decode it multiple times after each netmap update.
|
||||||
|
func (nm *NetworkMap) GetVIPServiceIPMap() tailcfg.ServiceIPMappings {
|
||||||
|
if nm == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !nm.SelfNode.Valid() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ipMaps, err := tailcfg.UnmarshalNodeCapJSON[tailcfg.ServiceIPMappings](nm.SelfNode.CapMap().AsMap(), tailcfg.NodeAttrServiceHost)
|
||||||
|
if len(ipMaps) != 1 || err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ipMaps[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIPVIPServiceMap returns a map of VIP addresses to the service
|
||||||
|
// names that has the VIP address. The service names are with the
|
||||||
|
// prefix "svc:".
|
||||||
|
func (nm *NetworkMap) GetIPVIPServiceMap() IPServiceMappings {
|
||||||
|
var res IPServiceMappings
|
||||||
|
if nm == nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
if !nm.SelfNode.Valid() {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceIPMap := nm.GetVIPServiceIPMap()
|
||||||
|
if serviceIPMap == nil {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
res = make(IPServiceMappings)
|
||||||
|
for svc, addrs := range serviceIPMap {
|
||||||
|
for _, addr := range addrs {
|
||||||
|
res[addr] = svc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
// AnyPeersAdvertiseRoutes reports whether any peer is advertising non-exit node routes.
|
// AnyPeersAdvertiseRoutes reports whether any peer is advertising non-exit node routes.
|
||||||
func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool {
|
func (nm *NetworkMap) AnyPeersAdvertiseRoutes() bool {
|
||||||
for _, p := range nm.Peers {
|
for _, p := range nm.Peers {
|
||||||
|
@ -50,6 +50,7 @@ import (
|
|||||||
"tailscale.com/types/netmap"
|
"tailscale.com/types/netmap"
|
||||||
"tailscale.com/types/nettype"
|
"tailscale.com/types/nettype"
|
||||||
"tailscale.com/util/clientmetric"
|
"tailscale.com/util/clientmetric"
|
||||||
|
"tailscale.com/util/set"
|
||||||
"tailscale.com/version"
|
"tailscale.com/version"
|
||||||
"tailscale.com/wgengine"
|
"tailscale.com/wgengine"
|
||||||
"tailscale.com/wgengine/filter"
|
"tailscale.com/wgengine/filter"
|
||||||
@ -200,6 +201,8 @@ type Impl struct {
|
|||||||
// updates.
|
// updates.
|
||||||
atomicIsLocalIPFunc syncs.AtomicValue[func(netip.Addr) bool]
|
atomicIsLocalIPFunc syncs.AtomicValue[func(netip.Addr) bool]
|
||||||
|
|
||||||
|
atomicIsVIPServiceIPFunc syncs.AtomicValue[func(netip.Addr) bool]
|
||||||
|
|
||||||
// forwardDialFunc, if non-nil, is the net.Dialer.DialContext-style
|
// forwardDialFunc, if non-nil, is the net.Dialer.DialContext-style
|
||||||
// function that is used to make outgoing connections when forwarding a
|
// function that is used to make outgoing connections when forwarding a
|
||||||
// TCP connection to another host (e.g. in subnet router mode).
|
// TCP connection to another host (e.g. in subnet router mode).
|
||||||
@ -387,6 +390,7 @@ func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magi
|
|||||||
}
|
}
|
||||||
ns.ctx, ns.ctxCancel = context.WithCancel(context.Background())
|
ns.ctx, ns.ctxCancel = context.WithCancel(context.Background())
|
||||||
ns.atomicIsLocalIPFunc.Store(ipset.FalseContainsIPFunc())
|
ns.atomicIsLocalIPFunc.Store(ipset.FalseContainsIPFunc())
|
||||||
|
ns.atomicIsVIPServiceIPFunc.Store(ipset.FalseContainsIPFunc())
|
||||||
ns.tundev.PostFilterPacketInboundFromWireGuard = ns.injectInbound
|
ns.tundev.PostFilterPacketInboundFromWireGuard = ns.injectInbound
|
||||||
ns.tundev.PreFilterPacketOutboundToWireGuardNetstackIntercept = ns.handleLocalPackets
|
ns.tundev.PreFilterPacketOutboundToWireGuardNetstackIntercept = ns.handleLocalPackets
|
||||||
stacksForMetrics.Store(ns, struct{}{})
|
stacksForMetrics.Store(ns, struct{}{})
|
||||||
@ -532,7 +536,7 @@ func (ns *Impl) wrapTCPProtocolHandler(h protocolHandlerFunc) protocolHandlerFun
|
|||||||
|
|
||||||
// Dynamically reconfigure ns's subnet addresses as needed for
|
// Dynamically reconfigure ns's subnet addresses as needed for
|
||||||
// outbound traffic.
|
// outbound traffic.
|
||||||
if !ns.isLocalIP(localIP) {
|
if !ns.isLocalIP(localIP) && !ns.isVIPServiceIP(localIP) {
|
||||||
ns.addSubnetAddress(localIP)
|
ns.addSubnetAddress(localIP)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -621,10 +625,17 @@ var v4broadcast = netaddr.IPv4(255, 255, 255, 255)
|
|||||||
func (ns *Impl) UpdateNetstackIPs(nm *netmap.NetworkMap) {
|
func (ns *Impl) UpdateNetstackIPs(nm *netmap.NetworkMap) {
|
||||||
var selfNode tailcfg.NodeView
|
var selfNode tailcfg.NodeView
|
||||||
if nm != nil {
|
if nm != nil {
|
||||||
|
vipServiceIPMap := nm.GetVIPServiceIPMap()
|
||||||
|
serviceAddrSet := set.Set[netip.Addr]{}
|
||||||
|
for _, addrs := range vipServiceIPMap {
|
||||||
|
serviceAddrSet.AddSlice(addrs)
|
||||||
|
}
|
||||||
ns.atomicIsLocalIPFunc.Store(ipset.NewContainsIPFunc(nm.GetAddresses()))
|
ns.atomicIsLocalIPFunc.Store(ipset.NewContainsIPFunc(nm.GetAddresses()))
|
||||||
|
ns.atomicIsVIPServiceIPFunc.Store(serviceAddrSet.Contains)
|
||||||
selfNode = nm.SelfNode
|
selfNode = nm.SelfNode
|
||||||
} else {
|
} else {
|
||||||
ns.atomicIsLocalIPFunc.Store(ipset.FalseContainsIPFunc())
|
ns.atomicIsLocalIPFunc.Store(ipset.FalseContainsIPFunc())
|
||||||
|
ns.atomicIsVIPServiceIPFunc.Store(ipset.FalseContainsIPFunc())
|
||||||
}
|
}
|
||||||
|
|
||||||
oldPfx := make(map[netip.Prefix]bool)
|
oldPfx := make(map[netip.Prefix]bool)
|
||||||
@ -952,6 +963,12 @@ func (ns *Impl) isLocalIP(ip netip.Addr) bool {
|
|||||||
return ns.atomicIsLocalIPFunc.Load()(ip)
|
return ns.atomicIsLocalIPFunc.Load()(ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isVIPServiceIP reports whether ip is an IP address that's
|
||||||
|
// assigned to a VIP service.
|
||||||
|
func (ns *Impl) isVIPServiceIP(ip netip.Addr) bool {
|
||||||
|
return ns.atomicIsVIPServiceIPFunc.Load()(ip)
|
||||||
|
}
|
||||||
|
|
||||||
func (ns *Impl) peerAPIPortAtomic(ip netip.Addr) *atomic.Uint32 {
|
func (ns *Impl) peerAPIPortAtomic(ip netip.Addr) *atomic.Uint32 {
|
||||||
if ip.Is4() {
|
if ip.Is4() {
|
||||||
return &ns.peerapiPort4Atomic
|
return &ns.peerapiPort4Atomic
|
||||||
@ -968,6 +985,7 @@ func (ns *Impl) shouldProcessInbound(p *packet.Parsed, t *tstun.Wrapper) bool {
|
|||||||
// Handle incoming peerapi connections in netstack.
|
// Handle incoming peerapi connections in netstack.
|
||||||
dstIP := p.Dst.Addr()
|
dstIP := p.Dst.Addr()
|
||||||
isLocal := ns.isLocalIP(dstIP)
|
isLocal := ns.isLocalIP(dstIP)
|
||||||
|
isService := ns.isVIPServiceIP(dstIP)
|
||||||
|
|
||||||
// Handle TCP connection to the Tailscale IP(s) in some cases:
|
// Handle TCP connection to the Tailscale IP(s) in some cases:
|
||||||
if ns.lb != nil && p.IPProto == ipproto.TCP && isLocal {
|
if ns.lb != nil && p.IPProto == ipproto.TCP && isLocal {
|
||||||
@ -990,6 +1008,13 @@ func (ns *Impl) shouldProcessInbound(p *packet.Parsed, t *tstun.Wrapper) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ns.lb != nil && p.IPProto == ipproto.TCP && isService {
|
||||||
|
// An assumption holds for this to work: when tun mode is on for a service,
|
||||||
|
// its tcp and web are not set. This is enforced in b.setServeConfigLocked.
|
||||||
|
if ns.lb.ShouldInterceptVIPServiceTCPPort(p.Dst) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
if p.IPVersion == 6 && !isLocal && viaRange.Contains(dstIP) {
|
if p.IPVersion == 6 && !isLocal && viaRange.Contains(dstIP) {
|
||||||
return ns.lb != nil && ns.lb.ShouldHandleViaIP(dstIP)
|
return ns.lb != nil && ns.lb.ShouldHandleViaIP(dstIP)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user