package iroh

import (
	
	
	
	
	
	
	
	
	
	

	
	itls 
	
	
	quic 
	
	
	
	
	
	
)

// Endpoint is a bound iroh node: it owns a secret key, a UDP socket, and the
// QUIC transport used to dial and accept connections. Create one with [Bind].
//
// An Endpoint is safe for concurrent use. Close it with [Endpoint.Shutdown].
type Endpoint struct {
	secretKey key.SecretKey
	alpns     []string

	udp          *net.UDPConn
	sock         *socket.Socket
	magic        *socket.MagicConn
	relay        *socket.RelayTransport // nil when relays are disabled
	serveStop    context.CancelFunc
	transport    *quic.Transport
	listener     *quic.EarlyListener
	quicConf     *quic.Config
	keyLogWriter io.Writer
	keyExchange  KeyExchangePolicy
	sessionCache *SessionCache
	disableIP    bool
	relayFirst   bool
	verifySource func(net.Addr) bool
	hooks        []EndpointHooks
	custom       []CustomTransport

	// remotes is the per-remote state registry. The endpoint owns it: it
	// registers every established connection so the actor for that remote can
	// track paths and select between them. The actor never holds a reference
	// back to the endpoint, so there is no import cycle.
	remotes *socket.RemoteMap
	lookup  *AddressLookupServices

	mu          sync.Mutex
	closed      bool
	closedCh    chan struct{}
	acceptOwner acceptOwner
	addrWatch   *watch.Value[netaddr.EndpointAddr]
	// externalPinned holds addresses pinned via AddExternalAddr until
	// RemoveExternalAddr. externalDiscovered holds the latest net report's
	// reflexive addresses, replaced wholesale per report. Kept apart so a
	// report cannot drop pinned candidates, nor pinning keep stale ones.
	externalPinned     []netip.AddrPort
	externalDiscovered []netip.AddrPort
	netReport          netReportRunner
	lastReport         *NetReport
	nextStable         uint64
	stableIDs          map[*quic.Conn]uint64
	metrics            endpointMetrics
}

type acceptOwner int

const (
	acceptOwnerNone acceptOwner = iota
	acceptOwnerAccept
	acceptOwnerListenStreams
	acceptOwnerRouter
)

// config holds the options assembled by [Option] values before [Bind].
type config struct {
	secretKey       key.SecretKey
	haveKey         bool
	alpns           []string
	bindAddr        netip.AddrPort
	bindOpts        BindOpts
	haveBindAddr    bool
	disableIP       bool
	relayMode       relay.Mode
	lookup          *AddressLookupServices
	enableNetReport bool
	netReport       netReportRunner
	netReportEvery  time.Duration
	natPMP          bool
	natPMPGateway   netip.Addr
	natPMPPort      uint16
	keyLogWriter    io.Writer
	keyExchange     KeyExchangePolicy
	transportConfig *QUICTransportConfig
	pathSelector    socket.PathSelector
	relayFirst      bool
	verifySource    func(net.Addr) bool
	hooks           []EndpointHooks
	custom          []CustomTransport
}

// Option configures an [Endpoint] at [Bind] time.
type Option func(*config) error

type netReportRunner func(context.Context) (*netreport.Report, error)

// BindOpts configures how a bound IP socket participates in route selection.
//
// PrefixLen is the network prefix length matched by this socket. IsRequired
// keeps parity with Rust's bind options: a required bind fails the endpoint when
// the socket cannot be opened, which is also the behavior of this single-socket
// Go build. IsDefaultRoute marks the socket as a default route when non-nil.
//
// The zero value is usable and means "host route, required, default inferred".
type BindOpts struct {
	PrefixLen      uint8
	IsRequired     bool
	IsDefaultRoute *bool
}

// QUICTransportConfig configures stable QUIC transport settings used by
// endpoints. A zero field keeps the default.
type QUICTransportConfig struct {
	KeepAlivePeriod time.Duration
	MaxIdleTimeout  time.Duration
	// InitialPacketSize is the initial QUIC packet size in bytes.
	InitialPacketSize uint16
	// MaxIncomingStreams is the maximum number of concurrent bidirectional
	// streams accepted from a peer.
	MaxIncomingStreams int64
}

// WithSecretKey sets the endpoint's identity. If unset, [Bind] generates a
// random key.
func ( key.SecretKey) Option {
	return func( *config) error {
		.secretKey = 
		.haveKey = true
		return nil
	}
}

// WithALPNs sets the ALPN protocols this endpoint accepts on incoming
// connections. ALPN is Application-Layer Protocol Negotiation, the TLS
// extension QUIC uses to agree on the application protocol carried by a
// connection.
//
// Each ALPN is an arbitrary byte string represented as a Go string, matching
// crypto/tls and quic-go. Printable ASCII such as "example/1" is common, but
// strings may contain arbitrary bytes.
func ( ...string) Option {
	return func( *config) error {
		.alpns = append(.alpns, ...)
		return nil
	}
}

// WithSourceAddressValidation sets the QUIC Retry policy for unvalidated
// incoming source addresses. The function receives the unvalidated remote
// address and returns true when qng should send a Retry packet before allowing
// the connection through to AcceptIncoming.
func ( func(net.Addr) bool) Option {
	return func( *config) error {
		.verifySource = 
		return nil
	}
}

// WithBindAddr sets the local UDP address to bind. The default is an
// OS-assigned port on the unspecified address.
func ( netip.AddrPort) Option {
	return func( *config) error {
		.bindAddr = 
		.bindOpts = BindOpts{}
		.haveBindAddr = true
		return nil
	}
}

// WithBindAddrOpts sets the local UDP address to bind with route-selection
// metadata. PrefixLen must fit the address family: at most 32 for IPv4 and at
// most 128 for IPv6.
func ( netip.AddrPort,  BindOpts) Option {
	return func( *config) error {
		if  := validateBindOpts(, );  != nil {
			return 
		}
		.bindAddr = 
		.bindOpts = 
		.haveBindAddr = true
		return nil
	}
}

func validateBindOpts( netip.AddrPort,  BindOpts) error {
	if !.IsValid() {
		return errors.New("iroh: invalid bind address")
	}
	if .Addr().Is4() {
		if .PrefixLen > 32 {
			return fmt.Errorf("iroh: invalid IPv4 bind prefix length %d", .PrefixLen)
		}
		return nil
	}
	if .PrefixLen > 128 {
		return fmt.Errorf("iroh: invalid IPv6 bind prefix length %d", .PrefixLen)
	}
	return nil
}

