package vnet
import (
"errors"
"fmt"
"net"
"sync"
"time"
"github.com/pion/logging"
)
var (
errNATRequriesMapping = errors .New ("1:1 NAT requires more than one mapping" )
errMismatchLengthIP = errors .New ("length mismtach between mappedIPs and localIPs" )
errNonUDPTranslationNotSupported = errors .New ("non-udp translation is not supported yet" )
errNoAssociatedLocalAddress = errors .New ("no associated local address" )
errNoNATBindingFound = errors .New ("no NAT binding found" )
errHasNoPermission = errors .New ("has no permission" )
)
type EndpointDependencyType uint8
const (
EndpointIndependent EndpointDependencyType = iota
EndpointAddrDependent
EndpointAddrPortDependent
)
type NATMode uint8
const (
NATModeNormal NATMode = iota
NATModeNAT1To1
)
const (
defaultNATMappingLifeTime = 30 * time .Second
)
type NATType struct {
Mode NATMode
MappingBehavior EndpointDependencyType
FilteringBehavior EndpointDependencyType
Hairpinning bool
PortPreservation bool
MappingLifeTime time .Duration
}
type natConfig struct {
name string
natType NATType
mappedIPs []net .IP
localIPs []net .IP
loggerFactory logging .LoggerFactory
}
type mapping struct {
proto string
local string
mapped string
bound string
filters map [string ]struct {}
expires time .Time
}
type networkAddressTranslator struct {
name string
natType NATType
mappedIPs []net .IP
localIPs []net .IP
outboundMap map [string ]*mapping
inboundMap map [string ]*mapping
udpPortCounter int
mutex sync .RWMutex
log logging .LeveledLogger
}
func newNAT(config *natConfig ) (*networkAddressTranslator , error ) {
natType := config .natType
if natType .Mode == NATModeNAT1To1 {
natType .MappingBehavior = EndpointIndependent
natType .FilteringBehavior = EndpointIndependent
natType .PortPreservation = true
natType .MappingLifeTime = 0
if len (config .mappedIPs ) == 0 {
return nil , errNATRequriesMapping
}
if len (config .mappedIPs ) != len (config .localIPs ) {
return nil , errMismatchLengthIP
}
} else {
natType .Mode = NATModeNormal
if natType .MappingLifeTime == 0 {
natType .MappingLifeTime = defaultNATMappingLifeTime
}
}
return &networkAddressTranslator {
name : config .name ,
natType : natType ,
mappedIPs : config .mappedIPs ,
localIPs : config .localIPs ,
outboundMap : map [string ]*mapping {},
inboundMap : map [string ]*mapping {},
log : config .loggerFactory .NewLogger ("vnet" ),
}, nil
}
func (n *networkAddressTranslator ) getPairedMappedIP (locIP net .IP ) net .IP {
for i , ip := range n .localIPs {
if ip .Equal (locIP ) {
return n .mappedIPs [i ]
}
}
return nil
}
func (n *networkAddressTranslator ) getPairedLocalIP (mappedIP net .IP ) net .IP {
for i , ip := range n .mappedIPs {
if ip .Equal (mappedIP ) {
return n .localIPs [i ]
}
}
return nil
}
func (n *networkAddressTranslator ) translateOutbound (from Chunk ) (Chunk , error ) {
n .mutex .Lock ()
defer n .mutex .Unlock ()
to := from .Clone ()
if from .Network () == udp {
if n .natType .Mode == NATModeNAT1To1 {
srcAddr := from .SourceAddr ().(*net .UDPAddr )
srcIP := n .getPairedMappedIP (srcAddr .IP )
if srcIP == nil {
n .log .Debugf ("[%s] drop outbound chunk %s with not route" , n .name , from .String ())
return nil , nil
}
srcPort := srcAddr .Port
if err := to .setSourceAddr (fmt .Sprintf ("%s:%d" , srcIP .String (), srcPort )); err != nil {
return nil , err
}
} else {
var bound , filterKey string
switch n .natType .MappingBehavior {
case EndpointIndependent :
bound = ""
case EndpointAddrDependent :
bound = from .getDestinationIP ().String ()
case EndpointAddrPortDependent :
bound = from .DestinationAddr ().String ()
}
switch n .natType .FilteringBehavior {
case EndpointIndependent :
filterKey = ""
case EndpointAddrDependent :
filterKey = from .getDestinationIP ().String ()
case EndpointAddrPortDependent :
filterKey = from .DestinationAddr ().String ()
}
oKey := fmt .Sprintf ("udp:%s:%s" , from .SourceAddr ().String (), bound )
m := n .findOutboundMapping (oKey )
if m == nil {
mappedPort := 0xC000 + n .udpPortCounter
n .udpPortCounter ++
m = &mapping {
proto : from .SourceAddr ().Network (),
local : from .SourceAddr ().String (),
bound : bound ,
mapped : fmt .Sprintf ("%s:%d" , n .mappedIPs [0 ].String (), mappedPort ),
filters : map [string ]struct {}{},
expires : time .Now ().Add (n .natType .MappingLifeTime ),
}
n .outboundMap [oKey ] = m
iKey := fmt .Sprintf ("udp:%s" , m .mapped )
n .log .Debugf ("[%s] created a new NAT binding oKey=%s iKey=%s" ,
n .name ,
oKey ,
iKey )
m .filters [filterKey ] = struct {}{}
n .log .Debugf ("[%s] permit access from %s to %s" , n .name , filterKey , m .mapped )
n .inboundMap [iKey ] = m
} else if _ , ok := m .filters [filterKey ]; !ok {
n .log .Debugf ("[%s] permit access from %s to %s" , n .name , filterKey , m .mapped )
m .filters [filterKey ] = struct {}{}
}
if err := to .setSourceAddr (m .mapped ); err != nil {
return nil , err
}
}
n .log .Debugf ("[%s] translate outbound chunk from %s to %s" , n .name , from .String (), to .String ())
return to , nil
}
return nil , errNonUDPTranslationNotSupported
}
func (n *networkAddressTranslator ) translateInbound (from Chunk ) (Chunk , error ) {
n .mutex .Lock ()
defer n .mutex .Unlock ()
to := from .Clone ()
if from .Network () == udp {
if n .natType .Mode == NATModeNAT1To1 {
dstAddr := from .DestinationAddr ().(*net .UDPAddr )
dstIP := n .getPairedLocalIP (dstAddr .IP )
if dstIP == nil {
return nil , fmt .Errorf ("drop %s as %w" , from .String (), errNoAssociatedLocalAddress )
}
dstPort := from .DestinationAddr ().(*net .UDPAddr ).Port
if err := to .setDestinationAddr (fmt .Sprintf ("%s:%d" , dstIP , dstPort )); err != nil {
return nil , err
}
} else {
iKey := fmt .Sprintf ("udp:%s" , from .DestinationAddr ().String ())
m := n .findInboundMapping (iKey )
if m == nil {
return nil , fmt .Errorf ("drop %s as %w" , from .String (), errNoNATBindingFound )
}
var filterKey string
switch n .natType .FilteringBehavior {
case EndpointIndependent :
filterKey = ""
case EndpointAddrDependent :
filterKey = from .getSourceIP ().String ()
case EndpointAddrPortDependent :
filterKey = from .SourceAddr ().String ()
}
if _ , ok := m .filters [filterKey ]; !ok {
return nil , fmt .Errorf ("drop %s as the remote %s %w" , from .String (), filterKey , errHasNoPermission )
}
if err := to .setDestinationAddr (m .local ); err != nil {
return nil , err
}
}
n .log .Debugf ("[%s] translate inbound chunk from %s to %s" , n .name , from .String (), to .String ())
return to , nil
}
return nil , errNonUDPTranslationNotSupported
}
func (n *networkAddressTranslator ) findOutboundMapping (oKey string ) *mapping {
now := time .Now ()
m , ok := n .outboundMap [oKey ]
if ok {
if now .After (m .expires ) {
n .removeMapping (m )
m = nil
} else {
m .expires = time .Now ().Add (n .natType .MappingLifeTime )
}
}
return m
}
func (n *networkAddressTranslator ) findInboundMapping (iKey string ) *mapping {
now := time .Now ()
m , ok := n .inboundMap [iKey ]
if !ok {
return nil
}
if now .After (m .expires ) {
n .removeMapping (m )
return nil
}
return m
}
func (n *networkAddressTranslator ) removeMapping (m *mapping ) {
oKey := fmt .Sprintf ("%s:%s:%s" , m .proto , m .local , m .bound )
iKey := fmt .Sprintf ("%s:%s" , m .proto , m .mapped )
delete (n .outboundMap , oKey )
delete (n .inboundMap , iKey )
}
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 .