wgengine/magicsock: fix panic when rebinding fails

We would replace the existing real implementation of nettype.PacketConn
with a blockForeverConn, but that violates the contract of atomic.Value
(where the type cannot change). Fix by switching to a pointer value
(atomic.Pointer[nettype.PacketConn]).

A longstanding issue, but became more prevalent when we started binding
connections to interfaces on macOS and iOS (#6566), which could lead to
the bind call failing if the interface was no longer available.

Fixes #6641

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
pull/6663/head
Mihai Parparita 2 years ago committed by Mihai Parparita
parent e27f4f022e
commit bdc45b9066

@ -3008,13 +3008,14 @@ func (c *Conn) ParseEndpoint(nodeKeyStr string) (conn.Endpoint, error) {
// RebindingUDPConn is a UDP socket that can be re-bound. // RebindingUDPConn is a UDP socket that can be re-bound.
// Unix has no notion of re-binding a socket, so we swap it out for a new one. // Unix has no notion of re-binding a socket, so we swap it out for a new one.
type RebindingUDPConn struct { type RebindingUDPConn struct {
// pconnAtomic is the same as pconn, but doesn't require acquiring mu. It's // pconnAtomic is a pointer to the value stored in pconn, but doesn't
// used for reads/writes and only upon failure do the reads/writes then // require acquiring mu. It's used for reads/writes and only upon failure
// check pconn (after acquiring mu) to see if there's been a rebind // do the reads/writes then check pconn (after acquiring mu) to see if
// meanwhile. // there's been a rebind meanwhile.
// pconn isn't really needed, but makes some of the code simpler // pconn isn't really needed, but makes some of the code simpler
// to keep it in a type safe form. // to keep it distinct.
pconnAtomic syncs.AtomicValue[nettype.PacketConn] // Neither is expected to be nil, sockets are bound on creation.
pconnAtomic atomic.Pointer[nettype.PacketConn]
mu sync.Mutex // held while changing pconn (and pconnAtomic) mu sync.Mutex // held while changing pconn (and pconnAtomic)
pconn nettype.PacketConn pconn nettype.PacketConn
@ -3023,7 +3024,7 @@ type RebindingUDPConn struct {
func (c *RebindingUDPConn) setConnLocked(p nettype.PacketConn) { func (c *RebindingUDPConn) setConnLocked(p nettype.PacketConn) {
c.pconn = p c.pconn = p
c.pconnAtomic.Store(p) c.pconnAtomic.Store(&p)
c.port = uint16(c.localAddrLocked().Port) c.port = uint16(c.localAddrLocked().Port)
} }
@ -3038,7 +3039,7 @@ func (c *RebindingUDPConn) currentConn() nettype.PacketConn {
// It returns the number of bytes copied and the source address. // It returns the number of bytes copied and the source address.
func (c *RebindingUDPConn) ReadFrom(b []byte) (int, net.Addr, error) { func (c *RebindingUDPConn) ReadFrom(b []byte) (int, net.Addr, error) {
for { for {
pconn := c.pconnAtomic.Load() pconn := *c.pconnAtomic.Load()
n, addr, err := pconn.ReadFrom(b) n, addr, err := pconn.ReadFrom(b)
if err != nil && pconn != c.currentConn() { if err != nil && pconn != c.currentConn() {
continue continue
@ -3056,7 +3057,7 @@ func (c *RebindingUDPConn) ReadFrom(b []byte) (int, net.Addr, error) {
// when c's underlying connection is a net.UDPConn. // when c's underlying connection is a net.UDPConn.
func (c *RebindingUDPConn) ReadFromNetaddr(b []byte) (n int, ipp netip.AddrPort, err error) { func (c *RebindingUDPConn) ReadFromNetaddr(b []byte) (n int, ipp netip.AddrPort, err error) {
for { for {
pconn := c.pconnAtomic.Load() pconn := *c.pconnAtomic.Load()
// Optimization: Treat *net.UDPConn specially. // Optimization: Treat *net.UDPConn specially.
// This lets us avoid allocations by calling ReadFromUDPAddrPort. // This lets us avoid allocations by calling ReadFromUDPAddrPort.
@ -3122,13 +3123,10 @@ func (c *RebindingUDPConn) closeLocked() error {
func (c *RebindingUDPConn) WriteTo(b []byte, addr net.Addr) (int, error) { func (c *RebindingUDPConn) WriteTo(b []byte, addr net.Addr) (int, error) {
for { for {
pconn := c.pconnAtomic.Load() pconn := *c.pconnAtomic.Load()
n, err := pconn.WriteTo(b, addr) n, err := pconn.WriteTo(b, addr)
if err != nil { if err != nil && pconn != c.currentConn() {
if pconn != c.currentConn() { continue
continue
}
} }
return n, err return n, err
} }
@ -3136,13 +3134,10 @@ func (c *RebindingUDPConn) WriteTo(b []byte, addr net.Addr) (int, error) {
func (c *RebindingUDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) { func (c *RebindingUDPConn) WriteToUDPAddrPort(b []byte, addr netip.AddrPort) (int, error) {
for { for {
pconn := c.pconnAtomic.Load() pconn := *c.pconnAtomic.Load()
n, err := pconn.WriteToUDPAddrPort(b, addr) n, err := pconn.WriteToUDPAddrPort(b, addr)
if err != nil { if err != nil && pconn != c.currentConn() {
if pconn != c.currentConn() { continue
continue
}
} }
return n, err return n, err
} }

@ -1803,3 +1803,16 @@ func TestDiscoMagicMatches(t *testing.T) {
t.Errorf("last 2 bytes of disco magic don't match, got %v want %v", discoMagic2, m2) t.Errorf("last 2 bytes of disco magic don't match, got %v want %v", discoMagic2, m2)
} }
} }
func TestRebindingUDPConn(t *testing.T) {
// Test that RebindingUDPConn can be re-bound to different connection
// types.
c := RebindingUDPConn{}
realConn, err := net.ListenPacket("udp4", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer realConn.Close()
c.setConnLocked(realConn.(nettype.PacketConn))
c.setConnLocked(newBlockForeverConn())
}

Loading…
Cancel
Save