package holepunch

import (
	
	
	
	
	

	
	
	
	
	
	
	ma 
	manet 
)

// ErrHolePunchActive is returned from DirectConnect when another hole punching attempt is currently running
var ErrHolePunchActive = errors.New("another hole punching attempt to this peer is active")

const maxRetries = 3

// The holePuncher is run on the peer that's behind a NAT / Firewall.
// It observes new incoming connections via a relay that it has a reservation with,
// and initiates the DCUtR protocol with them.
// It then first tries to establish a direct connection, and if that fails, it
// initiates a hole punch.
type holePuncher struct {
	ctx       context.Context
	ctxCancel context.CancelFunc

	host     host.Host
	refCount sync.WaitGroup

	ids         identify.IDService
	listenAddrs func() []ma.Multiaddr

	directDialTimeout time.Duration

	// active hole punches for deduplicating
	activeMx sync.Mutex
	active   map[peer.ID]struct{}

	closeMx sync.RWMutex
	closed  bool

	tracer *tracer
	filter AddrFilter

	// Prior to https://github.com/libp2p/go-libp2p/pull/3044, go-libp2p would
	// pick the opposite roles for client/server a hole punch. Setting this to
	// true preserves that behavior
	legacyBehavior bool
}

func newHolePuncher( host.Host,  identify.IDService,  func() []ma.Multiaddr,  *tracer,  AddrFilter) *holePuncher {
	 := &holePuncher{
		host:        ,
		ids:         ,
		active:      make(map[peer.ID]struct{}),
		tracer:      ,
		filter:      ,
		listenAddrs: ,

		legacyBehavior: true,
	}
	.ctx, .ctxCancel = context.WithCancel(context.Background())
	.Network().Notify((*netNotifiee)())
	return 
}

func ( *holePuncher) ( peer.ID) error {
	.closeMx.RLock()
	defer .closeMx.RUnlock()
	if .closed {
		return ErrClosed
	}

	.activeMx.Lock()
	defer .activeMx.Unlock()
	if ,  := .active[];  {
		return ErrHolePunchActive
	}

	.active[] = struct{}{}
	return nil
}

// DirectConnect attempts to make a direct connection with a remote peer.
// It first attempts a direct dial (if we have a public address of that peer), and then
// coordinates a hole punch over the given relay connection.
func ( *holePuncher) ( peer.ID) error {
	log.Debugw("beginDirectConnect", "host", .host.ID(), "peer", )
	if  := .beginDirectConnect();  != nil {
		return 
	}

	defer func() {
		.activeMx.Lock()
		delete(.active, )
		.activeMx.Unlock()
	}()

	return .directConnect()
}

func ( *holePuncher) ( peer.ID) error {
	// short-circuit check to see if we already have a direct connection
	if getDirectConnection(.host, ) != nil {
		log.Debugw("already connected", "host", .host.ID(), "peer", )
		return nil
	}

	log.Debugw("attempting direct dial", "host", .host.ID(), "peer", , "addrs", .host.Peerstore().Addrs())
	// short-circuit hole punching if a direct dial works.
	// attempt a direct connection ONLY if we have a public address for the remote peer
	for ,  := range .host.Peerstore().Addrs() {
		if !isRelayAddress() && manet.IsPublicAddr() {
			 := network.WithForceDirectDial(.ctx, "hole-punching")
			,  := context.WithTimeout(, .directDialTimeout)

			 := time.Now()
			// This dials *all* addresses, public and private, from the peerstore.
			 := .host.Connect(, peer.AddrInfo{ID: })
			 := time.Since()
			()

			if  != nil {
				.tracer.DirectDialFailed(, , )
				break
			}
			.tracer.DirectDialSuccessful(, )
			log.Debugw("direct connection to peer successful, no need for a hole punch", "peer", )
			return nil
		}
	}

	log.Debugw("got inbound proxy conn", "peer", )

	// hole punch
	for  := 1;  <= maxRetries; ++ {
		, , ,  := .initiateHolePunch()
		if  != nil {
			.tracer.ProtocolError(, )
			return 
		}
		 :=  / 2
		log.Debugf("peer RTT is %s; starting hole punch in %s", , )

		// wait for sync to reach the other peer and then punch a hole for it in our NAT
		// by attempting a connect to it.
		 := time.NewTimer()
		select {
		case  := <-.C:
			 := peer.AddrInfo{
				ID:    ,
				Addrs: ,
			}
			.tracer.StartHolePunch(, , )
			.tracer.HolePunchAttempt(.ID)
			,  := context.WithTimeout(.ctx, .directDialTimeout)
			 := true
			if .legacyBehavior {
				 = false
			}
			 := holePunchConnect(, .host, , )
			()
			 := time.Since()
			.tracer.EndHolePunch(, , )
			if  == nil {
				log.Debugw("hole punching with successful", "peer", , "time", )
				.tracer.HolePunchFinished("initiator", , , , getDirectConnection(.host, ))
				return nil
			}
		case <-.ctx.Done():
			.Stop()
			return .ctx.Err()
		}
		if  == maxRetries {
			.tracer.HolePunchFinished("initiator", maxRetries, , , nil)
		}
	}
	return fmt.Errorf("all retries for hole punch with peer %s failed", )
}

