mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-11 17:47:20 +10:00
Add MAC and hostname rule items
This commit is contained in:
@@ -2,6 +2,7 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
@@ -82,6 +83,8 @@ type InboundContext struct {
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *ConnectionOwner
|
||||
SourceMACAddress net.HardwareAddr
|
||||
SourceHostname string
|
||||
QueryType uint16
|
||||
FakeIP bool
|
||||
|
||||
|
||||
13
adapter/neighbor.go
Normal file
13
adapter/neighbor.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type NeighborResolver interface {
|
||||
LookupMAC(address netip.Addr) (net.HardwareAddr, bool)
|
||||
LookupHostname(address netip.Addr) (string, bool)
|
||||
Start() error
|
||||
Close() error
|
||||
}
|
||||
@@ -26,6 +26,8 @@ type Router interface {
|
||||
RuleSet(tag string) (RuleSet, bool)
|
||||
Rules() []Rule
|
||||
NeedFindProcess() bool
|
||||
NeedFindNeighbor() bool
|
||||
NeighborResolver() NeighborResolver
|
||||
AppendTracker(tracker ConnectionTracker)
|
||||
ResetNetwork()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [source_mac_address](#source_mac_address)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
@@ -149,6 +154,12 @@ icon: material/alert-decagram
|
||||
"default_interface_address": [
|
||||
"2000::/3"
|
||||
],
|
||||
"source_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"source_hostname": [
|
||||
"my-device"
|
||||
],
|
||||
"wifi_ssid": [
|
||||
"My WIFI"
|
||||
],
|
||||
@@ -408,6 +419,26 @@ Matches network interface (same values as `network_type`) address.
|
||||
|
||||
Match default interface address.
|
||||
|
||||
#### source_mac_address
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux with `route.find_neighbor` enabled.
|
||||
|
||||
Match source device MAC address.
|
||||
|
||||
#### source_hostname
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux with `route.find_neighbor` enabled.
|
||||
|
||||
Match source device hostname from DHCP leases.
|
||||
|
||||
#### wifi_ssid
|
||||
|
||||
!!! quote ""
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [source_mac_address](#source_mac_address)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
@@ -149,6 +154,12 @@ icon: material/alert-decagram
|
||||
"default_interface_address": [
|
||||
"2000::/3"
|
||||
],
|
||||
"source_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"source_hostname": [
|
||||
"my-device"
|
||||
],
|
||||
"wifi_ssid": [
|
||||
"My WIFI"
|
||||
],
|
||||
@@ -407,6 +418,26 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
匹配默认接口地址。
|
||||
|
||||
#### source_mac_address
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux,且需要 `route.find_neighbor` 已启用。
|
||||
|
||||
匹配源设备 MAC 地址。
|
||||
|
||||
#### source_hostname
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux,且需要 `route.find_neighbor` 已启用。
|
||||
|
||||
匹配源设备从 DHCP 租约获取的主机名。
|
||||
|
||||
#### wifi_ssid
|
||||
|
||||
!!! quote ""
|
||||
|
||||
@@ -134,6 +134,12 @@ icon: material/new-box
|
||||
"exclude_package": [
|
||||
"com.android.captiveportallogin"
|
||||
],
|
||||
"include_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"exclude_mac_address": [
|
||||
"66:77:88:99:aa:bb"
|
||||
],
|
||||
"platform": {
|
||||
"http_proxy": {
|
||||
"enabled": false,
|
||||
@@ -560,6 +566,30 @@ Limit android packages in route.
|
||||
|
||||
Exclude android packages in route.
|
||||
|
||||
#### include_mac_address
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
|
||||
|
||||
Limit MAC addresses in route. Not limited by default.
|
||||
|
||||
Conflict with `exclude_mac_address`.
|
||||
|
||||
#### exclude_mac_address
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
|
||||
|
||||
Exclude MAC addresses in route.
|
||||
|
||||
Conflict with `include_mac_address`.
|
||||
|
||||
#### platform
|
||||
|
||||
Platform-specific settings, provided by client applications.
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [include_mac_address](#include_mac_address)
|
||||
:material-plus: [exclude_mac_address](#exclude_mac_address)
|
||||
|
||||
!!! quote "sing-box 1.13.3 中的更改"
|
||||
|
||||
:material-alert: [strict_route](#strict_route)
|
||||
@@ -130,6 +135,12 @@ icon: material/new-box
|
||||
"exclude_package": [
|
||||
"com.android.captiveportallogin"
|
||||
],
|
||||
"include_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"exclude_mac_address": [
|
||||
"66:77:88:99:aa:bb"
|
||||
],
|
||||
"platform": {
|
||||
"http_proxy": {
|
||||
"enabled": false,
|
||||
@@ -543,6 +554,30 @@ TCP/IP 栈。
|
||||
|
||||
排除路由的 Android 应用包名。
|
||||
|
||||
#### include_mac_address
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux,且需要 `auto_route` 和 `auto_redirect` 已启用。
|
||||
|
||||
限制被路由的 MAC 地址。默认不限制。
|
||||
|
||||
与 `exclude_mac_address` 冲突。
|
||||
|
||||
#### exclude_mac_address
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux,且需要 `auto_route` 和 `auto_redirect` 已启用。
|
||||
|
||||
排除路由的 MAC 地址。
|
||||
|
||||
与 `include_mac_address` 冲突。
|
||||
|
||||
#### platform
|
||||
|
||||
平台特定的设置,由客户端应用提供。
|
||||
|
||||
@@ -4,6 +4,11 @@ icon: material/alert-decagram
|
||||
|
||||
# Route
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [find_neighbor](#find_neighbor)
|
||||
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [default_domain_resolver](#default_domain_resolver)
|
||||
@@ -35,6 +40,8 @@ icon: material/alert-decagram
|
||||
"override_android_vpn": false,
|
||||
"default_interface": "",
|
||||
"default_mark": 0,
|
||||
"find_neighbor": false,
|
||||
"dhcp_lease_files": [],
|
||||
"default_domain_resolver": "", // or {}
|
||||
"default_network_strategy": "",
|
||||
"default_network_type": [],
|
||||
@@ -107,6 +114,30 @@ Set routing mark by default.
|
||||
|
||||
Takes no effect if `outbound.routing_mark` is set.
|
||||
|
||||
#### find_neighbor
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux.
|
||||
|
||||
Enable neighbor resolution for source MAC address and hostname lookup.
|
||||
|
||||
Required for `source_mac_address` and `source_hostname` rule items.
|
||||
|
||||
#### dhcp_lease_files
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux.
|
||||
|
||||
Custom DHCP lease file paths for hostname and MAC address resolution.
|
||||
|
||||
Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea) if empty.
|
||||
|
||||
#### default_domain_resolver
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
@@ -4,6 +4,11 @@ icon: material/alert-decagram
|
||||
|
||||
# 路由
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [find_neighbor](#find_neighbor)
|
||||
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [default_domain_resolver](#default_domain_resolver)
|
||||
@@ -37,6 +42,8 @@ icon: material/alert-decagram
|
||||
"override_android_vpn": false,
|
||||
"default_interface": "",
|
||||
"default_mark": 0,
|
||||
"find_neighbor": false,
|
||||
"dhcp_lease_files": [],
|
||||
"default_network_strategy": "",
|
||||
"default_fallback_delay": ""
|
||||
}
|
||||
@@ -106,6 +113,30 @@ icon: material/alert-decagram
|
||||
|
||||
如果设置了 `outbound.routing_mark` 设置,则不生效。
|
||||
|
||||
#### find_neighbor
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux。
|
||||
|
||||
启用邻居解析以查找源 MAC 地址和主机名。
|
||||
|
||||
`source_mac_address` 和 `source_hostname` 规则项需要此选项。
|
||||
|
||||
#### dhcp_lease_files
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux。
|
||||
|
||||
用于主机名和 MAC 地址解析的自定义 DHCP 租约文件路径。
|
||||
|
||||
为空时自动从常见 DHCP 服务器(dnsmasq、odhcpd、ISC dhcpd、Kea)检测。
|
||||
|
||||
#### default_domain_resolver
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [source_mac_address](#source_mac_address)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
@@ -159,6 +164,12 @@ icon: material/new-box
|
||||
"tailscale",
|
||||
"wireguard"
|
||||
],
|
||||
"source_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"source_hostname": [
|
||||
"my-device"
|
||||
],
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
@@ -449,6 +460,26 @@ Match specified outbounds' preferred routes.
|
||||
| `tailscale` | Match MagicDNS domains and peers' allowed IPs |
|
||||
| `wireguard` | Match peers's allowed IPs |
|
||||
|
||||
#### source_mac_address
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux with `route.find_neighbor` enabled.
|
||||
|
||||
Match source device MAC address.
|
||||
|
||||
#### source_hostname
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux with `route.find_neighbor` enabled.
|
||||
|
||||
Match source device hostname from DHCP leases.
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [source_mac_address](#source_mac_address)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
@@ -157,6 +162,12 @@ icon: material/new-box
|
||||
"tailscale",
|
||||
"wireguard"
|
||||
],
|
||||
"source_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"source_hostname": [
|
||||
"my-device"
|
||||
],
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
@@ -447,6 +458,26 @@ icon: material/new-box
|
||||
| `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs |
|
||||
| `wireguard` | 匹配对端的 allowed IPs |
|
||||
|
||||
#### source_mac_address
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux,且需要 `route.find_neighbor` 已启用。
|
||||
|
||||
匹配源设备 MAC 地址。
|
||||
|
||||
#### source_hostname
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux,且需要 `route.find_neighbor` 已启用。
|
||||
|
||||
匹配源设备从 DHCP 租约获取的主机名。
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "自 sing-box 1.8.0 起"
|
||||
|
||||
6
go.mod
6
go.mod
@@ -14,11 +14,13 @@ require (
|
||||
github.com/godbus/dbus/v5 v5.2.2
|
||||
github.com/gofrs/uuid/v5 v5.4.0
|
||||
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91
|
||||
github.com/jsimonetti/rtnetlink v1.4.0
|
||||
github.com/keybase/go-keychain v0.0.1
|
||||
github.com/libdns/acmedns v0.5.0
|
||||
github.com/libdns/alidns v1.0.6
|
||||
github.com/libdns/cloudflare v0.2.2
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||
github.com/mdlayher/netlink v1.9.0
|
||||
github.com/metacubex/utls v1.8.4
|
||||
github.com/mholt/acmez/v3 v3.1.6
|
||||
github.com/miekg/dns v1.1.72
|
||||
@@ -39,7 +41,7 @@ require (
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
|
||||
github.com/sagernet/sing-tun v0.8.7
|
||||
github.com/sagernet/sing-tun v0.8.8-0.20260410061515-018f5eaae695
|
||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
|
||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1
|
||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7
|
||||
@@ -92,11 +94,9 @@ require (
|
||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/libdns/libdns v1.1.1 // indirect
|
||||
github.com/mdlayher/netlink v1.9.0 // indirect
|
||||
github.com/mdlayher/socket v0.5.1 // indirect
|
||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||
|
||||
4
go.sum
4
go.sum
@@ -248,8 +248,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnq
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
|
||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
|
||||
github.com/sagernet/sing-tun v0.8.7 h1:q49cI7Cbp+BcgzaJitQ9QdLO77BqnnaQRkSEMoGmF3g=
|
||||
github.com/sagernet/sing-tun v0.8.7/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs=
|
||||
github.com/sagernet/sing-tun v0.8.8-0.20260410061515-018f5eaae695 h1:2maqN3XuorEo5faXHIyYZQZ1/ybim4hImfCEWZwdPbk=
|
||||
github.com/sagernet/sing-tun v0.8.8-0.20260410061515-018f5eaae695/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs=
|
||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
|
||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
|
||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
|
||||
|
||||
@@ -9,6 +9,8 @@ type RouteOptions struct {
|
||||
RuleSet []RuleSet `json:"rule_set,omitempty"`
|
||||
Final string `json:"final,omitempty"`
|
||||
FindProcess bool `json:"find_process,omitempty"`
|
||||
FindNeighbor bool `json:"find_neighbor,omitempty"`
|
||||
DHCPLeaseFiles badoption.Listable[string] `json:"dhcp_lease_files,omitempty"`
|
||||
AutoDetectInterface bool `json:"auto_detect_interface,omitempty"`
|
||||
OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"`
|
||||
DefaultInterface string `json:"default_interface,omitempty"`
|
||||
|
||||
@@ -103,6 +103,8 @@ type RawDefaultRule struct {
|
||||
InterfaceAddress *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]] `json:"interface_address,omitempty"`
|
||||
NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"`
|
||||
DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"`
|
||||
SourceMACAddress badoption.Listable[string] `json:"source_mac_address,omitempty"`
|
||||
SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"`
|
||||
PreferredBy badoption.Listable[string] `json:"preferred_by,omitempty"`
|
||||
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
|
||||
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
|
||||
|
||||
@@ -106,6 +106,8 @@ type RawDefaultDNSRule struct {
|
||||
InterfaceAddress *badjson.TypedMap[string, badoption.Listable[*badoption.Prefixable]] `json:"interface_address,omitempty"`
|
||||
NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"`
|
||||
DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_interface_address,omitempty"`
|
||||
SourceMACAddress badoption.Listable[string] `json:"source_mac_address,omitempty"`
|
||||
SourceHostname badoption.Listable[string] `json:"source_hostname,omitempty"`
|
||||
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
|
||||
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
|
||||
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"`
|
||||
|
||||
@@ -39,6 +39,8 @@ type TunInboundOptions struct {
|
||||
IncludeAndroidUser badoption.Listable[int] `json:"include_android_user,omitempty"`
|
||||
IncludePackage badoption.Listable[string] `json:"include_package,omitempty"`
|
||||
ExcludePackage badoption.Listable[string] `json:"exclude_package,omitempty"`
|
||||
IncludeMACAddress badoption.Listable[string] `json:"include_mac_address,omitempty"`
|
||||
ExcludeMACAddress badoption.Listable[string] `json:"exclude_mac_address,omitempty"`
|
||||
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
|
||||
Stack string `json:"stack,omitempty"`
|
||||
Platform *TunPlatformOptions `json:"platform,omitempty"`
|
||||
|
||||
@@ -160,6 +160,22 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
if nfQueue == 0 {
|
||||
nfQueue = tun.DefaultAutoRedirectNFQueue
|
||||
}
|
||||
var includeMACAddress []net.HardwareAddr
|
||||
for i, macString := range options.IncludeMACAddress {
|
||||
mac, macErr := net.ParseMAC(macString)
|
||||
if macErr != nil {
|
||||
return nil, E.Cause(macErr, "parse include_mac_address[", i, "]")
|
||||
}
|
||||
includeMACAddress = append(includeMACAddress, mac)
|
||||
}
|
||||
var excludeMACAddress []net.HardwareAddr
|
||||
for i, macString := range options.ExcludeMACAddress {
|
||||
mac, macErr := net.ParseMAC(macString)
|
||||
if macErr != nil {
|
||||
return nil, E.Cause(macErr, "parse exclude_mac_address[", i, "]")
|
||||
}
|
||||
excludeMACAddress = append(excludeMACAddress, mac)
|
||||
}
|
||||
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
||||
multiPendingPackets := C.IsDarwin && ((options.Stack == "gvisor" && tunMTU < 32768) || (options.Stack != "gvisor" && options.MTU <= 9000))
|
||||
inbound := &Inbound{
|
||||
@@ -197,6 +213,8 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
||||
IncludeAndroidUser: options.IncludeAndroidUser,
|
||||
IncludePackage: options.IncludePackage,
|
||||
ExcludePackage: options.ExcludePackage,
|
||||
IncludeMACAddress: includeMACAddress,
|
||||
ExcludeMACAddress: excludeMACAddress,
|
||||
InterfaceMonitor: networkManager.InterfaceMonitor(),
|
||||
EXP_MultiPendingPackets: multiPendingPackets,
|
||||
},
|
||||
|
||||
596
route/neighbor_resolver_linux.go
Normal file
596
route/neighbor_resolver_linux.go
Normal file
@@ -0,0 +1,596 @@
|
||||
//go:build linux
|
||||
|
||||
package route
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"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.reloadLeaseFiles()
|
||||
go r.subscribeNeighborUpdates()
|
||||
if len(r.leaseFiles) > 0 {
|
||||
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
||||
Path: r.leaseFiles,
|
||||
Logger: r.logger,
|
||||
Callback: func(_ string) {
|
||||
r.reloadLeaseFiles()
|
||||
},
|
||||
})
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *neighborResolver) reloadLeaseFiles() {
|
||||
leaseIPToMAC := make(map[netip.Addr]net.HardwareAddr)
|
||||
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.leaseIPToMAC = leaseIPToMAC
|
||||
r.ipToHostname = ipToHostname
|
||||
r.macToHostname = macToHostname
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
14
route/neighbor_resolver_stub.go
Normal file
14
route/neighbor_resolver_stub.go
Normal file
@@ -0,0 +1,14 @@
|
||||
//go:build !linux
|
||||
|
||||
package route
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
func newNeighborResolver(_ logger.ContextLogger, _ []string) (adapter.NeighborResolver, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
@@ -438,6 +438,23 @@ func (r *Router) matchRule(
|
||||
metadata.ProcessInfo = processInfo
|
||||
}
|
||||
}
|
||||
if r.neighborResolver != nil && metadata.SourceMACAddress == nil && metadata.Source.Addr.IsValid() {
|
||||
mac, macFound := r.neighborResolver.LookupMAC(metadata.Source.Addr)
|
||||
if macFound {
|
||||
metadata.SourceMACAddress = mac
|
||||
}
|
||||
hostname, hostnameFound := r.neighborResolver.LookupHostname(metadata.Source.Addr)
|
||||
if hostnameFound {
|
||||
metadata.SourceHostname = hostname
|
||||
if macFound {
|
||||
r.logger.InfoContext(ctx, "found neighbor: ", mac, ", hostname: ", hostname)
|
||||
} else {
|
||||
r.logger.InfoContext(ctx, "found neighbor hostname: ", hostname)
|
||||
}
|
||||
} else if macFound {
|
||||
r.logger.InfoContext(ctx, "found neighbor: ", mac)
|
||||
}
|
||||
}
|
||||
if metadata.Destination.Addr.IsValid() && r.dnsTransport.FakeIP() != nil && r.dnsTransport.FakeIP().Store().Contains(metadata.Destination.Addr) {
|
||||
domain, loaded := r.dnsTransport.FakeIP().Store().Lookup(metadata.Destination.Addr)
|
||||
if !loaded {
|
||||
|
||||
@@ -35,10 +35,13 @@ type Router struct {
|
||||
network adapter.NetworkManager
|
||||
rules []adapter.Rule
|
||||
needFindProcess bool
|
||||
needFindNeighbor bool
|
||||
leaseFiles []string
|
||||
ruleSets []adapter.RuleSet
|
||||
ruleSetMap map[string]adapter.RuleSet
|
||||
processSearcher process.Searcher
|
||||
processCache freelru.Cache[processCacheKey, processCacheEntry]
|
||||
neighborResolver adapter.NeighborResolver
|
||||
pauseManager pause.Manager
|
||||
trackers []adapter.ConnectionTracker
|
||||
platformInterface adapter.PlatformInterface
|
||||
@@ -58,6 +61,8 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route
|
||||
rules: make([]adapter.Rule, 0, len(options.Rules)),
|
||||
ruleSetMap: make(map[string]adapter.RuleSet),
|
||||
needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess,
|
||||
needFindNeighbor: hasRule(options.Rules, isNeighborRule) || hasDNSRule(dnsOptions.Rules, isNeighborDNSRule) || options.FindNeighbor,
|
||||
leaseFiles: options.DHCPLeaseFiles,
|
||||
pauseManager: service.FromContext[pause.Manager](ctx),
|
||||
platformInterface: service.FromContext[adapter.PlatformInterface](ctx),
|
||||
}
|
||||
@@ -117,6 +122,7 @@ func (r *Router) Start(stage adapter.StartStage) error {
|
||||
}
|
||||
r.network.Initialize(r.ruleSets)
|
||||
needFindProcess := r.needFindProcess
|
||||
needFindNeighbor := r.needFindNeighbor
|
||||
for _, ruleSet := range r.ruleSets {
|
||||
metadata := ruleSet.Metadata()
|
||||
if metadata.ContainsProcessRule {
|
||||
@@ -151,6 +157,24 @@ func (r *Router) Start(stage adapter.StartStage) error {
|
||||
processCache.SetLifetime(200 * time.Millisecond)
|
||||
r.processCache = processCache
|
||||
}
|
||||
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 err != nil {
|
||||
r.logger.Warn(E.Cause(err, "start neighbor resolver"))
|
||||
} else {
|
||||
r.neighborResolver = resolver
|
||||
}
|
||||
}
|
||||
}
|
||||
case adapter.StartStatePostStart:
|
||||
for i, rule := range r.rules {
|
||||
monitor.Start("initialize rule[", i, "]")
|
||||
@@ -182,6 +206,13 @@ func (r *Router) Start(stage adapter.StartStage) error {
|
||||
func (r *Router) Close() error {
|
||||
monitor := taskmonitor.New(r.logger, C.StopTimeout)
|
||||
var err error
|
||||
if r.neighborResolver != nil {
|
||||
monitor.Start("close neighbor resolver")
|
||||
err = E.Append(err, r.neighborResolver.Close(), func(closeErr error) error {
|
||||
return E.Cause(closeErr, "close neighbor resolver")
|
||||
})
|
||||
monitor.Finish()
|
||||
}
|
||||
for i, rule := range r.rules {
|
||||
monitor.Start("close rule[", i, "]")
|
||||
err = E.Append(err, rule.Close(), func(err error) error {
|
||||
@@ -223,6 +254,14 @@ func (r *Router) NeedFindProcess() bool {
|
||||
return r.needFindProcess
|
||||
}
|
||||
|
||||
func (r *Router) NeedFindNeighbor() bool {
|
||||
return r.needFindNeighbor
|
||||
}
|
||||
|
||||
func (r *Router) NeighborResolver() adapter.NeighborResolver {
|
||||
return r.neighborResolver
|
||||
}
|
||||
|
||||
func (r *Router) ResetNetwork() {
|
||||
r.network.ResetNetwork()
|
||||
r.dns.ResetNetwork()
|
||||
|
||||
@@ -264,6 +264,16 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.SourceMACAddress) > 0 {
|
||||
item := NewSourceMACAddressItem(options.SourceMACAddress)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.SourceHostname) > 0 {
|
||||
item := NewSourceHostnameItem(options.SourceHostname)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.PreferredBy) > 0 {
|
||||
item := NewPreferredByItem(ctx, options.PreferredBy)
|
||||
rule.items = append(rule.items, item)
|
||||
|
||||
@@ -265,6 +265,16 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.SourceMACAddress) > 0 {
|
||||
item := NewSourceMACAddressItem(options.SourceMACAddress)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.SourceHostname) > 0 {
|
||||
item := NewSourceHostnameItem(options.SourceHostname)
|
||||
rule.items = append(rule.items, item)
|
||||
rule.allItems = append(rule.allItems, item)
|
||||
}
|
||||
if len(options.RuleSet) > 0 {
|
||||
//nolint:staticcheck
|
||||
if options.Deprecated_RulesetIPCIDRMatchSource {
|
||||
|
||||
42
route/rule/rule_item_source_hostname.go
Normal file
42
route/rule/rule_item_source_hostname.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
)
|
||||
|
||||
var _ RuleItem = (*SourceHostnameItem)(nil)
|
||||
|
||||
type SourceHostnameItem struct {
|
||||
hostnames []string
|
||||
hostnameMap map[string]bool
|
||||
}
|
||||
|
||||
func NewSourceHostnameItem(hostnameList []string) *SourceHostnameItem {
|
||||
rule := &SourceHostnameItem{
|
||||
hostnames: hostnameList,
|
||||
hostnameMap: make(map[string]bool),
|
||||
}
|
||||
for _, hostname := range hostnameList {
|
||||
rule.hostnameMap[hostname] = true
|
||||
}
|
||||
return rule
|
||||
}
|
||||
|
||||
func (r *SourceHostnameItem) Match(metadata *adapter.InboundContext) bool {
|
||||
if metadata.SourceHostname == "" {
|
||||
return false
|
||||
}
|
||||
return r.hostnameMap[metadata.SourceHostname]
|
||||
}
|
||||
|
||||
func (r *SourceHostnameItem) String() string {
|
||||
var description string
|
||||
if len(r.hostnames) == 1 {
|
||||
description = "source_hostname=" + r.hostnames[0]
|
||||
} else {
|
||||
description = "source_hostname=[" + strings.Join(r.hostnames, " ") + "]"
|
||||
}
|
||||
return description
|
||||
}
|
||||
48
route/rule/rule_item_source_mac_address.go
Normal file
48
route/rule/rule_item_source_mac_address.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
)
|
||||
|
||||
var _ RuleItem = (*SourceMACAddressItem)(nil)
|
||||
|
||||
type SourceMACAddressItem struct {
|
||||
addresses []string
|
||||
addressMap map[string]bool
|
||||
}
|
||||
|
||||
func NewSourceMACAddressItem(addressList []string) *SourceMACAddressItem {
|
||||
rule := &SourceMACAddressItem{
|
||||
addresses: addressList,
|
||||
addressMap: make(map[string]bool),
|
||||
}
|
||||
for _, address := range addressList {
|
||||
parsed, err := net.ParseMAC(address)
|
||||
if err == nil {
|
||||
rule.addressMap[parsed.String()] = true
|
||||
} else {
|
||||
rule.addressMap[address] = true
|
||||
}
|
||||
}
|
||||
return rule
|
||||
}
|
||||
|
||||
func (r *SourceMACAddressItem) Match(metadata *adapter.InboundContext) bool {
|
||||
if metadata.SourceMACAddress == nil {
|
||||
return false
|
||||
}
|
||||
return r.addressMap[metadata.SourceMACAddress.String()]
|
||||
}
|
||||
|
||||
func (r *SourceMACAddressItem) String() string {
|
||||
var description string
|
||||
if len(r.addresses) == 1 {
|
||||
description = "source_mac_address=" + r.addresses[0]
|
||||
} else {
|
||||
description = "source_mac_address=[" + strings.Join(r.addresses, " ") + "]"
|
||||
}
|
||||
return description
|
||||
}
|
||||
@@ -45,6 +45,14 @@ func isProcessDNSRule(rule option.DefaultDNSRule) bool {
|
||||
return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.ProcessPathRegex) > 0 || len(rule.PackageName) > 0 || len(rule.User) > 0 || len(rule.UserID) > 0
|
||||
}
|
||||
|
||||
func isNeighborRule(rule option.DefaultRule) bool {
|
||||
return len(rule.SourceMACAddress) > 0 || len(rule.SourceHostname) > 0
|
||||
}
|
||||
|
||||
func isNeighborDNSRule(rule option.DefaultDNSRule) bool {
|
||||
return len(rule.SourceMACAddress) > 0 || len(rule.SourceHostname) > 0
|
||||
}
|
||||
|
||||
func isWIFIRule(rule option.DefaultRule) bool {
|
||||
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user