mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
co-authored by
Colin Adler
parent
00da01fdf7
commit
9bd83e5ec7
@@ -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")
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user