// WithoutIPTransports prevents the endpoint from binding, advertising, or
// dialing direct IP addresses. Relay and custom transports still use the magic
// connection machinery.
func () Option {
	return func( *config) error {
		.disableIP = true
		return nil
	}
}

// WithoutRelayTransports disables relay connectivity.
func () Option {
	return func( *config) error {
		.relayMode = relay.ModeDisabled()
		return nil
	}
}

// WithRelayFirstDial makes Connect try relay addresses before direct IP
// addresses when both are present. Direct IP addresses are still registered as
// QNT candidates after the handshake, so a connection can establish through a
// relay and then migrate ordinary traffic to a validated direct path.
func () Option {
	return func( *config) error {
		.relayFirst = true
		return nil
	}
}

// WithAddressLookup sets the address-lookup services the endpoint uses to
// resolve additional addresses for a remote endpoint (pkarr, DNS, in-memory).
// The per-remote state machine consults them through its resolve hook. When
// unset, the endpoint does no lookup-driven address resolution and connects only
// to the addresses passed to [Endpoint.Connect].
func ( *AddressLookupServices) Option {
	return func( *config) error {
		.lookup = 
		return nil
	}
}

// WithDNSResolver configures DNS endpoint discovery through the number0
// production origin. It is a convenience wrapper around [WithAddressLookup].
func ( *dns.Resolver) Option {
	return func( *config) error {
		if .lookup == nil {
			.lookup = &AddressLookupServices{}
		}
		.lookup.AddResolver(NewDNSAddressLookup(dns.N0DNSEndpointOriginProd, ))
		return nil
	}
}

// WithRelayMode selects which relay servers the endpoint uses. The default is
// [relay.ModeDisabled] (no relays), matching this build's direct-only default.
// Pass [relay.ModeDefault], [relay.ModeStaging], or [relay.ModeCustom] to enable
// relay connectivity.
func ( relay.Mode) Option {
	return func( *config) error {
		.relayMode = 
		return nil
	}
}

// WithNetReport enables background net_report refreshes after [Bind]. When
// relays are configured, the report's QAD-derived global addresses are
// advertised as local QNT candidates for active remotes.
func () Option {
	return func( *config) error {
		.enableNetReport = true
		return nil
	}
}

// WithNATPMP enables NAT-PMP UDP port mapping through gateway.
//
// NAT-PMP does not define a portable default-gateway discovery mechanism; pass
// the IPv4 address of the gateway that should receive NAT-PMP requests.
func ( netip.Addr) Option {
	return func( *config) error {
		if !.IsValid() || !.Is4() {
			return errors.New("iroh: invalid nat-pmp gateway")
		}
		.natPMP = true
		.natPMPGateway = 
		return nil
	}
}

func withNATPMPPort( uint16) Option {
	return func( *config) error {
		.natPMPPort = 
		return nil
	}
}

// WithKeyLogWriter writes TLS traffic secrets for direct peer QUIC handshakes
// in NSS SSLKEYLOGFILE format. It is for debugging only; writing these secrets
// compromises connection confidentiality.
func ( io.Writer) Option {
	return func( *config) error {
		.keyLogWriter = 
		return nil
	}
}

// WithKeyExchangePolicy selects the TLS key-exchange groups used for direct
// peer connections. The zero policy keeps the package default.
func ( KeyExchangePolicy) Option {
	return func( *config) error {
		if !.valid() {
			return fmt.Errorf("iroh: invalid key exchange policy %d", )
		}
		.keyExchange = 
		return nil
	}
}

// WithHooks registers endpoint hooks. Hooks run in registration order and may
// reject outgoing dials or completed handshakes.
func ( EndpointHooks) Option {
	return func( *config) error {
		if  != nil {
			.hooks = append(.hooks, )
		}
		return nil
	}
}

// WithTransportConfig overrides stable QUIC transport settings. Unsupported
// qng internals remain private to the endpoint.
func ( *QUICTransportConfig) Option {
	return func( *config) error {
		.transportConfig = 
		return nil
	}
}

// WithPathSelector sets the policy used to choose among candidate network paths
// to a remote endpoint. When unset, the endpoint uses [BiasedRttPathSelector].
func ( PathSelector) Option {
	return func( *config) error {
		if  != nil {
			.pathSelector = pathSelectorAdapter{selector: }
		}
		return nil
	}
}

// WithCustomTransport adds a custom transport backend to the magic socket.
// Custom transports own their wire format and exchange datagrams using
// [netaddr.CustomAddr] values advertised in endpoint addresses.
func ( CustomTransport) Option {
	return func( *config) error {
		if  != nil {
			.custom = append(.custom, )
		}
		return nil
	}
}

