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

fix: block incoming endpoints from call-me-maybe #55

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
20 changes: 20 additions & 0 deletions wgengine/magicsock/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,26 @@ func (de *endpoint) deleteEndpointLocked(why string, ep netip.AddrPort) {
}
}

func (de *endpoint) clearEndpoints(why string) {
de.mu.Lock()
defer de.mu.Unlock()
de.debugUpdates.Add(EndpointChange{
When: time.Now(),
What: "clearEndpoints-" + why,
})
de.endpointState = map[netip.AddrPort]*endpointState{}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this, but I think we should pair it with a lower-level implementation of blockEndpoints

Right now, we check this field on the magicsock Conn before calling into methods on endpoint that could add an endpoint. There are 3 of these (updateFromNode, handleCallMeMaybe and addCandidateEndpoint), but you've only directly blocked the first two.

So, there is still a narrow race condition where you can set block endpoints, clear them all out, and a Disco Ping could come in and add an endpoint back in.

What do you think about extending endpoint to also have a blockEndpoints field that we sync with Conn, that way we can always directly check whether we are blocking endpoints at the moment we are about to add one?

if de.bestAddr.AddrPort.IsValid() {
de.debugUpdates.Add(EndpointChange{
When: time.Now(),
What: "clearEndpoints-bestAddr-" + why,
From: de.bestAddr,
})
de.bestAddr = addrLatency{}
}
de.c.logf("magicsock: disco: node %s %s now using DERP only (all endpoints deleted)",
de.publicKey.ShortString(), de.discoShort())
}

