mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-11 17:47:20 +10:00
225 lines
5.1 KiB
Go
225 lines
5.1 KiB
Go
//go:build linux
|
|
|
|
package route
|
|
|
|
import (
|
|
"net"
|
|
"net/netip"
|
|
"os"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/sagernet/fswatch"
|
|
"github.com/sagernet/sing-box/adapter"
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
"github.com/sagernet/sing/common/logger"
|
|
|
|
"github.com/jsimonetti/rtnetlink"
|
|
"github.com/mdlayher/netlink"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
var defaultLeaseFiles = []string{
|
|
"/tmp/dhcp.leases",
|
|
"/var/lib/dhcp/dhcpd.leases",
|
|
"/var/lib/dhcpd/dhcpd.leases",
|
|
"/var/lib/kea/kea-leases4.csv",
|
|
"/var/lib/kea/kea-leases6.csv",
|
|
}
|
|
|
|
type neighborResolver struct {
|
|
logger logger.ContextLogger
|
|
leaseFiles []string
|
|
access sync.RWMutex
|
|
neighborIPToMAC map[netip.Addr]net.HardwareAddr
|
|
leaseIPToMAC map[netip.Addr]net.HardwareAddr
|
|
ipToHostname map[netip.Addr]string
|
|
macToHostname map[string]string
|
|
watcher *fswatch.Watcher
|
|
done chan struct{}
|
|
}
|
|
|
|
func newNeighborResolver(resolverLogger logger.ContextLogger, leaseFiles []string) (adapter.NeighborResolver, error) {
|
|
if len(leaseFiles) == 0 {
|
|
for _, path := range defaultLeaseFiles {
|
|
info, err := os.Stat(path)
|
|
if err == nil && info.Size() > 0 {
|
|
leaseFiles = append(leaseFiles, path)
|
|
}
|
|
}
|
|
}
|
|
return &neighborResolver{
|
|
logger: resolverLogger,
|
|
leaseFiles: leaseFiles,
|
|
neighborIPToMAC: make(map[netip.Addr]net.HardwareAddr),
|
|
leaseIPToMAC: make(map[netip.Addr]net.HardwareAddr),
|
|
ipToHostname: make(map[netip.Addr]string),
|
|
macToHostname: make(map[string]string),
|
|
done: make(chan struct{}),
|
|
}, nil
|
|
}
|
|
|
|
func (r *neighborResolver) Start() error {
|
|
err := r.loadNeighborTable()
|
|
if err != nil {
|
|
r.logger.Warn(E.Cause(err, "load neighbor table"))
|
|
}
|
|
r.doReloadLeaseFiles()
|
|
go r.subscribeNeighborUpdates()
|
|
if len(r.leaseFiles) > 0 {
|
|
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
|
Path: r.leaseFiles,
|
|
Logger: r.logger,
|
|
Callback: func(_ string) {
|
|
r.doReloadLeaseFiles()
|
|
},
|
|
})
|
|
if err != nil {
|
|
r.logger.Warn(E.Cause(err, "create lease file watcher"))
|
|
} else {
|
|
r.watcher = watcher
|
|
err = watcher.Start()
|
|
if err != nil {
|
|
r.logger.Warn(E.Cause(err, "start lease file watcher"))
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *neighborResolver) Close() error {
|
|
close(r.done)
|
|
if r.watcher != nil {
|
|
return r.watcher.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *neighborResolver) LookupMAC(address netip.Addr) (net.HardwareAddr, bool) {
|
|
r.access.RLock()
|
|
defer r.access.RUnlock()
|
|
mac, found := r.neighborIPToMAC[address]
|
|
if found {
|
|
return mac, true
|
|
}
|
|
mac, found = r.leaseIPToMAC[address]
|
|
if found {
|
|
return mac, true
|
|
}
|
|
mac, found = extractMACFromEUI64(address)
|
|
if found {
|
|
return mac, true
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (r *neighborResolver) LookupHostname(address netip.Addr) (string, bool) {
|
|
r.access.RLock()
|
|
defer r.access.RUnlock()
|
|
hostname, found := r.ipToHostname[address]
|
|
if found {
|
|
return hostname, true
|
|
}
|
|
mac, macFound := r.neighborIPToMAC[address]
|
|
if !macFound {
|
|
mac, macFound = r.leaseIPToMAC[address]
|
|
}
|
|
if !macFound {
|
|
mac, macFound = extractMACFromEUI64(address)
|
|
}
|
|
if macFound {
|
|
hostname, found = r.macToHostname[mac.String()]
|
|
if found {
|
|
return hostname, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func (r *neighborResolver) loadNeighborTable() error {
|
|
connection, err := rtnetlink.Dial(nil)
|
|
if err != nil {
|
|
return E.Cause(err, "dial rtnetlink")
|
|
}
|
|
defer connection.Close()
|
|
neighbors, err := connection.Neigh.List()
|
|
if err != nil {
|
|
return E.Cause(err, "list neighbors")
|
|
}
|
|
r.access.Lock()
|
|
defer r.access.Unlock()
|
|
for _, neigh := range neighbors {
|
|
if neigh.Attributes == nil {
|
|
continue
|
|
}
|
|
if neigh.Attributes.LLAddress == nil || len(neigh.Attributes.Address) == 0 {
|
|
continue
|
|
}
|
|
address, ok := netip.AddrFromSlice(neigh.Attributes.Address)
|
|
if !ok {
|
|
continue
|
|
}
|
|
r.neighborIPToMAC[address] = slices.Clone(neigh.Attributes.LLAddress)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *neighborResolver) subscribeNeighborUpdates() {
|
|
connection, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{
|
|
Groups: 1 << (unix.RTNLGRP_NEIGH - 1),
|
|
})
|
|
if err != nil {
|
|
r.logger.Warn(E.Cause(err, "subscribe neighbor updates"))
|
|
return
|
|
}
|
|
defer connection.Close()
|
|
for {
|
|
select {
|
|
case <-r.done:
|
|
return
|
|
default:
|
|
}
|
|
err = connection.SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
if err != nil {
|
|
r.logger.Warn(E.Cause(err, "set netlink read deadline"))
|
|
return
|
|
}
|
|
messages, err := connection.Receive()
|
|
if err != nil {
|
|
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
|
continue
|
|
}
|
|
select {
|
|
case <-r.done:
|
|
return
|
|
default:
|
|
}
|
|
r.logger.Warn(E.Cause(err, "receive neighbor update"))
|
|
continue
|
|
}
|
|
for _, message := range messages {
|
|
address, mac, isDelete, ok := ParseNeighborMessage(message)
|
|
if !ok {
|
|
continue
|
|
}
|
|
r.access.Lock()
|
|
if isDelete {
|
|
delete(r.neighborIPToMAC, address)
|
|
} else {
|
|
r.neighborIPToMAC[address] = mac
|
|
}
|
|
r.access.Unlock()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *neighborResolver) doReloadLeaseFiles() {
|
|
leaseIPToMAC, ipToHostname, macToHostname := ReloadLeaseFiles(r.leaseFiles)
|
|
r.access.Lock()
|
|
r.leaseIPToMAC = leaseIPToMAC
|
|
r.ipToHostname = ipToHostname
|
|
r.macToHostname = macToHostname
|
|
r.access.Unlock()
|
|
}
|