feat: Add Tailscale networking (#3505)

* fix: Add coder user to docker group on installation

This makes for a simpler setup, and reduces the likelihood
a user runs into a strange issue.

* Add wgnet

* Add ping

* Add listening

* Finish refactor to make this work

* Add interface for swapping

* Fix conncache with interface

* chore: update gvisor

* fix tailscale types

* linting

* more linting

* Add coordinator

* Add coordinator tests

* Fix coordination

* It compiles!

* Move all connection negotiation in-memory

* Migrate coordinator to use net.conn

* Add closed func

* Fix close listener func

* Make reconnecting PTY work

* Fix reconnecting PTY

* Update CI to Go 1.19

* Add CLI flags for DERP mapping

* Fix Tailnet test

* Rename ConnCoordinator to TailnetCoordinator

* Remove print statement from workspace agent test

* Refactor wsconncache to use tailnet

* Remove STUN from unit tests

* Add migrate back to dump

* chore: Upgrade to Go 1.19

This is required as part of #3505.

* Fix reconnecting PTY tests

* fix: update wireguard-go to fix devtunnel

* fix migration numbers

* linting

* Return early for status if endpoints are empty

* Update cli/server.go

Co-authored-by: Colin Adler <colin1adler@gmail.com>

* Update cli/server.go

Co-authored-by: Colin Adler <colin1adler@gmail.com>

* Fix frontend entites

* Fix agent bicopy

* Fix race condition for the last node

* Fix down migration

* Fix connection RBAC

* Fix migration numbers

* Fix forwarding TCP to a local port

* Implement ping for tailnet

* Rename to ForceHTTP

* Add external derpmapping

* Expose DERP region names to the API

* Add global option to enable Tailscale networking for web

* Mark DERP flags hidden while testing

* Update DERP map on reconnect

* Add close func to workspace agents

* Fix race condition in upstream dependency

* Fix feature columns race condition

Co-authored-by: Colin Adler <colin1adler@gmail.com>
This commit is contained in:
Kyle Carberry
2022-08-31 20:09:44 -05:00
committed by GitHub
co-authored by Colin Adler
parent 00da01fdf7
commit 9bd83e5ec7
56 changed files with 2498 additions and 1817 deletions
-67
View File
@@ -1,67 +0,0 @@
package peerwg
import (
"net"
"tailscale.com/tailcfg"
"tailscale.com/wgengine/magicsock"
)
// This is currently set to use Tailscale's DERP server in DFW while we build in
// our own support for DERP servers.
var DerpMap = &tailcfg.DERPMap{
Regions: map[int]*tailcfg.DERPRegion{
9: {
RegionID: 9,
RegionCode: "dfw",
RegionName: "Dallas",
Avoid: false,
Nodes: []*tailcfg.DERPNode{
{
Name: "9a",
RegionID: 9,
HostName: "derp9.tailscale.com",
CertName: "",
IPv4: "207.148.3.137",
IPv6: "2001:19f0:6401:1d9c:5400:2ff:feef:bb82",
STUNPort: 0,
STUNOnly: false,
DERPPort: 0,
InsecureForTests: false,
STUNTestIP: "",
},
{
Name: "9c",
RegionID: 9,
HostName: "derp9c.tailscale.com",
CertName: "",
IPv4: "155.138.243.219",
IPv6: "2001:19f0:6401:fe7:5400:3ff:fe8d:6d9c",
STUNPort: 0,
STUNOnly: false,
DERPPort: 0,
InsecureForTests: false,
STUNTestIP: "",
},
{
Name: "9b",
RegionID: 9,
HostName: "derp9b.tailscale.com",
CertName: "",
IPv4: "144.202.67.195",
IPv6: "2001:19f0:6401:eb5:5400:3ff:fe8d:6d9b",
STUNPort: 0,
STUNOnly: false,
DERPPort: 0,
InsecureForTests: false,
STUNTestIP: "",
},
},
},
},
OmitDefaultRegions: true,
}
// DefaultDerpHome is the ipv4 representation of a DERP server. The port is the
// DERP id. We only support using DERP 9 for now.
var DefaultDerpHome = net.JoinHostPort(magicsock.DerpMagicIP, "9")
-94
View File
@@ -1,94 +0,0 @@
package peerwg
import (
"bytes"
"strconv"
"github.com/google/uuid"
"golang.org/x/xerrors"
"inet.af/netaddr"
"tailscale.com/types/key"
)
const handshakeSeparator byte = '|'
// Handshake is a message received from a wireguard peer, indicating
// it would like to connect.
type Handshake struct {
// Recipient is the uuid of the agent that the message was intended for.
Recipient uuid.UUID `json:"recipient"`
// DiscoPublicKey is the disco public key of the peer.
DiscoPublicKey key.DiscoPublic `json:"disco"`
// NodePublicKey is the public key of the peer.
NodePublicKey key.NodePublic `json:"public"`
// IPv6 is the IPv6 address of the peer.
IPv6 netaddr.IP `json:"ipv6"`
}
// HandshakeRecipientHint parses the first part of a serialized
// Handshake to quickly determine if the message is meant for the
// provided recipient.
func HandshakeRecipientHint(agentID []byte, msg []byte) (bool, error) {
idx := bytes.Index(msg, []byte{handshakeSeparator})
if idx == -1 {
return false, xerrors.Errorf("invalid peer message, no separator")
}
return bytes.Equal(agentID, msg[:idx]), nil
}
func (h *Handshake) UnmarshalText(text []byte) error {
sp := bytes.Split(text, []byte{handshakeSeparator})
if len(sp) != 4 {
return xerrors.Errorf("expected 4 parts, got %d", len(sp))
}
err := h.Recipient.UnmarshalText(sp[0])
if err != nil {
return xerrors.Errorf("parse recipient: %w", err)
}
err = h.DiscoPublicKey.UnmarshalText(sp[1])
if err != nil {
return xerrors.Errorf("parse disco: %w", err)
}
err = h.NodePublicKey.UnmarshalText(sp[2])
if err != nil {
return xerrors.Errorf("parse public: %w", err)
}
h.IPv6, err = netaddr.ParseIP(string(sp[3]))
if err != nil {
return xerrors.Errorf("parse ipv6: %w", err)
}
return nil
}
func (h Handshake) MarshalText() ([]byte, error) {
const expectedLen = 223
var buf bytes.Buffer
buf.Grow(expectedLen)
recp, _ := h.Recipient.MarshalText()
_, _ = buf.Write(recp)
_ = buf.WriteByte(handshakeSeparator)
disco, _ := h.DiscoPublicKey.MarshalText()
_, _ = buf.Write(disco)
_ = buf.WriteByte(handshakeSeparator)
pub, _ := h.NodePublicKey.MarshalText()
_, _ = buf.Write(pub)
_ = buf.WriteByte(handshakeSeparator)
ipv6 := h.IPv6.StringExpanded()
_, _ = buf.WriteString(ipv6)
// Ensure we're always allocating exactly enough.
if buf.Len() != expectedLen {
panic("buffer length mismatch: want 223, got " + strconv.Itoa(buf.Len()))
}
return buf.Bytes(), nil
}
-38
View File
@@ -1,38 +0,0 @@
package peerwg
import (
"context"
"net"
"golang.org/x/crypto/ssh"
"golang.org/x/xerrors"
"inet.af/netaddr"
)
func (n *Network) SSH(ctx context.Context, ip netaddr.IP) (net.Conn, error) {
netConn, err := n.Netstack.DialContextTCP(ctx, netaddr.IPPortFrom(ip, 12212))
if err != nil {
return nil, xerrors.Errorf("dial agent ssh: %w", err)
}
return netConn, nil
}
func (n *Network) SSHClient(ctx context.Context, ip netaddr.IP) (*ssh.Client, error) {
netConn, err := n.SSH(ctx, ip)
if err != nil {
return nil, xerrors.Errorf("ssh: %w", err)
}
sshConn, channels, requests, err := ssh.NewClientConn(netConn, "localhost:22", &ssh.ClientConfig{
// SSH host validation isn't helpful, because obtaining a peer
// connection already signifies user-intent to dial a workspace.
// #nosec
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
})
if err != nil {
return nil, xerrors.Errorf("new ssh client conn: %w", err)
}
return ssh.NewClient(sshConn, channels, requests), nil
}
-441
View File
@@ -1,441 +0,0 @@
package peerwg
import (
"context"
"fmt"
"hash/fnv"
"io"
"log"
"net"
"strconv"
"sync"
"time"
"github.com/google/uuid"
"github.com/tabbed/pqtype"
"golang.org/x/xerrors"
"inet.af/netaddr"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/dns"
"tailscale.com/net/netns"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/types/ipproto"
"tailscale.com/types/key"
tslogger "tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/magicsock"
"tailscale.com/wgengine/monitor"
"tailscale.com/wgengine/netstack"
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/wgcfg/nmcfg"
"cdr.dev/slog"
)
var Logf tslogger.Logf = log.Printf
func init() {
// Globally disable network namespacing.
// All networking happens in userspace.
netns.SetEnabled(false)
}
func UUIDToInet(uid uuid.UUID) pqtype.Inet {
uid = privateUUID(uid)
return pqtype.Inet{
Valid: true,
IPNet: net.IPNet{
IP: uid[:],
Mask: net.CIDRMask(128, 128),
},
}
}
func UUIDToNetaddr(uid uuid.UUID) netaddr.IP {
return netaddr.IPFrom16(privateUUID(uid))
}
// privateUUID sets the uid to have the tailscale private ipv6 prefix.
func privateUUID(uid uuid.UUID) uuid.UUID {
// fd7a:115c:a1e0
uid[0] = 0xfd
uid[1] = 0x7a
uid[2] = 0x11
uid[3] = 0x5c
uid[4] = 0xa1
uid[5] = 0xe0
return uid
}
type Network struct {
mu sync.Mutex
logger slog.Logger
Netstack *netstack.Impl
magicSock *magicsock.Conn
netMap *netmap.NetworkMap
router *router.Config
wgEngine wgengine.Engine
// listeners is a map of listening sockets that will be forwarded traffic
// from the wireguard interface.
listeners map[listenKey]*listener
DiscoPublicKey key.DiscoPublic
NodePrivateKey key.NodePrivate
}
// New constructs a Wireguard network that filters traffic
// to destinations matching the addresses provided.
func New(logger slog.Logger, addresses []netaddr.IPPrefix) (*Network, error) {
nodePrivateKey := key.NewNode()
nodePublicKey := nodePrivateKey.Public()
id, stableID := nodeIDs(nodePublicKey)
netMap := &netmap.NetworkMap{
NodeKey: nodePublicKey,
PrivateKey: nodePrivateKey,
Addresses: addresses,
PacketFilter: []filter.Match{{
// Allow any protocol!
IPProto: []ipproto.Proto{ipproto.TCP, ipproto.UDP, ipproto.ICMPv4, ipproto.ICMPv6, ipproto.SCTP},
// Allow traffic sourced from anywhere.
Srcs: []netaddr.IPPrefix{
netaddr.IPPrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0),
netaddr.IPPrefixFrom(netaddr.IPv6Unspecified(), 0),
},
// Allow traffic to route anywhere.
Dsts: []filter.NetPortRange{
{
Net: netaddr.IPPrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0),
Ports: filter.PortRange{
First: 0,
Last: 65535,
},
},
{
Net: netaddr.IPPrefixFrom(netaddr.IPv6Unspecified(), 0),
Ports: filter.PortRange{
First: 0,
Last: 65535,
},
},
},
Caps: []filter.CapMatch{},
}},
}
// Identify itself as a node on the network with the addresses provided.
netMap.SelfNode = &tailcfg.Node{
ID: id,
StableID: stableID,
Key: nodePublicKey,
Addresses: netMap.Addresses,
AllowedIPs: append(netMap.Addresses, netaddr.MustParseIPPrefix("::/0")),
Endpoints: []string{},
DERP: DefaultDerpHome,
}
wgMonitor, err := monitor.New(Logf)
if err != nil {
return nil, xerrors.Errorf("create link monitor: %w", err)
}
dialer := new(tsdial.Dialer)
dialer.Logf = Logf
// Create a wireguard engine in userspace.
engine, err := wgengine.NewUserspaceEngine(Logf, wgengine.Config{
LinkMonitor: wgMonitor,
Dialer: dialer,
})
if err != nil {
return nil, xerrors.Errorf("create wgengine: %w", err)
}
// This is taken from Tailscale:
// https://github.com/tailscale/tailscale/blob/0f05b2c13ff0c305aa7a1655fa9c17ed969d65be/tsnet/tsnet.go#L247-L255
// nolint
tunDev, magicConn, dnsManager, ok := engine.(wgengine.InternalsGetter).GetInternals()
if !ok {
return nil, xerrors.New("could not get wgengine internals")
}
// Update the keys for the magic connection!
err = magicConn.SetPrivateKey(nodePrivateKey)
if err != nil {
return nil, xerrors.Errorf("set node private key: %w", err)
}
netMap.SelfNode.DiscoKey = magicConn.DiscoPublicKey()
// Create the networking stack.
// This is called to route connections.
netStack, err := netstack.Create(Logf, tunDev, engine, magicConn, dialer, dnsManager)
if err != nil {
return nil, xerrors.Errorf("create netstack: %w", err)
}
netStack.ProcessLocalIPs = true
netStack.ProcessSubnets = true
dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
_, ok := engine.PeerForIP(ip)
return ok
}
dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
return netStack.DialContextTCP(ctx, dst)
}
err = netStack.Start()
if err != nil {
return nil, xerrors.Errorf("start netstack: %w", err)
}
engine = wgengine.NewWatchdog(engine)
// Update the wireguard configuration to allow traffic to flow.
cfg, err := nmcfg.WGCfg(netMap, Logf, netmap.AllowSingleHosts|netmap.AllowSubnetRoutes, netMap.SelfNode.StableID)
if err != nil {
return nil, xerrors.Errorf("create wgcfg: %w", err)
}
rtr := &router.Config{
LocalAddrs: cfg.Addresses,
}
err = engine.Reconfig(cfg, rtr, &dns.Config{}, &tailcfg.Debug{})
if err != nil {
return nil, xerrors.Errorf("reconfig: %w", err)
}
engine.SetDERPMap(DerpMap)
engine.SetNetworkMap(copyNetMap(netMap))
ipb := netaddr.IPSetBuilder{}
for _, addr := range netMap.Addresses {
ipb.AddPrefix(addr)
}
ips, _ := ipb.IPSet()
iplb := netaddr.IPSetBuilder{}
ipl, _ := iplb.IPSet()
engine.SetFilter(filter.New(netMap.PacketFilter, ips, ipl, nil, Logf))
wn := &Network{
logger: logger,
NodePrivateKey: nodePrivateKey,
DiscoPublicKey: magicConn.DiscoPublicKey(),
wgEngine: engine,
Netstack: netStack,
magicSock: magicConn,
netMap: netMap,
router: rtr,
listeners: map[listenKey]*listener{},
}
netStack.ForwardTCPIn = wn.forwardTCP
return wn, nil
}
// forwardTCP handles incoming connections from Wireguard in userspace.
func (n *Network) forwardTCP(conn net.Conn, port uint16) {
n.mu.Lock()
listener, ok := n.listeners[listenKey{"tcp", "", fmt.Sprint(port)}]
n.mu.Unlock()
if !ok {
// No in-memory listener exists, forward to host.
n.forwardTCPToLocalHandler(conn, port)
return
}
timer := time.NewTimer(time.Second)
defer timer.Stop()
select {
case listener.conn <- conn:
case <-timer.C:
_ = conn.Close()
}
}
// forwardTCPToLocalHandler forwards the provided net.Conn to the
// matching port bound to localhost.
func (n *Network) forwardTCPToLocalHandler(c net.Conn, port uint16) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
defer c.Close()
dialAddrStr := net.JoinHostPort("127.0.0.1", strconv.Itoa(int(port)))
var stdDialer net.Dialer
server, err := stdDialer.DialContext(ctx, "tcp", dialAddrStr)
if err != nil {
n.logger.Debug(ctx, "dial local port", slog.F("port", port), slog.Error(err))
return
}
defer server.Close()
connClosed := make(chan error, 2)
go func() {
_, err := io.Copy(server, c)
connClosed <- err
}()
go func() {
_, err := io.Copy(c, server)
connClosed <- err
}()
err = <-connClosed
if err != nil {
n.logger.Debug(ctx, "proxy connection closed with error", slog.Error(err))
}
n.logger.Debug(ctx, "forwarded connection closed", slog.F("local_addr", dialAddrStr))
}
// AddPeer allows connections from another Wireguard instance with the
// handshake credentials.
func (n *Network) AddPeer(handshake Handshake) error {
n.mu.Lock()
defer n.mu.Unlock()
// If the peer already exists in the network map, do nothing.
for _, p := range n.netMap.Peers {
if p.Key == handshake.NodePublicKey {
n.logger.Debug(context.Background(), "peer already in netmap", slog.F("peer", handshake.NodePublicKey.ShortString()))
return nil
}
}
// The Tailscale engine owns this slice, so we need to copy to make
// modifications.
peers := append(([]*tailcfg.Node)(nil), n.netMap.Peers...)
id, stableID := nodeIDs(handshake.NodePublicKey)
peers = append(peers, &tailcfg.Node{
ID: id,
StableID: stableID,
Name: handshake.NodePublicKey.String() + ".com",
Key: handshake.NodePublicKey,
DiscoKey: handshake.DiscoPublicKey,
Addresses: []netaddr.IPPrefix{netaddr.IPPrefixFrom(handshake.IPv6, 128)},
AllowedIPs: []netaddr.IPPrefix{netaddr.IPPrefixFrom(handshake.IPv6, 128)},
DERP: DefaultDerpHome,
Endpoints: []string{DefaultDerpHome},
})
n.netMap.Peers = peers
cfg, err := nmcfg.WGCfg(n.netMap, Logf, netmap.AllowSingleHosts|netmap.AllowSubnetRoutes, tailcfg.StableNodeID("nBBoJZ5CNTRL"))
if err != nil {
return xerrors.Errorf("create wgcfg: %w", err)
}
err = n.wgEngine.Reconfig(cfg, n.router, &dns.Config{}, &tailcfg.Debug{})
if err != nil {
return xerrors.Errorf("reconfig: %w", err)
}
// Always give the Tailscale engine a copy of our network map.
n.wgEngine.SetNetworkMap(copyNetMap(n.netMap))
return nil
}
// Ping sends a discovery ping to the provided peer.
// The peer address must be connected before a successful ping will work.
func (n *Network) Ping(ip netaddr.IP) *ipnstate.PingResult {
ch := make(chan *ipnstate.PingResult)
n.wgEngine.Ping(ip, tailcfg.PingDisco, func(pr *ipnstate.PingResult) {
ch <- pr
})
return <-ch
}
// Listener returns a net.Listener in userspace that can be used to accept
// connections from the Wireguard network to the specified address. If a
// listener exists for a given address, all connections will be forwarded to the
// listener instead of being routed to the host.
func (n *Network) Listen(network, addr string) (net.Listener, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return nil, xerrors.Errorf("split addr host port: %w", err)
}
lkey := listenKey{network, host, port}
ln := &listener{
wn: n,
key: lkey,
addr: addr,
conn: make(chan net.Conn, 1),
}
n.mu.Lock()
defer n.mu.Unlock()
if _, ok := n.listeners[lkey]; ok {
return nil, xerrors.Errorf("listener already open for %s, %s", network, addr)
}
n.listeners[lkey] = ln
return ln, nil
}
func (n *Network) Close() error {
// Close all listeners.
for _, l := range n.listeners {
_ = l.Close()
}
// Close the Wireguard netstack and engine.
_ = n.Netstack.Close()
n.wgEngine.Close()
return nil
}
type listenKey struct {
network string
host string
port string
}
type listener struct {
wn *Network
key listenKey
addr string
conn chan net.Conn
}
func (ln *listener) Accept() (net.Conn, error) {
c, ok := <-ln.conn
if !ok {
return nil, xerrors.Errorf("tsnet: %w", net.ErrClosed)
}
return c, nil
}
func (ln *listener) Addr() net.Addr { return addr{ln} }
func (ln *listener) Close() error {
ln.wn.mu.Lock()
defer ln.wn.mu.Unlock()
if v, ok := ln.wn.listeners[ln.key]; ok && v == ln {
delete(ln.wn.listeners, ln.key)
close(ln.conn)
}
return nil
}
type addr struct{ ln *listener }
func (a addr) Network() string { return a.ln.key.network }
func (a addr) String() string { return a.ln.addr }
// nodeIDs generates Tailscale node IDs for the provided public key.
func nodeIDs(public key.NodePublic) (tailcfg.NodeID, tailcfg.StableNodeID) {
idhash := fnv.New64()
pub, _ := public.MarshalText()
_, _ = idhash.Write(pub)
return tailcfg.NodeID(idhash.Sum64()), tailcfg.StableNodeID(pub)
}
func copyNetMap(nm *netmap.NetworkMap) *netmap.NetworkMap {
nmCopy := *nm
return &nmCopy
}