// initiateHolePunch opens a new hole punching coordination stream,
// exchanges the addresses and measures the RTT.
func ( *holePuncher) ( peer.ID) ([]ma.Multiaddr, []ma.Multiaddr, time.Duration, error) {
	 := network.WithAllowLimitedConn(.ctx, "hole-punch")
	 := network.WithNoDial(, "hole-punch")

	,  := .host.NewStream(, , Protocol)
	if  != nil {
		return nil, nil, 0, fmt.Errorf("failed to open hole-punching stream: %w", )
	}
	defer .Close()
	log.Debugf("initiateHolePunch: %s, %s", .Conn().RemotePeer(), .Conn().RemoteMultiaddr())

	, , ,  := .initiateHolePunchImpl()
	if  != nil {
		.Reset()
		return , , , fmt.Errorf("failed to initiateHolePunch: %w", )
	}
	return , , , 
}

func ( *holePuncher) ( network.Stream) ([]ma.Multiaddr, []ma.Multiaddr, time.Duration, error) {
	if  := .Scope().SetService(ServiceName);  != nil {
		return nil, nil, 0, fmt.Errorf("error attaching stream to holepunch service: %s", )
	}

	if  := .Scope().ReserveMemory(maxMsgSize, network.ReservationPriorityAlways);  != nil {
		return nil, nil, 0, fmt.Errorf("error reserving memory for stream: %s", )
	}
	defer .Scope().ReleaseMemory(maxMsgSize)

	 := pbio.NewDelimitedWriter()
	 := pbio.NewDelimitedReader(, maxMsgSize)

	.SetDeadline(time.Now().Add(StreamTimeout))

	// send a CONNECT and start RTT measurement.
	 := removeRelayAddrs(.listenAddrs())
	if .filter != nil {
		 = .filter.FilterLocal(.Conn().RemotePeer(), )
	}
	if len() == 0 {
		return nil, nil, 0, errors.New("aborting hole punch initiation as we have no public address")
	}
	log.Debugf("initiating hole punch with %s", )

	 := time.Now()
	if  := .WriteMsg(&pb.HolePunch{
		Type:     pb.HolePunch_CONNECT.Enum(),
		ObsAddrs: addrsToBytes(),
	});  != nil {
		.Reset()
		return nil, nil, 0, 
	}

	// wait for a CONNECT message from the remote peer
	var  pb.HolePunch
	if  := .ReadMsg(&);  != nil {
		return nil, nil, 0, fmt.Errorf("failed to read CONNECT message from remote peer: %w", )
	}
	 := time.Since()
	if  := .GetType();  != pb.HolePunch_CONNECT {
		return nil, nil, 0, fmt.Errorf("expect CONNECT message, got %s", )
	}

	 := removeRelayAddrs(addrsFromBytes(.ObsAddrs))
	if .filter != nil {
		 = .filter.FilterRemote(.Conn().RemotePeer(), )
	}

	if len() == 0 {
		return nil, nil, 0, errors.New("didn't receive any public addresses in CONNECT")
	}

	if  := .WriteMsg(&pb.HolePunch{Type: pb.HolePunch_SYNC.Enum()});  != nil {
		return nil, nil, 0, fmt.Errorf("failed to send SYNC message for hole punching: %w", )
	}
	return , , , nil
}

func ( *holePuncher) () error {
	.closeMx.Lock()
	.closed = true
	.closeMx.Unlock()
	.ctxCancel()
	.refCount.Wait()
	return nil
}

type netNotifiee holePuncher

func ( *netNotifiee) ( network.Network,  network.Conn) {
	 := (*holePuncher)()

	// Hole punch if it's an inbound proxy connection.
	// If we already have a direct connection with the remote peer, this will be a no-op.
	if .Stat().Direction == network.DirInbound && isRelayAddress(.RemoteMultiaddr()) {
		.refCount.Add(1)
		go func() {
			defer .refCount.Done()

			select {
			// waiting for Identify here will allow us to access the peer's public and observed addresses
			// that we can dial to for a hole punch.
			case <-.ids.IdentifyWait():
			case <-.ctx.Done():
				return
			}

			 := .DirectConnect(.RemotePeer())
			if  != nil {
				log.Debugf("attempt to perform DirectConnect to %s failed: %s", .RemotePeer(), )
			}
		}()
	}
}

func ( *netNotifiee) ( network.Network,  network.Conn) {}
func ( *netNotifiee) ( network.Network,  ma.Multiaddr)       {}
func ( *netNotifiee) ( network.Network,  ma.Multiaddr)  {}