Add macOS support for MAC and hostname rule items
This commit is contained in:
@@ -1,23 +1,13 @@
|
|||||||
//go:build linux
|
|
||||||
|
|
||||||
package libbox
|
package libbox
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"slices"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/route"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
"github.com/mdlayher/netlink"
|
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type NeighborEntry struct {
|
type NeighborEntry struct {
|
||||||
Address string
|
Address string
|
||||||
MACAddress string
|
MacAddress string
|
||||||
Hostname string
|
Hostname string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,88 +20,16 @@ type NeighborSubscription struct {
|
|||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SubscribeNeighborTable(listener NeighborUpdateListener) (*NeighborSubscription, error) {
|
|
||||||
entries, err := route.ReadNeighborEntries()
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "initial neighbor dump")
|
|
||||||
}
|
|
||||||
table := make(map[netip.Addr]net.HardwareAddr)
|
|
||||||
for _, entry := range entries {
|
|
||||||
table[entry.Address] = entry.MACAddress
|
|
||||||
}
|
|
||||||
listener.UpdateNeighborTable(tableToIterator(table))
|
|
||||||
connection, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{
|
|
||||||
Groups: 1 << (unix.RTNLGRP_NEIGH - 1),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "subscribe neighbor updates")
|
|
||||||
}
|
|
||||||
subscription := &NeighborSubscription{
|
|
||||||
done: make(chan struct{}),
|
|
||||||
}
|
|
||||||
go subscription.loop(listener, connection, table)
|
|
||||||
return subscription, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *NeighborSubscription) Close() {
|
func (s *NeighborSubscription) Close() {
|
||||||
close(s.done)
|
close(s.done)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NeighborSubscription) loop(listener NeighborUpdateListener, connection *netlink.Conn, table map[netip.Addr]net.HardwareAddr) {
|
|
||||||
defer connection.Close()
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-s.done:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
err := connection.SetReadDeadline(time.Now().Add(3 * time.Second))
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
messages, err := connection.Receive()
|
|
||||||
if err != nil {
|
|
||||||
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-s.done:
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
changed := false
|
|
||||||
for _, message := range messages {
|
|
||||||
address, mac, isDelete, ok := route.ParseNeighborMessage(message)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if isDelete {
|
|
||||||
if _, exists := table[address]; exists {
|
|
||||||
delete(table, address)
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
existing, exists := table[address]
|
|
||||||
if !exists || !slices.Equal(existing, mac) {
|
|
||||||
table[address] = mac
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if changed {
|
|
||||||
listener.UpdateNeighborTable(tableToIterator(table))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func tableToIterator(table map[netip.Addr]net.HardwareAddr) NeighborEntryIterator {
|
func tableToIterator(table map[netip.Addr]net.HardwareAddr) NeighborEntryIterator {
|
||||||
entries := make([]*NeighborEntry, 0, len(table))
|
entries := make([]*NeighborEntry, 0, len(table))
|
||||||
for address, mac := range table {
|
for address, mac := range table {
|
||||||
entries = append(entries, &NeighborEntry{
|
entries = append(entries, &NeighborEntry{
|
||||||
Address: address.String(),
|
Address: address.String(),
|
||||||
MACAddress: mac.String(),
|
MacAddress: mac.String(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return &neighborEntryIterator{entries}
|
return &neighborEntryIterator{entries}
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package libbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/route"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
xroute "golang.org/x/net/route"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SubscribeNeighborTable(listener NeighborUpdateListener) (*NeighborSubscription, error) {
|
||||||
|
entries, err := route.ReadNeighborEntries()
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initial neighbor dump")
|
||||||
|
}
|
||||||
|
table := make(map[netip.Addr]net.HardwareAddr)
|
||||||
|
for _, entry := range entries {
|
||||||
|
table[entry.Address] = entry.MACAddress
|
||||||
|
}
|
||||||
|
listener.UpdateNeighborTable(tableToIterator(table))
|
||||||
|
routeSocket, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "open route socket")
|
||||||
|
}
|
||||||
|
err = unix.SetNonblock(routeSocket, true)
|
||||||
|
if err != nil {
|
||||||
|
unix.Close(routeSocket)
|
||||||
|
return nil, E.Cause(err, "set route socket nonblock")
|
||||||
|
}
|
||||||
|
subscription := &NeighborSubscription{
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
go subscription.loop(listener, routeSocket, table)
|
||||||
|
return subscription, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NeighborSubscription) loop(listener NeighborUpdateListener, routeSocket int, table map[netip.Addr]net.HardwareAddr) {
|
||||||
|
routeSocketFile := os.NewFile(uintptr(routeSocket), "route")
|
||||||
|
defer routeSocketFile.Close()
|
||||||
|
buffer := buf.NewPacket()
|
||||||
|
defer buffer.Release()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
tv := unix.NsecToTimeval(int64(3 * time.Second))
|
||||||
|
_ = unix.SetsockoptTimeval(routeSocket, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv)
|
||||||
|
n, err := routeSocketFile.Read(buffer.FreeBytes())
|
||||||
|
if err != nil {
|
||||||
|
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
messages, err := xroute.ParseRIB(xroute.RIBTypeRoute, buffer.FreeBytes()[:n])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
changed := false
|
||||||
|
for _, message := range messages {
|
||||||
|
routeMessage, isRouteMessage := message.(*xroute.RouteMessage)
|
||||||
|
if !isRouteMessage {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if routeMessage.Flags&unix.RTF_LLINFO == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
address, mac, isDelete, ok := route.ParseRouteNeighborMessage(routeMessage)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isDelete {
|
||||||
|
if _, exists := table[address]; exists {
|
||||||
|
delete(table, address)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
existing, exists := table[address]
|
||||||
|
if !exists || !slices.Equal(existing, mac) {
|
||||||
|
table[address] = mac
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
listener.UpdateNeighborTable(tableToIterator(table))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadBootpdLeases() NeighborEntryIterator {
|
||||||
|
leaseIPToMAC, ipToHostname, macToHostname := route.ReloadLeaseFiles([]string{"/var/db/dhcpd_leases"})
|
||||||
|
entries := make([]*NeighborEntry, 0, len(leaseIPToMAC))
|
||||||
|
for address, mac := range leaseIPToMAC {
|
||||||
|
entry := &NeighborEntry{
|
||||||
|
Address: address.String(),
|
||||||
|
MacAddress: mac.String(),
|
||||||
|
}
|
||||||
|
hostname, found := ipToHostname[address]
|
||||||
|
if !found {
|
||||||
|
hostname = macToHostname[mac.String()]
|
||||||
|
}
|
||||||
|
entry.Hostname = hostname
|
||||||
|
entries = append(entries, entry)
|
||||||
|
}
|
||||||
|
return &neighborEntryIterator{entries}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package libbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/route"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
"github.com/mdlayher/netlink"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SubscribeNeighborTable(listener NeighborUpdateListener) (*NeighborSubscription, error) {
|
||||||
|
entries, err := route.ReadNeighborEntries()
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initial neighbor dump")
|
||||||
|
}
|
||||||
|
table := make(map[netip.Addr]net.HardwareAddr)
|
||||||
|
for _, entry := range entries {
|
||||||
|
table[entry.Address] = entry.MACAddress
|
||||||
|
}
|
||||||
|
listener.UpdateNeighborTable(tableToIterator(table))
|
||||||
|
connection, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{
|
||||||
|
Groups: 1 << (unix.RTNLGRP_NEIGH - 1),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "subscribe neighbor updates")
|
||||||
|
}
|
||||||
|
subscription := &NeighborSubscription{
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
go subscription.loop(listener, connection, table)
|
||||||
|
return subscription, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NeighborSubscription) loop(listener NeighborUpdateListener, connection *netlink.Conn, table map[netip.Addr]net.HardwareAddr) {
|
||||||
|
defer connection.Close()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
err := connection.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
messages, err := connection.Receive()
|
||||||
|
if err != nil {
|
||||||
|
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
changed := false
|
||||||
|
for _, message := range messages {
|
||||||
|
address, mac, isDelete, ok := route.ParseNeighborMessage(message)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if isDelete {
|
||||||
|
if _, exists := table[address]; exists {
|
||||||
|
delete(table, address)
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
existing, exists := table[address]
|
||||||
|
if !exists || !slices.Equal(existing, mac) {
|
||||||
|
table[address] = mac
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
listener.UpdateNeighborTable(tableToIterator(table))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,9 @@
|
|||||||
//go:build !linux
|
//go:build !linux && !darwin
|
||||||
|
|
||||||
package libbox
|
package libbox
|
||||||
|
|
||||||
import "os"
|
import "os"
|
||||||
|
|
||||||
type NeighborEntry struct {
|
func SubscribeNeighborTable(_ NeighborUpdateListener) (*NeighborSubscription, error) {
|
||||||
Address string
|
|
||||||
MACAddress string
|
|
||||||
Hostname string
|
|
||||||
}
|
|
||||||
|
|
||||||
type NeighborEntryIterator interface {
|
|
||||||
Next() *NeighborEntry
|
|
||||||
HasNext() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type NeighborSubscription struct{}
|
|
||||||
|
|
||||||
func SubscribeNeighborTable(listener NeighborUpdateListener) (*NeighborSubscription, error) {
|
|
||||||
return nil, os.ErrInvalid
|
return nil, os.ErrInvalid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NeighborSubscription) Close() {}
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type PlatformInterface interface {
|
|||||||
SendNotification(notification *Notification) error
|
SendNotification(notification *Notification) error
|
||||||
StartNeighborMonitor(listener NeighborUpdateListener) error
|
StartNeighborMonitor(listener NeighborUpdateListener) error
|
||||||
CloseNeighborMonitor(listener NeighborUpdateListener) error
|
CloseNeighborMonitor(listener NeighborUpdateListener) error
|
||||||
|
RegisterMyInterface(name string)
|
||||||
}
|
}
|
||||||
|
|
||||||
type NeighborUpdateListener interface {
|
type NeighborUpdateListener interface {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ func (w *platformInterfaceWrapper) OpenInterface(options *tun.Options, platformO
|
|||||||
}
|
}
|
||||||
options.FileDescriptor = dupFd
|
options.FileDescriptor = dupFd
|
||||||
w.myTunName = options.Name
|
w.myTunName = options.Name
|
||||||
|
w.iif.RegisterMyInterface(options.Name)
|
||||||
return tun.New(*options)
|
return tun.New(*options)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,11 +241,14 @@ func (w *neighborUpdateListenerWrapper) UpdateNeighborTable(entries NeighborEntr
|
|||||||
var result []adapter.NeighborEntry
|
var result []adapter.NeighborEntry
|
||||||
for entries.HasNext() {
|
for entries.HasNext() {
|
||||||
entry := entries.Next()
|
entry := entries.Next()
|
||||||
|
if entry == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
address, err := netip.ParseAddr(entry.Address)
|
address, err := netip.ParseAddr(entry.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
macAddress, err := net.ParseMAC(entry.MACAddress)
|
macAddress, err := net.ParseMAC(entry.MacAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,239 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package route
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/fswatch"
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
|
||||||
|
"golang.org/x/net/route"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultLeaseFiles = []string{
|
||||||
|
"/var/db/dhcpd_leases",
|
||||||
|
"/tmp/dhcp.leases",
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
entries, err := ReadNeighborEntries()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.access.Lock()
|
||||||
|
defer r.access.Unlock()
|
||||||
|
for _, entry := range entries {
|
||||||
|
r.neighborIPToMAC[entry.Address] = entry.MACAddress
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *neighborResolver) subscribeNeighborUpdates() {
|
||||||
|
routeSocket, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Warn(E.Cause(err, "subscribe neighbor updates"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = unix.SetNonblock(routeSocket, true)
|
||||||
|
if err != nil {
|
||||||
|
unix.Close(routeSocket)
|
||||||
|
r.logger.Warn(E.Cause(err, "set route socket nonblock"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
routeSocketFile := os.NewFile(uintptr(routeSocket), "route")
|
||||||
|
defer routeSocketFile.Close()
|
||||||
|
buffer := buf.NewPacket()
|
||||||
|
defer buffer.Release()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-r.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
err = setReadDeadline(routeSocketFile, 3*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Warn(E.Cause(err, "set route socket read deadline"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n, err := routeSocketFile.Read(buffer.FreeBytes())
|
||||||
|
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
|
||||||
|
}
|
||||||
|
messages, err := route.ParseRIB(route.RIBTypeRoute, buffer.FreeBytes()[:n])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, message := range messages {
|
||||||
|
routeMessage, isRouteMessage := message.(*route.RouteMessage)
|
||||||
|
if !isRouteMessage {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if routeMessage.Flags&unix.RTF_LLINFO == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
address, mac, isDelete, ok := ParseRouteNeighborMessage(routeMessage)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setReadDeadline(file *os.File, timeout time.Duration) error {
|
||||||
|
rawConn, err := file.SyscallConn()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var controlErr error
|
||||||
|
err = rawConn.Control(func(fd uintptr) {
|
||||||
|
tv := unix.NsecToTimeval(int64(timeout))
|
||||||
|
controlErr = unix.SetsockoptTimeval(int(fd), unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return controlErr
|
||||||
|
}
|
||||||
@@ -0,0 +1,386 @@
|
|||||||
|
package route
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/hex"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseLeaseFile(path string, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
if strings.HasSuffix(path, "dhcpd_leases") {
|
||||||
|
parseBootpdLeases(file, ipToMAC, ipToHostname, macToHostname)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(path, "kea-leases4.csv") {
|
||||||
|
parseKeaCSV4(file, ipToMAC, ipToHostname, macToHostname)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(path, "kea-leases6.csv") {
|
||||||
|
parseKeaCSV6(file, ipToMAC, ipToHostname, macToHostname)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(path, "dhcpd.leases") {
|
||||||
|
parseISCDhcpd(file, ipToMAC, ipToHostname, macToHostname)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parseDnsmasqOdhcpd(file, ipToMAC, ipToHostname, macToHostname)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReloadLeaseFiles(leaseFiles []string) (leaseIPToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
|
||||||
|
leaseIPToMAC = make(map[netip.Addr]net.HardwareAddr)
|
||||||
|
ipToHostname = make(map[netip.Addr]string)
|
||||||
|
macToHostname = make(map[string]string)
|
||||||
|
for _, path := range leaseFiles {
|
||||||
|
parseLeaseFile(path, leaseIPToMAC, ipToHostname, macToHostname)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDnsmasqOdhcpd(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if strings.HasPrefix(line, "duid ") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "# ") {
|
||||||
|
parseOdhcpdLine(line[2:], ipToMAC, ipToHostname, macToHostname)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 4 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
expiry, err := strconv.ParseInt(fields[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if expiry != 0 && expiry < now {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(fields[1], ":") {
|
||||||
|
mac, macErr := net.ParseMAC(fields[1])
|
||||||
|
if macErr != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[2]))
|
||||||
|
if !addrOK {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
address = address.Unmap()
|
||||||
|
ipToMAC[address] = mac
|
||||||
|
hostname := fields[3]
|
||||||
|
if hostname != "*" {
|
||||||
|
ipToHostname[address] = hostname
|
||||||
|
macToHostname[mac.String()] = hostname
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var mac net.HardwareAddr
|
||||||
|
if len(fields) >= 5 {
|
||||||
|
duid, duidErr := parseDUID(fields[4])
|
||||||
|
if duidErr == nil {
|
||||||
|
mac, _ = extractMACFromDUID(duid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[2]))
|
||||||
|
if !addrOK {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
address = address.Unmap()
|
||||||
|
if mac != nil {
|
||||||
|
ipToMAC[address] = mac
|
||||||
|
}
|
||||||
|
hostname := fields[3]
|
||||||
|
if hostname != "*" {
|
||||||
|
ipToHostname[address] = hostname
|
||||||
|
if mac != nil {
|
||||||
|
macToHostname[mac.String()] = hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOdhcpdLine(line string, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 5 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
validTime, err := strconv.ParseInt(fields[4], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if validTime == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if validTime > 0 && validTime < time.Now().Unix() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hostname := fields[3]
|
||||||
|
if hostname == "-" || strings.HasPrefix(hostname, `broken\x20`) {
|
||||||
|
hostname = ""
|
||||||
|
}
|
||||||
|
if len(fields) >= 8 && fields[2] == "ipv4" {
|
||||||
|
mac, macErr := net.ParseMAC(fields[1])
|
||||||
|
if macErr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addressField := fields[7]
|
||||||
|
slashIndex := strings.IndexByte(addressField, '/')
|
||||||
|
if slashIndex >= 0 {
|
||||||
|
addressField = addressField[:slashIndex]
|
||||||
|
}
|
||||||
|
address, addrOK := netip.AddrFromSlice(net.ParseIP(addressField))
|
||||||
|
if !addrOK {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
address = address.Unmap()
|
||||||
|
ipToMAC[address] = mac
|
||||||
|
if hostname != "" {
|
||||||
|
ipToHostname[address] = hostname
|
||||||
|
macToHostname[mac.String()] = hostname
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var mac net.HardwareAddr
|
||||||
|
duidHex := fields[1]
|
||||||
|
duidBytes, hexErr := hex.DecodeString(duidHex)
|
||||||
|
if hexErr == nil {
|
||||||
|
mac, _ = extractMACFromDUID(duidBytes)
|
||||||
|
}
|
||||||
|
for i := 7; i < len(fields); i++ {
|
||||||
|
addressField := fields[i]
|
||||||
|
slashIndex := strings.IndexByte(addressField, '/')
|
||||||
|
if slashIndex >= 0 {
|
||||||
|
addressField = addressField[:slashIndex]
|
||||||
|
}
|
||||||
|
address, addrOK := netip.AddrFromSlice(net.ParseIP(addressField))
|
||||||
|
if !addrOK {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
address = address.Unmap()
|
||||||
|
if mac != nil {
|
||||||
|
ipToMAC[address] = mac
|
||||||
|
}
|
||||||
|
if hostname != "" {
|
||||||
|
ipToHostname[address] = hostname
|
||||||
|
if mac != nil {
|
||||||
|
macToHostname[mac.String()] = hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseISCDhcpd(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
var currentIP netip.Addr
|
||||||
|
var currentMAC net.HardwareAddr
|
||||||
|
var currentHostname string
|
||||||
|
var currentActive bool
|
||||||
|
var inLease bool
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if strings.HasPrefix(line, "lease ") && strings.HasSuffix(line, "{") {
|
||||||
|
ipString := strings.TrimSuffix(strings.TrimPrefix(line, "lease "), " {")
|
||||||
|
parsed, addrOK := netip.AddrFromSlice(net.ParseIP(ipString))
|
||||||
|
if addrOK {
|
||||||
|
currentIP = parsed.Unmap()
|
||||||
|
inLease = true
|
||||||
|
currentMAC = nil
|
||||||
|
currentHostname = ""
|
||||||
|
currentActive = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if line == "}" && inLease {
|
||||||
|
if currentActive && currentMAC != nil {
|
||||||
|
ipToMAC[currentIP] = currentMAC
|
||||||
|
if currentHostname != "" {
|
||||||
|
ipToHostname[currentIP] = currentHostname
|
||||||
|
macToHostname[currentMAC.String()] = currentHostname
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
delete(ipToMAC, currentIP)
|
||||||
|
delete(ipToHostname, currentIP)
|
||||||
|
}
|
||||||
|
inLease = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !inLease {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(line, "hardware ethernet ") {
|
||||||
|
macString := strings.TrimSuffix(strings.TrimPrefix(line, "hardware ethernet "), ";")
|
||||||
|
parsed, macErr := net.ParseMAC(macString)
|
||||||
|
if macErr == nil {
|
||||||
|
currentMAC = parsed
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(line, "client-hostname ") {
|
||||||
|
hostname := strings.TrimSuffix(strings.TrimPrefix(line, "client-hostname "), ";")
|
||||||
|
hostname = strings.Trim(hostname, "\"")
|
||||||
|
if hostname != "" {
|
||||||
|
currentHostname = hostname
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(line, "binding state ") {
|
||||||
|
state := strings.TrimSuffix(strings.TrimPrefix(line, "binding state "), ";")
|
||||||
|
currentActive = state == "active"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseKeaCSV4(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
firstLine := true
|
||||||
|
for scanner.Scan() {
|
||||||
|
if firstLine {
|
||||||
|
firstLine = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields := strings.Split(scanner.Text(), ",")
|
||||||
|
if len(fields) < 10 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fields[9] != "0" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[0]))
|
||||||
|
if !addrOK {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
address = address.Unmap()
|
||||||
|
mac, macErr := net.ParseMAC(fields[1])
|
||||||
|
if macErr != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ipToMAC[address] = mac
|
||||||
|
hostname := ""
|
||||||
|
if len(fields) > 8 {
|
||||||
|
hostname = fields[8]
|
||||||
|
}
|
||||||
|
if hostname != "" {
|
||||||
|
ipToHostname[address] = hostname
|
||||||
|
macToHostname[mac.String()] = hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseKeaCSV6(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
firstLine := true
|
||||||
|
for scanner.Scan() {
|
||||||
|
if firstLine {
|
||||||
|
firstLine = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields := strings.Split(scanner.Text(), ",")
|
||||||
|
if len(fields) < 14 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fields[13] != "0" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[0]))
|
||||||
|
if !addrOK {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
address = address.Unmap()
|
||||||
|
var mac net.HardwareAddr
|
||||||
|
if fields[12] != "" {
|
||||||
|
mac, _ = net.ParseMAC(fields[12])
|
||||||
|
}
|
||||||
|
if mac == nil {
|
||||||
|
duid, duidErr := hex.DecodeString(strings.ReplaceAll(fields[1], ":", ""))
|
||||||
|
if duidErr == nil {
|
||||||
|
mac, _ = extractMACFromDUID(duid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hostname := ""
|
||||||
|
if len(fields) > 11 {
|
||||||
|
hostname = fields[11]
|
||||||
|
}
|
||||||
|
if mac != nil {
|
||||||
|
ipToMAC[address] = mac
|
||||||
|
}
|
||||||
|
if hostname != "" {
|
||||||
|
ipToHostname[address] = hostname
|
||||||
|
if mac != nil {
|
||||||
|
macToHostname[mac.String()] = hostname
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseBootpdLeases(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
var currentName string
|
||||||
|
var currentIP netip.Addr
|
||||||
|
var currentMAC net.HardwareAddr
|
||||||
|
var currentLease int64
|
||||||
|
var inBlock bool
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "{" {
|
||||||
|
inBlock = true
|
||||||
|
currentName = ""
|
||||||
|
currentIP = netip.Addr{}
|
||||||
|
currentMAC = nil
|
||||||
|
currentLease = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if line == "}" && inBlock {
|
||||||
|
if currentMAC != nil && currentIP.IsValid() {
|
||||||
|
if currentLease == 0 || currentLease >= now {
|
||||||
|
ipToMAC[currentIP] = currentMAC
|
||||||
|
if currentName != "" {
|
||||||
|
ipToHostname[currentIP] = currentName
|
||||||
|
macToHostname[currentMAC.String()] = currentName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inBlock = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !inBlock {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key, value, found := strings.Cut(line, "=")
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch key {
|
||||||
|
case "name":
|
||||||
|
currentName = value
|
||||||
|
case "ip_address":
|
||||||
|
parsed, addrOK := netip.AddrFromSlice(net.ParseIP(value))
|
||||||
|
if addrOK {
|
||||||
|
currentIP = parsed.Unmap()
|
||||||
|
}
|
||||||
|
case "hw_address":
|
||||||
|
typeAndMAC, hasSep := strings.CutPrefix(value, "1,")
|
||||||
|
if hasSep {
|
||||||
|
mac, macErr := net.ParseMAC(typeAndMAC)
|
||||||
|
if macErr == nil {
|
||||||
|
currentMAC = mac
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "lease":
|
||||||
|
leaseHex := strings.TrimPrefix(value, "0x")
|
||||||
|
parsed, parseErr := strconv.ParseInt(leaseHex, 16, 64)
|
||||||
|
if parseErr == nil {
|
||||||
|
currentLease = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,14 +3,10 @@
|
|||||||
package route
|
package route
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"encoding/hex"
|
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -69,14 +65,14 @@ func (r *neighborResolver) Start() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
r.logger.Warn(E.Cause(err, "load neighbor table"))
|
r.logger.Warn(E.Cause(err, "load neighbor table"))
|
||||||
}
|
}
|
||||||
r.reloadLeaseFiles()
|
r.doReloadLeaseFiles()
|
||||||
go r.subscribeNeighborUpdates()
|
go r.subscribeNeighborUpdates()
|
||||||
if len(r.leaseFiles) > 0 {
|
if len(r.leaseFiles) > 0 {
|
||||||
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
||||||
Path: r.leaseFiles,
|
Path: r.leaseFiles,
|
||||||
Logger: r.logger,
|
Logger: r.logger,
|
||||||
Callback: func(_ string) {
|
Callback: func(_ string) {
|
||||||
r.reloadLeaseFiles()
|
r.doReloadLeaseFiles()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -218,312 +214,11 @@ func (r *neighborResolver) subscribeNeighborUpdates() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *neighborResolver) reloadLeaseFiles() {
|
func (r *neighborResolver) doReloadLeaseFiles() {
|
||||||
leaseIPToMAC := make(map[netip.Addr]net.HardwareAddr)
|
leaseIPToMAC, ipToHostname, macToHostname := ReloadLeaseFiles(r.leaseFiles)
|
||||||
ipToHostname := make(map[netip.Addr]string)
|
|
||||||
macToHostname := make(map[string]string)
|
|
||||||
for _, path := range r.leaseFiles {
|
|
||||||
r.parseLeaseFile(path, leaseIPToMAC, ipToHostname, macToHostname)
|
|
||||||
}
|
|
||||||
r.access.Lock()
|
r.access.Lock()
|
||||||
r.leaseIPToMAC = leaseIPToMAC
|
r.leaseIPToMAC = leaseIPToMAC
|
||||||
r.ipToHostname = ipToHostname
|
r.ipToHostname = ipToHostname
|
||||||
r.macToHostname = macToHostname
|
r.macToHostname = macToHostname
|
||||||
r.access.Unlock()
|
r.access.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *neighborResolver) parseLeaseFile(path string, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
|
|
||||||
file, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
if strings.HasSuffix(path, "kea-leases4.csv") {
|
|
||||||
r.parseKeaCSV4(file, ipToMAC, ipToHostname, macToHostname)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(path, "kea-leases6.csv") {
|
|
||||||
r.parseKeaCSV6(file, ipToMAC, ipToHostname, macToHostname)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(path, "dhcpd.leases") {
|
|
||||||
r.parseISCDhcpd(file, ipToMAC, ipToHostname, macToHostname)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
r.parseDnsmasqOdhcpd(file, ipToMAC, ipToHostname, macToHostname)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *neighborResolver) parseDnsmasqOdhcpd(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
|
|
||||||
now := time.Now().Unix()
|
|
||||||
scanner := bufio.NewScanner(file)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if strings.HasPrefix(line, "duid ") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(line, "# ") {
|
|
||||||
r.parseOdhcpdLine(line[2:], ipToMAC, ipToHostname, macToHostname)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fields := strings.Fields(line)
|
|
||||||
if len(fields) < 4 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
expiry, err := strconv.ParseInt(fields[0], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if expiry != 0 && expiry < now {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.Contains(fields[1], ":") {
|
|
||||||
mac, macErr := net.ParseMAC(fields[1])
|
|
||||||
if macErr != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[2]))
|
|
||||||
if !addrOK {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
address = address.Unmap()
|
|
||||||
ipToMAC[address] = mac
|
|
||||||
hostname := fields[3]
|
|
||||||
if hostname != "*" {
|
|
||||||
ipToHostname[address] = hostname
|
|
||||||
macToHostname[mac.String()] = hostname
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var mac net.HardwareAddr
|
|
||||||
if len(fields) >= 5 {
|
|
||||||
duid, duidErr := parseDUID(fields[4])
|
|
||||||
if duidErr == nil {
|
|
||||||
mac, _ = extractMACFromDUID(duid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[2]))
|
|
||||||
if !addrOK {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
address = address.Unmap()
|
|
||||||
if mac != nil {
|
|
||||||
ipToMAC[address] = mac
|
|
||||||
}
|
|
||||||
hostname := fields[3]
|
|
||||||
if hostname != "*" {
|
|
||||||
ipToHostname[address] = hostname
|
|
||||||
if mac != nil {
|
|
||||||
macToHostname[mac.String()] = hostname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *neighborResolver) parseOdhcpdLine(line string, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
|
|
||||||
fields := strings.Fields(line)
|
|
||||||
if len(fields) < 5 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
validTime, err := strconv.ParseInt(fields[4], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if validTime == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if validTime > 0 && validTime < time.Now().Unix() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hostname := fields[3]
|
|
||||||
if hostname == "-" || strings.HasPrefix(hostname, `broken\x20`) {
|
|
||||||
hostname = ""
|
|
||||||
}
|
|
||||||
if len(fields) >= 8 && fields[2] == "ipv4" {
|
|
||||||
mac, macErr := net.ParseMAC(fields[1])
|
|
||||||
if macErr != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
addressField := fields[7]
|
|
||||||
slashIndex := strings.IndexByte(addressField, '/')
|
|
||||||
if slashIndex >= 0 {
|
|
||||||
addressField = addressField[:slashIndex]
|
|
||||||
}
|
|
||||||
address, addrOK := netip.AddrFromSlice(net.ParseIP(addressField))
|
|
||||||
if !addrOK {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
address = address.Unmap()
|
|
||||||
ipToMAC[address] = mac
|
|
||||||
if hostname != "" {
|
|
||||||
ipToHostname[address] = hostname
|
|
||||||
macToHostname[mac.String()] = hostname
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var mac net.HardwareAddr
|
|
||||||
duidHex := fields[1]
|
|
||||||
duidBytes, hexErr := hex.DecodeString(duidHex)
|
|
||||||
if hexErr == nil {
|
|
||||||
mac, _ = extractMACFromDUID(duidBytes)
|
|
||||||
}
|
|
||||||
for i := 7; i < len(fields); i++ {
|
|
||||||
addressField := fields[i]
|
|
||||||
slashIndex := strings.IndexByte(addressField, '/')
|
|
||||||
if slashIndex >= 0 {
|
|
||||||
addressField = addressField[:slashIndex]
|
|
||||||
}
|
|
||||||
address, addrOK := netip.AddrFromSlice(net.ParseIP(addressField))
|
|
||||||
if !addrOK {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
address = address.Unmap()
|
|
||||||
if mac != nil {
|
|
||||||
ipToMAC[address] = mac
|
|
||||||
}
|
|
||||||
if hostname != "" {
|
|
||||||
ipToHostname[address] = hostname
|
|
||||||
if mac != nil {
|
|
||||||
macToHostname[mac.String()] = hostname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *neighborResolver) parseISCDhcpd(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
|
|
||||||
scanner := bufio.NewScanner(file)
|
|
||||||
var currentIP netip.Addr
|
|
||||||
var currentMAC net.HardwareAddr
|
|
||||||
var currentHostname string
|
|
||||||
var currentActive bool
|
|
||||||
var inLease bool
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := strings.TrimSpace(scanner.Text())
|
|
||||||
if strings.HasPrefix(line, "lease ") && strings.HasSuffix(line, "{") {
|
|
||||||
ipString := strings.TrimSuffix(strings.TrimPrefix(line, "lease "), " {")
|
|
||||||
parsed, addrOK := netip.AddrFromSlice(net.ParseIP(ipString))
|
|
||||||
if addrOK {
|
|
||||||
currentIP = parsed.Unmap()
|
|
||||||
inLease = true
|
|
||||||
currentMAC = nil
|
|
||||||
currentHostname = ""
|
|
||||||
currentActive = false
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if line == "}" && inLease {
|
|
||||||
if currentActive && currentMAC != nil {
|
|
||||||
ipToMAC[currentIP] = currentMAC
|
|
||||||
if currentHostname != "" {
|
|
||||||
ipToHostname[currentIP] = currentHostname
|
|
||||||
macToHostname[currentMAC.String()] = currentHostname
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
delete(ipToMAC, currentIP)
|
|
||||||
delete(ipToHostname, currentIP)
|
|
||||||
}
|
|
||||||
inLease = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !inLease {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(line, "hardware ethernet ") {
|
|
||||||
macString := strings.TrimSuffix(strings.TrimPrefix(line, "hardware ethernet "), ";")
|
|
||||||
parsed, macErr := net.ParseMAC(macString)
|
|
||||||
if macErr == nil {
|
|
||||||
currentMAC = parsed
|
|
||||||
}
|
|
||||||
} else if strings.HasPrefix(line, "client-hostname ") {
|
|
||||||
hostname := strings.TrimSuffix(strings.TrimPrefix(line, "client-hostname "), ";")
|
|
||||||
hostname = strings.Trim(hostname, "\"")
|
|
||||||
if hostname != "" {
|
|
||||||
currentHostname = hostname
|
|
||||||
}
|
|
||||||
} else if strings.HasPrefix(line, "binding state ") {
|
|
||||||
state := strings.TrimSuffix(strings.TrimPrefix(line, "binding state "), ";")
|
|
||||||
currentActive = state == "active"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *neighborResolver) parseKeaCSV4(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
|
|
||||||
scanner := bufio.NewScanner(file)
|
|
||||||
firstLine := true
|
|
||||||
for scanner.Scan() {
|
|
||||||
if firstLine {
|
|
||||||
firstLine = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fields := strings.Split(scanner.Text(), ",")
|
|
||||||
if len(fields) < 10 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if fields[9] != "0" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[0]))
|
|
||||||
if !addrOK {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
address = address.Unmap()
|
|
||||||
mac, macErr := net.ParseMAC(fields[1])
|
|
||||||
if macErr != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ipToMAC[address] = mac
|
|
||||||
hostname := ""
|
|
||||||
if len(fields) > 8 {
|
|
||||||
hostname = fields[8]
|
|
||||||
}
|
|
||||||
if hostname != "" {
|
|
||||||
ipToHostname[address] = hostname
|
|
||||||
macToHostname[mac.String()] = hostname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *neighborResolver) parseKeaCSV6(file *os.File, ipToMAC map[netip.Addr]net.HardwareAddr, ipToHostname map[netip.Addr]string, macToHostname map[string]string) {
|
|
||||||
scanner := bufio.NewScanner(file)
|
|
||||||
firstLine := true
|
|
||||||
for scanner.Scan() {
|
|
||||||
if firstLine {
|
|
||||||
firstLine = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fields := strings.Split(scanner.Text(), ",")
|
|
||||||
if len(fields) < 14 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if fields[13] != "0" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
address, addrOK := netip.AddrFromSlice(net.ParseIP(fields[0]))
|
|
||||||
if !addrOK {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
address = address.Unmap()
|
|
||||||
var mac net.HardwareAddr
|
|
||||||
if fields[12] != "" {
|
|
||||||
mac, _ = net.ParseMAC(fields[12])
|
|
||||||
}
|
|
||||||
if mac == nil {
|
|
||||||
duid, duidErr := hex.DecodeString(strings.ReplaceAll(fields[1], ":", ""))
|
|
||||||
if duidErr == nil {
|
|
||||||
mac, _ = extractMACFromDUID(duid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hostname := ""
|
|
||||||
if len(fields) > 11 {
|
|
||||||
hostname = fields[11]
|
|
||||||
}
|
|
||||||
if mac != nil {
|
|
||||||
ipToMAC[address] = mac
|
|
||||||
}
|
|
||||||
if hostname != "" {
|
|
||||||
ipToHostname[address] = hostname
|
|
||||||
if mac != nil {
|
|
||||||
macToHostname[mac.String()] = hostname
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//go:build !linux
|
//go:build !linux && !darwin
|
||||||
|
|
||||||
package route
|
package route
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package route
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
"golang.org/x/net/route"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ReadNeighborEntries() ([]adapter.NeighborEntry, error) {
|
||||||
|
var entries []adapter.NeighborEntry
|
||||||
|
ipv4Entries, err := readNeighborEntriesAF(syscall.AF_INET)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read IPv4 neighbors")
|
||||||
|
}
|
||||||
|
entries = append(entries, ipv4Entries...)
|
||||||
|
ipv6Entries, err := readNeighborEntriesAF(syscall.AF_INET6)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "read IPv6 neighbors")
|
||||||
|
}
|
||||||
|
entries = append(entries, ipv6Entries...)
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNeighborEntriesAF(addressFamily int) ([]adapter.NeighborEntry, error) {
|
||||||
|
rib, err := route.FetchRIB(addressFamily, route.RIBType(syscall.NET_RT_FLAGS), syscall.RTF_LLINFO)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
messages, err := route.ParseRIB(route.RIBType(syscall.NET_RT_FLAGS), rib)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var entries []adapter.NeighborEntry
|
||||||
|
for _, message := range messages {
|
||||||
|
routeMessage, isRouteMessage := message.(*route.RouteMessage)
|
||||||
|
if !isRouteMessage {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
address, macAddress, ok := parseRouteNeighborEntry(routeMessage)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entries = append(entries, adapter.NeighborEntry{
|
||||||
|
Address: address,
|
||||||
|
MACAddress: macAddress,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return entries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRouteNeighborEntry(message *route.RouteMessage) (address netip.Addr, macAddress net.HardwareAddr, ok bool) {
|
||||||
|
if len(message.Addrs) <= unix.RTAX_GATEWAY {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
gateway, isLinkAddr := message.Addrs[unix.RTAX_GATEWAY].(*route.LinkAddr)
|
||||||
|
if !isLinkAddr || len(gateway.Addr) < 6 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch destination := message.Addrs[unix.RTAX_DST].(type) {
|
||||||
|
case *route.Inet4Addr:
|
||||||
|
address = netip.AddrFrom4(destination.IP)
|
||||||
|
case *route.Inet6Addr:
|
||||||
|
address = netip.AddrFrom16(destination.IP)
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
macAddress = net.HardwareAddr(make([]byte, len(gateway.Addr)))
|
||||||
|
copy(macAddress, gateway.Addr)
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseRouteNeighborMessage(message *route.RouteMessage) (address netip.Addr, macAddress net.HardwareAddr, isDelete bool, ok bool) {
|
||||||
|
isDelete = message.Type == unix.RTM_DELETE
|
||||||
|
if len(message.Addrs) <= unix.RTAX_GATEWAY {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch destination := message.Addrs[unix.RTAX_DST].(type) {
|
||||||
|
case *route.Inet4Addr:
|
||||||
|
address = netip.AddrFrom4(destination.IP)
|
||||||
|
case *route.Inet6Addr:
|
||||||
|
address = netip.AddrFrom16(destination.IP)
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !isDelete {
|
||||||
|
gateway, isLinkAddr := message.Addrs[unix.RTAX_GATEWAY].(*route.LinkAddr)
|
||||||
|
if !isLinkAddr || len(gateway.Addr) < 6 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
macAddress = net.HardwareAddr(make([]byte, len(gateway.Addr)))
|
||||||
|
copy(macAddress, gateway.Addr)
|
||||||
|
}
|
||||||
|
ok = true
|
||||||
|
return
|
||||||
|
}
|
||||||
+1
-2
@@ -169,8 +169,7 @@ func (r *Router) Start(stage adapter.StartStage) error {
|
|||||||
} else {
|
} else {
|
||||||
r.neighborResolver = resolver
|
r.neighborResolver = resolver
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
if r.neighborResolver == nil {
|
|
||||||
monitor.Start("initialize neighbor resolver")
|
monitor.Start("initialize neighbor resolver")
|
||||||
resolver, err := newNeighborResolver(r.logger, r.leaseFiles)
|
resolver, err := newNeighborResolver(r.logger, r.leaseFiles)
|
||||||
monitor.Finish()
|
monitor.Finish()
|
||||||
|
|||||||
Reference in New Issue
Block a user