Skip to content

Commit 5c40335

Browse files
authored
Merge pull request #2 from Snawoot/iface_spec
Iface spec
2 parents 6fdc801 + 3174c81 commit 5c40335

File tree

5 files changed

+288
-4
lines changed

5 files changed

+288
-4
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Configuration example:
3737

3838
```yaml
3939
listen:
40-
- 239.82.71.65:8271
40+
- 239.82.71.65:8271 # or "239.82.71.65:8271@eth0" or "239.82.71.65:8271@192.168.0.0/16"
4141
- 127.0.0.1:8282
4242

4343
groups:

agent/agent.go

+34-1
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import (
55
"fmt"
66
"log"
77
"net"
8+
"net/netip"
9+
"strings"
810
"sync"
911
"time"
1012

1113
"github.com/Snawoot/rgap/config"
1214
"github.com/Snawoot/rgap/protocol"
15+
"github.com/Snawoot/rgap/util"
1316
"github.com/hashicorp/go-multierror"
1417
)
1518

@@ -92,7 +95,12 @@ func (a *Agent) singleRun(ctx context.Context, t time.Time) error {
9295
}
9396

9497
func (a *Agent) sendSingle(ctx context.Context, msg []byte, dst string) error {
95-
conn, err := a.cfg.Dialer.DialContext(ctx, "udp", dst)
98+
dstAddr, iface, err := util.SplitAndResolveAddrSpec(dst)
99+
if err != nil {
100+
return fmt.Errorf("destination %s: interface resolving failed: %w", dst, err)
101+
}
102+
103+
conn, err := a.dialInterfaceContext(ctx, "udp", dstAddr, iface)
96104
if err != nil {
97105
return fmt.Errorf("Agent.sendSingle dial failed: %w", err)
98106
}
@@ -111,3 +119,28 @@ func (a *Agent) sendSingle(ctx context.Context, msg []byte, dst string) error {
111119
}
112120
return nil
113121
}
122+
123+
func (a *Agent) dialInterfaceContext(ctx context.Context, network, addr string, iif *net.Interface) (net.Conn, error) {
124+
if iif == nil {
125+
return a.cfg.Dialer.DialContext(ctx, network, addr)
126+
}
127+
128+
var hints []string
129+
addrs, err := iif.Addrs()
130+
if err != nil {
131+
return nil, err
132+
}
133+
for _, addr := range addrs {
134+
ipnet, ok := addr.(*net.IPNet)
135+
if !ok {
136+
return nil, fmt.Errorf("unexpected type returned as address interface: %T", addr)
137+
}
138+
netipAddr, ok := netip.AddrFromSlice(ipnet.IP)
139+
if !ok {
140+
return nil, fmt.Errorf("interface %v has invalid address %s", iif.Name, ipnet.IP)
141+
}
142+
hints = append(hints, netipAddr.Unmap().String())
143+
}
144+
boundDialer := util.NewBoundDialer(a.cfg.Dialer, strings.Join(hints, ","))
145+
return boundDialer.DialContext(ctx, network, addr)
146+
}

listener/udpsource.go

+8-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"net"
88

99
"github.com/Snawoot/rgap/protocol"
10+
"github.com/Snawoot/rgap/util"
1011
)
1112

1213
type UDPSource struct {
@@ -33,15 +34,20 @@ func (s *UDPSource) Start() error {
3334
s.ctxCancel = cancel
3435
s.loopDone = make(chan struct{})
3536

36-
udpAddr, err := net.ResolveUDPAddr("udp", s.address)
37+
listenAddr, iface, err := util.SplitAndResolveAddrSpec(s.address)
38+
if err != nil {
39+
return fmt.Errorf("UDP source %s: interface resolving failed: %w", s.address, err)
40+
}
41+
42+
udpAddr, err := net.ResolveUDPAddr("udp", listenAddr)
3743
if err != nil {
3844
return fmt.Errorf("bad UDP listen address: %w", err)
3945
}
4046

4147
var conn *net.UDPConn
4248

4349
if udpAddr.IP.IsMulticast() {
44-
conn, err = net.ListenMulticastUDP("udp4", nil, udpAddr)
50+
conn, err = net.ListenMulticastUDP("udp", iface, udpAddr)
4551
if err != nil {
4652
return fmt.Errorf("UDP listen failed: %w", err)
4753
}

util/hintdialer.go

+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package util
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"net"
8+
"os"
9+
"strings"
10+
11+
"github.com/hashicorp/go-multierror"
12+
)
13+
14+
var (
15+
ErrNoSuitableAddress = errors.New("no suitable address")
16+
ErrBadIPAddressLength = errors.New("bad IP address length")
17+
ErrUnknownNetwork = errors.New("unknown network")
18+
)
19+
20+
type BoundDialerContextKey struct{}
21+
22+
type BoundDialerContextValue struct {
23+
Hints *string
24+
LocalAddr string
25+
}
26+
27+
type BoundDialerDefaultSink interface {
28+
DialContext(ctx context.Context, network, address string) (net.Conn, error)
29+
}
30+
31+
type BoundDialer struct {
32+
defaultDialer BoundDialerDefaultSink
33+
defaultHints string
34+
}
35+
36+
func NewBoundDialer(defaultDialer BoundDialerDefaultSink, defaultHints string) *BoundDialer {
37+
if defaultDialer == nil {
38+
defaultDialer = &net.Dialer{}
39+
}
40+
return &BoundDialer{
41+
defaultDialer: defaultDialer,
42+
defaultHints: defaultHints,
43+
}
44+
}
45+
46+
func (d *BoundDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
47+
hints := d.defaultHints
48+
lAddr := ""
49+
if hintsOverride := ctx.Value(BoundDialerContextKey{}); hintsOverride != nil {
50+
if hintsOverrideValue, ok := hintsOverride.(BoundDialerContextValue); ok {
51+
if hintsOverrideValue.Hints != nil {
52+
hints = *hintsOverrideValue.Hints
53+
}
54+
lAddr = hintsOverrideValue.LocalAddr
55+
}
56+
}
57+
58+
parsedHints, err := parseHints(hints, lAddr)
59+
if err != nil {
60+
return nil, fmt.Errorf("dial failed: %w", err)
61+
}
62+
63+
if len(parsedHints) == 0 {
64+
return d.defaultDialer.DialContext(ctx, network, address)
65+
}
66+
67+
var netBase string
68+
switch network {
69+
case "tcp", "tcp4", "tcp6":
70+
netBase = "tcp"
71+
case "udp", "udp4", "udp6":
72+
netBase = "udp"
73+
case "ip", "ip4", "ip6":
74+
netBase = "ip"
75+
default:
76+
return d.defaultDialer.DialContext(ctx, network, address)
77+
}
78+
79+
var resErr error
80+
for _, lIP := range parsedHints {
81+
lAddr, restrictedNetwork, err := ipToLAddr(netBase, lIP)
82+
if err != nil {
83+
resErr = multierror.Append(resErr, fmt.Errorf("ipToLAddr(%q) failed: %w", lIP.String(), err))
84+
continue
85+
}
86+
if network != netBase && network != restrictedNetwork {
87+
continue
88+
}
89+
90+
conn, err := (&net.Dialer{
91+
LocalAddr: lAddr,
92+
}).DialContext(ctx, restrictedNetwork, address)
93+
if err != nil {
94+
resErr = multierror.Append(resErr, fmt.Errorf("dial failed: %w", err))
95+
} else {
96+
return conn, nil
97+
}
98+
}
99+
100+
if resErr == nil {
101+
resErr = ErrNoSuitableAddress
102+
}
103+
return nil, resErr
104+
}
105+
106+
func (d *BoundDialer) Dial(network, address string) (net.Conn, error) {
107+
return d.DialContext(context.Background(), network, address)
108+
}
109+
110+
func ipToLAddr(network string, ip net.IP) (net.Addr, string, error) {
111+
v6 := true
112+
if ip4 := ip.To4(); len(ip4) == net.IPv4len {
113+
ip = ip4
114+
v6 = false
115+
} else if len(ip) != net.IPv6len {
116+
return nil, "", ErrBadIPAddressLength
117+
}
118+
119+
var lAddr net.Addr
120+
var lNetwork string
121+
switch network {
122+
case "tcp", "tcp4", "tcp6":
123+
lAddr = &net.TCPAddr{
124+
IP: ip,
125+
}
126+
if v6 {
127+
lNetwork = "tcp6"
128+
} else {
129+
lNetwork = "tcp4"
130+
}
131+
case "udp", "udp4", "udp6":
132+
lAddr = &net.UDPAddr{
133+
IP: ip,
134+
}
135+
if v6 {
136+
lNetwork = "udp6"
137+
} else {
138+
lNetwork = "udp4"
139+
}
140+
case "ip", "ip4", "ip6":
141+
lAddr = &net.IPAddr{
142+
IP: ip,
143+
}
144+
if v6 {
145+
lNetwork = "ip6"
146+
} else {
147+
lNetwork = "ip4"
148+
}
149+
default:
150+
return nil, "", ErrUnknownNetwork
151+
}
152+
153+
return lAddr, lNetwork, nil
154+
}
155+
156+
func parseHints(hints, lAddr string) ([]net.IP, error) {
157+
hints = os.Expand(hints, func(key string) string {
158+
switch key {
159+
case "lAddr":
160+
return lAddr
161+
default:
162+
return fmt.Sprintf("<bad key:%q>", key)
163+
}
164+
})
165+
res, err := parseIPList(hints)
166+
if err != nil {
167+
return nil, fmt.Errorf("unable to parse source IP hints %q: %w", hints, err)
168+
}
169+
return res, nil
170+
}
171+
172+
func parseIPList(list string) ([]net.IP, error) {
173+
res := make([]net.IP, 0)
174+
for _, elem := range strings.Split(list, ",") {
175+
elem = strings.TrimSpace(elem)
176+
if len(elem) == 0 {
177+
continue
178+
}
179+
if parsed := net.ParseIP(elem); parsed == nil {
180+
return nil, fmt.Errorf("unable to parse IP address %q", elem)
181+
} else {
182+
res = append(res, parsed)
183+
}
184+
}
185+
return res, nil
186+
}

util/util.go

+59
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@ package util
22

33
import (
44
"bytes"
5+
"errors"
56
"fmt"
7+
"log"
8+
"net"
69
"net/netip"
10+
"strings"
711

812
"gopkg.in/yaml.v3"
913
)
@@ -58,3 +62,58 @@ func CheckedUnmarshal(doc *yaml.Node, dst interface{}) error {
5862
}
5963
return nil
6064
}
65+
66+
func SplitAndResolveAddrSpec(spec string) (string, *net.Interface, error) {
67+
addrSpec, ifaceSpec, found := strings.Cut(spec, "@")
68+
if !found {
69+
return addrSpec, nil, nil
70+
}
71+
iface, err := ResolveInterface(ifaceSpec)
72+
if err != nil {
73+
return addrSpec, nil, fmt.Errorf("unable to resolve interface spec %q: %w", ifaceSpec, err)
74+
}
75+
return addrSpec, iface, nil
76+
}
77+
78+
func ResolveInterface(spec string) (*net.Interface, error) {
79+
ifaces, err := net.Interfaces()
80+
if err != nil {
81+
return nil, fmt.Errorf("unable to enumerate interfaces: %w", err)
82+
}
83+
if pfx, err := netip.ParsePrefix(spec); err == nil {
84+
// look for address
85+
for i := range ifaces {
86+
addrs, err := ifaces[i].Addrs()
87+
if err != nil {
88+
// may be a problem with some interface,
89+
// but we still probably can find the right one
90+
log.Printf("WARNING: interface %s is failing to report its addresses: %v", ifaces[i].Name, err)
91+
continue
92+
}
93+
for _, addr := range addrs {
94+
ipnet, ok := addr.(*net.IPNet)
95+
if !ok {
96+
return nil, fmt.Errorf("unexpected type returned as address interface: %T", addr)
97+
}
98+
netipAddr, ok := netip.AddrFromSlice(ipnet.IP)
99+
if !ok {
100+
return nil, fmt.Errorf("interface %v has invalid address %s", ifaces[i].Name, ipnet.IP)
101+
}
102+
netipAddr = netipAddr.Unmap()
103+
if pfx.Contains(netipAddr) {
104+
res := ifaces[i]
105+
return &res, nil
106+
}
107+
}
108+
}
109+
} else {
110+
// look for iface name
111+
for i := range ifaces {
112+
if ifaces[i].Name == spec {
113+
res := ifaces[i]
114+
return &res, nil
115+
}
116+
}
117+
}
118+
return nil, errors.New("specified interface not found")
119+
}

0 commit comments

Comments
 (0)