ipn/ipnlocal, tailcfg: wire up ingress peerapi

Updates tailscale/corp#7515

Co-authored-by: Shayne Sweeney <shayne@tailscale.com>
Change-Id: I7eac7b4ac37fd8e8a9e0469594c1e9e7dd0da666
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
pull/6267/head
Brad Fitzpatrick 2 years ago committed by Brad Fitzpatrick
parent 65f3dab4c6
commit 9dfb0916c2

@ -76,13 +76,20 @@ func (src *ServeConfig) Clone() *ServeConfig {
dst.Web[k] = v.Clone()
}
}
if dst.AllowIngress != nil {
dst.AllowIngress = map[HostPort]bool{}
for k, v := range src.AllowIngress {
dst.AllowIngress[k] = v
}
}
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ServeConfigCloneNeedsRegeneration = ServeConfig(struct {
TCP map[int]*TCPPortHandler
Web map[HostPort]*WebServerConfig
TCP map[int]*TCPPortHandler
Web map[HostPort]*WebServerConfig
AllowIngress map[HostPort]bool
}{})
// Clone makes a deep copy of TCPPortHandler.

@ -176,10 +176,15 @@ func (v ServeConfigView) Web() views.MapFn[HostPort, *WebServerConfig, WebServer
})
}
func (v ServeConfigView) AllowIngress() views.Map[HostPort, bool] {
return views.MapOf(v.ж.AllowIngress)
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ServeConfigViewNeedsRegeneration = ServeConfig(struct {
TCP map[int]*TCPPortHandler
Web map[HostPort]*WebServerConfig
TCP map[int]*TCPPortHandler
Web map[HostPort]*WebServerConfig
AllowIngress map[HostPort]bool
}{})
// View returns a readonly view of TCPPortHandler.

@ -34,6 +34,7 @@ import (
"github.com/kortschak/wol"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
@ -572,6 +573,9 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "/v0/interfaces":
h.handleServeInterfaces(w, r)
return
case "/v0/ingress":
h.handleServeIngress(w, r)
return
}
who := h.peerUser.DisplayName
fmt.Fprintf(w, `<html>
@ -586,6 +590,63 @@ 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, http.StatusMethodNotAllowed)
}
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 := r.Header.Get("Tailscale-Ingress-Target")
if target == "" {
bad("Tailscale-Target-Target header not set")
return
}
if _, _, err := net.SplitHostPort(target); err != nil {
bad("Tailscale-Target-Target header invalid; want host:port")
return
}
getConn := 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 conn, true
}
sendRST := func() {
http.Error(w, "denied", http.StatusForbidden)
}
h.ps.b.HandleIngressTCPConn(h.peerNode, ipn.HostPort(target), srcAddr, getConn, sendRST)
}
func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Request) {
if !h.canDebug() {
http.Error(w, "denied; no debug access", http.StatusForbidden)
@ -694,6 +755,13 @@ func (h *peerAPIHandler) canWakeOnLAN() bool {
return h.isSelf || h.peerHasCap(tailcfg.CapabilityWakeOnLAN)
}
var allowSelfIngress = envknob.RegisterBool("TS_ALLOW_SELF_INGRESS")
// canIngress reports whether h can send ingress requests to this node.
func (h *peerAPIHandler) canIngress() bool {
return h.peerHasCap(tailcfg.CapabilityIngress) || (allowSelfIngress() && h.isSelf)
}
func (h *peerAPIHandler) peerHasCap(wantCap string) bool {
for _, hasCap := range h.ps.b.PeerCaps(h.remoteAddr.Addr()) {
if hasCap == wantCap {

@ -16,11 +16,13 @@ import (
"net/netip"
"net/url"
pathpkg "path"
"strconv"
"strings"
"time"
"tailscale.com/ipn"
"tailscale.com/net/netutil"
"tailscale.com/tailcfg"
)
// serveHTTPContextKey is the context.Value key for a *serveHTTPContext.
@ -31,6 +33,40 @@ type serveHTTPContext struct {
DestPort uint16
}
func (b *LocalBackend) HandleIngressTCPConn(ingressPeer *tailcfg.Node, target ipn.HostPort, srcAddr netip.AddrPort, getConn func() (net.Conn, bool), sendRST func()) {
b.mu.Lock()
sc := b.serveConfig
b.mu.Unlock()
if !sc.Valid() {
b.logf("localbackend: got ingress conn w/o serveConfig; rejecting")
sendRST()
return
}
if !sc.AllowIngress().Get(target) {
b.logf("localbackend: got ingress conn for unconfigured %q; rejecting", target)
sendRST()
return
}
_, port, err := net.SplitHostPort(string(target))
if err != nil {
b.logf("localbackend: got ingress conn for bad target %q; rejecting", target)
sendRST()
return
}
port16, err := strconv.ParseUint(port, 10, 16)
if err != nil {
b.logf("localbackend: got ingress conn for bad target %q; rejecting", target)
sendRST()
return
}
// TODO(bradfitz): pass ingressPeer etc in context to HandleInterceptedTCPConn,
// extend serveHTTPContext or similar.
b.HandleInterceptedTCPConn(uint16(port16), srcAddr, getConn, sendRST)
}
func (b *LocalBackend) HandleInterceptedTCPConn(dport uint16, srcAddr netip.AddrPort, getConn func() (net.Conn, bool), sendRST func()) {
b.mu.Lock()
sc := b.serveConfig

@ -81,6 +81,10 @@ type ServeConfig struct {
// Web maps from "$SNI_NAME:$PORT" to a set of HTTP handlers
// keyed by mount point ("/", "/foo", etc)
Web map[HostPort]*WebServerConfig `json:",omitempty"`
// AllowIngress is the set of SNI:port values for which ingress
// traffic is allowed, from trusted ingress peers.
AllowIngress map[HostPort]bool
}
// HostPort is an SNI name and port number, joined by a colon.

@ -1672,6 +1672,8 @@ const (
CapabilityDebugPeer = "https://tailscale.com/cap/debug-peer"
// CapabilityWakeOnLAN grants the ability to send a Wake-On-LAN packet.
CapabilityWakeOnLAN = "https://tailscale.com/cap/wake-on-lan"
// CapabilityIngress grants the ability for a peer to send ingress traffic.
CapabilityIngress = "https://tailscale.com/cap/ingress"
)
// SetDNSRequest is a request to add a DNS record.

Loading…
Cancel
Save