// Bind binds a UDP socket and returns a ready [Endpoint].
//
// By default the endpoint enables qng datagrams and advertises the iroh
// multipath path limit. Direct UDP works without relays; relay transport,
// address discovery, and QNT hole-punching are separate connectivity features.
func ( context.Context,  ...Option) (*Endpoint, error) {
	var  config
	for ,  := range  {
		if  := (&);  != nil {
			return nil, 
		}
	}
	if .netReportEvery == 0 {
		.netReportEvery = 5 * time.Minute
	}
	if !.haveKey {
		,  := key.GenerateSecretKey()
		if  != nil {
			return nil, fmt.Errorf("iroh: generate key: %w", )
		}
		.secretKey = 
	}

	 := .bindAddr
	if !.haveBindAddr {
		 = netip.AddrPortFrom(netip.IPv6Unspecified(), 0)
	}
	,  := bindPacketConn(, )
	if  != nil {
		return nil, fmt.Errorf("iroh: bind udp: %w", )
	}

	 := &quic.Config{
		KeepAlivePeriod:                HeartbeatInterval,
		MaxIdleTimeout:                 RelayPathMaxIdleTimeout,
		EnableDatagrams:                true,
		InitialMaxPathID:               initialMaxPathID(),
		MaxRemoteNATTraversalAddresses: maxRemoteNATTraversalAddresses(),
		Tracer:                         qlog.DefaultConnectionTracer,
		// Accept 0-RTT early data on incoming connections that resume a prior
		// session. Allow0RTT is ignored for dialed connections, so sharing this
		// config with Connect is safe. Mirrors the Rust server enabling early
		// data with max_early_data_size = u32::MAX (iroh/src/tls.rs:118).
		Allow0RTT: true,
		// Remember the server's NEW_TOKEN frames so a resuming dial can present a
		// validation token. Without it the server cannot validate the client's
		// address ahead of the handshake and rejects 0-RTT. Tokens are keyed by
		// the TLS server name (ServerName(id)), the same per-peer bucketing the
		// session cache uses. The capacity matches maxTLSTickets.
		TokenStore: quic.NewLRUTokenStore(32, 8),
	}
	if .transportConfig != nil {
		if .transportConfig.KeepAlivePeriod != 0 {
			.KeepAlivePeriod = .transportConfig.KeepAlivePeriod
		}
		if .transportConfig.MaxIdleTimeout != 0 {
			.MaxIdleTimeout = .transportConfig.MaxIdleTimeout
		}
		if .transportConfig.InitialPacketSize != 0 {
			.InitialPacketSize = .transportConfig.InitialPacketSize
		}
		if .transportConfig.MaxIncomingStreams != 0 {
			.MaxIncomingStreams = .transportConfig.MaxIncomingStreams
		}
	}
	// The QUIC transport is driven over the magic socket rather than the raw
	// UDP socket: a single net.PacketConn that multiplexes every iroh path. The
	// magic socket always carries the direct-IP transport and, when relays are
	// configured, a relay transport.
	 := socket.NewSocket()

	var  *socket.RelayActor
	 := .relayMode.Map()
	if !.IsEmpty() {
		 = socket.NewRelayActor(socket.RelayActorConfig{
			SecretKey: .secretKey,
			Map:       ,
		})
	}

	 := customTransportAdapters(.custom)
	var  *socket.MagicConn
	if  == nil {
		 = socket.NewMagicConnRelayOnly(, , ...)
	} else {
		 = socket.NewMagicConnWithTransports(, , , ...)
	}
	,  := context.WithCancel(context.Background())
	go .Serve()

	 := &Endpoint{
		secretKey: .secretKey,
		alpns:     slices.Clone(.alpns),
		udp:       ,
		sock:      ,
		magic:     ,
		relay:     .Relay(),
		serveStop: ,
		transport: &quic.Transport{
			Conn:                ,
			ConnectionIDLength:  8,
			VerifySourceAddress: .verifySource,
		},
		quicConf:     ,
		keyLogWriter: .keyLogWriter,
		keyExchange:  .keyExchange,
		sessionCache: NewSessionCache(),
		// A nil udp means there is no IP transport (relay-only bind, or the js
		// build where bindPacketConn never returns a socket), so IP addresses
		// must not be advertised regardless of the requested disableIP.
		disableIP:    .disableIP ||  == nil,
		relayFirst:   .relayFirst,
		verifySource: .verifySource,
		hooks:        append([]EndpointHooks(nil), .hooks...),
		custom:       append([]CustomTransport(nil), .custom...),
		lookup:       .lookup,
		closedCh:     make(chan struct{}),
		stableIDs:    make(map[*quic.Conn]uint64),
	}
	// Assigned after the literal: the runner needs ep.transport so QAD
	// probes ride the endpoint's own socket (see qadDialer).
	.netReport = endpointNetReportRunner(, , .qadDialer())
	// The per-remote state registry shares the serve context: its actors stop
	// when the endpoint's recv loop stops. Its resolve hook is backed by the
	// endpoint's address-lookup services (slice G), passed down as a func value
	// so internal/socket does not import iroh.
	.remotes = socket.NewRemoteMapWithMetrics(, .pathSelector, .resolveFunc(), .MetricsSet())
	if .disableIP {
		// No IP transports: upgrade-tick hole punching has nothing to punch
		// toward and wedges in-flight relay streams (perflab: relay-forced
		// transfers stall at the 60 s upgrade tick).
		.remotes.DisableHolepunch()
	}
	// Reaped remotes release their mapped addresses, so the socket's tables do
	// not grow without bound under peer churn (upstream iroh issue #4293).
	.remotes.SetOnEvict(.EvictRemote)
	.magic.SetEndpointSender(func( key.EndpointID,  []byte) bool {
		 := .remotes.Actor().SendDatagram(, func( socket.Addr,  []byte) bool {
			return .magic.SendAddr(, )
		})
		return  == nil
	})

	// Select an initial home relay so relay connectivity starts before the first
	// net_report finishes. applyNetReport switches to net_report's preferred
	// relay once latency data is available.
	if .relay != nil {
		if  := .URLs(); len() > 0 {
			.relay.SetHomeRelay([0])
		}
	}

	if len(.alpns) > 0 {
		if  := .startListener();  != nil {
			()
			if  != nil {
				.Close()
			}
			return nil, 
		}
	}
	.addrWatch = watch.NewValueFunc(.Addr(), endpointAddrEqual)
	if .netReport != nil {
		go .runNetReport(, .netReportEvery)
	}
	if .natPMP {
		go .runNATPMP(, .natPMPGateway, .natPMPPort)
	}
	return , nil
}

func ( *Endpoint) ( func(net.Addr) bool) {
	.mu.Lock()
	defer .mu.Unlock()
	.verifySource = 
	.transport.VerifySourceAddress = 
}

func ( *Endpoint) () func(net.Addr) bool {
	.mu.Lock()
	defer .mu.Unlock()
	return .verifySource
}

func endpointNetReportRunner( config,  *relay.Map,  netreport.QADDialer) netReportRunner {
	if .netReport != nil {
		return .netReport
	}
	if !.enableNetReport || .IsEmpty() {
		return nil
	}
	 := netreport.NewClient()
	if  != nil {
		 = .WithQADDialer()
	}
	return func( context.Context) (*netreport.Report, error) {
		return .GetReport(, netreport.IfStateDetails{HaveV4: true, HaveV6: true}, false)
	}
}

// qadDialer returns the dialer net_report uses for QAD probes. Dials on the
// endpoint's own transport share its UDP socket, so the address a relay
// observes is the endpoint's real public mapping — a usable dial and
// hole-punch candidate. Nil when there is no IP transport; net_report then
// falls back to a private per-probe socket.
func ( *Endpoint) () netreport.QADDialer {
	if .udp == nil {
		return nil
	}
	return func( context.Context,  netip.AddrPort,  *itls.Config,  *quic.Config) (*quic.Conn, error) {
		return .transport.Dial(, net.UDPAddrFromAddrPort(), , )
	}
}

func initialMaxPathID() *uint32 {
	 := uint32(MaxMultipathPaths)
	return &
}

func maxRemoteNATTraversalAddresses() *uint8 {
	 := uint8(MaxQNTAddresses)
	return &
}

