Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add magicsock opt to block direct endpoints #44

Merged
merged 6 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions wgengine/magicsock/magicsock.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,10 @@ type Conn struct {
// that will call Conn.doPeriodicSTUN.
periodicReSTUNTimer *time.Timer

// blockEndpoints is whether to avoid capturing, storing and sending
// endpoints gathered from local interfaces or STUN. Only DERP endpoints
// will be sent.
blockEndpoints bool
// endpointsUpdateActive indicates that updateEndpoints is
// currently running. It's used to deduplicate concurrent endpoint
// update requests.
Expand Down Expand Up @@ -330,6 +334,13 @@ type Options struct {
// endpoints change. The called func does not own the slice.
EndpointsFunc func([]tailcfg.Endpoint)

// BlockEndpoints is whether to avoid capturing, storing and sending
// endpoints gathered from local interfaces or STUN. Only DERP endpoints
// will be sent.
// This does not disable the UDP socket or portmapping attempts as this
// setting can be toggled at runtime.
BlockEndpoints bool
deansheather marked this conversation as resolved.
Show resolved Hide resolved

// DERPActiveFunc optionally provides a func to be called when
// a connection is made to a DERP server.
DERPActiveFunc func()
Expand Down Expand Up @@ -580,6 +591,11 @@ func (c *Conn) setEndpoints(endpoints []tailcfg.Endpoint) (changed bool) {
c.mu.Lock()
defer c.mu.Unlock()

if c.blockEndpoints {
anySTUN = false
endpoints = []tailcfg.Endpoint{}
}

if !anySTUN && c.derpMap == nil && !inTest() {
// Don't bother storing or reporting this yet. We
// don't have a DERP map or any STUN entries, so we're
Expand Down Expand Up @@ -826,6 +842,31 @@ func (c *Conn) DiscoPublicKey() key.DiscoPublic {
return c.discoPublic
}

// SetBlockEndpoints sets the blockEndpoints field. If changed, endpoints will
// be updated to apply the new settings. Existing connections may continue to
// use the old setting until they are reestablished. Disabling endpoints does
// not affect the UDP socket or portmapper.
func (c *Conn) SetBlockEndpoints(block bool) {
c.mu.Lock()
defer c.mu.Unlock()
didChange := c.blockEndpoints != block
c.blockEndpoints = block
if !didChange {
return
}

const why = "SetBlockEndpoints"
if c.endpointsUpdateActive {
if c.wantEndpointsUpdate != why {
c.dlogf("[v1] magicsock: SetBlockEndpoints: endpoint update active, need another later")
c.wantEndpointsUpdate = why
}
} else {
c.endpointsUpdateActive = true
go c.updateEndpoints(why)
}
}

// determineEndpoints returns the machine's endpoint addresses. It
// does a STUN lookup (via netcheck) to determine its public address.
//
Expand Down Expand Up @@ -1648,6 +1689,9 @@ func (c *Conn) enqueueCallMeMaybe(derpAddr netip.AddrPort, de *endpoint) {
for _, ep := range c.lastEndpoints {
eps = append(eps, ep.Addr)
}
// NOTE: sending an empty call-me-maybe (e.g. when BlockEndpoints is true)
// is still valid and results in the other side forgetting all the endpoints
// it knows of ours.
go de.c.sendDiscoMessage(derpAddr, de.publicKey, epDisco.key, &disco.CallMeMaybe{MyNumber: eps}, discoLog)
if debugSendCallMeUnknownPeer() {
// Send a callMeMaybe packet to a non-existent peer
Expand Down
129 changes: 129 additions & 0 deletions wgengine/magicsock/magicsock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3000,3 +3000,132 @@ func TestDERPForceWebsockets(t *testing.T) {
t.Errorf("no websocket upgrade requests seen")
}
}

func TestBlockEndpoints(t *testing.T) {
logf, closeLogf := logger.LogfCloser(t.Logf)
defer closeLogf()

derpMap, cleanup := runDERPAndStun(t, t.Logf, localhostListener{}, netaddr.IPv4(127, 0, 0, 1))
defer cleanup()

m := &natlab.Machine{Name: "m1"}
ms := newMagicStackFunc(t, logger.WithPrefix(logf, "conn1: "), m, derpMap, nil)
defer ms.Close()

// Check that some endpoints exist. This should be the case as we should use
// interface addresses as endpoints instantly on startup, and we already
// have a DERP connection due to newMagicStackFunc.
ms.conn.mu.Lock()
haveEndpoint := false
for _, ep := range ms.conn.lastEndpoints {
if ep.Addr.Addr() == tailcfg.DerpMagicIPAddr {
t.Fatal("DERP IP in endpoints list?", ep.Addr)
}
haveEndpoint = true
break
}
ms.conn.mu.Unlock()
if !haveEndpoint {
t.Fatal("no endpoints found")
}

// Block endpoints, should result in an update.
ms.conn.SetBlockEndpoints(true)

// Wait for endpoints to finish updating.
waitForNoEndpoints(t, ms.conn)
}

func TestBlockEndpointsDERPOK(t *testing.T) {
// This test is similar to TestBlockEndpoints, but it tests that we don't
// mess up DERP somehow.

mstun := &natlab.Machine{Name: "stun"}
m1 := &natlab.Machine{Name: "m1"}
m2 := &natlab.Machine{Name: "m2"}
inet := natlab.NewInternet()
sif := mstun.Attach("eth0", inet)
m1if := m1.Attach("eth0", inet)
m2if := m2.Attach("eth0", inet)

d := &devices{
m1: m1,
m1IP: m1if.V4(),
m2: m2,
m2IP: m2if.V4(),
stun: mstun,
stunIP: sif.V4(),
}

logf, closeLogf := logger.LogfCloser(t.Logf)
defer closeLogf()

derpMap, cleanup := runDERPAndStun(t, t.Logf, localhostListener{}, netaddr.IPv4(127, 0, 0, 1))
defer cleanup()

ms1 := newMagicStack(t, logger.WithPrefix(logf, "conn1: "), d.m1, derpMap)
defer ms1.Close()
ms2 := newMagicStack(t, logger.WithPrefix(logf, "conn2: "), d.m2, derpMap)
defer ms2.Close()

cleanup = meshStacks(logf, nil, ms1, ms2)
defer cleanup()

m1IP := ms1.IP()
m2IP := ms2.IP()
logf("IPs: %s %s", m1IP, m2IP)

// SetBlockEndpoints is called later since it's incompatible with the test
// meshStacks implementations.
ms1.conn.SetBlockEndpoints(true)
ms2.conn.SetBlockEndpoints(true)
waitForNoEndpoints(t, ms1.conn)
waitForNoEndpoints(t, ms2.conn)

cleanup = newPinger(t, logf, ms1, ms2)
defer cleanup()

// Wait for both peers to know about each other.
for {
if s1 := ms1.Status(); len(s1.Peer) != 1 {
time.Sleep(10 * time.Millisecond)
continue
}
if s2 := ms2.Status(); len(s2.Peer) != 1 {
time.Sleep(10 * time.Millisecond)
continue
}
break
}

cleanup = newPinger(t, t.Logf, ms1, ms2)
defer cleanup()

if len(ms1.conn.activeDerp) == 0 {
t.Errorf("unexpected DERP empty got: %v want: >0", len(ms1.conn.activeDerp))
}
if len(ms2.conn.activeDerp) == 0 {
t.Errorf("unexpected DERP empty got: %v want: >0", len(ms2.conn.activeDerp))
}
}

func waitForNoEndpoints(t *testing.T, ms *Conn) {
t.Helper()
ok := false
parentLoop:
for i := 0; i < 50; i++ {
time.Sleep(100 * time.Millisecond)
ms.mu.Lock()
for _, ep := range ms.lastEndpoints {
deansheather marked this conversation as resolved.
Show resolved Hide resolved
t.Errorf("endpoint %v was not blocked", ep.Addr)
ms.mu.Unlock()
continue parentLoop
}
ms.mu.Unlock()
ok = true
break
}
if !ok {
t.Fatal("endpoints were not blocked after 50 attempts")
}
}
Loading