mirror of
https://github.com/Quad4-Software/Reticulum-Go
synced 2026-08-29 23:48:44 -04:00
147 lines
4 KiB
Go
147 lines
4 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright (c) 2024-2026 Quad4.io
|
|
|
|
package rnsutil
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"quad4/reticulum-go/pkg/link"
|
|
"quad4/reticulum-go/pkg/transport"
|
|
)
|
|
|
|
const (
|
|
// LinkEstablishmentMargin is added to link.EstablishmentTimeout when an
|
|
// application must wait for handshake completion.
|
|
LinkEstablishmentMargin = 6 * time.Second
|
|
)
|
|
|
|
// FirstHopTimeout is the instance-aware first-hop wait. Shared-instance
|
|
// clients query the owner over RPC so the value reflects real interface
|
|
// bitrates instead of the local client socket.
|
|
func FirstHopTimeout(tr *transport.Transport, destHash []byte) time.Duration {
|
|
sec := float64(transport.EstablishmentTimeoutPerHop)
|
|
if tr != nil {
|
|
sec = tr.FirstHopTimeout(destHash)
|
|
if to, _, ok := sharedInstanceWindows(tr, destHash); ok {
|
|
sec = to
|
|
}
|
|
}
|
|
if sec < 0 {
|
|
sec = float64(transport.EstablishmentTimeoutPerHop)
|
|
}
|
|
return time.Duration(sec * float64(time.Second))
|
|
}
|
|
|
|
// PathResponseWindow sizes a client wait for a path response. Shared-instance
|
|
// clients use the owner's first-hop timeout and slowest online bitrate.
|
|
func PathResponseWindow(tr *transport.Transport, destHash []byte) time.Duration {
|
|
if tr == nil {
|
|
return transport.PathResponseWindowFrom(float64(transport.EstablishmentTimeoutPerHop), 0)
|
|
}
|
|
firstHop := tr.FirstHopTimeout(destHash)
|
|
bitrate := tr.SlowestOnlineBitrate()
|
|
if to, br, ok := sharedInstanceWindows(tr, destHash); ok {
|
|
firstHop = to
|
|
if br > 0 {
|
|
bitrate = br
|
|
}
|
|
}
|
|
return transport.PathResponseWindowFrom(firstHop, bitrate)
|
|
}
|
|
|
|
// LinkEstablishmentWindow is link.EstablishmentTimeout plus a small margin.
|
|
func LinkEstablishmentWindow(l *link.Link) time.Duration {
|
|
if l == nil {
|
|
return LinkEstablishmentMargin
|
|
}
|
|
return l.EstablishmentTimeout() + LinkEstablishmentMargin
|
|
}
|
|
|
|
// BoundWait returns a child context limited to window unless parent already
|
|
// has a deadline (an explicit caller timeout wins). The returned cancel must
|
|
// be invoked by the caller when non-nil.
|
|
func BoundWait(parent context.Context, window time.Duration) (context.Context, context.CancelFunc) {
|
|
if parent == nil {
|
|
parent = context.Background()
|
|
}
|
|
if _, ok := parent.Deadline(); ok {
|
|
return parent, func() {}
|
|
}
|
|
if window <= 0 {
|
|
return parent, func() {}
|
|
}
|
|
return context.WithTimeout(parent, window) // #nosec G118 -- caller defers cancel()
|
|
}
|
|
|
|
// CLIWaitContext applies timeout when positive. Zero means adaptive waits
|
|
// in WaitPathWindow and link establishment.
|
|
func CLIWaitContext(timeout time.Duration) (context.Context, context.CancelFunc) {
|
|
return BoundWait(context.Background(), timeout)
|
|
}
|
|
|
|
// WaitPathWindow waits for a path using PathResponseWindow when ctx has no
|
|
// deadline.
|
|
func WaitPathWindow(ctx context.Context, tr *transport.Transport, destHash []byte) error {
|
|
if tr == nil {
|
|
return fmt.Errorf("nil transport")
|
|
}
|
|
return tr.AwaitPath(ctx, destHash)
|
|
}
|
|
|
|
func activateOutboundLink(ctx context.Context, l *link.Link) error {
|
|
if err := l.Establish(); err != nil {
|
|
return err
|
|
}
|
|
wait, cancel := BoundWait(ctx, LinkEstablishmentWindow(l))
|
|
defer cancel()
|
|
if err := WaitLinkActive(wait, l); err != nil {
|
|
l.Teardown()
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sharedInstanceWindows(tr *transport.Transport, destHash []byte) (firstHop float64, bitrate int64, ok bool) {
|
|
if tr == nil || !tr.ConnectedToSharedInstance() {
|
|
return 0, 0, false
|
|
}
|
|
cfg := tr.GetConfig()
|
|
if cfg == nil {
|
|
return 0, 0, false
|
|
}
|
|
client, err := DialRPC(cfg, nil)
|
|
if err != nil {
|
|
return 0, 0, false
|
|
}
|
|
client.SetTimeout(2 * time.Second)
|
|
to, err := client.GetFirstHopTimeout(destHash)
|
|
if err != nil {
|
|
return 0, 0, false
|
|
}
|
|
stats, err := client.GetInterfaceStats()
|
|
if err == nil {
|
|
bitrate = slowestOnlineBitrateFromStats(stats)
|
|
}
|
|
return to, bitrate, true
|
|
}
|
|
|
|
func slowestOnlineBitrateFromStats(stats transport.InterfaceStatsResponse) int64 {
|
|
var slowest int64
|
|
found := false
|
|
for _, st := range stats.Interfaces {
|
|
if !st.Status || st.Bitrate <= 0 {
|
|
continue
|
|
}
|
|
if !found || st.Bitrate < slowest {
|
|
slowest = st.Bitrate
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return 0
|
|
}
|
|
return slowest
|
|
}
|