// startListener begins accepting incoming connections with the current ALPNs.
// It uses an early listener so the QUIC stack can accept 0-RTT early data from
// peers that resume a prior session.
func ( *Endpoint) () error {
	,  := serverTLSConfigWithCurves(.secretKey, .alpns, .keyExchange.curves())
	if  != nil {
		return 
	}
	.KeyLogWriter = .keyLogWriter
	,  := .transport.ListenEarly(, .quicConf)
	if  != nil {
		return fmt.Errorf("iroh: listen: %w", )
	}
	.listener = 
	return nil
}

// SetALPNs sets the ALPN protocols the endpoint accepts and begins (or
// continues) listening for incoming connections. It is the Go analog of the Rust
// Endpoint::set_alpns (iroh/src/endpoint.rs), used by [Router.Spawn] to register
// every protocol's ALPN at once.
//
// SetALPNs replaces the accepted ALPN set. If a listener is already running, it
// is closed first; established connections are unaffected. SetALPNs returns an
// error while an accept loop owner such as [Endpoint.Accept], [Endpoint.AcceptIncoming],
// [Endpoint.ListenStreams], or [Router] is active. Pass each ALPN as an arbitrary
// byte string represented as a Go string; see [WithALPNs].
func ( *Endpoint) ( []string) error {
	return .setALPNs(, acceptOwnerNone)
}

func ( *Endpoint) ( []string,  acceptOwner) error {
	.mu.Lock()
	defer .mu.Unlock()
	if .closed {
		return ErrEndpointClosed
	}
	if .acceptOwner != acceptOwnerNone && .acceptOwner !=  {
		return ErrEndpointAcceptLoopInUse
	}
	 := slices.Clone()
	if .listener != nil {
		if  := .listener.Close();  != nil {
			return fmt.Errorf("iroh: close listener: %w", )
		}
		.listener = nil
	}
	 := .alpns
	.alpns = 
	if  := .startListener();  != nil {
		.alpns = 
		return 
	}
	return nil
}

// ID returns the endpoint's network identifier.
func ( *Endpoint) () key.EndpointID { return .secretKey.Public().EndpointID() }

// SecretKey returns the endpoint's secret key.
func ( *Endpoint) () key.SecretKey { return .secretKey }

// LocalAddr returns the bound UDP address.
func ( *Endpoint) () netip.AddrPort {
	if .udp == nil {
		return netip.AddrPort{}
	}
	return .udp.LocalAddr().(*net.UDPAddr).AddrPort()
}

// externalNATLocked returns the pinned and net-report-discovered external
// candidates, pinned first, deduplicated. e.mu must be held.
func ( *Endpoint) () []netip.AddrPort {
	 := append([]netip.AddrPort(nil), .externalPinned...)
	for ,  := range .externalDiscovered {
		 = appendUniqueNATTraversalCandidate(, )
	}
	return 
}

// localNATTraversalCandidates returns concrete local direct addresses this
// endpoint can hand to qng's QNT state. The default bind address is unspecified
// ([::]:port), which is not a usable candidate and must not be advertised.
// QAD-derived external addresses are appended after the same canonicalization
// and validity checks.
func ( *Endpoint) () []netip.AddrPort {
	var  []netip.AddrPort
	if .disableIP {
		return 
	}
	if ,  := canonicalNATTraversalCandidate(.LocalAddr());  {
		 = appendUniqueNATTraversalCandidate(, )
	}
	.mu.Lock()
	 := .externalNATLocked()
	.mu.Unlock()
	for ,  := range  {
		 = appendUniqueNATTraversalCandidate(, )
	}
	return 
}

// setExternalNATTraversalCandidates replaces the discovered external
// candidate set: net reports are authoritative, and replacement retires
// mappings the NAT rebound. Pinned addresses are a separate set.
func ( *Endpoint) ( ...netip.AddrPort) bool {
	var  []netip.AddrPort
	for ,  := range  {
		 = appendUniqueNATTraversalCandidate(, )
	}

	.mu.Lock()
	if equalAddrPorts(.externalDiscovered, ) {
		.mu.Unlock()
		return false
	}
	.externalDiscovered = 
	.updateAddrWatchLocked()
	.mu.Unlock()

	.advertiseNATTraversalCandidates()
	return true
}

// AddExternalAddr pins addr as an externally reachable address and advertises
// it as a QNT NAT traversal candidate until RemoveExternalAddr; net reports
// never drop it. Invalid, unspecified, or zero-port addresses are ignored.
func ( *Endpoint) ( netip.AddrPort) {
	if .disableIP {
		return
	}
	.mu.Lock()
	 := appendUniqueNATTraversalCandidate(append([]netip.AddrPort(nil), .externalPinned...), )
	if equalAddrPorts(.externalPinned, ) {
		.mu.Unlock()
		return
	}
	.externalPinned = 
	.updateAddrWatchLocked()
	.mu.Unlock()
	.advertiseNATTraversalCandidates()
}

// RemoveExternalAddr removes addr from the endpoint's externally reachable
// addresses and stops advertising it as a QNT NAT traversal candidate. It
// returns true if addr was present. Invalid, unspecified, or zero-port addresses
// are ignored.
func ( *Endpoint) ( netip.AddrPort) bool {
	if .disableIP {
		return false
	}
	,  := canonicalNATTraversalCandidate()
	if ! {
		return false
	}

	.mu.Lock()
	 := slices.Index(.externalPinned, )
	if  < 0 {
		.mu.Unlock()
		return false
	}
	.externalPinned = slices.Delete(.externalPinned, , +1)
	.updateAddrWatchLocked()
	.mu.Unlock()
	.advertiseNATTraversalCandidates()
	return true
}

func ( *Endpoint) ( netreport.Report) bool {
	 := netReportFromInternal()
	.mu.Lock()
	.lastReport = &
	.mu.Unlock()

	 := .setExternalNATTraversalCandidates(.GlobalV4, .GlobalV6)
	if .relay != nil && !.PreferredRelay.IsZero() {
		 := .relay.HomeRelayStatus().Current()
		if  == nil || !.URL.Equal(.PreferredRelay) {
			.relay.SetHomeRelay(.PreferredRelay)
			if .magic != nil {
				.magic.RecordRelayHomeChange()
			}
			.mu.Lock()
			.updateAddrWatchLocked()
			.mu.Unlock()
			 = true
		}
	}
	return 
}

// NetReport returns the most recent network report applied to the endpoint.
// The boolean result is false when no report has completed yet.
func ( *Endpoint) () (NetReport, bool) {
	.mu.Lock()
	defer .mu.Unlock()
	if .lastReport == nil {
		return NetReport{}, false
	}
	return .lastReport.clone(), true
}

