package nat
import (
"context"
"errors"
"fmt"
"net/netip"
"sync"
"sync/atomic"
"time"
logging "github.com/ipfs/go-log/v2"
"github.com/libp2p/go-libp2p/p2p/net/nat/internal/nat"
)
var ErrNoMapping = errors .New ("mapping not established" )
var log = logging .Logger ("nat" )
const MappingDuration = time .Minute
const CacheTime = 15 * time .Second
type entry struct {
protocol string
port int
}
var discoverGateway = nat .DiscoverGateway
func DiscoverNAT (ctx context .Context ) (*NAT , error ) {
natInstance , err := discoverGateway (ctx )
if err != nil {
return nil , err
}
var extAddr netip .Addr
extIP , err := natInstance .GetExternalAddress ()
if err == nil {
extAddr , _ = netip .AddrFromSlice (extIP )
}
addr , err := natInstance .GetDeviceAddress ()
if err != nil {
log .Debug ("DiscoverGateway address error:" , err )
} else {
log .Debug ("DiscoverGateway address:" , addr )
}
ctx , cancel := context .WithCancel (context .Background ())
nat := &NAT {
nat : natInstance ,
mappings : make (map [entry ]int ),
ctx : ctx ,
ctxCancel : cancel ,
}
nat .extAddr .Store (&extAddr )
nat .refCount .Add (1 )
go func () {
defer nat .refCount .Done ()
nat .background ()
}()
return nat , nil
}
type NAT struct {
natmu sync .Mutex
nat nat .NAT
extAddr atomic .Pointer [netip .Addr ]
refCount sync .WaitGroup
ctx context .Context
ctxCancel context .CancelFunc
mappingmu sync .RWMutex
closed bool
mappings map [entry ]int
}
func (nat *NAT ) Close () error {
nat .mappingmu .Lock ()
nat .closed = true
nat .mappingmu .Unlock ()
nat .ctxCancel ()
nat .refCount .Wait ()
return nil
}
func (nat *NAT ) GetMapping (protocol string , port int ) (addr netip .AddrPort , found bool ) {
nat .mappingmu .Lock ()
defer nat .mappingmu .Unlock ()
if !nat .extAddr .Load ().IsValid () {
return netip .AddrPort {}, false
}
extPort , found := nat .mappings [entry {protocol : protocol , port : port }]
if !found || extPort == 0 {
return netip .AddrPort {}, false
}
return netip .AddrPortFrom (*nat .extAddr .Load (), uint16 (extPort )), true
}
func (nat *NAT ) AddMapping (ctx context .Context , protocol string , port int ) error {
switch protocol {
case "tcp" , "udp" :
default :
return fmt .Errorf ("invalid protocol: %s" , protocol )
}
nat .mappingmu .Lock ()
defer nat .mappingmu .Unlock ()
if nat .closed {
return errors .New ("closed" )
}
extPort := nat .establishMapping (ctx , protocol , port )
nat .mappings [entry {protocol : protocol , port : port }] = extPort
return nil
}
func (nat *NAT ) RemoveMapping (ctx context .Context , protocol string , port int ) error {
nat .mappingmu .Lock ()
defer nat .mappingmu .Unlock ()
switch protocol {
case "tcp" , "udp" :
e := entry {protocol : protocol , port : port }
if _ , ok := nat .mappings [e ]; ok {
delete (nat .mappings , e )
return nat .nat .DeletePortMapping (ctx , protocol , port )
}
return errors .New ("unknown mapping" )
default :
return fmt .Errorf ("invalid protocol: %s" , protocol )
}
}
func (nat *NAT ) background () {
const mappingUpdate = MappingDuration / 3
now := time .Now ()
nextMappingUpdate := now .Add (mappingUpdate )
nextAddrUpdate := now .Add (CacheTime )
t := time .NewTimer (minTime (nextMappingUpdate , nextAddrUpdate ).Sub (now ))
defer t .Stop ()
var in []entry
var out []int
for {
select {
case now := <- t .C :
if now .After (nextMappingUpdate ) {
in = in [:0 ]
out = out [:0 ]
nat .mappingmu .Lock ()
for e := range nat .mappings {
in = append (in , e )
}
nat .mappingmu .Unlock ()
for _ , e := range in {
out = append (out , nat .establishMapping (nat .ctx , e .protocol , e .port ))
}
nat .mappingmu .Lock ()
for i , p := range in {
if _ , ok := nat .mappings [p ]; !ok {
continue
}
nat .mappings [p ] = out [i ]
}
nat .mappingmu .Unlock ()
nextMappingUpdate = time .Now ().Add (mappingUpdate )
}
if now .After (nextAddrUpdate ) {
var extAddr netip .Addr
extIP , err := nat .nat .GetExternalAddress ()
if err == nil {
extAddr , _ = netip .AddrFromSlice (extIP )
}
nat .extAddr .Store (&extAddr )
nextAddrUpdate = time .Now ().Add (CacheTime )
}
t .Reset (time .Until (minTime (nextAddrUpdate , nextMappingUpdate )))
case <- nat .ctx .Done ():
nat .mappingmu .Lock ()
ctx , cancel := context .WithTimeout (context .Background (), 10 *time .Second )
defer cancel ()
for e := range nat .mappings {
delete (nat .mappings , e )
nat .nat .DeletePortMapping (ctx , e .protocol , e .port )
}
nat .mappingmu .Unlock ()
return
}
}
}
func (nat *NAT ) establishMapping (ctx context .Context , protocol string , internalPort int ) (externalPort int ) {
log .Debugf ("Attempting port map: %s/%d" , protocol , internalPort )
const comment = "libp2p"
nat .natmu .Lock ()
var err error
externalPort , err = nat .nat .AddPortMapping (ctx , protocol , internalPort , comment , MappingDuration )
if err != nil {
externalPort , err = nat .nat .AddPortMapping (ctx , protocol , internalPort , comment , 0 )
}
nat .natmu .Unlock ()
if err != nil || externalPort == 0 {
if err != nil {
log .Warnf ("NAT port mapping failed: protocol=%s internal_port=%d error=%q" , protocol , internalPort , err )
} else {
log .Warnf ("NAT port mapping failed: protocol=%s internal_port=%d external_port=0" , protocol , internalPort )
}
return 0
}
log .Debugf ("NAT Mapping: %d --> %d (%s)" , externalPort , internalPort , protocol )
return externalPort
}
func minTime(a , b time .Time ) time .Time {
if a .Before (b ) {
return a
}
return b
}
The pages are generated with Golds v0.8.2 . (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 .