multinet/dialer.go
Evgenii Stratonikov b92dae991b Initial commit
Signed-off-by: Evgenii Stratonikov <stratonikov@runbox.com>
2023-08-16 20:10:49 +03:00

219 lines
5.1 KiB
Go

package multinet
import (
"bytes"
"context"
"fmt"
"net"
"net/netip"
"sort"
"sync"
)
// Dialer contains the single most important method from the net.Dialer.
type Dialer interface {
DialContext(ctx context.Context, network, address string) (net.Conn, error)
}
// Multidialer is like Dialer, but supports link state updates.
type Multidialer interface {
Dialer
UpdateInterface(name string, addr netip.Addr, status bool) error
}
var (
_ Dialer = (*net.Dialer)(nil)
_ Dialer = (*dialer)(nil)
)
type dialer struct {
// Protects subnets field (recursively).
mtx sync.RWMutex
subnets []Subnet
// Default options for the net.Dialer.
dialer net.Dialer
// If true, allow to dial only configured subnets.
restrict bool
// Algorithm to which picks source address for each ip.
balancer balancer
// Hook used in tests, overrides `net.Dialer.DialContext()`
testHookDialContext func(d *net.Dialer, ctx context.Context, network, address string) (net.Conn, error)
}
// Subnet represents a single subnet, possibly routable from multiple interfaces.
type Subnet struct {
Mask netip.Prefix
Interfaces []Source
}
// Source represents a single source IP belonging to a particular subnet.
type Source struct {
Name string
LocalAddr *net.TCPAddr
Down bool
}
// Config contains Multidialer configuration.
type Config struct {
// Routable subnets to prioritize in CIDR format.
Subnets []string
// If true, the only configurd subnets available through this dialer.
// Otherwise, a failback to the net.DefaultDialer.
Restrict bool
// Dialer containes default options for the net.Dialer to use.
// LocalAddr is overriden.
Dialer net.Dialer
// Balancer specifies algorithm used to pick source address.
Balancer BalancerType
}
// NewDialer ...
func NewDialer(c Config) (Multidialer, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
sort.Slice(ifaces, func(i, j int) bool {
return ifaces[i].Name < ifaces[j].Name
})
var sources []iface
for i := range ifaces {
info, err := processIface(ifaces[i])
if err != nil {
return nil, err
}
sources = append(sources, info)
}
var d dialer
for _, subnet := range c.Subnets {
s, err := processSubnet(subnet, sources)
if err != nil {
return nil, err
}
d.subnets = append(d.subnets, s)
}
switch c.Balancer {
case BalancerTypeNoop:
d.balancer = &firstEnabled{d: &d}
case BalancerTypeRoundRobin:
d.balancer = &roundRobin{d: &d}
default:
return nil, fmt.Errorf("invalid balancer type: %s", c.Balancer)
}
d.restrict = c.Restrict
return &d, nil
}
type iface struct {
info net.Interface
addrs []netip.Prefix
}
func processIface(info net.Interface) (iface, error) {
ips, err := info.Addrs()
if err != nil {
return iface{}, err
}
var addrs []netip.Prefix
for i := range ips {
p, err := netip.ParsePrefix(ips[i].String())
if err != nil {
return iface{}, err
}
addrs = append(addrs, p)
}
return iface{info: info, addrs: addrs}, nil
}
func processSubnet(subnet string, sources []iface) (Subnet, error) {
s, err := netip.ParsePrefix(subnet)
if err != nil {
return Subnet{}, err
}
var ifs []Source
for _, source := range sources {
for i := range source.addrs {
src := source.addrs[i].Addr()
if s.Contains(src) {
ifs = append(ifs, Source{
Name: source.info.Name,
LocalAddr: &net.TCPAddr{IP: net.IP(src.AsSlice())},
})
}
}
}
sort.Slice(ifs, func(i, j int) bool {
if ifs[i].Name != ifs[j].Name {
return ifs[i].Name < ifs[j].Name
}
return bytes.Compare(ifs[i].LocalAddr.IP, ifs[j].LocalAddr.IP) == -1
})
return Subnet{
Mask: s,
Interfaces: ifs,
}, nil
}
// DialContext implements the Dialer interface.
// Hostnames for address are currently not supported.
func (d *dialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
addr, err := netip.ParseAddrPort(address)
if err != nil {
return nil, err
}
d.mtx.RLock()
defer d.mtx.RUnlock()
for i := range d.subnets {
if d.subnets[i].Mask.Contains(addr.Addr()) {
return d.balancer.DialContext(ctx, &d.subnets[i], network, address)
}
}
if d.restrict {
return nil, fmt.Errorf("no suitable interface for: [%s]%s", network, address)
}
return d.dialer.DialContext(ctx, network, address)
}
func (d *dialer) dialContext(nd *net.Dialer, ctx context.Context, network, address string) (net.Conn, error) {
if h := d.testHookDialContext; h != nil {
return h(nd, ctx, "tcp", address)
}
return nd.DialContext(ctx, "tcp", address)
}
// UpdateInterface implements the Multidialer interface.
// Updating address on a specific interface is currently not supported.
func (d *dialer) UpdateInterface(iface string, addr netip.Addr, up bool) error {
d.mtx.Lock()
defer d.mtx.Unlock()
for i := range d.subnets {
for j := range d.subnets[i].Interfaces {
matchIface := d.subnets[i].Interfaces[j].Name == iface
if matchIface {
d.subnets[i].Interfaces[j].Down = !up
continue
}
a, _ := netip.AddrFromSlice(d.subnets[i].Interfaces[j].LocalAddr.IP)
matchAddr := a.IsUnspecified() || addr == a
if matchAddr {
d.subnets[i].Interfaces[j].Down = !up
}
}
}
return nil
}