// RemoteInfo returns a snapshot of known addressing information for remote.
// It returns false if the endpoint has no recent state for remote.
func ( *Endpoint) ( key.EndpointID) (RemoteInfo, bool) {
	if  == nil || .remotes == nil {
		return RemoteInfo{}, false
	}
	,  := .remotes.RemoteInfo()
	if ! {
		return RemoteInfo{}, false
	}
	return remoteInfoFromSocket(), true
}

func ( *Endpoint) ( context.Context) error {
	if .netReport == nil {
		return nil
	}
	,  := .netReport()
	if  != nil {
		.metrics.netReportReports.Add(1)
		if .Full {
			.metrics.netReportReportsFull.Add(1)
			.metrics.netReportPortmapAttempts.Add(1)
		}
		if .applyNetReport(*) {
			.metrics.netReportPortmapExternalAddressUpdated.Add(1)
		}
	}
	if  != nil {
		.metrics.netReportFailed.Add(1)
		return fmt.Errorf("iroh: netreport: %w", )
	}
	return nil
}

func ( *Endpoint) ( context.Context,  time.Duration) {
	if  <= 0 {
		 = 5 * time.Minute
	}
	_ = .refreshNetReport()
	 := time.NewTicker()
	defer .Stop()
	for {
		select {
		case <-.Done():
			return
		case <-.C:
			_ = .refreshNetReport()
		}
	}
}

func ( *Endpoint) ( context.Context,  netip.Addr,  uint16) {
	if .disableIP {
		return
	}
	 := .LocalAddr()
	if !.IsValid() || .Port() == 0 {
		return
	}
	 := portmapper.NATPMPClient{
		Gateway: ,
		Port:    ,
		Timeout: 2 * time.Second,
	}
	const  = time.Hour
	 := .Port()
	var  netip.AddrPort
	defer func() {
		if .IsValid() {
			.RemoveExternalAddr()
			,  := context.WithTimeout(context.Background(), 2*time.Second)
			defer ()
			_, _ = .MapUDP(, , .Port(), 0)
		}
	}()

	for {
		,  := .MapUDP(, , , )
		if  == nil && .ExternalAddr.IsValid() {
			if .IsValid() &&  != .ExternalAddr {
				.RemoveExternalAddr()
			}
			 = .ExternalAddr
			.AddExternalAddr()
			.metrics.netReportPortmapAttempts.Add(1)
			.metrics.netReportPortmapExternalAddressUpdated.Add(1)
		} else if  != nil {
			.metrics.netReportFailed.Add(1)
		}
		 :=  / 2
		if  == nil && .Lifetime > 0 {
			 = .Lifetime / 2
		}
		if  < 30*time.Second {
			 = 30 * time.Second
		}
		 := time.NewTimer()
		select {
		case <-.Done():
			.Stop()
			return
		case <-.C:
		}
	}
}

func ( *Endpoint) () {
	if .remotes == nil {
		return
	}
	 := .localNATTraversalCandidates()
	.remotes.AddNATTraversalAddresses()
}

func canonicalNATTraversalCandidate( netip.AddrPort) (netip.AddrPort, bool) {
	if !.IsValid() || .Port() == 0 || .Addr().IsUnspecified() {
		return netip.AddrPort{}, false
	}
	return netip.AddrPortFrom(.Addr().Unmap(), .Port()), true
}

func appendUniqueNATTraversalCandidate( []netip.AddrPort,  netip.AddrPort) []netip.AddrPort {
	,  := canonicalNATTraversalCandidate()
	if ! {
		return 
	}
	for ,  := range  {
		if  ==  {
			return 
		}
	}
	return append(, )
}

func equalAddrPorts(,  []netip.AddrPort) bool {
	if len() != len() {
		return false
	}
	for  := range  {
		if [] != [] {
			return false
		}
	}
	return true
}

// Addr returns the endpoint's [netaddr.EndpointAddr] from currently-known local
// information: its id, the bound direct address, any custom transport
// addresses, and (when relays are enabled and a home relay is connected) its
// home relay URL. Later slices add reflexive addresses.
func ( *Endpoint) () netaddr.EndpointAddr {
	 := netaddr.NewEndpointAddr(.ID())
	if !.disableIP {
		// The bind address is unspecified ([::]:port) unless the caller chose
		// one, and that is not something a peer can dial: it means "every
		// interface on that host". Advertising it gives peers a target that
		// resolves to their own loopback, and it makes NAT traversal probes
		// arrive from a source the connection does not recognize.
		if ,  := canonicalNATTraversalCandidate(.LocalAddr());  {
			 = .WithIP()
		}
	}
	.mu.Lock()
	 := .externalNATLocked()
	.mu.Unlock()
	if !.disableIP {
		for ,  := range  {
			 = .WithIP()
		}
	}
	for ,  := range .localCustomAddrs(context.Background()) {
		 = .WithAddrs()
	}
	if .relay != nil {
		if  := .relay.HomeRelayStatus().Current();  != nil {
			 = .WithRelayURL(.URL)
		}
	}
	return 
}

// WatchAddr returns a watcher over the endpoint's current advertised address.
// It updates when local external NAT candidates are added or replaced.
func ( *Endpoint) () watch.Observer[netaddr.EndpointAddr] {
	.mu.Lock()
	defer .mu.Unlock()
	if .addrWatch == nil {
		.addrWatch = watch.NewValueFunc(.addrLocked(), endpointAddrEqual)
	}
	return .addrWatch.Watch()
}

func ( *Endpoint) () {
	if .addrWatch != nil {
		.addrWatch.Set(.addrLocked())
	}
}

func endpointAddrEqual(,  netaddr.EndpointAddr) bool {
	return .ID.Equal(.ID) && equalTransportAddrs(.Addrs(), .Addrs())
}

func ( *Endpoint) () netaddr.EndpointAddr {
	 := netaddr.NewEndpointAddr(.ID())
	if !.disableIP {
		if ,  := canonicalNATTraversalCandidate(.LocalAddr());  {
			 = .WithIP()
		}
		for ,  := range .externalNATLocked() {
			 = .WithIP()
		}
	}
	for ,  := range .localCustomAddrs(context.Background()) {
		 = .WithAddrs()
	}
	if .relay != nil {
		if  := .relay.HomeRelayStatus().Current();  != nil {
			 = .WithRelayURL(.URL)
		}
	}
	return 
}

func ( *Endpoint) ( context.Context) []netaddr.CustomAddr {
	return customTransportLocalAddrs(, .custom)
}

