Add Android support for MAC and hostname rule items

This commit is contained in:
世界
2026-03-05 00:15:37 +08:00
parent 04c0490992
commit 45339d101b
12 changed files with 463 additions and 87 deletions

View File

@@ -5,9 +5,19 @@ import (
"net/netip"
)
type NeighborEntry struct {
Address netip.Addr
MACAddress net.HardwareAddr
Hostname string
}
type NeighborResolver interface {
LookupMAC(address netip.Addr) (net.HardwareAddr, bool)
LookupHostname(address netip.Addr) (string, bool)
Start() error
Close() error
}
type NeighborUpdateListener interface {
UpdateNeighborTable(entries []NeighborEntry)
}

View File

@@ -36,6 +36,10 @@ type PlatformInterface interface {
UsePlatformNotification() bool
SendNotification(notification *Notification) error
UsePlatformNeighborResolver() bool
StartNeighborMonitor(listener NeighborUpdateListener) error
CloseNeighborMonitor(listener NeighborUpdateListener) error
}
type FindConnectionOwnerRequest struct {

View File

@@ -144,6 +144,18 @@ func (s *platformInterfaceStub) SendNotification(notification *adapter.Notificat
return nil
}
func (s *platformInterfaceStub) UsePlatformNeighborResolver() bool {
return false
}
func (s *platformInterfaceStub) StartNeighborMonitor(listener adapter.NeighborUpdateListener) error {
return os.ErrInvalid
}
func (s *platformInterfaceStub) CloseNeighborMonitor(listener adapter.NeighborUpdateListener) error {
return nil
}
func (s *platformInterfaceStub) UsePlatformLocalDNSTransport() bool {
return false
}

View File

@@ -0,0 +1,135 @@
//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"
)
type NeighborEntry struct {
Address string
MACAddress string
Hostname string
}
type NeighborEntryIterator interface {
Next() *NeighborEntry
HasNext() bool
}
type NeighborSubscription 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() {
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 {
entries := make([]*NeighborEntry, 0, len(table))
for address, mac := range table {
entries = append(entries, &NeighborEntry{
Address: address.String(),
MACAddress: mac.String(),
})
}
return &neighborEntryIterator{entries}
}
type neighborEntryIterator struct {
entries []*NeighborEntry
}
func (i *neighborEntryIterator) HasNext() bool {
return len(i.entries) > 0
}
func (i *neighborEntryIterator) Next() *NeighborEntry {
if len(i.entries) == 0 {
return nil
}
entry := i.entries[0]
i.entries = i.entries[1:]
return entry
}

View File

@@ -0,0 +1,24 @@
//go:build !linux
package libbox
import "os"
type NeighborEntry struct {
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
}
func (s *NeighborSubscription) Close() {}

View File

@@ -21,6 +21,12 @@ type PlatformInterface interface {
SystemCertificates() StringIterator
ClearDNSCache()
SendNotification(notification *Notification) error
StartNeighborMonitor(listener NeighborUpdateListener) error
CloseNeighborMonitor(listener NeighborUpdateListener) error
}
type NeighborUpdateListener interface {
UpdateNeighborTable(entries NeighborEntryIterator)
}
type ConnectionOwner struct {

View File

@@ -220,6 +220,43 @@ func (w *platformInterfaceWrapper) SendNotification(notification *adapter.Notifi
return w.iif.SendNotification((*Notification)(notification))
}
func (w *platformInterfaceWrapper) UsePlatformNeighborResolver() bool {
return true
}
func (w *platformInterfaceWrapper) StartNeighborMonitor(listener adapter.NeighborUpdateListener) error {
return w.iif.StartNeighborMonitor(&neighborUpdateListenerWrapper{listener: listener})
}
func (w *platformInterfaceWrapper) CloseNeighborMonitor(listener adapter.NeighborUpdateListener) error {
return w.iif.CloseNeighborMonitor(nil)
}
type neighborUpdateListenerWrapper struct {
listener adapter.NeighborUpdateListener
}
func (w *neighborUpdateListenerWrapper) UpdateNeighborTable(entries NeighborEntryIterator) {
var result []adapter.NeighborEntry
for entries.HasNext() {
entry := entries.Next()
address, err := netip.ParseAddr(entry.Address)
if err != nil {
continue
}
macAddress, err := net.ParseMAC(entry.MACAddress)
if err != nil {
continue
}
result = append(result, adapter.NeighborEntry{
Address: address,
MACAddress: macAddress,
Hostname: entry.Hostname,
})
}
w.listener.UpdateNeighborTable(result)
}
func AvailablePort(startPort int32) (int32, error) {
for port := int(startPort); ; port++ {
if port > 65535 {

View File

@@ -4,7 +4,6 @@ package route
import (
"bufio"
"encoding/binary"
"encoding/hex"
"net"
"net/netip"
@@ -204,43 +203,17 @@ func (r *neighborResolver) subscribeNeighborUpdates() {
continue
}
for _, message := range messages {
switch message.Header.Type {
case unix.RTM_NEWNEIGH:
var neighMessage rtnetlink.NeighMessage
unmarshalErr := neighMessage.UnmarshalBinary(message.Data)
if unmarshalErr != nil {
continue
}
if neighMessage.Attributes == nil {
continue
}
if neighMessage.Attributes.LLAddress == nil || len(neighMessage.Attributes.Address) == 0 {
continue
}
address, ok := netip.AddrFromSlice(neighMessage.Attributes.Address)
if !ok {
continue
}
r.access.Lock()
r.neighborIPToMAC[address] = slices.Clone(neighMessage.Attributes.LLAddress)
r.access.Unlock()
case unix.RTM_DELNEIGH:
var neighMessage rtnetlink.NeighMessage
unmarshalErr := neighMessage.UnmarshalBinary(message.Data)
if unmarshalErr != nil {
continue
}
if neighMessage.Attributes == nil || len(neighMessage.Attributes.Address) == 0 {
continue
}
address, ok := netip.AddrFromSlice(neighMessage.Attributes.Address)
if !ok {
continue
}
r.access.Lock()
delete(r.neighborIPToMAC, address)
r.access.Unlock()
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()
}
}
}
@@ -554,43 +527,3 @@ func (r *neighborResolver) parseKeaCSV6(file *os.File, ipToMAC map[netip.Addr]ne
}
}
}
func extractMACFromDUID(duid []byte) (net.HardwareAddr, bool) {
if len(duid) < 4 {
return nil, false
}
duidType := binary.BigEndian.Uint16(duid[0:2])
hwType := binary.BigEndian.Uint16(duid[2:4])
if hwType != 1 {
return nil, false
}
switch duidType {
case 1:
if len(duid) < 14 {
return nil, false
}
return net.HardwareAddr(slices.Clone(duid[8:14])), true
case 3:
if len(duid) < 10 {
return nil, false
}
return net.HardwareAddr(slices.Clone(duid[4:10])), true
}
return nil, false
}
func extractMACFromEUI64(address netip.Addr) (net.HardwareAddr, bool) {
if !address.Is6() {
return nil, false
}
b := address.As16()
if b[11] != 0xff || b[12] != 0xfe {
return nil, false
}
return net.HardwareAddr{b[8] ^ 0x02, b[9], b[10], b[13], b[14], b[15]}, true
}
func parseDUID(s string) ([]byte, error) {
cleaned := strings.ReplaceAll(s, ":", "")
return hex.DecodeString(cleaned)
}

View File

@@ -0,0 +1,50 @@
package route
import (
"encoding/binary"
"encoding/hex"
"net"
"net/netip"
"slices"
"strings"
)
func extractMACFromDUID(duid []byte) (net.HardwareAddr, bool) {
if len(duid) < 4 {
return nil, false
}
duidType := binary.BigEndian.Uint16(duid[0:2])
hwType := binary.BigEndian.Uint16(duid[2:4])
if hwType != 1 {
return nil, false
}
switch duidType {
case 1:
if len(duid) < 14 {
return nil, false
}
return net.HardwareAddr(slices.Clone(duid[8:14])), true
case 3:
if len(duid) < 10 {
return nil, false
}
return net.HardwareAddr(slices.Clone(duid[4:10])), true
}
return nil, false
}
func extractMACFromEUI64(address netip.Addr) (net.HardwareAddr, bool) {
if !address.Is6() {
return nil, false
}
b := address.As16()
if b[11] != 0xff || b[12] != 0xfe {
return nil, false
}
return net.HardwareAddr{b[8] ^ 0x02, b[9], b[10], b[13], b[14], b[15]}, true
}
func parseDUID(s string) ([]byte, error) {
cleaned := strings.ReplaceAll(s, ":", "")
return hex.DecodeString(cleaned)
}

View File

@@ -0,0 +1,84 @@
package route
import (
"net"
"net/netip"
"sync"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/logger"
)
type platformNeighborResolver struct {
logger logger.ContextLogger
platform adapter.PlatformInterface
access sync.RWMutex
ipToMAC map[netip.Addr]net.HardwareAddr
ipToHostname map[netip.Addr]string
macToHostname map[string]string
}
func newPlatformNeighborResolver(resolverLogger logger.ContextLogger, platform adapter.PlatformInterface) adapter.NeighborResolver {
return &platformNeighborResolver{
logger: resolverLogger,
platform: platform,
ipToMAC: make(map[netip.Addr]net.HardwareAddr),
ipToHostname: make(map[netip.Addr]string),
macToHostname: make(map[string]string),
}
}
func (r *platformNeighborResolver) Start() error {
return r.platform.StartNeighborMonitor(r)
}
func (r *platformNeighborResolver) Close() error {
return r.platform.CloseNeighborMonitor(r)
}
func (r *platformNeighborResolver) LookupMAC(address netip.Addr) (net.HardwareAddr, bool) {
r.access.RLock()
defer r.access.RUnlock()
mac, found := r.ipToMAC[address]
if found {
return mac, true
}
return extractMACFromEUI64(address)
}
func (r *platformNeighborResolver) LookupHostname(address netip.Addr) (string, bool) {
r.access.RLock()
defer r.access.RUnlock()
hostname, found := r.ipToHostname[address]
if found {
return hostname, true
}
mac, found := r.ipToMAC[address]
if !found {
mac, found = extractMACFromEUI64(address)
}
if !found {
return "", false
}
hostname, found = r.macToHostname[mac.String()]
return hostname, found
}
func (r *platformNeighborResolver) UpdateNeighborTable(entries []adapter.NeighborEntry) {
ipToMAC := make(map[netip.Addr]net.HardwareAddr)
ipToHostname := make(map[netip.Addr]string)
macToHostname := make(map[string]string)
for _, entry := range entries {
ipToMAC[entry.Address] = entry.MACAddress
if entry.Hostname != "" {
ipToHostname[entry.Address] = entry.Hostname
macToHostname[entry.MACAddress.String()] = entry.Hostname
}
}
r.access.Lock()
r.ipToMAC = ipToMAC
r.ipToHostname = ipToHostname
r.macToHostname = macToHostname
r.access.Unlock()
r.logger.Info("updated neighbor table: ", len(entries), " entries")
}

View File

@@ -0,0 +1,68 @@
//go:build linux
package route
import (
"net"
"net/netip"
"slices"
"github.com/sagernet/sing-box/adapter"
E "github.com/sagernet/sing/common/exceptions"
"github.com/jsimonetti/rtnetlink"
"github.com/mdlayher/netlink"
"golang.org/x/sys/unix"
)
func ReadNeighborEntries() ([]adapter.NeighborEntry, error) {
connection, err := rtnetlink.Dial(nil)
if err != nil {
return nil, E.Cause(err, "dial rtnetlink")
}
defer connection.Close()
neighbors, err := connection.Neigh.List()
if err != nil {
return nil, E.Cause(err, "list neighbors")
}
var entries []adapter.NeighborEntry
for _, neighbor := range neighbors {
if neighbor.Attributes == nil {
continue
}
if neighbor.Attributes.LLAddress == nil || len(neighbor.Attributes.Address) == 0 {
continue
}
address, ok := netip.AddrFromSlice(neighbor.Attributes.Address)
if !ok {
continue
}
entries = append(entries, adapter.NeighborEntry{
Address: address,
MACAddress: slices.Clone(neighbor.Attributes.LLAddress),
})
}
return entries, nil
}
func ParseNeighborMessage(message netlink.Message) (address netip.Addr, macAddress net.HardwareAddr, isDelete bool, ok bool) {
var neighMessage rtnetlink.NeighMessage
err := neighMessage.UnmarshalBinary(message.Data)
if err != nil {
return
}
if neighMessage.Attributes == nil || len(neighMessage.Attributes.Address) == 0 {
return
}
address, ok = netip.AddrFromSlice(neighMessage.Attributes.Address)
if !ok {
return
}
isDelete = message.Header.Type == unix.RTM_DELNEIGH
if !isDelete && neighMessage.Attributes.LLAddress == nil {
ok = false
return
}
macAddress = slices.Clone(neighMessage.Attributes.LLAddress)
return
}

View File

@@ -159,21 +159,34 @@ func (r *Router) Start(stage adapter.StartStage) error {
}
r.needFindNeighbor = needFindNeighbor
if needFindNeighbor {
monitor.Start("initialize neighbor resolver")
resolver, err := newNeighborResolver(r.logger, r.leaseFiles)
monitor.Finish()
if err != nil {
if err != os.ErrInvalid {
r.logger.Warn(E.Cause(err, "create neighbor resolver"))
}
} else {
err = resolver.Start()
if r.platformInterface != nil && r.platformInterface.UsePlatformNeighborResolver() {
monitor.Start("initialize neighbor resolver")
resolver := newPlatformNeighborResolver(r.logger, r.platformInterface)
err := resolver.Start()
monitor.Finish()
if err != nil {
r.logger.Warn(E.Cause(err, "start neighbor resolver"))
r.logger.Error(E.Cause(err, "start neighbor resolver"))
} else {
r.neighborResolver = resolver
}
}
if r.neighborResolver == nil {
monitor.Start("initialize neighbor resolver")
resolver, err := newNeighborResolver(r.logger, r.leaseFiles)
monitor.Finish()
if err != nil {
if err != os.ErrInvalid {
r.logger.Error(E.Cause(err, "create neighbor resolver"))
}
} else {
err = resolver.Start()
if err != nil {
r.logger.Error(E.Cause(err, "start neighbor resolver"))
} else {
r.neighborResolver = resolver
}
}
}
}
case adapter.StartStatePostStart:
for i, rule := range r.rules {