hysteria/app/cmd/realm_portmap.go

123 lines
3.2 KiB
Go

package cmd
import (
"context"
"net"
"net/netip"
"slices"
"strings"
"time"
"go.uber.org/zap"
"github.com/apernet/hysteria/extras/v2/realm"
)
type realmPortMappingConfig struct {
Enabled bool `mapstructure:"enabled"`
Timeout time.Duration `mapstructure:"timeout"`
Lifetime time.Duration `mapstructure:"lifetime"`
}
// newRealmPortMapper maps localPort on the gateway via UPnP/NAT-PMP.
// Failures are non-fatal by design: it logs a warning and returns nil,
// in which case the realm flow continues with STUN-discovered addresses only.
func newRealmPortMapper(ctx context.Context, realmID string, localPort int, config realmPortMappingConfig) *realm.PortMapper {
logger.Debug("realm port mapping started",
zap.String("realm", realmID),
zap.Int("port", localPort))
start := time.Now()
mapper, err := realm.NewPortMapper(ctx, localPort, realm.PortMapConfig{
Timeout: config.Timeout,
Lifetime: config.Lifetime,
})
if err != nil {
logger.Warn("realm port mapping failed; continuing without it",
zap.String("realm", realmID),
zap.Error(err))
return nil
}
logger.Debug("realm port mapping added",
zap.String("realm", realmID),
zap.String("gateway", mapper.GatewayType()),
zap.Int("port", localPort),
zap.String("external", mapper.ExternalAddr().String()),
zap.String("duration", formatLogDuration(time.Since(start))))
return mapper
}
// realmPortMapLoop renews the mapping at half its lease lifetime until ctx is
// cancelled, then removes it from the gateway.
func realmPortMapLoop(ctx context.Context, realmID string, mapper *realm.PortMapper) {
defer func() {
if err := mapper.Close(); err != nil {
logger.Debug("realm port mapping removal failed",
zap.String("realm", realmID),
zap.Error(err))
} else {
logger.Debug("realm port mapping removed", zap.String("realm", realmID))
}
}()
interval := mapper.Lifetime() / 2
if interval <= 0 {
interval = time.Minute
}
t := time.NewTicker(interval)
defer t.Stop()
failing := false
for {
select {
case <-ctx.Done():
return
case <-t.C:
changed, err := mapper.Renew(ctx)
if err != nil {
if ctx.Err() != nil {
return
}
// Warn only on the first failure
if !failing {
logger.Warn("realm port mapping renewal failed",
zap.String("realm", realmID),
zap.Error(err))
failing = true
}
continue
}
if failing {
logger.Info("realm port mapping recovered",
zap.String("realm", realmID),
zap.String("external", mapper.ExternalAddr().String()))
failing = false
}
logger.Debug("realm port mapping renewed",
zap.String("realm", realmID),
zap.String("external", mapper.ExternalAddr().String()),
zap.Bool("changed", changed))
}
}
}
type cleanupPacketConn struct {
net.PacketConn
cleanup func()
}
func (c *cleanupPacketConn) Close() error {
c.cleanup()
return c.PacketConn.Close()
}
func mergeMappedAddr(addrs []netip.AddrPort, addr netip.AddrPort) []netip.AddrPort {
if !addr.IsValid() {
return addrs
}
out := append([]netip.AddrPort(nil), addrs...)
i, found := slices.BinarySearchFunc(out, addr, func(a, b netip.AddrPort) int {
return strings.Compare(a.String(), b.String())
})
if found {
return out
}
return slices.Insert(out, i, addr)
}