tailcfg,wgengine: add initial support for WireGuard only peers

A peer can have IsWireGuardOnly, which means it will not support DERP or
Disco, and it must have Endpoints filled in order to be usable.

In the present implementation only the first Endpoint will be used as
the bestAddr.

Updates tailscale/corp#10351

Co-authored-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
Co-authored-by: James Tucker <james@tailscale.com>
Signed-off-by: James Tucker <james@tailscale.com>
This commit is contained in:
Charlotte Brandhorst-Satzkorn
2023-04-05 17:28:28 -07:00
committed by James Tucker
parent 6cfcb3cae4
commit c573bef0aa
8 changed files with 254 additions and 36 deletions

View File

@@ -208,6 +208,12 @@ func (m *peerMap) upsertEndpoint(ep *endpoint, oldDiscoKey key.DiscoPublic) {
delete(m.nodesOfDisco[oldDiscoKey], ep.publicKey)
}
if epDisco == nil {
// If the peer does not support Disco, but it does have an endpoint address,
// attempt to use that (e.g. WireGuardOnly peers).
if ep.bestAddr.AddrPort.IsValid() {
m.setNodeKeyForIPPort(ep.bestAddr.AddrPort, ep.publicKey)
}
return
}
set := m.nodesOfDisco[epDisco.key]
@@ -2732,8 +2738,14 @@ func (c *Conn) SetNetworkMap(nm *netmap.NetworkMap) {
// handle full set updates.
for _, n := range nm.Peers {
if ep, ok := c.peerMap.endpointForNodeKey(n.Key); ok {
if n.DiscoKey.IsZero() {
// Discokey transitioned from non-zero to zero? Ignore. Server's confused.
if n.DiscoKey.IsZero() && !n.IsWireGuardOnly {
// Discokey transitioned from non-zero to zero? This should not
// happen in the wild, however it could mean:
// 1. A node was downgraded from post 0.100 to pre 0.100.
// 2. A Tailscale node key was extracted and used on a
// non-Tailscale node (should not enter here due to the
// IsWireGuardOnly check)
// 3. The server is misbehaving.
c.peerMap.deleteEndpoint(ep)
continue
}
@@ -2745,9 +2757,8 @@ func (c *Conn) SetNetworkMap(nm *netmap.NetworkMap) {
c.peerMap.upsertEndpoint(ep, oldDiscoKey) // maybe update discokey mappings in peerMap
continue
}
if n.DiscoKey.IsZero() {
// Ancient pre-0.100 node. Ignore, so we can assume elsewhere in magicsock
// that all nodes have a DiscoKey.
if n.DiscoKey.IsZero() && !n.IsWireGuardOnly {
// Ancient pre-0.100 node, which does not have a disco key, and will only be reachable via DERP.
continue
}
@@ -2763,35 +2774,40 @@ func (c *Conn) SetNetworkMap(nm *netmap.NetworkMap) {
if len(n.Addresses) > 0 {
ep.nodeAddr = n.Addresses[0].Addr()
}
ep.disco.Store(&endpointDisco{
key: n.DiscoKey,
short: n.DiscoKey.ShortString(),
})
ep.initFakeUDPAddr()
if debugDisco() { // rather than making a new knob
c.logf("magicsock: created endpoint key=%s: disco=%s; %v", n.Key.ShortString(), n.DiscoKey.ShortString(), logger.ArgWriter(func(w *bufio.Writer) {
const derpPrefix = "127.3.3.40:"
if strings.HasPrefix(n.DERP, derpPrefix) {
ipp, _ := netip.ParseAddrPort(n.DERP)
regionID := int(ipp.Port())
code := c.derpRegionCodeLocked(regionID)
if code != "" {
code = "(" + code + ")"
}
fmt.Fprintf(w, "derp=%v%s ", regionID, code)
}
if n.DiscoKey.IsZero() {
ep.disco.Store(nil)
} else {
ep.disco.Store(&endpointDisco{
key: n.DiscoKey,
short: n.DiscoKey.ShortString(),
})
for _, a := range n.AllowedIPs {
if a.IsSingleIP() {
fmt.Fprintf(w, "aip=%v ", a.Addr())
} else {
fmt.Fprintf(w, "aip=%v ", a)
if debugDisco() { // rather than making a new knob
c.logf("magicsock: created endpoint key=%s: disco=%s; %v", n.Key.ShortString(), n.DiscoKey.ShortString(), logger.ArgWriter(func(w *bufio.Writer) {
const derpPrefix = "127.3.3.40:"
if strings.HasPrefix(n.DERP, derpPrefix) {
ipp, _ := netip.ParseAddrPort(n.DERP)
regionID := int(ipp.Port())
code := c.derpRegionCodeLocked(regionID)
if code != "" {
code = "(" + code + ")"
}
fmt.Fprintf(w, "derp=%v%s ", regionID, code)
}
}
for _, ep := range n.Endpoints {
fmt.Fprintf(w, "ep=%v ", ep)
}
}))
for _, a := range n.AllowedIPs {
if a.IsSingleIP() {
fmt.Fprintf(w, "aip=%v ", a.Addr())
} else {
fmt.Fprintf(w, "aip=%v ", a)
}
}
for _, ep := range n.Endpoints {
fmt.Fprintf(w, "ep=%v ", ep)
}
}))
}
}
ep.updateFromNode(n, heartbeatDisabled)
c.peerMap.upsertEndpoint(ep, key.DiscoPublic{})
@@ -4627,6 +4643,22 @@ func (de *endpoint) updateFromNode(n *tailcfg.Node, heartbeatDisabled bool) {
de.heartbeatDisabled = heartbeatDisabled
de.expired = n.Expired
// TODO(#7826): add support for more than one endpoint for pure WireGuard
// peers, and/or support for probing "bestness" for endpoints.
if n.IsWireGuardOnly {
for _, ep := range n.Endpoints {
ipp, err := netip.ParseAddrPort(ep)
if err != nil {
de.c.logf("magicsock: invalid endpoint: %s %s", ep, err)
continue
}
de.bestAddr = addrLatency{
AddrPort: ipp,
}
break
}
}
epDisco := de.disco.Load()
var discoKey key.DiscoPublic
if epDisco != nil {

View File

@@ -39,6 +39,7 @@ import (
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/connstats"
"tailscale.com/net/netaddr"
"tailscale.com/net/packet"
"tailscale.com/net/stun/stuntest"
"tailscale.com/net/tstun"
"tailscale.com/tailcfg"
@@ -200,6 +201,7 @@ func newMagicStackWithKey(t testing.TB, logf logger.Logf, l nettype.PacketListen
}
func (s *magicStack) Reconfig(cfg *wgcfg.Config) error {
s.tsTun.SetWGConfig(cfg)
s.wgLogger.SetPeers(cfg.Peers)
return wgcfg.ReconfigDevice(s.dev, cfg, s.conn.logf)
}
@@ -2102,3 +2104,175 @@ func Test_batchingUDPConn_coalesceMessages(t *testing.T) {
})
}
}
// newWireguard starts up a new wireguard-go device attached to a test tun, and
// returns the device, tun and netpoint address. To add peers call device.IpcSet
// with UAPI instructions.
func newWireguard(t *testing.T, uapi string, aips []netip.Prefix) (*device.Device, *tuntest.ChannelTUN, netip.AddrPort) {
wgtun := tuntest.NewChannelTUN()
wglogf := func(f string, args ...any) {
t.Logf("wg-go: "+f, args...)
}
wglog := device.Logger{
Verbosef: func(string, ...any) {},
Errorf: wglogf,
}
wgdev := wgcfg.NewDevice(wgtun.TUN(), wgconn.NewDefaultBind(), &wglog)
if err := wgdev.IpcSet(uapi); err != nil {
t.Fatal(err)
}
if err := wgdev.Up(); err != nil {
t.Fatal(err)
}
var wgEp netip.AddrPort
s, err := wgdev.IpcGet()
if err != nil {
t.Fatal(err)
}
for _, line := range strings.Split(s, "\n") {
line = strings.TrimSpace(line)
if len(line) == 0 {
continue
}
k, v, _ := strings.Cut(line, "=")
if k == "listen_port" {
wgEp = netip.MustParseAddrPort("127.0.0.1:" + v)
break
}
}
if !wgEp.IsValid() {
t.Fatalf("failed to get endpoint out of wg-go")
}
t.Logf("wg-go endpoint: %s", wgEp)
return wgdev, wgtun, wgEp
}
func TestIsWireGuardOnlyPeer(t *testing.T) {
derpMap, cleanup := runDERPAndStun(t, t.Logf, localhostListener{}, netaddr.IPv4(127, 0, 0, 1))
defer cleanup()
tskey := key.NewNode()
tsaip := netip.MustParsePrefix("100.111.222.111/32")
wgkey := key.NewNode()
wgaip := netip.MustParsePrefix("100.222.111.222/32")
uapi := fmt.Sprintf("private_key=%s\npublic_key=%s\nallowed_ip=%s\n\n",
wgkey.UntypedHexString(), tskey.Public().UntypedHexString(), tsaip.String())
wgdev, wgtun, wgEp := newWireguard(t, uapi, []netip.Prefix{wgaip})
defer wgdev.Close()
m := newMagicStackWithKey(t, t.Logf, localhostListener{}, derpMap, tskey)
defer m.Close()
nm := &netmap.NetworkMap{
Name: "ts",
PrivateKey: m.privateKey,
NodeKey: m.privateKey.Public(),
Addresses: []netip.Prefix{tsaip},
Peers: []*tailcfg.Node{
{
Key: wgkey.Public(),
Endpoints: []string{wgEp.String()},
IsWireGuardOnly: true,
Addresses: []netip.Prefix{wgaip},
AllowedIPs: []netip.Prefix{wgaip},
},
},
}
m.conn.SetNetworkMap(nm)
cfg, err := nmcfg.WGCfg(nm, t.Logf, netmap.AllowSingleHosts|netmap.AllowSubnetRoutes, "")
if err != nil {
t.Fatal(err)
}
m.Reconfig(cfg)
pbuf := tuntest.Ping(wgaip.Addr(), tsaip.Addr())
m.tun.Outbound <- pbuf
select {
case p := <-wgtun.Inbound:
if !bytes.Equal(p, pbuf) {
t.Errorf("got unexpected packet: %x", p)
}
case <-time.After(time.Second):
t.Fatal("no packet after 1s")
}
}
func TestIsWireGuardOnlyPeerWithMasquerade(t *testing.T) {
derpMap, cleanup := runDERPAndStun(t, t.Logf, localhostListener{}, netaddr.IPv4(127, 0, 0, 1))
defer cleanup()
tskey := key.NewNode()
tsaip := netip.MustParsePrefix("100.111.222.111/32")
wgkey := key.NewNode()
wgaip := netip.MustParsePrefix("10.64.0.1/32")
// the ip that the wireguard peer has in allowed ips and expects as a masq source
masqip := netip.MustParsePrefix("10.64.0.2/32")
uapi := fmt.Sprintf("private_key=%s\npublic_key=%s\nallowed_ip=%s\n\n",
wgkey.UntypedHexString(), tskey.Public().UntypedHexString(), masqip.String())
wgdev, wgtun, wgEp := newWireguard(t, uapi, []netip.Prefix{wgaip})
defer wgdev.Close()
m := newMagicStackWithKey(t, t.Logf, localhostListener{}, derpMap, tskey)
defer m.Close()
nm := &netmap.NetworkMap{
Name: "ts",
PrivateKey: m.privateKey,
NodeKey: m.privateKey.Public(),
Addresses: []netip.Prefix{tsaip},
Peers: []*tailcfg.Node{
{
Key: wgkey.Public(),
Endpoints: []string{wgEp.String()},
IsWireGuardOnly: true,
Addresses: []netip.Prefix{wgaip},
AllowedIPs: []netip.Prefix{wgaip},
SelfNodeV4MasqAddrForThisPeer: masqip.Addr(),
},
},
}
m.conn.SetNetworkMap(nm)
cfg, err := nmcfg.WGCfg(nm, t.Logf, netmap.AllowSingleHosts|netmap.AllowSubnetRoutes, "")
if err != nil {
t.Fatal(err)
}
m.Reconfig(cfg)
pbuf := tuntest.Ping(wgaip.Addr(), tsaip.Addr())
m.tun.Outbound <- pbuf
select {
case p := <-wgtun.Inbound:
// TODO(raggi): move to a bytes.Equal based test later, once
// tuntest.Ping produces correct checksums!
var pkt packet.Parsed
pkt.Decode(p)
if pkt.ICMP4Header().Type != packet.ICMP4EchoRequest {
t.Fatalf("unexpected packet: %x", p)
}
if pkt.Src.Addr() != masqip.Addr() {
t.Fatalf("bad source IP, got %s, want %s", pkt.Src.Addr(), masqip.Addr())
}
if pkt.Dst.Addr() != wgaip.Addr() {
t.Fatalf("bad source IP, got %s, want %s", pkt.Src.Addr(), masqip.Addr())
}
case <-time.After(time.Second):
t.Fatal("no packet after 1s")
}
}