mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-13 22:47:30 +00:00
net/packet, wgengine, tstun: add inter-node TSMP protocol for connect errors
This adds a new IP Protocol type, TSMP on protocol number 99 for sending inter-tailscale messages over WireGuard, currently just for why a peer rejects TCP SYNs (ACL rejection, shields up, and in the future: nothing listening, something listening on that port but wrong interface, etc) Updates #1094 Updates tailscale/corp#1185 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:

committed by
Brad Fitzpatrick

parent
01e8b7fb7e
commit
b560386c1a
@@ -39,6 +39,8 @@ type Filter struct {
|
||||
// to an outbound connection that this node made, even if those
|
||||
// incoming packets don't get accepted by matches above.
|
||||
state *filterState
|
||||
|
||||
shieldsUp bool
|
||||
}
|
||||
|
||||
// filterState is a state cache of past seen packets.
|
||||
@@ -54,15 +56,18 @@ const lruMax = 512
|
||||
type Response int
|
||||
|
||||
const (
|
||||
Drop Response = iota // do not continue processing packet.
|
||||
Accept // continue processing packet.
|
||||
noVerdict // no verdict yet, continue running filter
|
||||
Drop Response = iota // do not continue processing packet.
|
||||
DropSilently // do not continue processing packet, but also don't log
|
||||
Accept // continue processing packet.
|
||||
noVerdict // no verdict yet, continue running filter
|
||||
)
|
||||
|
||||
func (r Response) String() string {
|
||||
switch r {
|
||||
case Drop:
|
||||
return "Drop"
|
||||
case DropSilently:
|
||||
return "DropSilently"
|
||||
case Accept:
|
||||
return "Accept"
|
||||
case noVerdict:
|
||||
@@ -72,6 +77,10 @@ func (r Response) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
func (r Response) IsDrop() bool {
|
||||
return r == Drop || r == DropSilently
|
||||
}
|
||||
|
||||
// RunFlags controls the filter's debug log verbosity at runtime.
|
||||
type RunFlags int
|
||||
|
||||
@@ -123,6 +132,12 @@ func NewAllowNone(logf logger.Logf) *Filter {
|
||||
return New(nil, nil, nil, logf)
|
||||
}
|
||||
|
||||
func NewShieldsUpFilter(logf logger.Logf) *Filter {
|
||||
f := New(nil, nil, nil, logf)
|
||||
f.shieldsUp = true
|
||||
return f
|
||||
}
|
||||
|
||||
// New creates a new packet filter. The filter enforces that incoming
|
||||
// packets must be destined to an IP in localNets, and must be allowed
|
||||
// by matches. If shareStateWith is non-nil, the returned filter
|
||||
@@ -253,6 +268,10 @@ func (f *Filter) CheckTCP(srcIP, dstIP netaddr.IP, dstPort uint16) Response {
|
||||
return f.RunIn(pkt, 0)
|
||||
}
|
||||
|
||||
// ShieldsUp reports whether this is a "shields up" (block everything
|
||||
// incoming) filter.
|
||||
func (f *Filter) ShieldsUp() bool { return f.shieldsUp }
|
||||
|
||||
// RunIn determines whether this node is allowed to receive q from a
|
||||
// Tailscale peer.
|
||||
func (f *Filter) RunIn(q *packet.Parsed, rf RunFlags) Response {
|
||||
@@ -339,6 +358,8 @@ func (f *Filter) runIn4(q *packet.Parsed) (r Response, why string) {
|
||||
if f.matches4.match(q) {
|
||||
return Accept, "udp ok"
|
||||
}
|
||||
case packet.TSMP:
|
||||
return Accept, "tsmp ok"
|
||||
default:
|
||||
return Drop, "Unknown proto"
|
||||
}
|
||||
|
@@ -32,35 +32,47 @@ type pendingOpenFlow struct {
|
||||
timer *time.Timer // until giving up on the flow
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) removeFlow(f flowtrack.Tuple) (removed bool) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
of, ok := e.pendOpen[f]
|
||||
if !ok {
|
||||
// Not a tracked flow (likely already removed)
|
||||
return false
|
||||
}
|
||||
of.timer.Stop()
|
||||
delete(e.pendOpen, f)
|
||||
return true
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) trackOpenPreFilterIn(pp *packet.Parsed, t *tstun.TUN) (res filter.Response) {
|
||||
res = filter.Accept // always
|
||||
|
||||
if pp.IPProto == packet.TSMP {
|
||||
res = filter.DropSilently
|
||||
rh, ok := pp.AsTailscaleRejectedHeader()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if f := rh.Flow(); e.removeFlow(f) {
|
||||
e.logf("open-conn-track: flow %v %v > %v rejected due to %v", rh.Proto, rh.Src, rh.Dst, rh.Reason)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if pp.IPVersion == 0 ||
|
||||
pp.IPProto != packet.TCP ||
|
||||
pp.TCPFlags&(packet.TCPSyn|packet.TCPRst) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
flow := flowtrack.Tuple{Dst: pp.Src, Src: pp.Dst} // src/dst reversed
|
||||
// Either a SYN or a RST came back. Remove it in either case.
|
||||
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
of, ok := e.pendOpen[flow]
|
||||
if !ok {
|
||||
// Not a tracked flow.
|
||||
return
|
||||
f := flowtrack.Tuple{Dst: pp.Src, Src: pp.Dst} // src/dst reversed
|
||||
removed := e.removeFlow(f)
|
||||
if removed && pp.TCPFlags&packet.TCPRst != 0 {
|
||||
e.logf("open-conn-track: flow TCP %v got RST by peer", f)
|
||||
}
|
||||
of.timer.Stop()
|
||||
delete(e.pendOpen, flow)
|
||||
|
||||
if pp.TCPFlags&packet.TCPRst != 0 {
|
||||
// TODO(bradfitz): have peer send a IP proto 99 "why"
|
||||
// packet first with details and log that instead
|
||||
// (e.g. ACL prohibited, shields up, etc).
|
||||
e.logf("open-conn-track: flow %v got RST by peer", flow)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
@@ -218,8 +218,8 @@ func (t *TUN) poll() {
|
||||
func (t *TUN) filterOut(p *packet.Parsed) filter.Response {
|
||||
|
||||
if t.PreFilterOut != nil {
|
||||
if t.PreFilterOut(p, t) == filter.Drop {
|
||||
return filter.Drop
|
||||
if res := t.PreFilterOut(p, t); res.IsDrop() {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,8 +234,8 @@ func (t *TUN) filterOut(p *packet.Parsed) filter.Response {
|
||||
}
|
||||
|
||||
if t.PostFilterOut != nil {
|
||||
if t.PostFilterOut(p, t) == filter.Drop {
|
||||
return filter.Drop
|
||||
if res := t.PostFilterOut(p, t); res.IsDrop() {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,12 +264,12 @@ func (t *TUN) Read(buf []byte, offset int) (int, error) {
|
||||
return 0, io.EOF
|
||||
case err := <-t.errors:
|
||||
return 0, err
|
||||
case packet := <-t.outbound:
|
||||
n = copy(buf[offset:], packet)
|
||||
case pkt := <-t.outbound:
|
||||
n = copy(buf[offset:], pkt)
|
||||
// t.buffer has a fixed location in memory,
|
||||
// so this is the easiest way to tell when it has been consumed.
|
||||
// &packet[0] can be used because empty packets do not reach t.outbound.
|
||||
if &packet[0] == &t.buffer[PacketStartOffset] {
|
||||
// &pkt[0] can be used because empty packets do not reach t.outbound.
|
||||
if &pkt[0] == &t.buffer[PacketStartOffset] {
|
||||
t.bufferConsumed <- struct{}{}
|
||||
} else {
|
||||
// If the packet is not from t.buffer, then it is an injected packet.
|
||||
@@ -307,8 +307,8 @@ func (t *TUN) filterIn(buf []byte) filter.Response {
|
||||
p.Decode(buf)
|
||||
|
||||
if t.PreFilterIn != nil {
|
||||
if t.PreFilterIn(p, t) == filter.Drop {
|
||||
return filter.Drop
|
||||
if res := t.PreFilterIn(p, t); res.IsDrop() {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,6 +319,29 @@ func (t *TUN) filterIn(buf []byte) filter.Response {
|
||||
}
|
||||
|
||||
if filt.RunIn(p, t.filterFlags) != filter.Accept {
|
||||
|
||||
// Tell them, via TSMP, we're dropping them due to the ACL.
|
||||
// Their host networking stack can translate this into ICMP
|
||||
// or whatnot as required. But notably, their GUI or tailscale CLI
|
||||
// can show them a rejection history with reasons.
|
||||
if p.IPVersion == 4 && p.IPProto == packet.TCP && p.TCPFlags&packet.TCPSyn != 0 {
|
||||
rj := packet.TailscaleRejectedHeader{
|
||||
IPSrc: p.Dst.IP,
|
||||
IPDst: p.Src.IP,
|
||||
Src: p.Src,
|
||||
Dst: p.Dst,
|
||||
Proto: p.IPProto,
|
||||
Reason: packet.RejectedDueToACLs,
|
||||
}
|
||||
if filt.ShieldsUp() {
|
||||
rj.Reason = packet.RejectedDueToShieldsUp
|
||||
}
|
||||
pkt := packet.Generate(rj, nil)
|
||||
t.InjectOutbound(pkt)
|
||||
|
||||
// TODO(bradfitz): also send a TCP RST, after the TSMP message.
|
||||
}
|
||||
|
||||
return filter.Drop
|
||||
}
|
||||
|
||||
@@ -331,10 +354,15 @@ func (t *TUN) filterIn(buf []byte) filter.Response {
|
||||
return filter.Accept
|
||||
}
|
||||
|
||||
// Write accepts an incoming packet. The packet begins at buf[offset:],
|
||||
// like wireguard-go/tun.Device.Write.
|
||||
func (t *TUN) Write(buf []byte, offset int) (int, error) {
|
||||
if !t.disableFilter {
|
||||
response := t.filterIn(buf[offset:])
|
||||
if response != filter.Accept {
|
||||
res := t.filterIn(buf[offset:])
|
||||
if res == filter.DropSilently {
|
||||
return len(buf), nil
|
||||
}
|
||||
if res != filter.Accept {
|
||||
return 0, ErrFiltered
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user