mirror of
https://github.com/tailscale/tailscale.git
synced 2025-10-09 08:01:31 +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
@@ -36,9 +36,6 @@ type Header interface {
|
||||
// purpose of computing length and checksum fields. Marshal
|
||||
// implementations must not allocate memory.
|
||||
Marshal(buf []byte) error
|
||||
// ToResponse transforms the header into one for a response packet.
|
||||
// For instance, this swaps the source and destination IPs.
|
||||
ToResponse()
|
||||
}
|
||||
|
||||
// Generate generates a new packet with the given Header and
|
||||
|
@@ -24,6 +24,17 @@ const (
|
||||
TCP IPProto = 0x06
|
||||
UDP IPProto = 0x11
|
||||
|
||||
// TSMP is the Tailscale Message Protocol (our ICMP-ish
|
||||
// thing), an IP protocol used only between Tailscale nodes
|
||||
// (still encrypted by WireGuard) that communicates why things
|
||||
// failed, etc.
|
||||
//
|
||||
// Proto number 99 is reserved for "any private encryption
|
||||
// scheme". We never accept these from the host OS stack nor
|
||||
// send them to the host network stack. It's only used between
|
||||
// nodes.
|
||||
TSMP IPProto = 99
|
||||
|
||||
// Fragment represents any non-first IP fragment, for which we
|
||||
// don't have the sub-protocol header (and therefore can't
|
||||
// figure out what the sub-protocol is).
|
||||
@@ -47,6 +58,8 @@ func (p IPProto) String() string {
|
||||
return "UDP"
|
||||
case TCP:
|
||||
return "TCP"
|
||||
case TSMP:
|
||||
return "TSMP"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
|
@@ -204,6 +204,10 @@ func (q *Parsed) decode4(b []byte) {
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.dataofs = q.subofs + udpHeaderLength
|
||||
return
|
||||
case TSMP:
|
||||
// Inter-tailscale messages.
|
||||
q.dataofs = q.subofs
|
||||
return
|
||||
default:
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
@@ -291,6 +295,10 @@ func (q *Parsed) decode6(b []byte) {
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.dataofs = q.subofs + udpHeaderLength
|
||||
case TSMP:
|
||||
// Inter-tailscale messages.
|
||||
q.dataofs = q.subofs
|
||||
return
|
||||
default:
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
|
@@ -274,7 +274,38 @@ var igmpPacketDecode = Parsed{
|
||||
Dst: mustIPPort("224.0.0.251:0"),
|
||||
}
|
||||
|
||||
func TestParsed(t *testing.T) {
|
||||
var ipv4TSMPBuffer = []byte{
|
||||
// IPv4 header:
|
||||
0x45, 0x00,
|
||||
0x00, 0x1b, // 20 + 7 bytes total
|
||||
0x00, 0x00, // ID
|
||||
0x00, 0x00, // Fragment
|
||||
0x40, // TTL
|
||||
byte(TSMP),
|
||||
0x5f, 0xc3, // header checksum (wrong here)
|
||||
// source IP:
|
||||
0x64, 0x5e, 0x0c, 0x0e,
|
||||
// dest IP:
|
||||
0x64, 0x4a, 0x46, 0x03,
|
||||
byte(TSMPTypeRejectedConn),
|
||||
byte(TCP),
|
||||
byte(RejectedDueToACLs),
|
||||
0x00, 123, // src port
|
||||
0x00, 80, // dst port
|
||||
}
|
||||
|
||||
var ipv4TSMPDecode = Parsed{
|
||||
b: ipv4TSMPBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 20,
|
||||
length: 27,
|
||||
IPVersion: 4,
|
||||
IPProto: TSMP,
|
||||
Src: mustIPPort("100.94.12.14:0"),
|
||||
Dst: mustIPPort("100.74.70.3:0"),
|
||||
}
|
||||
|
||||
func TestParsedString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
qdecode Parsed
|
||||
@@ -288,6 +319,7 @@ func TestParsed(t *testing.T) {
|
||||
{"icmp6", icmp6PacketDecode, "ICMPv6{[fe80::fb57:1dea:9c39:8fb7]:0 > [ff02::2]:0}"},
|
||||
{"igmp", igmpPacketDecode, "IGMP{192.168.1.82:0 > 224.0.0.251:0}"},
|
||||
{"unknown", unknownPacketDecode, "Unknown{???}"},
|
||||
{"ipv4_tsmp", ipv4TSMPDecode, "TSMP{100.94.12.14:0 > 100.74.70.3:0}"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -324,6 +356,7 @@ func TestDecode(t *testing.T) {
|
||||
{"igmp", igmpPacketBuffer, igmpPacketDecode},
|
||||
{"unknown", unknownPacketBuffer, unknownPacketDecode},
|
||||
{"invalid4", invalid4RequestBuffer, invalid4RequestDecode},
|
||||
{"ipv4_tsmp", ipv4TSMPBuffer, ipv4TSMPDecode},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -331,7 +364,7 @@ func TestDecode(t *testing.T) {
|
||||
var got Parsed
|
||||
got.Decode(tt.buf)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("mismatch\n got: %#v\nwant: %#v", got, tt.want)
|
||||
t.Errorf("mismatch\n got: %s %#v\nwant: %s %#v", got.String(), got, tt.want.String(), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -416,9 +449,16 @@ func TestMarshalResponse(t *testing.T) {
|
||||
icmpHeader := icmp4RequestDecode.ICMP4Header()
|
||||
udpHeader := udp4RequestDecode.UDP4Header()
|
||||
|
||||
type HeaderToResponser interface {
|
||||
Header
|
||||
// ToResponse transforms the header into one for a response packet.
|
||||
// For instance, this swaps the source and destination IPs.
|
||||
ToResponse()
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
header Header
|
||||
header HeaderToResponser
|
||||
want []byte
|
||||
}{
|
||||
{"icmp", &icmpHeader, icmp4ReplyBuffer},
|
||||
|
140
net/packet/tsmp.go
Normal file
140
net/packet/tsmp.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// TSMP is our ICMP-like "Tailscale Message Protocol" for signaling
|
||||
// Tailscale-specific messages between nodes. It uses IP protocol 99
|
||||
// (reserved for "any private encryption scheme") within
|
||||
// Wireguard's normal encryption between peers and never hits the host
|
||||
// network stack.
|
||||
|
||||
package packet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/flowtrack"
|
||||
)
|
||||
|
||||
// TailscaleRejectedHeader is a TSMP message that says that one
|
||||
// Tailscale node has rejected the connection from another. Unlike a
|
||||
// TCP RST, this includes a reason.
|
||||
//
|
||||
// On the wire, after the IP header, it's currently 7 bytes:
|
||||
// * '!'
|
||||
// * IPProto byte (IANA protocol number: TCP or UDP)
|
||||
// * 'A' or 'S' (RejectedDueToACLs, RejectedDueToShieldsUp)
|
||||
// * srcPort big endian uint16
|
||||
// * dstPort big endian uint16
|
||||
//
|
||||
// In the future it might also accept 16 byte IP flow src/dst IPs
|
||||
// after the header, if they're different than the IP-level ones.
|
||||
type TailscaleRejectedHeader struct {
|
||||
IPSrc netaddr.IP // IPv4 or IPv6 header's src IP
|
||||
IPDst netaddr.IP // IPv4 or IPv6 header's dst IP
|
||||
Src netaddr.IPPort // rejected flow's src
|
||||
Dst netaddr.IPPort // rejected flow's dst
|
||||
Proto IPProto // proto that was rejected (TCP or UDP)
|
||||
Reason TailscaleRejectReason // why the connection was rejected
|
||||
}
|
||||
|
||||
func (rh TailscaleRejectedHeader) Flow() flowtrack.Tuple {
|
||||
return flowtrack.Tuple{Src: rh.Src, Dst: rh.Dst}
|
||||
}
|
||||
|
||||
func (rh TailscaleRejectedHeader) String() string {
|
||||
return fmt.Sprintf("TSMP-reject-flow{%s %s > %s}: %s", rh.Proto, rh.Src, rh.Dst, rh.Reason)
|
||||
}
|
||||
|
||||
type TSMPType uint8
|
||||
|
||||
const (
|
||||
TSMPTypeRejectedConn TSMPType = '!'
|
||||
)
|
||||
|
||||
type TailscaleRejectReason byte
|
||||
|
||||
const (
|
||||
RejectedDueToACLs TailscaleRejectReason = 'A'
|
||||
RejectedDueToShieldsUp TailscaleRejectReason = 'S'
|
||||
)
|
||||
|
||||
func (r TailscaleRejectReason) String() string {
|
||||
switch r {
|
||||
case RejectedDueToACLs:
|
||||
return "acl"
|
||||
case RejectedDueToShieldsUp:
|
||||
return "shields"
|
||||
}
|
||||
return fmt.Sprintf("0x%02x", byte(r))
|
||||
}
|
||||
|
||||
func (h TailscaleRejectedHeader) Len() int {
|
||||
var ipHeaderLen int
|
||||
if h.IPSrc.Is4() {
|
||||
ipHeaderLen = ip4HeaderLength
|
||||
} else if h.IPSrc.Is6() {
|
||||
ipHeaderLen = ip6HeaderLength
|
||||
}
|
||||
return ipHeaderLen +
|
||||
1 + // TSMPType byte
|
||||
1 + // IPProto byte
|
||||
1 + // TailscaleRejectReason byte
|
||||
2*2 // 2 uint16 ports
|
||||
}
|
||||
|
||||
func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
if h.Src.IP.Is4() {
|
||||
iph := IP4Header{
|
||||
IPProto: TSMP,
|
||||
Src: h.IPSrc,
|
||||
Dst: h.IPDst,
|
||||
}
|
||||
iph.Marshal(buf)
|
||||
buf = buf[ip4HeaderLength:]
|
||||
} else if h.Src.IP.Is6() {
|
||||
iph := IP6Header{
|
||||
IPProto: TSMP,
|
||||
Src: h.IPSrc,
|
||||
Dst: h.IPDst,
|
||||
}
|
||||
iph.Marshal(buf)
|
||||
buf = buf[ip6HeaderLength:]
|
||||
} else {
|
||||
return errors.New("bogus src IP")
|
||||
}
|
||||
buf[0] = byte(TSMPTypeRejectedConn)
|
||||
buf[1] = byte(h.Proto)
|
||||
buf[2] = byte(h.Reason)
|
||||
binary.BigEndian.PutUint16(buf[3:5], h.Src.Port)
|
||||
binary.BigEndian.PutUint16(buf[5:7], h.Dst.Port)
|
||||
return nil
|
||||
}
|
||||
|
||||
// AsTailscaleRejectedHeader parses pp as an incoming rejection
|
||||
// connection TSMP message.
|
||||
//
|
||||
// ok reports whether pp was a valid TSMP rejection packet.
|
||||
func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok bool) {
|
||||
p := pp.Payload()
|
||||
if len(p) < 7 || p[0] != byte(TSMPTypeRejectedConn) {
|
||||
return
|
||||
}
|
||||
return TailscaleRejectedHeader{
|
||||
Proto: IPProto(p[1]),
|
||||
Reason: TailscaleRejectReason(p[2]),
|
||||
IPSrc: pp.Src.IP,
|
||||
IPDst: pp.Dst.IP,
|
||||
Src: netaddr.IPPort{IP: pp.Dst.IP, Port: binary.BigEndian.Uint16(p[3:5])},
|
||||
Dst: netaddr.IPPort{IP: pp.Src.IP, Port: binary.BigEndian.Uint16(p[5:7])},
|
||||
}, true
|
||||
}
|
63
net/packet/tsmp_test.go
Normal file
63
net/packet/tsmp_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package packet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func TestTailscaleRejectedHeader(t *testing.T) {
|
||||
tests := []struct {
|
||||
h TailscaleRejectedHeader
|
||||
wantStr string
|
||||
}{
|
||||
{
|
||||
h: TailscaleRejectedHeader{
|
||||
IPSrc: netaddr.MustParseIP("5.5.5.5"),
|
||||
IPDst: netaddr.MustParseIP("1.2.3.4"),
|
||||
Src: netaddr.MustParseIPPort("1.2.3.4:567"),
|
||||
Dst: netaddr.MustParseIPPort("5.5.5.5:443"),
|
||||
Proto: TCP,
|
||||
Reason: RejectedDueToACLs,
|
||||
},
|
||||
wantStr: "TSMP-reject-flow{TCP 1.2.3.4:567 > 5.5.5.5:443}: acl",
|
||||
},
|
||||
{
|
||||
h: TailscaleRejectedHeader{
|
||||
IPSrc: netaddr.MustParseIP("2::2"),
|
||||
IPDst: netaddr.MustParseIP("1::1"),
|
||||
Src: netaddr.MustParseIPPort("[1::1]:567"),
|
||||
Dst: netaddr.MustParseIPPort("[2::2]:443"),
|
||||
Proto: UDP,
|
||||
Reason: RejectedDueToShieldsUp,
|
||||
},
|
||||
wantStr: "TSMP-reject-flow{UDP [1::1]:567 > [2::2]:443}: shields",
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
gotStr := tt.h.String()
|
||||
if gotStr != tt.wantStr {
|
||||
t.Errorf("%v. String = %q; want %q", i, gotStr, tt.wantStr)
|
||||
continue
|
||||
}
|
||||
pkt := make([]byte, tt.h.Len())
|
||||
tt.h.Marshal(pkt)
|
||||
|
||||
var p Parsed
|
||||
p.Decode(pkt)
|
||||
t.Logf("Parsed: %+v", p)
|
||||
t.Logf("Parsed: %s", p.String())
|
||||
back, ok := p.AsTailscaleRejectedHeader()
|
||||
if !ok {
|
||||
t.Errorf("%v. %q (%02x) didn't parse back", i, gotStr, pkt)
|
||||
continue
|
||||
}
|
||||
if back != tt.h {
|
||||
t.Errorf("%v. %q parsed back as %q", i, tt.h, back)
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user