package iroh
import (
"context"
"errors"
"fmt"
"io"
"iter"
"net"
"net/netip"
"slices"
"sync"
"time"
"github.com/tmc/go-iroh/dns"
itls "github.com/tmc/go-iroh/internal/itls/tls"
"github.com/tmc/go-iroh/internal/netreport"
"github.com/tmc/go-iroh/internal/portmapper"
quic "github.com/tmc/go-iroh/internal/qng"
"github.com/tmc/go-iroh/internal/qng/qlog"
"github.com/tmc/go-iroh/internal/socket"
"github.com/tmc/go-iroh/key"
"github.com/tmc/go-iroh/netaddr"
"github.com/tmc/go-iroh/relay"
"github.com/tmc/go-iroh/watch"
)
type Endpoint struct {
secretKey key .SecretKey
alpns []string
udp *net .UDPConn
sock *socket .Socket
magic *socket .MagicConn
relay *socket .RelayTransport
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 *socket .RemoteMap
lookup *AddressLookupServices
mu sync .Mutex
closed bool
closedCh chan struct {}
acceptOwner acceptOwner
addrWatch *watch .Value [netaddr .EndpointAddr ]
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
)
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
}
type Option func (*config ) error
type netReportRunner func (context .Context ) (*netreport .Report , error )
type BindOpts struct {
PrefixLen uint8
IsRequired bool
IsDefaultRoute *bool
}
type QUICTransportConfig struct {
KeepAlivePeriod time .Duration
MaxIdleTimeout time .Duration
InitialPacketSize uint16
MaxIncomingStreams int64
}
func WithSecretKey (sk key .SecretKey ) Option {
return func (c *config ) error {
c .secretKey = sk
c .haveKey = true
return nil
}
}
func WithALPNs (alpns ...string ) Option {
return func (c *config ) error {
c .alpns = append (c .alpns , alpns ...)
return nil
}
}
func WithSourceAddressValidation (f func (net .Addr ) bool ) Option {
return func (c *config ) error {
c .verifySource = f
return nil
}
}
func WithBindAddr (addr netip .AddrPort ) Option {
return func (c *config ) error {
c .bindAddr = addr
c .bindOpts = BindOpts {}
c .haveBindAddr = true
return nil
}
}
func WithBindAddrOpts (addr netip .AddrPort , opts BindOpts ) Option {
return func (c *config ) error {
if err := validateBindOpts (addr , opts ); err != nil {
return err
}
c .bindAddr = addr
c .bindOpts = opts
c .haveBindAddr = true
return nil
}
}
func validateBindOpts(addr netip .AddrPort , opts BindOpts ) error {
if !addr .IsValid () {
return errors .New ("iroh: invalid bind address" )
}
if addr .Addr ().Is4 () {
if opts .PrefixLen > 32 {
return fmt .Errorf ("iroh: invalid IPv4 bind prefix length %d" , opts .PrefixLen )
}
return nil
}
if opts .PrefixLen > 128 {
return fmt .Errorf ("iroh: invalid IPv6 bind prefix length %d" , opts .PrefixLen )
}
return nil
}
func WithoutIPTransports () Option {
return func (c *config ) error {
c .disableIP = true
return nil
}
}
func WithoutRelayTransports () Option {
return func (c *config ) error {
c .relayMode = relay .ModeDisabled ()
return nil
}
}
func WithRelayFirstDial () Option {
return func (c *config ) error {
c .relayFirst = true
return nil
}
}
func WithAddressLookup (s *AddressLookupServices ) Option {
return func (c *config ) error {
c .lookup = s
return nil
}
}
func WithDNSResolver (r *dns .Resolver ) Option {
return func (c *config ) error {
if c .lookup == nil {
c .lookup = &AddressLookupServices {}
}
c .lookup .AddResolver (NewDNSAddressLookup (dns .N0DNSEndpointOriginProd , r ))
return nil
}
}
func WithRelayMode (mode relay .Mode ) Option {
return func (c *config ) error {
c .relayMode = mode
return nil
}
}
func WithNetReport () Option {
return func (c *config ) error {
c .enableNetReport = true
return nil
}
}
func WithNATPMP (gateway netip .Addr ) Option {
return func (c *config ) error {
if !gateway .IsValid () || !gateway .Is4 () {
return errors .New ("iroh: invalid nat-pmp gateway" )
}
c .natPMP = true
c .natPMPGateway = gateway
return nil
}
}
func withNATPMPPort(port uint16 ) Option {
return func (c *config ) error {
c .natPMPPort = port
return nil
}
}
func WithKeyLogWriter (w io .Writer ) Option {
return func (c *config ) error {
c .keyLogWriter = w
return nil
}
}
func WithKeyExchangePolicy (policy KeyExchangePolicy ) Option {
return func (c *config ) error {
if !policy .valid () {
return fmt .Errorf ("iroh: invalid key exchange policy %d" , policy )
}
c .keyExchange = policy
return nil
}
}
func WithHooks (h EndpointHooks ) Option {
return func (c *config ) error {
if h != nil {
c .hooks = append (c .hooks , h )
}
return nil
}
}
func WithTransportConfig (tc *QUICTransportConfig ) Option {
return func (c *config ) error {
c .transportConfig = tc
return nil
}
}
func WithPathSelector (selector PathSelector ) Option {
return func (c *config ) error {
if selector != nil {
c .pathSelector = pathSelectorAdapter {selector : selector }
}
return nil
}
}
func WithCustomTransport (t CustomTransport ) Option {
return func (c *config ) error {
if t != nil {
c .custom = append (c .custom , t )
}
return nil
}
}
func Bind (ctx context .Context , opts ...Option ) (*Endpoint , error ) {
var c config
for _ , opt := range opts {
if err := opt (&c ); err != nil {
return nil , err
}
}
if c .netReportEvery == 0 {
c .netReportEvery = 5 * time .Minute
}
if !c .haveKey {
sk , err := key .GenerateSecretKey ()
if err != nil {
return nil , fmt .Errorf ("iroh: generate key: %w" , err )
}
c .secretKey = sk
}
bind := c .bindAddr
if !c .haveBindAddr {
bind = netip .AddrPortFrom (netip .IPv6Unspecified (), 0 )
}
udp , err := bindPacketConn (c , bind )
if err != nil {
return nil , fmt .Errorf ("iroh: bind udp: %w" , err )
}
quicConf := &quic .Config {
KeepAlivePeriod : HeartbeatInterval ,
MaxIdleTimeout : RelayPathMaxIdleTimeout ,
EnableDatagrams : true ,
InitialMaxPathID : initialMaxPathID (),
MaxRemoteNATTraversalAddresses : maxRemoteNATTraversalAddresses (),
Tracer : qlog .DefaultConnectionTracer ,
Allow0RTT : true ,
TokenStore : quic .NewLRUTokenStore (32 , 8 ),
}
if c .transportConfig != nil {
if c .transportConfig .KeepAlivePeriod != 0 {
quicConf .KeepAlivePeriod = c .transportConfig .KeepAlivePeriod
}
if c .transportConfig .MaxIdleTimeout != 0 {
quicConf .MaxIdleTimeout = c .transportConfig .MaxIdleTimeout
}
if c .transportConfig .InitialPacketSize != 0 {
quicConf .InitialPacketSize = c .transportConfig .InitialPacketSize
}
if c .transportConfig .MaxIncomingStreams != 0 {
quicConf .MaxIncomingStreams = c .transportConfig .MaxIncomingStreams
}
}
sock := socket .NewSocket ()
var relayActor *socket .RelayActor
relayMap := c .relayMode .Map ()
if !relayMap .IsEmpty () {
relayActor = socket .NewRelayActor (socket .RelayActorConfig {
SecretKey : c .secretKey ,
Map : relayMap ,
})
}
custom := customTransportAdapters (c .custom )
var magic *socket .MagicConn
if udp == nil {
magic = socket .NewMagicConnRelayOnly (sock , relayActor , custom ...)
} else {
magic = socket .NewMagicConnWithTransports (sock , udp , relayActor , custom ...)
}
serveCtx , serveStop := context .WithCancel (context .Background ())
go magic .Serve (serveCtx )
ep := &Endpoint {
secretKey : c .secretKey ,
alpns : slices .Clone (c .alpns ),
udp : udp ,
sock : sock ,
magic : magic ,
relay : magic .Relay (),
serveStop : serveStop ,
transport : &quic .Transport {
Conn : magic ,
ConnectionIDLength : 8 ,
VerifySourceAddress : c .verifySource ,
},
quicConf : quicConf ,
keyLogWriter : c .keyLogWriter ,
keyExchange : c .keyExchange ,
sessionCache : NewSessionCache (),
disableIP : c .disableIP || udp == nil ,
relayFirst : c .relayFirst ,
verifySource : c .verifySource ,
hooks : append ([]EndpointHooks (nil ), c .hooks ...),
custom : append ([]CustomTransport (nil ), c .custom ...),
lookup : c .lookup ,
closedCh : make (chan struct {}),
stableIDs : make (map [*quic .Conn ]uint64 ),
}
ep .netReport = endpointNetReportRunner (c , relayMap , ep .qadDialer ())
ep .remotes = socket .NewRemoteMapWithMetrics (serveCtx , c .pathSelector , ep .resolveFunc (), magic .MetricsSet ())
if ep .disableIP {
ep .remotes .DisableHolepunch ()
}
ep .remotes .SetOnEvict (sock .EvictRemote )
ep .magic .SetEndpointSender (func (id key .EndpointID , p []byte ) bool {
err := ep .remotes .Actor (id ).SendDatagram (p , func (addr socket .Addr , data []byte ) bool {
return ep .magic .SendAddr (addr , data )
})
return err == nil
})
if ep .relay != nil {
if urls := relayMap .URLs (); len (urls ) > 0 {
ep .relay .SetHomeRelay (urls [0 ])
}
}
if len (c .alpns ) > 0 {
if err := ep .startListener (); err != nil {
serveStop ()
if udp != nil {
udp .Close ()
}
return nil , err
}
}
ep .addrWatch = watch .NewValueFunc (ep .Addr (), endpointAddrEqual )
if ep .netReport != nil {
go ep .runNetReport (serveCtx , c .netReportEvery )
}
if c .natPMP {
go ep .runNATPMP (serveCtx , c .natPMPGateway , c .natPMPPort )
}
return ep , nil
}
func (e *Endpoint ) setSourceAddressValidation (f func (net .Addr ) bool ) {
e .mu .Lock ()
defer e .mu .Unlock ()
e .verifySource = f
e .transport .VerifySourceAddress = f
}
func (e *Endpoint ) sourceAddressValidation () func (net .Addr ) bool {
e .mu .Lock ()
defer e .mu .Unlock ()
return e .verifySource
}
func endpointNetReportRunner(c config , relayMap *relay .Map , dialer netreport .QADDialer ) netReportRunner {
if c .netReport != nil {
return c .netReport
}
if !c .enableNetReport || relayMap .IsEmpty () {
return nil
}
client := netreport .NewClient (relayMap )
if dialer != nil {
client = client .WithQADDialer (dialer )
}
return func (ctx context .Context ) (*netreport .Report , error ) {
return client .GetReport (ctx , netreport .IfStateDetails {HaveV4 : true , HaveV6 : true }, false )
}
}
func (e *Endpoint ) qadDialer () netreport .QADDialer {
if e .udp == nil {
return nil
}
return func (ctx context .Context , addr netip .AddrPort , tlsConf *itls .Config , cfg *quic .Config ) (*quic .Conn , error ) {
return e .transport .Dial (ctx , net .UDPAddrFromAddrPort (addr ), tlsConf , cfg )
}
}
func initialMaxPathID() *uint32 {
v := uint32 (MaxMultipathPaths )
return &v
}
func maxRemoteNATTraversalAddresses() *uint8 {
v := uint8 (MaxQNTAddresses )
return &v
}
func (e *Endpoint ) startListener () error {
serverTLS , err := serverTLSConfigWithCurves (e .secretKey , e .alpns , e .keyExchange .curves ())
if err != nil {
return err
}
serverTLS .KeyLogWriter = e .keyLogWriter
ln , err := e .transport .ListenEarly (serverTLS , e .quicConf )
if err != nil {
return fmt .Errorf ("iroh: listen: %w" , err )
}
e .listener = ln
return nil
}
func (e *Endpoint ) SetALPNs (alpns []string ) error {
return e .setALPNs (alpns , acceptOwnerNone )
}
func (e *Endpoint ) setALPNs (alpns []string , allowedOwner acceptOwner ) error {
e .mu .Lock ()
defer e .mu .Unlock ()
if e .closed {
return ErrEndpointClosed
}
if e .acceptOwner != acceptOwnerNone && e .acceptOwner != allowedOwner {
return ErrEndpointAcceptLoopInUse
}
next := slices .Clone (alpns )
if e .listener != nil {
if err := e .listener .Close (); err != nil {
return fmt .Errorf ("iroh: close listener: %w" , err )
}
e .listener = nil
}
prev := e .alpns
e .alpns = next
if err := e .startListener (); err != nil {
e .alpns = prev
return err
}
return nil
}
func (e *Endpoint ) ID () key .EndpointID { return e .secretKey .Public ().EndpointID () }
func (e *Endpoint ) SecretKey () key .SecretKey { return e .secretKey }
func (e *Endpoint ) LocalAddr () netip .AddrPort {
if e .udp == nil {
return netip .AddrPort {}
}
return e .udp .LocalAddr ().(*net .UDPAddr ).AddrPort ()
}
func (e *Endpoint ) externalNATLocked () []netip .AddrPort {
out := append ([]netip .AddrPort (nil ), e .externalPinned ...)
for _ , addr := range e .externalDiscovered {
out = appendUniqueNATTraversalCandidate (out , addr )
}
return out
}
func (e *Endpoint ) localNATTraversalCandidates () []netip .AddrPort {
var addrs []netip .AddrPort
if e .disableIP {
return addrs
}
if addr , ok := canonicalNATTraversalCandidate (e .LocalAddr ()); ok {
addrs = appendUniqueNATTraversalCandidate (addrs , addr )
}
e .mu .Lock ()
external := e .externalNATLocked ()
e .mu .Unlock ()
for _ , addr := range external {
addrs = appendUniqueNATTraversalCandidate (addrs , addr )
}
return addrs
}
func (e *Endpoint ) setExternalNATTraversalCandidates (addrs ...netip .AddrPort ) bool {
var next []netip .AddrPort
for _ , addr := range addrs {
next = appendUniqueNATTraversalCandidate (next , addr )
}
e .mu .Lock ()
if equalAddrPorts (e .externalDiscovered , next ) {
e .mu .Unlock ()
return false
}
e .externalDiscovered = next
e .updateAddrWatchLocked ()
e .mu .Unlock ()
e .advertiseNATTraversalCandidates ()
return true
}
func (e *Endpoint ) AddExternalAddr (addr netip .AddrPort ) {
if e .disableIP {
return
}
e .mu .Lock ()
next := appendUniqueNATTraversalCandidate (append ([]netip .AddrPort (nil ), e .externalPinned ...), addr )
if equalAddrPorts (e .externalPinned , next ) {
e .mu .Unlock ()
return
}
e .externalPinned = next
e .updateAddrWatchLocked ()
e .mu .Unlock ()
e .advertiseNATTraversalCandidates ()
}
func (e *Endpoint ) RemoveExternalAddr (addr netip .AddrPort ) bool {
if e .disableIP {
return false
}
addr , ok := canonicalNATTraversalCandidate (addr )
if !ok {
return false
}
e .mu .Lock ()
i := slices .Index (e .externalPinned , addr )
if i < 0 {
e .mu .Unlock ()
return false
}
e .externalPinned = slices .Delete (e .externalPinned , i , i +1 )
e .updateAddrWatchLocked ()
e .mu .Unlock ()
e .advertiseNATTraversalCandidates ()
return true
}
func (e *Endpoint ) applyNetReport (report netreport .Report ) bool {
publicReport := netReportFromInternal (report )
e .mu .Lock ()
e .lastReport = &publicReport
e .mu .Unlock ()
changed := e .setExternalNATTraversalCandidates (report .GlobalV4 , report .GlobalV6 )
if e .relay != nil && !report .PreferredRelay .IsZero () {
current := e .relay .HomeRelayStatus ().Current ()
if current == nil || !current .URL .Equal (report .PreferredRelay ) {
e .relay .SetHomeRelay (report .PreferredRelay )
if e .magic != nil {
e .magic .RecordRelayHomeChange ()
}
e .mu .Lock ()
e .updateAddrWatchLocked ()
e .mu .Unlock ()
changed = true
}
}
return changed
}
func (e *Endpoint ) NetReport () (NetReport , bool ) {
e .mu .Lock ()
defer e .mu .Unlock ()
if e .lastReport == nil {
return NetReport {}, false
}
return e .lastReport .clone (), true
}
func (e *Endpoint ) RemoteInfo (remote key .EndpointID ) (RemoteInfo , bool ) {
if e == nil || e .remotes == nil {
return RemoteInfo {}, false
}
info , ok := e .remotes .RemoteInfo (remote )
if !ok {
return RemoteInfo {}, false
}
return remoteInfoFromSocket (info ), true
}
func (e *Endpoint ) refreshNetReport (ctx context .Context ) error {
if e .netReport == nil {
return nil
}
report , err := e .netReport (ctx )
if report != nil {
e .metrics .netReportReports .Add (1 )
if report .Full {
e .metrics .netReportReportsFull .Add (1 )
e .metrics .netReportPortmapAttempts .Add (1 )
}
if e .applyNetReport (*report ) {
e .metrics .netReportPortmapExternalAddressUpdated .Add (1 )
}
}
if err != nil {
e .metrics .netReportFailed .Add (1 )
return fmt .Errorf ("iroh: netreport: %w" , err )
}
return nil
}
func (e *Endpoint ) runNetReport (ctx context .Context , interval time .Duration ) {
if interval <= 0 {
interval = 5 * time .Minute
}
_ = e .refreshNetReport (ctx )
t := time .NewTicker (interval )
defer t .Stop ()
for {
select {
case <- ctx .Done ():
return
case <- t .C :
_ = e .refreshNetReport (ctx )
}
}
}
func (e *Endpoint ) runNATPMP (ctx context .Context , gateway netip .Addr , port uint16 ) {
if e .disableIP {
return
}
local := e .LocalAddr ()
if !local .IsValid () || local .Port () == 0 {
return
}
client := portmapper .NATPMPClient {
Gateway : gateway ,
Port : port ,
Timeout : 2 * time .Second ,
}
const requestedLifetime = time .Hour
internalPort := local .Port ()
var current netip .AddrPort
defer func () {
if current .IsValid () {
e .RemoveExternalAddr (current )
deleteCtx , cancel := context .WithTimeout (context .Background (), 2 *time .Second )
defer cancel ()
_, _ = client .MapUDP (deleteCtx , internalPort , current .Port (), 0 )
}
}()
for {
mapping , err := client .MapUDP (ctx , internalPort , internalPort , requestedLifetime )
if err == nil && mapping .ExternalAddr .IsValid () {
if current .IsValid () && current != mapping .ExternalAddr {
e .RemoveExternalAddr (current )
}
current = mapping .ExternalAddr
e .AddExternalAddr (current )
e .metrics .netReportPortmapAttempts .Add (1 )
e .metrics .netReportPortmapExternalAddressUpdated .Add (1 )
} else if err != nil {
e .metrics .netReportFailed .Add (1 )
}
wait := requestedLifetime / 2
if err == nil && mapping .Lifetime > 0 {
wait = mapping .Lifetime / 2
}
if wait < 30 *time .Second {
wait = 30 * time .Second
}
t := time .NewTimer (wait )
select {
case <- ctx .Done ():
t .Stop ()
return
case <- t .C :
}
}
}
func (e *Endpoint ) advertiseNATTraversalCandidates () {
if e .remotes == nil {
return
}
candidates := e .localNATTraversalCandidates ()
e .remotes .AddNATTraversalAddresses (candidates )
}
func canonicalNATTraversalCandidate(addr netip .AddrPort ) (netip .AddrPort , bool ) {
if !addr .IsValid () || addr .Port () == 0 || addr .Addr ().IsUnspecified () {
return netip .AddrPort {}, false
}
return netip .AddrPortFrom (addr .Addr ().Unmap (), addr .Port ()), true
}
func appendUniqueNATTraversalCandidate(addrs []netip .AddrPort , addr netip .AddrPort ) []netip .AddrPort {
addr , ok := canonicalNATTraversalCandidate (addr )
if !ok {
return addrs
}
for _ , a := range addrs {
if a == addr {
return addrs
}
}
return append (addrs , addr )
}
func equalAddrPorts(a , b []netip .AddrPort ) bool {
if len (a ) != len (b ) {
return false
}
for i := range a {
if a [i ] != b [i ] {
return false
}
}
return true
}
func (e *Endpoint ) Addr () netaddr .EndpointAddr {
a := netaddr .NewEndpointAddr (e .ID ())
if !e .disableIP {
if addr , ok := canonicalNATTraversalCandidate (e .LocalAddr ()); ok {
a = a .WithIP (addr )
}
}
e .mu .Lock ()
external := e .externalNATLocked ()
e .mu .Unlock ()
if !e .disableIP {
for _ , addr := range external {
a = a .WithIP (addr )
}
}
for _ , addr := range e .localCustomAddrs (context .Background ()) {
a = a .WithAddrs (addr )
}
if e .relay != nil {
if st := e .relay .HomeRelayStatus ().Current (); st != nil {
a = a .WithRelayURL (st .URL )
}
}
return a
}
func (e *Endpoint ) WatchAddr () watch .Observer [netaddr .EndpointAddr ] {
e .mu .Lock ()
defer e .mu .Unlock ()
if e .addrWatch == nil {
e .addrWatch = watch .NewValueFunc (e .addrLocked (), endpointAddrEqual )
}
return e .addrWatch .Watch ()
}
func (e *Endpoint ) updateAddrWatchLocked () {
if e .addrWatch != nil {
e .addrWatch .Set (e .addrLocked ())
}
}
func endpointAddrEqual(a , b netaddr .EndpointAddr ) bool {
return a .ID .Equal (b .ID ) && equalTransportAddrs (a .Addrs (), b .Addrs ())
}
func (e *Endpoint ) addrLocked () netaddr .EndpointAddr {
a := netaddr .NewEndpointAddr (e .ID ())
if !e .disableIP {
if addr , ok := canonicalNATTraversalCandidate (e .LocalAddr ()); ok {
a = a .WithIP (addr )
}
for _ , addr := range e .externalNATLocked () {
a = a .WithIP (addr )
}
}
for _ , addr := range e .localCustomAddrs (context .Background ()) {
a = a .WithAddrs (addr )
}
if e .relay != nil {
if st := e .relay .HomeRelayStatus ().Current (); st != nil {
a = a .WithRelayURL (st .URL )
}
}
return a
}
func (e *Endpoint ) localCustomAddrs (ctx context .Context ) []netaddr .CustomAddr {
return customTransportLocalAddrs (ctx , e .custom )
}
func equalTransportAddrs(a , b []netaddr .TransportAddr ) bool {
if len (a ) != len (b ) {
return false
}
for i := range a {
if a [i ].Compare (b [i ]) != 0 {
return false
}
}
return true
}
type RelayStatus = socket .RelayStatus
type RelayConfig = relay .Config
func (e *Endpoint ) HomeRelayStatus () watch .Observer [*RelayStatus ] {
if e .relay == nil {
return watch .NewValue [*RelayStatus ](nil ).Watch ()
}
return e .relay .HomeRelayStatus ()
}
func (e *Endpoint ) Online (ctx context .Context ) error {
if e .relay == nil {
return ErrNoRelay
}
w := e .relay .HomeRelayStatus ()
for {
if st := w .Current (); st != nil && st .IsConnected () {
return nil
}
if _ , err := w .Updated (ctx ); err != nil {
return err
}
}
}
var ErrNoRelay = errors .New ("iroh: no relays configured" )
func (e *Endpoint ) InsertRelay (url netaddr .RelayURL , cfg *RelayConfig ) (*RelayConfig , error ) {
if e .isClosed () {
return nil , ErrEndpointClosed
}
if e .relay == nil {
return nil , ErrNoRelay
}
next := RelayConfig {URL : url }
if cfg != nil {
next = *cfg
next .URL = url
}
prev , ok := e .relay .InsertRelay (url , next )
e .mu .Lock ()
e .updateAddrWatchLocked ()
e .mu .Unlock ()
if !ok {
return nil , nil
}
return &prev , nil
}
func (e *Endpoint ) RemoveRelay (url netaddr .RelayURL ) *RelayConfig {
if e .isClosed () || e .relay == nil {
return nil
}
prev , ok := e .relay .RemoveRelay (url )
e .mu .Lock ()
e .updateAddrWatchLocked ()
e .mu .Unlock ()
if !ok {
return nil
}
return &prev
}
var ErrEndpointClosed = errors .New ("iroh: endpoint closed" )
var ErrEndpointAcceptLoopInUse = errors .New ("iroh: endpoint accept loop in use" )
var ErrSelfConnect = errors .New ("iroh: cannot connect to self" )
var ErrNoAddress = errors .New ("iroh: no reachable address for endpoint" )
var ErrConnectRejected = errors .New ("iroh: connect rejected by hook" )
var ErrHandshakeRejected = errors .New ("iroh: handshake rejected by hook" )
var ErrConnClosedDuringHandshake = errors .New ("iroh: connection closed during handshake" )
func (e *Endpoint ) Connect (ctx context .Context , addr netaddr .EndpointAddr , alpn string ) (*Conn , error ) {
e .metrics .connectsStarted .Add (1 )
ok := false
defer func () {
if !ok {
e .metrics .connectsFailed .Add (1 )
}
}()
if _ , has := ctx .Deadline (); !has {
var cancel context .CancelFunc
ctx , cancel = context .WithTimeout (ctx , ConnectTimeout )
defer cancel ()
}
c , err := e .connectEarly (ctx , addr , alpn )
if err != nil {
return nil , err
}
conn , err := c .Connection (ctx )
if err != nil {
return nil , err
}
e .metrics .connectsAccepted .Add (1 )
ok = true
return conn , nil
}
func (e *Endpoint ) ConnectEarly (ctx context .Context , addr netaddr .EndpointAddr , alpn string ) (*Connecting , error ) {
return e .connectEarly (ctx , addr , alpn )
}
func (e *Endpoint ) connectEarly (ctx context .Context , addr netaddr .EndpointAddr , alpn string ) (*Connecting , error ) {
if e .isClosed () {
return nil , ErrEndpointClosed
}
if addr .ID .Equal (e .ID ()) {
return nil , ErrSelfConnect
}
if err := e .beforeConnect (ctx , addr , alpn ); err != nil {
return nil , err
}
dials := e .dialTargets (addr )
if len (dials ) == 0 {
return nil , ErrNoAddress
}
clientTLS , err := clientTLSConfigWithCurves (e .secretKey , addr .ID , []string {alpn }, e .sessionCache , e .keyExchange .curves ())
if err != nil {
return nil , err
}
clientTLS .KeyLogWriter = e .keyLogWriter
if _ , ok := ctx .Deadline (); !ok {
var cancel context .CancelFunc
ctx , cancel = context .WithTimeout (ctx , ConnectTimeout )
defer cancel ()
}
var firstErr error
for _ , target := range dials {
qc , err := e .transport .DialEarly (ctx , target , clientTLS , e .quicConf )
if err != nil {
if firstErr == nil {
firstErr = err
}
continue
}
return &Connecting {ep : e , qc : qc , remoteID : addr .ID , addr : addr , alpn : alpn }, nil
}
return nil , fmt .Errorf ("iroh: connect to %s: %w" , addr .ID , firstErr )
}
func (e *Endpoint ) Dial (ctx context .Context , addr netaddr .EndpointAddr , alpn string ) (net .Conn , error ) {
conn , err := e .Connect (ctx , addr , alpn )
if err != nil {
return nil , err
}
stream , err := conn .OpenStreamConn (ctx )
if err != nil {
conn .CloseWithError (0 , "" )
return nil , err
}
return stream , nil
}
func (e *Endpoint ) dialTargets (addr netaddr .EndpointAddr ) []net .Addr {
var ips , customs , relays []net .Addr
if !e .disableIP {
for _ , ip := range addr .IPAddrs () {
ips = append (ips , net .UDPAddrFromAddrPort (ip ))
}
}
for _ , ta := range addr .Addrs () {
c , ok := ta .(netaddr .CustomAddr )
if !ok {
continue
}
m := e .sock .CustomMappedAddrFor (c )
customs = append (customs , net .UDPAddrFromAddrPort (m .AddrPort ()))
}
if e .relay != nil {
for _ , u := range addr .RelayURLs () {
m := e .sock .RelayMappedAddrFor (u , addr .ID )
relays = append (relays , net .UDPAddrFromAddrPort (m .AddrPort ()))
}
}
var targets []net .Addr
if e .relayFirst {
targets = append (targets , relays ...)
targets = append (targets , ips ...)
} else {
targets = append (targets , ips ...)
}
targets = append (targets , customs ...)
if !e .relayFirst {
targets = append (targets , relays ...)
}
return targets
}
func (e *Endpoint ) AcceptIncoming (ctx context .Context ) (*Incoming , error ) {
if err := e .acquireAcceptOwner (acceptOwnerAccept ); err != nil {
return nil , err
}
defer e .releaseAcceptOwner (acceptOwnerAccept )
return e .acceptIncoming (ctx )
}
func (e *Endpoint ) acceptIncoming (ctx context .Context ) (*Incoming , error ) {
e .mu .Lock ()
closed := e .closed
ln := e .listener
e .mu .Unlock ()
if closed {
return nil , ErrEndpointClosed
}
if ln == nil {
return nil , errors .New ("iroh: no ALPNs configured; nothing to accept" )
}
qc , err := ln .Accept (ctx )
if err != nil {
return nil , err
}
return &Incoming {ep : e , qc : qc }, nil
}
func (e *Endpoint ) Accept (ctx context .Context ) (*Conn , error ) {
e .metrics .acceptsStarted .Add (1 )
if err := e .acquireAcceptOwner (acceptOwnerAccept ); err != nil {
e .metrics .acceptsFailed .Add (1 )
return nil , err
}
defer e .releaseAcceptOwner (acceptOwnerAccept )
conn , err := e .accept (ctx )
if err != nil {
e .metrics .acceptsFailed .Add (1 )
return nil , err
}
e .metrics .acceptsAccepted .Add (1 )
return conn , nil
}
func (e *Endpoint ) accept (ctx context .Context ) (*Conn , error ) {
for {
in , err := e .acceptIncoming (ctx )
if err != nil {
return nil , err
}
accepting , err := in .Accept ()
if err != nil {
return nil , err
}
conn , err := accepting .Connection (ctx )
if errors .Is (err , ErrConnClosedDuringHandshake ) {
continue
}
if err != nil {
return nil , err
}
return conn , nil
}
}
func (e *Endpoint ) finishAccepting (ctx context .Context , qc *quic .Conn ) (*Conn , error ) {
select {
case <- qc .HandshakeComplete ():
return e .connFromHandshake (ctx , qc )
default :
}
select {
case <- qc .HandshakeComplete ():
case <- qc .Context ().Done ():
return nil , fmt .Errorf ("%w: %w" , ErrConnClosedDuringHandshake , context .Cause (qc .Context ()))
case <- ctx .Done ():
qc .CloseWithError (0 , "" )
return nil , ctx .Err ()
}
return e .connFromHandshake (ctx , qc )
}
func (e *Endpoint ) connFromHandshake (ctx context .Context , qc *quic .Conn ) (*Conn , error ) {
remote , err := peerEndpointID (qc .ConnectionState ().TLS )
if err != nil {
qc .CloseWithError (0 , "bad peer certificate" )
return nil , err
}
alpn := qc .ConnectionState ().TLS .NegotiatedProtocol
conn , err := newConn (qc , remote , alpn , SideServer , e .connStableID (qc ))
if err != nil {
return nil , err
}
conn .pathState , conn .pathConn = e .registerConn (remote , qc , netaddr .NewEndpointAddr (remote ))
if err := e .afterHandshake (ctx , conn ); err != nil {
conn .CloseWithError (0 , "rejected by hook" )
return nil , err
}
return conn , nil
}
func (e *Endpoint ) beforeConnect (ctx context .Context , addr netaddr .EndpointAddr , alpn string ) error {
for _ , h := range e .hooks {
if err := h .BeforeConnect (ctx , addr , alpn ); err != nil {
return err
}
}
return nil
}
func (e *Endpoint ) afterHandshake (ctx context .Context , conn *Conn ) error {
for _ , h := range e .hooks {
err := h .AfterHandshake (ctx , conn )
if err != nil {
var reject *HandshakeRejectError
if errors .As (err , &reject ) {
if closeErr := conn .CloseWithError (reject .Code , reject .Reason ); closeErr != nil {
return closeErr
}
return fmt .Errorf ("%w: %w" , ErrHandshakeRejected , err )
}
return err
}
}
return nil
}
func (e *Endpoint ) registerConn (remote key .EndpointID , qc *quic .Conn , remoteAddr netaddr .EndpointAddr ) (*socket .RemoteStateActor , *connAdapter ) {
if e .remotes == nil {
return nil , nil
}
if remoteAddr .ID .IsZero () || !remoteAddr .ID .Equal (remote ) {
remoteAddr = netaddr .NewEndpointAddr (remote )
}
pathAddr := e .sock .PathAddr (remote , qc .RemoteAddr ())
adapter := newConnAdapter (qc , pathAddr )
if pathAddr .Kind () == socket .AddrIP {
for _ , u := range remoteAddr .RelayURLs () {
m := e .sock .RelayMappedAddrFor (u , remote )
qc .SetMigrationFallbackRemote (net .UDPAddrFromAddrPort (m .AddrPort ()))
break
}
}
_ , actor := e .remotes .AddConnectionActor (remote , adapter )
go func () {
_ = e .remotes .ResolveRemote (remoteAddr )
}()
if !qc .ConnectionState ().MultipathNegotiated {
return actor , adapter
}
_ = actor .AddNATTraversalAddresses (e .localNATTraversalCandidates ())
_ = actor .AddRemoteNATTraversalAddresses (remoteAddr .IPAddrs ())
go func () {
select {
case <- qc .NATTraversalRemoteAddrsReady ():
_ = actor .TriggerHolepunchConn (adapter )
case <- qc .Context ().Done ():
}
}()
return actor , adapter
}
func (e *Endpoint ) connStableID (qc *quic .Conn ) uint64 {
if qc == nil {
return 0
}
e .mu .Lock ()
if id , ok := e .stableIDs [qc ]; ok {
e .mu .Unlock ()
return id
}
e .nextStable ++
id := e .nextStable
e .stableIDs [qc ] = id
e .mu .Unlock ()
go e .removeStableIDWhenClosed (qc )
return id
}
func (e *Endpoint ) removeStableIDWhenClosed (qc *quic .Conn ) {
<-qc .Context ().Done ()
e .mu .Lock ()
delete (e .stableIDs , qc )
e .mu .Unlock ()
}
func (e *Endpoint ) resolveFunc () socket .ResolveFunc {
lookup := e .lookup
if lookup == nil {
return nil
}
return func (ctx context .Context , id key .EndpointID ) iter .Seq2 [socket .ResolvedAddr , error ] {
return func (yield func (socket .ResolvedAddr , error ) bool ) {
for item , err := range lookup .Resolve (ctx , id ) {
if err != nil {
if !yield (socket .ResolvedAddr {}, err ) {
return
}
continue
}
for _ , addr := range item .Addr ().Addrs () {
if !yield (socket .ResolvedAddr {
Addr : addr ,
Provenance : item .Provenance (),
}, nil ) {
return
}
}
}
}
}
}
func (e *Endpoint ) Shutdown (ctx context .Context ) error {
e .mu .Lock ()
if e .closed {
e .mu .Unlock ()
return nil
}
e .closed = true
close (e .closedCh )
e .mu .Unlock ()
var firstErr error
if e .listener != nil {
if err := e .listener .Close (); err != nil {
firstErr = err
}
}
e .serveStop ()
if err := e .transport .Close (); err != nil && firstErr == nil {
firstErr = err
}
if e .udp != nil {
if err := e .udp .Close (); err != nil && firstErr == nil && !errors .Is (err , net .ErrClosed ) {
firstErr = err
}
}
return firstErr
}
func (e *Endpoint ) Closed () <-chan struct {} { return e .closedCh }
func (e *Endpoint ) isClosed () bool {
e .mu .Lock ()
defer e .mu .Unlock ()
return e .closed
}
func (e *Endpoint ) acquireAcceptOwner (owner acceptOwner ) error {
e .mu .Lock ()
defer e .mu .Unlock ()
if e .closed {
return ErrEndpointClosed
}
if e .acceptOwner != acceptOwnerNone {
return ErrEndpointAcceptLoopInUse
}
e .acceptOwner = owner
return nil
}
func (e *Endpoint ) releaseAcceptOwner (owner acceptOwner ) {
e .mu .Lock ()
defer e .mu .Unlock ()
if e .acceptOwner == owner {
e .acceptOwner = acceptOwnerNone
}
}
The pages are generated with Golds v0.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 .