621 lines
14 KiB
Go
621 lines
14 KiB
Go
package vnet
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/pion/logging"
|
|
)
|
|
|
|
const (
|
|
defaultRouterQueueSize = 0 // unlimited
|
|
)
|
|
|
|
var (
|
|
errInvalidLocalIPinStaticIPs = errors.New("invalid local IP in StaticIPs")
|
|
errLocalIPBeyondStaticIPsSubset = errors.New("mapped in StaticIPs is beyond subnet")
|
|
errLocalIPNoStaticsIPsAssociated = errors.New("all StaticIPs must have associated local IPs")
|
|
errRouterAlreadyStarted = errors.New("router already started")
|
|
errRouterAlreadyStopped = errors.New("router already stopped")
|
|
errStaticIPisBeyondSubnet = errors.New("static IP is beyond subnet")
|
|
errAddressSpaceExhausted = errors.New("address space exhausted")
|
|
errNoIPAddrEth0 = errors.New("no IP address is assigned for eth0")
|
|
)
|
|
|
|
// Generate a unique router name
|
|
var assignRouterName = func() func() string { //nolint:gochecknoglobals
|
|
var routerIDCtr uint64
|
|
|
|
return func() string {
|
|
n := atomic.AddUint64(&routerIDCtr, 1)
|
|
return fmt.Sprintf("router%d", n)
|
|
}
|
|
}()
|
|
|
|
// RouterConfig ...
|
|
type RouterConfig struct {
|
|
// Name of router. If not specified, a unique name will be assigned.
|
|
Name string
|
|
// CIDR notation, like "192.0.2.0/24"
|
|
CIDR string
|
|
// StaticIPs is an array of static IP addresses to be assigned for this router.
|
|
// If no static IP address is given, the router will automatically assign
|
|
// an IP address.
|
|
// This will be ignored if this router is the root.
|
|
StaticIPs []string
|
|
// StaticIP is deprecated. Use StaticIPs.
|
|
StaticIP string
|
|
// Internal queue size
|
|
QueueSize int
|
|
// Effective only when this router has a parent router
|
|
NATType *NATType
|
|
// Minimum Delay
|
|
MinDelay time.Duration
|
|
// Max Jitter
|
|
MaxJitter time.Duration
|
|
// Logger factory
|
|
LoggerFactory logging.LoggerFactory
|
|
}
|
|
|
|
// NIC is a nework inerface controller that interfaces Router
|
|
type NIC interface {
|
|
getInterface(ifName string) (*Interface, error)
|
|
onInboundChunk(c Chunk)
|
|
getStaticIPs() []net.IP
|
|
setRouter(r *Router) error
|
|
}
|
|
|
|
// ChunkFilter is a handler users can add to filter chunks.
|
|
// If the filter returns false, the packet will be dropped.
|
|
type ChunkFilter func(c Chunk) bool
|
|
|
|
// Router ...
|
|
type Router struct {
|
|
name string // read-only
|
|
interfaces []*Interface // read-only
|
|
ipv4Net *net.IPNet // read-only
|
|
staticIPs []net.IP // read-only
|
|
staticLocalIPs map[string]net.IP // read-only,
|
|
lastID byte // requires mutex [x], used to assign the last digit of IPv4 address
|
|
queue *chunkQueue // read-only
|
|
parent *Router // read-only
|
|
children []*Router // read-only
|
|
natType *NATType // read-only
|
|
nat *networkAddressTranslator // read-only
|
|
nics map[string]NIC // read-only
|
|
stopFunc func() // requires mutex [x]
|
|
resolver *resolver // read-only
|
|
chunkFilters []ChunkFilter // requires mutex [x]
|
|
minDelay time.Duration // requires mutex [x]
|
|
maxJitter time.Duration // requires mutex [x]
|
|
mutex sync.RWMutex // thread-safe
|
|
pushCh chan struct{} // writer requires mutex
|
|
loggerFactory logging.LoggerFactory // read-only
|
|
log logging.LeveledLogger // read-only
|
|
}
|
|
|
|
// NewRouter ...
|
|
func NewRouter(config *RouterConfig) (*Router, error) {
|
|
loggerFactory := config.LoggerFactory
|
|
log := loggerFactory.NewLogger("vnet")
|
|
|
|
_, ipv4Net, err := net.ParseCIDR(config.CIDR)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
queueSize := defaultRouterQueueSize
|
|
if config.QueueSize > 0 {
|
|
queueSize = config.QueueSize
|
|
}
|
|
|
|
// set up network interface, lo0
|
|
lo0 := NewInterface(net.Interface{
|
|
Index: 1,
|
|
MTU: 16384,
|
|
Name: lo0String,
|
|
HardwareAddr: nil,
|
|
Flags: net.FlagUp | net.FlagLoopback | net.FlagMulticast,
|
|
})
|
|
lo0.AddAddr(&net.IPAddr{IP: net.ParseIP("127.0.0.1"), Zone: ""})
|
|
|
|
// set up network interface, eth0
|
|
eth0 := NewInterface(net.Interface{
|
|
Index: 2,
|
|
MTU: 1500,
|
|
Name: "eth0",
|
|
HardwareAddr: newMACAddress(),
|
|
Flags: net.FlagUp | net.FlagMulticast,
|
|
})
|
|
|
|
// local host name resolver
|
|
resolver := newResolver(&resolverConfig{
|
|
LoggerFactory: config.LoggerFactory,
|
|
})
|
|
|
|
name := config.Name
|
|
if len(name) == 0 {
|
|
name = assignRouterName()
|
|
}
|
|
|
|
var staticIPs []net.IP
|
|
staticLocalIPs := map[string]net.IP{}
|
|
for _, ipStr := range config.StaticIPs {
|
|
ipPair := strings.Split(ipStr, "/")
|
|
if ip := net.ParseIP(ipPair[0]); ip != nil {
|
|
if len(ipPair) > 1 {
|
|
locIP := net.ParseIP(ipPair[1])
|
|
if locIP == nil {
|
|
return nil, errInvalidLocalIPinStaticIPs
|
|
}
|
|
if !ipv4Net.Contains(locIP) {
|
|
return nil, fmt.Errorf("local IP %s %w", locIP.String(), errLocalIPBeyondStaticIPsSubset)
|
|
}
|
|
staticLocalIPs[ip.String()] = locIP
|
|
}
|
|
staticIPs = append(staticIPs, ip)
|
|
}
|
|
}
|
|
if len(config.StaticIP) > 0 {
|
|
log.Warn("StaticIP is deprecated. Use StaticIPs instead")
|
|
if ip := net.ParseIP(config.StaticIP); ip != nil {
|
|
staticIPs = append(staticIPs, ip)
|
|
}
|
|
}
|
|
|
|
if nStaticLocal := len(staticLocalIPs); nStaticLocal > 0 {
|
|
if nStaticLocal != len(staticIPs) {
|
|
return nil, errLocalIPNoStaticsIPsAssociated
|
|
}
|
|
}
|
|
|
|
return &Router{
|
|
name: name,
|
|
interfaces: []*Interface{lo0, eth0},
|
|
ipv4Net: ipv4Net,
|
|
staticIPs: staticIPs,
|
|
staticLocalIPs: staticLocalIPs,
|
|
queue: newChunkQueue(queueSize, 0),
|
|
natType: config.NATType,
|
|
nics: map[string]NIC{},
|
|
resolver: resolver,
|
|
minDelay: config.MinDelay,
|
|
maxJitter: config.MaxJitter,
|
|
pushCh: make(chan struct{}, 1),
|
|
loggerFactory: loggerFactory,
|
|
log: log,
|
|
}, nil
|
|
}
|
|
|
|
// caller must hold the mutex
|
|
func (r *Router) getInterfaces() ([]*Interface, error) {
|
|
if len(r.interfaces) == 0 {
|
|
return nil, fmt.Errorf("%w is available", errNoInterface)
|
|
}
|
|
|
|
return r.interfaces, nil
|
|
}
|
|
|
|
func (r *Router) getInterface(ifName string) (*Interface, error) {
|
|
r.mutex.RLock()
|
|
defer r.mutex.RUnlock()
|
|
|
|
ifs, err := r.getInterfaces()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, ifc := range ifs {
|
|
if ifc.Name == ifName {
|
|
return ifc, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("interface %s %w", ifName, errNotFound)
|
|
}
|
|
|
|
// Start ...
|
|
func (r *Router) Start() error {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
|
|
if r.stopFunc != nil {
|
|
return errRouterAlreadyStarted
|
|
}
|
|
|
|
cancelCh := make(chan struct{})
|
|
|
|
go func() {
|
|
loop:
|
|
for {
|
|
d, err := r.processChunks()
|
|
if err != nil {
|
|
r.log.Errorf("[%s] %s", r.name, err.Error())
|
|
break
|
|
}
|
|
|
|
if d <= 0 {
|
|
select {
|
|
case <-r.pushCh:
|
|
case <-cancelCh:
|
|
break loop
|
|
}
|
|
} else {
|
|
t := time.NewTimer(d)
|
|
select {
|
|
case <-t.C:
|
|
case <-cancelCh:
|
|
break loop
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
r.stopFunc = func() {
|
|
close(cancelCh)
|
|
}
|
|
|
|
for _, child := range r.children {
|
|
if err := child.Start(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop ...
|
|
func (r *Router) Stop() error {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
|
|
if r.stopFunc == nil {
|
|
return errRouterAlreadyStopped
|
|
}
|
|
|
|
for _, router := range r.children {
|
|
r.mutex.Unlock()
|
|
err := router.Stop()
|
|
r.mutex.Lock()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
r.stopFunc()
|
|
r.stopFunc = nil
|
|
return nil
|
|
}
|
|
|
|
// caller must hold the mutex
|
|
func (r *Router) addNIC(nic NIC) error {
|
|
ifc, err := nic.getInterface("eth0")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var ips []net.IP
|
|
|
|
if ips = nic.getStaticIPs(); len(ips) == 0 {
|
|
// assign an IP address
|
|
ip, err2 := r.assignIPAddress()
|
|
if err2 != nil {
|
|
return err2
|
|
}
|
|
ips = append(ips, ip)
|
|
}
|
|
|
|
for _, ip := range ips {
|
|
if !r.ipv4Net.Contains(ip) {
|
|
return fmt.Errorf("%w: %s", errStaticIPisBeyondSubnet, r.ipv4Net.String())
|
|
}
|
|
|
|
ifc.AddAddr(&net.IPNet{
|
|
IP: ip,
|
|
Mask: r.ipv4Net.Mask,
|
|
})
|
|
|
|
r.nics[ip.String()] = nic
|
|
}
|
|
|
|
if err = nic.setRouter(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// AddRouter adds a chile Router.
|
|
func (r *Router) AddRouter(router *Router) error {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
|
|
// Router is a NIC. Add it as a NIC so that packets are routed to this child
|
|
// router.
|
|
err := r.addNIC(router)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = router.setRouter(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
r.children = append(r.children, router)
|
|
return nil
|
|
}
|
|
|
|
// AddChildRouter is like AddRouter, but does not add the child routers NIC to
|
|
// the parent. This has to be done manually by calling AddNet, which allows to
|
|
// use a wrapper around the subrouters NIC.
|
|
// AddNet MUST be called before AddChildRouter.
|
|
func (r *Router) AddChildRouter(router *Router) error {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
if err := router.setRouter(r); err != nil {
|
|
return err
|
|
}
|
|
|
|
r.children = append(r.children, router)
|
|
return nil
|
|
}
|
|
|
|
// AddNet ...
|
|
func (r *Router) AddNet(nic NIC) error {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
|
|
return r.addNIC(nic)
|
|
}
|
|
|
|
// AddHost adds a mapping of hostname and an IP address to the local resolver.
|
|
func (r *Router) AddHost(hostName string, ipAddr string) error {
|
|
return r.resolver.addHost(hostName, ipAddr)
|
|
}
|
|
|
|
// AddChunkFilter adds a filter for chunks traversing this router.
|
|
// You may add more than one filter. The filters are called in the order of this method call.
|
|
// If a chunk is dropped by a filter, subsequent filter will not receive the chunk.
|
|
func (r *Router) AddChunkFilter(filter ChunkFilter) {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
|
|
r.chunkFilters = append(r.chunkFilters, filter)
|
|
}
|
|
|
|
// caller should hold the mutex
|
|
func (r *Router) assignIPAddress() (net.IP, error) {
|
|
// See: https://stackoverflow.com/questions/14915188/ip-address-ending-with-zero
|
|
|
|
if r.lastID == 0xfe {
|
|
return nil, errAddressSpaceExhausted
|
|
}
|
|
|
|
ip := make(net.IP, 4)
|
|
copy(ip, r.ipv4Net.IP[:3])
|
|
r.lastID++
|
|
ip[3] = r.lastID
|
|
return ip, nil
|
|
}
|
|
|
|
func (r *Router) push(c Chunk) {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
|
|
r.log.Debugf("[%s] route %s", r.name, c.String())
|
|
if r.stopFunc != nil {
|
|
c.setTimestamp()
|
|
if r.queue.push(c) {
|
|
select {
|
|
case r.pushCh <- struct{}{}:
|
|
default:
|
|
}
|
|
} else {
|
|
r.log.Warnf("[%s] queue was full. dropped a chunk", r.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *Router) processChunks() (time.Duration, error) {
|
|
r.mutex.Lock()
|
|
defer r.mutex.Unlock()
|
|
|
|
// Introduce jitter by delaying the processing of chunks.
|
|
if r.maxJitter > 0 {
|
|
jitter := time.Duration(rand.Int63n(int64(r.maxJitter))) //nolint:gosec
|
|
time.Sleep(jitter)
|
|
}
|
|
|
|
// cutOff
|
|
// v min delay
|
|
// |<--->|
|
|
// +------------:--
|
|
// |OOOOOOXXXXX : --> time
|
|
// +------------:--
|
|
// |<--->| now
|
|
// due
|
|
|
|
enteredAt := time.Now()
|
|
cutOff := enteredAt.Add(-r.minDelay)
|
|
|
|
var d time.Duration // the next sleep duration
|
|
|
|
for {
|
|
d = 0
|
|
|
|
c := r.queue.peek()
|
|
if c == nil {
|
|
break // no more chunk in the queue
|
|
}
|
|
|
|
// check timestamp to find if the chunk is due
|
|
if c.getTimestamp().After(cutOff) {
|
|
// There is one or more chunk in the queue but none of them are due.
|
|
// Calculate the next sleep duration here.
|
|
nextExpire := c.getTimestamp().Add(r.minDelay)
|
|
d = nextExpire.Sub(enteredAt)
|
|
break
|
|
}
|
|
|
|
var ok bool
|
|
if c, ok = r.queue.pop(); !ok {
|
|
break // no more chunk in the queue
|
|
}
|
|
|
|
blocked := false
|
|
for i := 0; i < len(r.chunkFilters); i++ {
|
|
filter := r.chunkFilters[i]
|
|
if !filter(c) {
|
|
blocked = true
|
|
break
|
|
}
|
|
}
|
|
if blocked {
|
|
continue // discard
|
|
}
|
|
|
|
dstIP := c.getDestinationIP()
|
|
|
|
// check if the desination is in our subnet
|
|
if r.ipv4Net.Contains(dstIP) {
|
|
// search for the destination NIC
|
|
var nic NIC
|
|
if nic, ok = r.nics[dstIP.String()]; !ok {
|
|
// NIC not found. drop it.
|
|
r.log.Debugf("[%s] %s unreachable", r.name, c.String())
|
|
continue
|
|
}
|
|
|
|
// found the NIC, forward the chunk to the NIC.
|
|
// call to NIC must unlock mutex
|
|
r.mutex.Unlock()
|
|
nic.onInboundChunk(c)
|
|
r.mutex.Lock()
|
|
continue
|
|
}
|
|
|
|
// the destination is outside of this subnet
|
|
// is this WAN?
|
|
if r.parent == nil {
|
|
// this WAN. No route for this chunk
|
|
r.log.Debugf("[%s] no route found for %s", r.name, c.String())
|
|
continue
|
|
}
|
|
|
|
// Pass it to the parent via NAT
|
|
toParent, err := r.nat.translateOutbound(c)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if toParent == nil {
|
|
continue
|
|
}
|
|
|
|
//nolint:godox
|
|
/* FIXME: this implementation would introduce a duplicate packet!
|
|
if r.nat.natType.Hairpining {
|
|
hairpinned, err := r.nat.translateInbound(toParent)
|
|
if err != nil {
|
|
r.log.Warnf("[%s] %s", r.name, err.Error())
|
|
} else {
|
|
go func() {
|
|
r.push(hairpinned)
|
|
}()
|
|
}
|
|
}
|
|
*/
|
|
|
|
// call to parent router mutex unlock mutex
|
|
r.mutex.Unlock()
|
|
r.parent.push(toParent)
|
|
r.mutex.Lock()
|
|
}
|
|
|
|
return d, nil
|
|
}
|
|
|
|
// caller must hold the mutex
|
|
func (r *Router) setRouter(parent *Router) error {
|
|
r.parent = parent
|
|
r.resolver.setParent(parent.resolver)
|
|
|
|
// when this method is called, one or more IP address has already been assigned by
|
|
// the parent router.
|
|
ifc, err := r.getInterface("eth0")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(ifc.addrs) == 0 {
|
|
return errNoIPAddrEth0
|
|
}
|
|
|
|
mappedIPs := []net.IP{}
|
|
localIPs := []net.IP{}
|
|
|
|
for _, ifcAddr := range ifc.addrs {
|
|
var ip net.IP
|
|
switch addr := ifcAddr.(type) {
|
|
case *net.IPNet:
|
|
ip = addr.IP
|
|
case *net.IPAddr: // Do we really need this case?
|
|
ip = addr.IP
|
|
default:
|
|
}
|
|
|
|
if ip == nil {
|
|
continue
|
|
}
|
|
|
|
mappedIPs = append(mappedIPs, ip)
|
|
|
|
if locIP := r.staticLocalIPs[ip.String()]; locIP != nil {
|
|
localIPs = append(localIPs, locIP)
|
|
}
|
|
}
|
|
|
|
// Set up NAT here
|
|
if r.natType == nil {
|
|
r.natType = &NATType{
|
|
MappingBehavior: EndpointIndependent,
|
|
FilteringBehavior: EndpointAddrPortDependent,
|
|
Hairpining: false,
|
|
PortPreservation: false,
|
|
MappingLifeTime: 30 * time.Second,
|
|
}
|
|
}
|
|
r.nat, err = newNAT(&natConfig{
|
|
name: r.name,
|
|
natType: *r.natType,
|
|
mappedIPs: mappedIPs,
|
|
localIPs: localIPs,
|
|
loggerFactory: r.loggerFactory,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *Router) onInboundChunk(c Chunk) {
|
|
fromParent, err := r.nat.translateInbound(c)
|
|
if err != nil {
|
|
r.log.Warnf("[%s] %s", r.name, err.Error())
|
|
return
|
|
}
|
|
|
|
r.push(fromParent)
|
|
}
|
|
|
|
func (r *Router) getStaticIPs() []net.IP {
|
|
return r.staticIPs
|
|
}
|