package connection import ( "context" "crypto/tls" "fmt" "net" "net/netip" "runtime" "sync" "github.com/quic-go/quic-go" "github.com/rs/zerolog" "github.com/cloudflare/cloudflared/connection/dialopts" cfdquic "github.com/cloudflare/cloudflared/quic" ) var ( portForConnIndex = make(map[uint8]int, 0) portMapMutex sync.Mutex ) func DialQuic( ctx context.Context, quicConfig *quic.Config, tlsConfig *tls.Config, edgeAddr netip.AddrPort, localAddr net.IP, connIndex uint8, logger *zerolog.Logger, opts dialopts.DialOpts, ) (cfdquic.QUICConnection, error) { udpConn, err := createUDPConnForConnIndex(connIndex, localAddr, edgeAddr, opts, logger) if err != nil { return nil, err } conn, err := quic.Dial(ctx, udpConn, net.UDPAddrFromAddrPort(edgeAddr), tlsConfig, quicConfig) if err != nil { // close the udp server socket in case of error connecting to the edge _ = udpConn.Close() return nil, &EdgeQuicDialError{Cause: err} } // wrap the session, so that the UDPConn is closed after session is closed. return cfdquic.NewQUICConnection(conn, udpConn) } func createUDPConnForConnIndex(connIndex uint8, localIP net.IP, edgeIP netip.AddrPort, opts dialopts.DialOpts, logger *zerolog.Logger) (*net.UDPConn, error) { listenNetwork := "udp" // https://github.com/quic-go/quic-go/issues/3793 DF bit cannot be set for dual stack listener ("udp") on macOS, // to set the DF bit properly, the network string needs to be specific to the IP family. if runtime.GOOS == "darwin" { if edgeIP.Addr().Is4() { listenNetwork = "udp4" } else { listenNetwork = "udp6" } } // Probes skip port reuse entirely to avoid interfering with the main connection flow. // They use a random ephemeral port for each dial. if opts.SkipPortReuse { return net.ListenUDP(listenNetwork, &net.UDPAddr{IP: localIP, Port: 0}) } portMapMutex.Lock() defer portMapMutex.Unlock() // if port was not set yet, it will be zero, so bind will randomly allocate one. if port, ok := portForConnIndex[connIndex]; ok { udpConn, err := net.ListenUDP(listenNetwork, &net.UDPAddr{IP: localIP, Port: port}) // if there wasn't an error, or if port was 0 (independently of error or not, just return) if err == nil { return udpConn, nil } logger.Debug().Err(err).Msgf("Unable to reuse port %d for connIndex %d. Falling back to random allocation.", port, connIndex) } // if we reached here, then there was an error or port as not been allocated it. udpConn, err := net.ListenUDP(listenNetwork, &net.UDPAddr{IP: localIP, Port: 0}) if err == nil { udpAddr, ok := (udpConn.LocalAddr()).(*net.UDPAddr) if !ok { return nil, fmt.Errorf("unable to cast to udpConn") } portForConnIndex[connIndex] = udpAddr.Port } else { delete(portForConnIndex, connIndex) } return udpConn, err }