package netreportimport (itlsquic)// IfStateDetails describes the host's interface capabilities, used to decide// when enough probes have completed. It is a port of net_report's// IfStateDetails (iroh/src/net_report/reportgen.rs:74).typeIfStateDetailsstruct {// HaveV4 reports whether the host has IPv4 connectivity. HaveV4 bool// HaveV6 reports whether the host has IPv6 connectivity. HaveV6 bool}// Client runs net_report probes against a set of relays and tracks report// history so it can apply preferred-relay hysteresis across runs. It is a port// of net_report::Client (iroh/src/net_report.rs:90).//// A Client is safe for concurrent use; GetReport serializes internally.//// The zero value is not usable; construct a Client with [NewClient].typeClientstruct { relayMap *relay.Map// dnsResolver resolves relay hostnames for HTTPS and QAD probes. If nil, // net.DefaultResolver is used. dnsResolver *net.Resolver// tlsConfig overrides TLS verification for HTTPS probes (used in tests). tlsConfig *tls.Config// qadTLS supplies the QAD QUIC TLS verification policy. If nil, the relay's // WebPKI certificate is verified against the system roots. Tests set a // config with InsecureSkipVerify to trust a self-signed relay. qadTLS *itls.Config// quicConfig overrides QAD transport defaults; if nil, defaultQADConfig is // used. quicConfig *quic.Config// qadDialer, if non-nil, opens QAD probe connections; see WithQADDialer. qadDialer QADDialer// now returns the current time; overridable in tests for deterministic // hysteresis-window pruning. now func() time.Time mu sync.Mutex last *Report// the most recent report prev map[time.Time]*Report// reports within reportHistoryMaxAge lastFull time.Time// when the last full report was generated}// NewClient returns a Client that probes the relays in relayMap.func ( *relay.Map) *Client {if == nil { = relay.NewMap() }return &Client{relayMap: ,now: time.Now,prev: map[time.Time]*Report{}, }}// WithDNSResolver sets the resolver used to look up relay hostnames.func ( *Client) ( *net.Resolver) *Client { .dnsResolver = return}// WithTLSConfig sets the TLS configuration used for HTTPS probes. It is used in// tests to trust self-signed relay certificates.func ( *Client) ( *tls.Config) *Client { .tlsConfig = return}// WithQUICConfig sets the QAD QUIC transport configuration.func ( *Client) ( *quic.Config) *Client { .quicConfig = return}// WithQADTLSConfig sets the TLS verification policy for QAD QUIC connections.// It is used in tests to trust a self-signed relay certificate.func ( *Client) ( *itls.Config) *Client { .qadTLS = return}// QADDialer opens the QUIC connection for one QAD probe to addr; tlsConf// already carries the QAD ALPN and server name.typeQADDialerfunc(ctx context.Context, addr netip.AddrPort, tlsConf *itls.Config, cfg *quic.Config) (*quic.Conn, error)// WithQADDialer routes QAD probes through d instead of a private per-probe// UDP socket, so the observed address is the mapping of the dialer's own// socket. A per-probe socket's mapping dies with it and its port is nobody's// dial candidate; per-probe mappings also differ between relays, which makes// MappingVariesByDest misreport symmetric NAT.func ( *Client) ( QADDialer) *Client { .qadDialer = return}// dialQAD opens the QAD connection for a probe to addr, via the configured// dialer or a private per-probe socket.func ( *Client) ( context.Context, netip.AddrPort, string) (*qadConn, error) {if .qadDialer == nil {returnnewQADClient(, , .qadTLS, .quicConfig) } := .quicConfigif == nil { = defaultQADConfig() } , := context.WithTimeout(, probesTimeout)defer () , := .qadDialer(, , qadTLSConfig(, .qadTLS), )if != nil {returnnil, }// ownsTransport stays false: the dialer's transport outlives the probe.return &qadConn{conn: }, nil}// GetReport runs a single net_report. doFull forces a full report (captive// portal check and reset probe history); otherwise the report is full only if// no report has been generated within [fullReportInterval]. ifState informs the// sufficiency check.//// The whole call is bounded by [overallReportTimeout]; the probes within it are// bounded by [probesTimeout].//// If the relay map is empty the report is empty: no probes run and// PreferredRelay is the zero value.func ( *Client) ( context.Context, IfStateDetails, bool) (*Report, error) { , := context.WithTimeout(, overallReportTimeout)defer () .mu.Lock() := || .last == nil || .now().Sub(.lastFull) >= fullReportInterval := .relayMap .mu.Unlock() := &Report{Full: }if !.IsEmpty() { .runProbes(, , )// The captive-portal check runs only on full reports, and only after a // short delay so good QAD probes finish first // (iroh/src/net_report/reportgen.rs:278).if { .runCaptivePortal(, , ) } } .addReportHistoryAndSetPreferredRelay() .mu.Lock()if { .lastFull = .now() } .mu.Unlock()return , .Err()}// runProbes runs the HTTPS and QAD probes for each relay in parallel, bounded// by probesTimeout, and folds the results into report. QAD probes run on at// most maxRelays relays.func ( *Client) ( context.Context, *relay.Map, *Report) { , := context.WithTimeout(, probesTimeout)defer () := .Configs()var (sync.WaitGroupsync.Mutex []*probeReport ) := func( *probeReport) {if == nil {return } .Lock() = append(, ) .Unlock() } := 0for , := range { := // HTTPS probe for every relay. .Add(1)gofunc() {defer .Done() , := runHTTPSProbe(, .URL, .tlsConfig)if == nil { () } }()// QAD probes (v4 and v6) for up to maxRelays relays that enable QUIC.if .QUIC != nil && < maxRelays { ++ .Add(2)gofunc() {defer .Done() (.runQADProbe(, , ProbeQADv4)) }()gofunc() {defer .Done() (.runQADProbe(, , ProbeQADv6)) }() } } := make(chanstruct{})gofunc() { .Wait(); close() }()select {case<-:case<-.Done(): }// Fold results in a stable order so a report is deterministic given the // same probe outcomes. .Lock()sort.Slice(, func(, int) bool {if [].probe != [].probe {return [].probe < [].probe }return [].relay.Compare([].relay) < 0 })for , := range { .update() } .Unlock()}// runQADProbe resolves the relay's QUIC address for the given family, opens a// QAD connection, records its RTT, captures any observed-address report that has// already arrived, and gracefully closes it. If no report is available yet, the// probe is latency-only (see the package doc).func ( *Client) ( context.Context, relay.Config, Probe) *probeReport { := .URL.Host()if == "" {returnnil } := defaultRelayQuicPortif .QUIC != nil && .QUIC.Port != 0 { = int(.QUIC.Port) } , := .resolveQADAddr(, , , )if ! {returnnil } , := .dialQAD(, , )if != nil {returnnil }defer .close(qadCloseCode, qadCloseReason)// Read the connection RTT (iroh-relay/src/quic.rs:345) and the relay's // observed-address report, which observedAddr waits briefly for because it // is sent just after the handshake this dial already completed. It returns // ErrExtensionNotNegotiated when the relay does not report or none arrives // in time, in which case the probe is latency-only. := .rtt(0) , := .observedAddr() := &probeReport{probe: , relay: .URL, latency: }if == nil { .addr = }return}// resolveQADAddr resolves host to an address of the family implied by probe and// returns it with port. It returns ok=false if no matching address is found.func ( *Client) ( context.Context, string, int, Probe) (netip.AddrPort, bool) {if , := netip.ParseAddr(); == nil {if !addrMatchesProbe(, ) {returnnetip.AddrPort{}, false }returnnetip.AddrPortFrom(, uint16()), true } , := lookupIPStaggered(, .dnsResolver, )if != nil {returnnetip.AddrPort{}, false }for , := range { , := netip.AddrFromSlice(.IP)if ! {continue } = .Unmap()ifaddrMatchesProbe(, ) {returnnetip.AddrPortFrom(, uint16()), true } }returnnetip.AddrPort{}, false}func addrMatchesProbe( netip.Addr, Probe) bool {switch {caseProbeQADv4:return .Is4()caseProbeQADv6:return .Is6() && !.Is4In6()default:returnfalse }}// runCaptivePortal runs the captive-portal check against one relay after a// short delay, bounded by captivePortalTimeout, and records the result in// report.CaptivePortal. iroh/src/net_report/reportgen.rs:614.func ( *Client) ( context.Context, *relay.Map, *Report) {select {case<-time.After(captivePortalDelay):case<-.Done():return } := .URLs()iflen() == 0 {return } , := context.WithTimeout(, captivePortalTimeout)defer () , := checkCaptivePortal(, [0], .tlsConfig)if != nil {return } .CaptivePortal = boolPtr()}// addReportHistoryAndSetPreferredRelay adds r to the recent-report history,// prunes reports older than reportHistoryMaxAge, and sets r.PreferredRelay to// the relay with the best recent latency, applying hysteresis. It is a port of// add_report_history_and_set_preferred_relay (iroh/src/net_report.rs:698).func ( *Client) ( *Report) { .mu.Lock()defer .mu.Unlock()varnetaddr.RelayURLif .last != nil { = .last.PreferredRelay// Carry forward mapping-varies info when this report lacks it.if .MappingVariesByDestV4 == nil { .MappingVariesByDestV4 = .last.MappingVariesByDestV4 }if .MappingVariesByDestV6 == nil { .MappingVariesByDestV6 = .last.MappingVariesByDestV6 } } := .now()// Best recent latency per relay across the history window and this report.varRelayLatenciesfor , := range .prev {if .Sub() > reportHistoryMaxAge {delete(.prev, )continue } .merge(&.RelayLatency) } .merge(&.RelayLatency)// Pick the currently-alive relay with the best recent latency, recording // the old preferred relay's current latency for the hysteresis check.vartime.Durationvartime.Duration// Iterate this report's relays in a deterministic order. := .RelayLatency.relays()sort.Slice(, func(, int) bool { return [].Compare([]) < 0 }) := falsefor , := range { , := .RelayLatency.get()if !.IsZero() && .Equal() { = } , := .get()if && (! || < ) { = .PreferredRelay = = true } }// Hysteresis: if we changed away from a still-responsive old relay but the // new one is not at least 1/3 faster, stick with the old one. // iroh/src/net_report.rs:760.if !.IsZero() && !.PreferredRelay.Equal() && != 0 && > /3*2 { .PreferredRelay = } := * .prev[] = & := * .last = &}
The pages are generated with Goldsv0.8.4. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.