func equalTransportAddrs(,  []netaddr.TransportAddr) bool {
	if len() != len() {
		return false
	}
	for  := range  {
		if [].Compare([]) != 0 {
			return false
		}
	}
	return true
}

// RelayStatus is the connection status of the endpoint's home relay, observed
// through [Endpoint.HomeRelayStatus].
type RelayStatus = socket.RelayStatus

// RelayConfig configures a relay server used by an endpoint.
type RelayConfig = relay.Config

// HomeRelayStatus returns a watcher over the endpoint's home relay connection
// status. The watched value is nil until a home relay is selected; it updates
// whenever the home relay or its connection state changes. When relays are
// disabled the watcher always reports nil.
//
// It is the Go analog of the Rust Endpoint::home_relay_status
// (iroh/src/endpoint.rs:1324).
func ( *Endpoint) () watch.Observer[*RelayStatus] {
	if .relay == nil {
		return watch.NewValue[*RelayStatus](nil).Watch()
	}
	return .relay.HomeRelayStatus()
}

// Online blocks until the endpoint has a connected home relay, or until ctx is
// done. It returns nil once connected, or ctx.Err() if the context ends first.
// When relays are disabled it returns [ErrNoRelay] immediately.
//
// It is the Go analog of the Rust Endpoint::online (iroh/src/endpoint.rs:1295).
func ( *Endpoint) ( context.Context) error {
	if .relay == nil {
		return ErrNoRelay
	}
	 := .relay.HomeRelayStatus()
	for {
		if  := .Current();  != nil && .IsConnected() {
			return nil
		}
		if ,  := .Updated();  != nil {
			return 
		}
	}
}

// ErrNoRelay is returned by [Endpoint.Online] when the endpoint has no relays
// configured (relays disabled), so it can never come online via a relay.
var ErrNoRelay = errors.New("iroh: no relays configured")

// InsertRelay adds or replaces a relay server configuration. It returns the
// previous config for url when one existed.
func ( *Endpoint) ( netaddr.RelayURL,  *RelayConfig) (*RelayConfig, error) {
	if .isClosed() {
		return nil, ErrEndpointClosed
	}
	if .relay == nil {
		return nil, ErrNoRelay
	}
	 := RelayConfig{URL: }
	if  != nil {
		 = *
		.URL = 
	}
	,  := .relay.InsertRelay(, )
	.mu.Lock()
	.updateAddrWatchLocked()
	.mu.Unlock()
	if ! {
		return nil, nil
	}
	return &, nil
}

// RemoveRelay removes a relay server configuration. It returns the removed
// config, or nil if url was not configured.
func ( *Endpoint) ( netaddr.RelayURL) *RelayConfig {
	if .isClosed() || .relay == nil {
		return nil
	}
	,  := .relay.RemoveRelay()
	.mu.Lock()
	.updateAddrWatchLocked()
	.mu.Unlock()
	if ! {
		return nil
	}
	return &
}

// ErrEndpointClosed is returned by operations on a closed [Endpoint].
var ErrEndpointClosed = errors.New("iroh: endpoint closed")

// ErrEndpointAcceptLoopInUse is returned when an operation would start or
// reconfigure an endpoint accept loop while another accept owner is active.
var ErrEndpointAcceptLoopInUse = errors.New("iroh: endpoint accept loop in use")

// ErrSelfConnect is returned by [Endpoint.Connect] when asked to dial the
// endpoint's own id.
var ErrSelfConnect = errors.New("iroh: cannot connect to self")

// ErrNoAddress is returned when an [netaddr.EndpointAddr] has no usable address:
// no direct IP and no relay URL (or relays are disabled on this endpoint).
var ErrNoAddress = errors.New("iroh: no reachable address for endpoint")

// ErrConnectRejected is returned when an endpoint hook rejects a dial before
// any packet is sent.
var ErrConnectRejected = errors.New("iroh: connect rejected by hook")

// ErrHandshakeRejected is returned when an endpoint hook rejects a completed
// handshake.
var ErrHandshakeRejected = errors.New("iroh: handshake rejected by hook")

// ErrConnClosedDuringHandshake is returned when an incoming connection attempt
// dies before completing its handshake (for example, a handshake timeout).
// [Endpoint.Accept] skips such attempts and keeps accepting.
var ErrConnClosedDuringHandshake = errors.New("iroh: connection closed during handshake")

// Connect dials the endpoint identified by addr and negotiates alpn, returning
// an established [Conn]. It tries the direct IP addresses in addr in order, then
// (if relays are enabled) the relay URLs in addr. A relay path carries the QUIC
// handshake over a relay mapped address that routes through the relay transport.
//
// Connect blocks until the handshake completes and the peer identity is
// verified. To send 0-RTT early data before the handshake completes, use
// [Endpoint.ConnectEarly] and [Connecting.Into0RTT].
func ( *Endpoint) ( context.Context,  netaddr.EndpointAddr,  string) (*Conn, error) {
	.metrics.connectsStarted.Add(1)
	 := false
	defer func() {
		if ! {
			.metrics.connectsFailed.Add(1)
		}
	}()
	// Bound the whole connect, including the handshake hooks run by Connection,
	// by ConnectTimeout. connectEarly sees this deadline and does not re-wrap.
	if ,  := .Deadline(); ! {
		var  context.CancelFunc
		,  = context.WithTimeout(, ConnectTimeout)
		defer ()
	}
	,  := .connectEarly(, , )
	if  != nil {
		return nil, 
	}
	,  := .Connection()
	if  != nil {
		return nil, 
	}
	.metrics.connectsAccepted.Add(1)
	 = true
	return , nil
}

// ConnectEarly begins dialing the endpoint identified by addr for alpn and
// returns immediately with a [Connecting] handle, without waiting for the
// handshake. It tries the same dial targets as [Endpoint.Connect].
//
// Await [Connecting.Connection] for the verified [Conn] (the same result
// [Endpoint.Connect] returns), or call [Connecting.Into0RTT] to send 0-RTT early
// data before the handshake completes when a resumable session is cached.
func ( *Endpoint) ( context.Context,  netaddr.EndpointAddr,  string) (*Connecting, error) {
	return .connectEarly(, , )
}