// initFakeUDPAddr populates fakeWGAddr with a globally unique fake UDPAddr.
// The current implementation just uses the pointer value of de jammed into an IPv6
// address, but it could also be, say, a counter.
Expand Down
33 changes: 29 additions & 4 deletions wgengine/magicsock/magicsock.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ type Conn struct {
// blockEndpoints is whether to avoid capturing, storing and sending
// endpoints gathered from local interfaces or STUN. Only DERP endpoints
// will be sent.
// This will also block incoming endpoints received via call-me-maybe disco
// packets.
blockEndpoints bool
// endpointsUpdateActive indicates that updateEndpoints is
// currently running. It's used to deduplicate concurrent endpoint
Expand Down Expand Up @@ -843,10 +845,10 @@ 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.
// SetBlockEndpoints sets the blockEndpoints field. If enabled, all peer
// endpoints will be cleared from the peer map and every connection will
// immediately switch to DERP. Disabling endpoints does not affect the UDP
// socket or portmapper.
func (c *Conn) SetBlockEndpoints(block bool) {
c.mu.Lock()
defer c.mu.Unlock()
Expand All @@ -856,6 +858,7 @@ func (c *Conn) SetBlockEndpoints(block bool) {
return
}

// Re-gather local endpoints.
const why = "SetBlockEndpoints"
if c.endpointsUpdateActive {
if c.wantEndpointsUpdate != why {
Expand All @@ -866,6 +869,13 @@ func (c *Conn) SetBlockEndpoints(block bool) {
c.endpointsUpdateActive = true
go c.updateEndpoints(why)
}

// Clear any endpoints from the peerMap if block is true.
if block {
c.peerMap.forEachEndpoint(func(ep *endpoint) {
ep.clearEndpoints("SetBlockEndpoints")
})
}
}

// determineEndpoints returns the machine's endpoint addresses. It
Expand Down Expand Up @@ -1523,6 +1533,14 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netip.AddrPort, derpNodeSrc ke
c.logf("[unexpected] CallMeMaybe from peer via DERP whose netmap discokey != disco source")
return
}
if c.blockEndpoints {
c.dlogf("[v1] magicsock: disco: %v<-%v (%v, %v) got call-me-maybe with %d endpoints, but endpoints blocked",
c.discoShort, epDisco.short,
ep.publicKey.ShortString(), derpStr(src.String()),
len(dm.MyNumber),
)
dm.MyNumber = []netip.AddrPort{}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we even delivering the CallMeMaybe? We clear out any previously learned endpoints when we set blockEndpoints

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for incoming call-me-maybe packets. If only one side has "--disable-direct-connections" then the other side will still send call-me-maybe packets populated with endpoints to the other peer, which is why they still connect directly if they are on the same LAN.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clearing the endpoints once won't prevent them from being readded to the peer map when the peer sends us another call-me-maybe packet.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize this is for incoming call-me-maybe. I'm asking why we strip out the endpoints and then process the empty call-me-maybe, rather than just drop the call-me-maybe packets on the floor without further processing. What benefit do we get from sending an empty call-me-maybe thru handleCallMeMaybe()?

}
c.dlogf("[v1] magicsock: disco: %v<-%v (%v, %v) got call-me-maybe, %d endpoints",
c.discoShort, epDisco.short,
ep.publicKey.ShortString(), derpStr(src.String()),
Expand Down Expand Up @@ -1921,6 +1939,13 @@ func (c *Conn) SetNetworkMap(nm *netmap.NetworkMap) {
// we'll fall through to the next pass, which allocates but can
// handle full set updates.
for _, n := range nm.Peers {
if c.blockEndpoints {
c.dlogf("[v1] magicsock: SetNetworkingMap: %v (%v, %v) received %d endpoints, but endpoints blocked",
n.DiscoKey.ShortString(), n.Key.ShortString(),
len(n.Endpoints),
)
n.Endpoints = []string{}
}
if ep, ok := c.peerMap.endpointForNodeKey(n.Key); ok {
if n.DiscoKey.IsZero() && !n.IsWireGuardOnly {
// Discokey transitioned from non-zero to zero? This should not
Expand Down
78 changes: 58 additions & 20 deletions wgengine/magicsock/magicsock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3022,7 +3022,6 @@ func TestBlockEndpoints(t *testing.T) {
t.Fatal("DERP IP in endpoints list?", ep.Addr)
}
haveEndpoint = true
break
}
ms.conn.mu.Unlock()
if !haveEndpoint {
Expand Down Expand Up @@ -3060,30 +3059,27 @@ func TestBlockEndpointsDERPOK(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()
derpMap, cleanupDerp := runDERPAndStun(t, t.Logf, localhostListener{}, netaddr.IPv4(127, 0, 0, 1))
defer cleanupDerp()

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

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

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()
cleanupPinger1 := newPinger(t, logf, ms1, ms2)
defer cleanupPinger1()
cleanupPinger2 := newPinger(t, logf, ms2, ms1)
defer cleanupPinger2()

// Wait for both peers to know about each other.
for {
Expand All @@ -3098,14 +3094,39 @@ func TestBlockEndpointsDERPOK(t *testing.T) {
break
}

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

if len(ms1.conn.activeDerp) == 0 {
t.Errorf("unexpected DERP empty got: %v want: >0", len(ms1.conn.activeDerp))
// SetBlockEndpoints is called later since it's incompatible with the test
// meshStacks implementations.
// We only set it on ms1, since ms2's endpoints should be ignored by ms1.
ms1.conn.SetBlockEndpoints(true)

// All endpoints should've been immediately removed from ms1.
ep2, ok := ms1.conn.peerMap.endpointForNodeKey(ms2.Public())
if !ok {
t.Fatalf("endpoint not found for ms2")
}
if len(ms2.conn.activeDerp) == 0 {
t.Errorf("unexpected DERP empty got: %v want: >0", len(ms2.conn.activeDerp))
ep2.mu.Lock()
if len(ep2.endpointState) != 0 {
ep2.mu.Unlock()
t.Fatalf("endpoints not removed from ms1")
}
ep2.mu.Unlock()

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

// Give time for another call-me-maybe packet to arrive. I couldn't think of
// a better way than sleeping without making a bunch of changes.
t.Logf("sleeping for call-me-maybe packet to be received and ignored")
time.Sleep(time.Second)
deansheather marked this conversation as resolved.
Show resolved Hide resolved
t.Logf("done sleeping")

ep2.mu.Lock()
defer ep2.mu.Unlock()
for i := range ep2.endpointState {
t.Fatalf("endpoint %q not missing", i.String())
}
}

Expand All @@ -3129,3 +3150,20 @@ func waitForNoEndpoints(t *testing.T, ms *Conn) {
}
t.Log("endpoints are blocked")
}

func waitForEndpoints(t *testing.T, ms *Conn) {
t.Helper()
for i := 0; i < 50; i++ {
time.Sleep(100 * time.Millisecond)
ms.mu.Lock()
for _, ep := range ms.lastEndpoints {
if ep.Addr.Addr() != tailcfg.DerpMagicIPAddr {
t.Log("endpoint found")
ms.mu.Unlock()
return
}
}
ms.mu.Unlock()
}
t.Fatal("endpoint was not found after 50 attempts")
}
Loading