// connectEarly performs the shared dial setup for Connect and ConnectEarly: it
// validates the endpoint, runs the BeforeConnect hooks, resolves dial targets,
// builds the client TLS config, and dials with 0-RTT enabled. It returns a
// Connecting holding the early QUIC connection, before afterHandshake runs.
//
// DialEarly attempts 0-RTT: if the session cache holds a valid ticket for
// addr.ID (bucketed by its SNI), the QUIC stack restores the session and
// DialEarly returns before the handshake completes, with the connection ready
// for 0-RTT early data. Without a ticket, DialEarly returns only once the
// handshake completes, exactly like Dial.
//
// The peer identity is the dialed addr.ID; the RFC 7250 VerifyConnection check
// enforces it once the handshake completes, so an early connection carries an
// asserted-but-not-yet-authenticated identity. Callers that sent 0-RTT data wait
// on [Conn.HandshakeComplete] and check [Conn.Used0RTT] to learn whether the
// server accepted the early data; on rejection the data must be resent.
func ( *Endpoint) ( context.Context,  netaddr.EndpointAddr,  string) (*Connecting, error) {
	if .isClosed() {
		return nil, ErrEndpointClosed
	}
	if .ID.Equal(.ID()) {
		return nil, ErrSelfConnect
	}
	if  := .beforeConnect(, , );  != nil {
		return nil, 
	}

	 := .dialTargets()
	if len() == 0 {
		return nil, ErrNoAddress
	}

	,  := clientTLSConfigWithCurves(.secretKey, .ID, []string{}, .sessionCache, .keyExchange.curves())
	if  != nil {
		return nil, 
	}
	.KeyLogWriter = .keyLogWriter

	if ,  := .Deadline(); ! {
		var  context.CancelFunc
		,  = context.WithTimeout(, ConnectTimeout)
		defer ()
	}

	var  error
	for ,  := range  {
		,  := .transport.DialEarly(, , , .quicConf)
		if  != nil {
			if  == nil {
				 = 
			}
			continue
		}
		return &Connecting{ep: , qc: , remoteID: .ID, addr: , alpn: }, nil
	}
	return nil, fmt.Errorf("iroh: connect to %s: %w", .ID, )
}

// Dial dials addr, negotiates alpn, opens a bidirectional stream, and returns it
// as a [net.Conn].
func ( *Endpoint) ( context.Context,  netaddr.EndpointAddr,  string) (net.Conn, error) {
	,  := .Connect(, , )
	if  != nil {
		return nil, 
	}
	,  := .OpenStreamConn()
	if  != nil {
		.CloseWithError(0, "")
		return nil, 
	}
	return , nil
}

// dialTargets returns the ordered net.Addr dial targets for addr: real UDP
// addresses for direct IPs, custom mapped addresses, then relay mapped
// addresses (when relays are enabled). Each mapped target is registered in the
// mapped-address table so the magic socket routes its QUIC packets to the
// selected transport.
func ( *Endpoint) ( netaddr.EndpointAddr) []net.Addr {
	var , ,  []net.Addr
	if !.disableIP {
		for ,  := range .IPAddrs() {
			 = append(, net.UDPAddrFromAddrPort())
		}
	}
	for ,  := range .Addrs() {
		,  := .(netaddr.CustomAddr)
		if ! {
			continue
		}
		 := .sock.CustomMappedAddrFor()
		 = append(, net.UDPAddrFromAddrPort(.AddrPort()))
	}
	if .relay != nil {
		for ,  := range .RelayURLs() {
			 := .sock.RelayMappedAddrFor(, .ID)
			 = append(, net.UDPAddrFromAddrPort(.AddrPort()))
		}
	}
	var  []net.Addr
	if .relayFirst {
		 = append(, ...)
		 = append(, ...)
	} else {
		 = append(, ...)
	}
	 = append(, ...)
	if !.relayFirst {
		 = append(, ...)
	}
	return 
}

// AcceptIncoming blocks until an incoming connection attempt arrives. The
// returned [Incoming] can be accepted, refused, retried, or ignored.
func ( *Endpoint) ( context.Context) (*Incoming, error) {
	if  := .acquireAcceptOwner(acceptOwnerAccept);  != nil {
		return nil, 
	}
	defer .releaseAcceptOwner(acceptOwnerAccept)
	return .acceptIncoming()
}

func ( *Endpoint) ( context.Context) (*Incoming, error) {
	.mu.Lock()
	 := .closed
	 := .listener
	.mu.Unlock()
	if  {
		return nil, ErrEndpointClosed
	}
	if  == nil {
		return nil, errors.New("iroh: no ALPNs configured; nothing to accept")
	}
	,  := .Accept()
	if  != nil {
		return nil, 
	}
	return &Incoming{ep: , qc: }, nil
}

// Accept blocks until an incoming connection completes its handshake, then
// returns it as a [Conn]. It returns an error if the endpoint is closed or has
// no configured ALPNs. ctx cancels the wait.
func ( *Endpoint) ( context.Context) (*Conn, error) {
	.metrics.acceptsStarted.Add(1)
	if  := .acquireAcceptOwner(acceptOwnerAccept);  != nil {
		.metrics.acceptsFailed.Add(1)
		return nil, 
	}
	defer .releaseAcceptOwner(acceptOwnerAccept)
	,  := .accept()
	if  != nil {
		.metrics.acceptsFailed.Add(1)
		return nil, 
	}
	.metrics.acceptsAccepted.Add(1)
	return , nil
}

func ( *Endpoint) ( context.Context) (*Conn, error) {
	for {
		,  := .acceptIncoming()
		if  != nil {
			return nil, 
		}
		,  := .Accept()
		if  != nil {
			return nil, 
		}
		,  := .Connection()
		if errors.Is(, ErrConnClosedDuringHandshake) {
			// A connection attempt dying before its handshake completes must
			// not tear down the acceptor; wait for the next incoming
			// connection instead.
			continue
		}
		if  != nil {
			return nil, 
		}
		return , nil
	}
}

func ( *Endpoint) ( context.Context,  *quic.Conn) (*Conn, error) {
	// The early listener returns connections before the handshake completes so
	// the QUIC stack can buffer 0-RTT early data. The peer's identity is only
	// authenticated once the handshake finishes, so wait for it before reading
	// the verified peer id and negotiated ALPN. Any 0-RTT streams are preserved
	// and surface through Accept{,Uni}Stream after this returns.
	select {
	case <-.HandshakeComplete():
		return .connFromHandshake(, )
	default:
	}
	select {
	case <-.HandshakeComplete():
	case <-.Context().Done():
		// The connection attempt died before completing its handshake
		// (e.g. handshake timeout). HandshakeComplete only closes on
		// success, so without this case the accept would block forever.
		return nil, fmt.Errorf("%w: %w", ErrConnClosedDuringHandshake, context.Cause(.Context()))
	case <-.Done():
		.CloseWithError(0, "")
		return nil, .Err()
	}
	return .connFromHandshake(, )
}

func ( *Endpoint) ( context.Context,  *quic.Conn) (*Conn, error) {
	,  := peerEndpointID(.ConnectionState().TLS)
	if  != nil {
		.CloseWithError(0, "bad peer certificate")
		return nil, 
	}
	 := .ConnectionState().TLS.NegotiatedProtocol
	,  := newConn(, , , SideServer, .connStableID())
	if  != nil {
		return nil, 
	}
	.pathState, .pathConn = .registerConn(, , netaddr.NewEndpointAddr())
	if  := .afterHandshake(, );  != nil {
		.CloseWithError(0, "rejected by hook")
		return nil, 
	}
	return , nil
}

func ( *Endpoint) ( context.Context,  netaddr.EndpointAddr,  string) error {
	for ,  := range .hooks {
		if  := .BeforeConnect(, , );  != nil {
			return 
		}
	}
	return nil
}

func ( *Endpoint) ( context.Context,  *Conn) error {
	for ,  := range .hooks {
		 := .AfterHandshake(, )
		if  != nil {
			var  *HandshakeRejectError
			if errors.As(, &) {
				if  := .CloseWithError(.Code, .Reason);  != nil {
					return 
				}
				return fmt.Errorf("%w: %w", ErrHandshakeRejected, )
			}
			return 
		}
	}
	return nil
}

// registerConn registers an established QUIC connection with the per-remote
// state actor for remote, so the actor tracks its path and selects between
// available paths. Registration failures are non-fatal: the connection still
// works; it just is not path-managed. It mirrors the Rust RemoteMap::add_connection
// (iroh/src/socket/remote_map.rs:273).
func ( *Endpoint) ( key.EndpointID,  *quic.Conn,  netaddr.EndpointAddr) (*socket.RemoteStateActor, *connAdapter) {
	if .remotes == nil {
		return nil, nil
	}
	if .ID.IsZero() || !.ID.Equal() {
		 = netaddr.NewEndpointAddr()
	}
	 := .sock.PathAddr(, .RemoteAddr())
	 := newConnAdapter(, )
	if .Kind() == socket.AddrIP {
		for ,  := range .RelayURLs() {
			 := .sock.RelayMappedAddrFor(, )
			.SetMigrationFallbackRemote(net.UDPAddrFromAddrPort(.AddrPort()))
			break
		}
	}
	,  := .remotes.AddConnectionActor(, )
	go func() {
		_ = .remotes.ResolveRemote()
	}()
	if !.ConnectionState().MultipathNegotiated {
		return , 
	}
	// Candidate seeding is opportunistic: QNT may still be disabled or
	// incomplete, and path management must not make an otherwise-established
	// connection fail. The actor/qng layers keep the failure visible to explicit
	// hole-punch calls.
	_ = .AddNATTraversalAddresses(.localNATTraversalCandidates())
	_ = .AddRemoteNATTraversalAddresses(.IPAddrs())
	// Punch as soon as a remote candidate is known instead of waiting for
	// the 60s upgrade tick: immediately when the dial carried IP addresses
	// (the seed above closed the channel), or when the server's first
	// ADD_ADDRESS lands after a relay-won dial. The server side of QNT
	// receives no ADD_ADDRESS and parks here until the connection closes.
	go func() {
		select {
		case <-.NATTraversalRemoteAddrsReady():
			_ = .TriggerHolepunchConn()
		case <-.Context().Done():
		}
	}()
	return , 
}

func ( *Endpoint) ( *quic.Conn) uint64 {
	if  == nil {
		return 0
	}
	.mu.Lock()
	if ,  := .stableIDs[];  {
		.mu.Unlock()
		return 
	}
	.nextStable++
	 := .nextStable
	.stableIDs[] = 
	.mu.Unlock()
	go .removeStableIDWhenClosed()
	return 
}

func ( *Endpoint) ( *quic.Conn) {
	<-.Context().Done()
	.mu.Lock()
	delete(.stableIDs, )
	.mu.Unlock()
}

// resolveFunc returns the address-lookup hook the RemoteMap actors use to
// resolve additional addresses for a remote, or nil when no lookup services are
// configured. It adapts the iroh AddressLookupServices stream to the socket
// package's ResolveFunc, so internal/socket does not import iroh.
func ( *Endpoint) () socket.ResolveFunc {
	 := .lookup
	if  == nil {
		return nil
	}
	return func( context.Context,  key.EndpointID) iter.Seq2[socket.ResolvedAddr, error] {
		return func( func(socket.ResolvedAddr, error) bool) {
			for ,  := range .Resolve(, ) {
				if  != nil {
					if !(socket.ResolvedAddr{}, ) {
						return
					}
					continue
				}
				for ,  := range .Addr().Addrs() {
					if !(socket.ResolvedAddr{
						Addr:       ,
						Provenance: .Provenance(),
					}, nil) {
						return
					}
				}
			}
		}
	}
}

// Shutdown shuts down the endpoint: it stops accepting, closes the QUIC
// transport, and releases the UDP socket. In-flight connections are not
// forcibly closed.
func ( *Endpoint) ( context.Context) error {
	.mu.Lock()
	if .closed {
		.mu.Unlock()
		return nil
	}
	.closed = true
	close(.closedCh)
	.mu.Unlock()

	var  error
	if .listener != nil {
		if  := .listener.Close();  != nil {
			 = 
		}
	}
	// Stop the magic socket's recv loop, then close the QUIC transport (which
	// closes the MagicConn and, through it, the UDP socket).
	.serveStop()
	if  := .transport.Close();  != nil &&  == nil {
		 = 
	}
	if .udp != nil {
		if  := .udp.Close();  != nil &&  == nil && !errors.Is(, net.ErrClosed) {
			 = 
		}
	}
	return 
}

// Closed returns a channel closed when the endpoint is closed.
func ( *Endpoint) () <-chan struct{} { return .closedCh }

func ( *Endpoint) () bool {
	.mu.Lock()
	defer .mu.Unlock()
	return .closed
}

func ( *Endpoint) ( acceptOwner) error {
	.mu.Lock()
	defer .mu.Unlock()
	if .closed {
		return ErrEndpointClosed
	}
	if .acceptOwner != acceptOwnerNone {
		return ErrEndpointAcceptLoopInUse
	}
	.acceptOwner = 
	return nil
}

func ( *Endpoint) ( acceptOwner) {
	.mu.Lock()
	defer .mu.Unlock()
	if .acceptOwner ==  {
		.acceptOwner = acceptOwnerNone
	}
}