mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-13 20:28:32 +10:00
Compare commits
10 Commits
stable
...
v1.14.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
734f3c9a21 | ||
|
|
7df171ff20 | ||
|
|
46eda3e96f | ||
|
|
727a9d18d6 | ||
|
|
20f60b8c7b | ||
|
|
84b0ddff7f | ||
|
|
811ea13b73 | ||
|
|
bdb90f0a01 | ||
|
|
c9ab6458fa | ||
|
|
16a249f672 |
2
.github/CRONET_GO_VERSION
vendored
2
.github/CRONET_GO_VERSION
vendored
@@ -1 +1 @@
|
|||||||
2fef65f9dba90ddb89a87d00a6eb6165487c10c1
|
ea7cd33752aed62603775af3df946c1b83f4b0b3
|
||||||
|
|||||||
14
adapter/certificate_provider.go
Normal file
14
adapter/certificate_provider.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CertificateProvider interface {
|
||||||
|
GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ACMECertificateProvider interface {
|
||||||
|
CertificateProvider
|
||||||
|
GetACMENextProtos() []string
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -82,6 +83,8 @@ type InboundContext struct {
|
|||||||
SourceGeoIPCode string
|
SourceGeoIPCode string
|
||||||
GeoIPCode string
|
GeoIPCode string
|
||||||
ProcessInfo *ConnectionOwner
|
ProcessInfo *ConnectionOwner
|
||||||
|
SourceMACAddress net.HardwareAddr
|
||||||
|
SourceHostname string
|
||||||
QueryType uint16
|
QueryType uint16
|
||||||
FakeIP bool
|
FakeIP bool
|
||||||
|
|
||||||
|
|||||||
23
adapter/neighbor.go
Normal file
23
adapter/neighbor.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"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)
|
||||||
|
}
|
||||||
@@ -36,6 +36,10 @@ type PlatformInterface interface {
|
|||||||
|
|
||||||
UsePlatformNotification() bool
|
UsePlatformNotification() bool
|
||||||
SendNotification(notification *Notification) error
|
SendNotification(notification *Notification) error
|
||||||
|
|
||||||
|
UsePlatformNeighborResolver() bool
|
||||||
|
StartNeighborMonitor(listener NeighborUpdateListener) error
|
||||||
|
CloseNeighborMonitor(listener NeighborUpdateListener) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type FindConnectionOwnerRequest struct {
|
type FindConnectionOwnerRequest struct {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ type Router interface {
|
|||||||
RuleSet(tag string) (RuleSet, bool)
|
RuleSet(tag string) (RuleSet, bool)
|
||||||
Rules() []Rule
|
Rules() []Rule
|
||||||
NeedFindProcess() bool
|
NeedFindProcess() bool
|
||||||
|
NeedFindNeighbor() bool
|
||||||
|
NeighborResolver() NeighborResolver
|
||||||
AppendTracker(tracker ConnectionTracker)
|
AppendTracker(tracker ConnectionTracker)
|
||||||
ResetNetwork()
|
ResetNetwork()
|
||||||
}
|
}
|
||||||
|
|||||||
36
box.go
36
box.go
@@ -272,6 +272,24 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for i, serviceOptions := range options.Services {
|
||||||
|
var tag string
|
||||||
|
if serviceOptions.Tag != "" {
|
||||||
|
tag = serviceOptions.Tag
|
||||||
|
} else {
|
||||||
|
tag = F.ToString(i)
|
||||||
|
}
|
||||||
|
err = serviceManager.Create(
|
||||||
|
ctx,
|
||||||
|
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
||||||
|
tag,
|
||||||
|
serviceOptions.Type,
|
||||||
|
serviceOptions.Options,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "initialize service[", i, "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
for i, outboundOptions := range options.Outbounds {
|
for i, outboundOptions := range options.Outbounds {
|
||||||
var tag string
|
var tag string
|
||||||
if outboundOptions.Tag != "" {
|
if outboundOptions.Tag != "" {
|
||||||
@@ -298,24 +316,6 @@ func New(options Options) (*Box, error) {
|
|||||||
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for i, serviceOptions := range options.Services {
|
|
||||||
var tag string
|
|
||||||
if serviceOptions.Tag != "" {
|
|
||||||
tag = serviceOptions.Tag
|
|
||||||
} else {
|
|
||||||
tag = F.ToString(i)
|
|
||||||
}
|
|
||||||
err = serviceManager.Create(
|
|
||||||
ctx,
|
|
||||||
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
|
||||||
tag,
|
|
||||||
serviceOptions.Type,
|
|
||||||
serviceOptions.Options,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "initialize service[", i, "]")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outboundManager.Initialize(func() (adapter.Outbound, error) {
|
outboundManager.Initialize(func() (adapter.Outbound, error) {
|
||||||
return direct.NewOutbound(
|
return direct.NewOutbound(
|
||||||
ctx,
|
ctx,
|
||||||
|
|||||||
Submodule clients/android updated: 7777469b5d...0d31ac467f
Submodule clients/apple updated: c19945f65b...22dcf646ce
@@ -1,3 +1,5 @@
|
|||||||
package tls
|
package tls
|
||||||
|
|
||||||
const ACMETLS1Protocol = "acme-tls/1"
|
import C "github.com/sagernet/sing-box/constant"
|
||||||
|
|
||||||
|
const ACMETLS1Protocol = C.ACMETLS1Protocol
|
||||||
|
|||||||
@@ -18,14 +18,77 @@ import (
|
|||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/ntp"
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errInsecureUnused = E.New("tls: insecure unused")
|
var errInsecureUnused = E.New("tls: insecure unused")
|
||||||
|
|
||||||
|
type managedCertificateProvider interface {
|
||||||
|
adapter.CertificateProvider
|
||||||
|
Start() error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type acmeServiceCertificateProvider struct {
|
||||||
|
ctx context.Context
|
||||||
|
serviceTag string
|
||||||
|
once sync.Once
|
||||||
|
provider adapter.ACMECertificateProvider
|
||||||
|
resolveErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *acmeServiceCertificateProvider) Start() error {
|
||||||
|
_, err := p.resolveProvider()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *acmeServiceCertificateProvider) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *acmeServiceCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
provider, err := p.resolveProvider()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return provider.GetCertificate(hello)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *acmeServiceCertificateProvider) GetACMENextProtos() []string {
|
||||||
|
provider, err := p.resolveProvider()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return provider.GetACMENextProtos()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *acmeServiceCertificateProvider) resolveProvider() (adapter.ACMECertificateProvider, error) {
|
||||||
|
p.once.Do(func() {
|
||||||
|
serviceManager := service.FromContext[adapter.ServiceManager](p.ctx)
|
||||||
|
if serviceManager == nil {
|
||||||
|
p.resolveErr = E.New("missing service manager in context")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
providerService, found := serviceManager.Get(p.serviceTag)
|
||||||
|
if !found {
|
||||||
|
p.resolveErr = E.New("certificate provider service not found: ", p.serviceTag)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
provider, ok := providerService.(adapter.ACMECertificateProvider)
|
||||||
|
if !ok {
|
||||||
|
p.resolveErr = E.New("service ", p.serviceTag, " is not an ACME certificate service")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
p.provider = provider
|
||||||
|
})
|
||||||
|
return p.provider, p.resolveErr
|
||||||
|
}
|
||||||
|
|
||||||
type STDServerConfig struct {
|
type STDServerConfig struct {
|
||||||
access sync.RWMutex
|
access sync.RWMutex
|
||||||
config *tls.Config
|
config *tls.Config
|
||||||
logger log.Logger
|
logger log.Logger
|
||||||
|
certificateProvider managedCertificateProvider
|
||||||
acmeService adapter.SimpleLifecycle
|
acmeService adapter.SimpleLifecycle
|
||||||
certificate []byte
|
certificate []byte
|
||||||
key []byte
|
key []byte
|
||||||
@@ -53,18 +116,17 @@ func (c *STDServerConfig) SetServerName(serverName string) {
|
|||||||
func (c *STDServerConfig) NextProtos() []string {
|
func (c *STDServerConfig) NextProtos() []string {
|
||||||
c.access.RLock()
|
c.access.RLock()
|
||||||
defer c.access.RUnlock()
|
defer c.access.RUnlock()
|
||||||
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
|
if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
|
||||||
return c.config.NextProtos[1:]
|
return c.config.NextProtos[1:]
|
||||||
} else {
|
|
||||||
return c.config.NextProtos
|
|
||||||
}
|
}
|
||||||
|
return c.config.NextProtos
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
||||||
c.access.Lock()
|
c.access.Lock()
|
||||||
defer c.access.Unlock()
|
defer c.access.Unlock()
|
||||||
config := c.config.Clone()
|
config := c.config.Clone()
|
||||||
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
|
if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
|
||||||
config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
|
config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
|
||||||
} else {
|
} else {
|
||||||
config.NextProtos = nextProto
|
config.NextProtos = nextProto
|
||||||
@@ -72,6 +134,18 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
|||||||
c.config = config
|
c.config = config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *STDServerConfig) hasACMEALPN() bool {
|
||||||
|
if c.acmeService != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if c.certificateProvider != nil {
|
||||||
|
if acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider); isACME {
|
||||||
|
return len(acmeProvider.GetACMENextProtos()) > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) STDConfig() (*STDConfig, error) {
|
func (c *STDServerConfig) STDConfig() (*STDConfig, error) {
|
||||||
return c.config, nil
|
return c.config, nil
|
||||||
}
|
}
|
||||||
@@ -91,15 +165,24 @@ func (c *STDServerConfig) Clone() Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) Start() error {
|
func (c *STDServerConfig) Start() error {
|
||||||
if c.acmeService != nil {
|
if c.certificateProvider != nil {
|
||||||
return c.acmeService.Start()
|
err := c.certificateProvider.Start()
|
||||||
} else {
|
|
||||||
err := c.startWatcher()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.logger.Warn("create fsnotify watcher: ", err)
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
c.updateProviderNextProtos()
|
||||||
}
|
}
|
||||||
|
if c.acmeService != nil {
|
||||||
|
err := c.acmeService.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err := c.startWatcher()
|
||||||
|
if err != nil {
|
||||||
|
c.logger.Warn("create fsnotify watcher: ", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) startWatcher() error {
|
func (c *STDServerConfig) startWatcher() error {
|
||||||
@@ -203,23 +286,58 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) Close() error {
|
func (c *STDServerConfig) Close() error {
|
||||||
if c.acmeService != nil {
|
return common.Close(c.certificateProvider, c.acmeService, c.watcher)
|
||||||
return c.acmeService.Close()
|
}
|
||||||
|
|
||||||
|
func (c *STDServerConfig) updateProviderNextProtos() {
|
||||||
|
if c.certificateProvider == nil {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if c.watcher != nil {
|
acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider)
|
||||||
return c.watcher.Close()
|
if !isACME {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return nil
|
nextProtos := acmeProvider.GetACMENextProtos()
|
||||||
|
if len(nextProtos) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
config := c.config.Clone()
|
||||||
|
mergedNextProtos := append([]string{}, nextProtos...)
|
||||||
|
for _, nextProto := range config.NextProtos {
|
||||||
|
if !common.Contains(mergedNextProtos, nextProto) {
|
||||||
|
mergedNextProtos = append(mergedNextProtos, nextProto)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.NextProtos = mergedNextProtos
|
||||||
|
c.config = config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||||
if !options.Enabled {
|
if !options.Enabled {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
if options.CertificateProvider != nil && options.ACME != nil {
|
||||||
|
return nil, E.New("certificate_provider and acme are mutually exclusive")
|
||||||
|
}
|
||||||
var tlsConfig *tls.Config
|
var tlsConfig *tls.Config
|
||||||
|
var certificateProvider managedCertificateProvider
|
||||||
var acmeService adapter.SimpleLifecycle
|
var acmeService adapter.SimpleLifecycle
|
||||||
var err error
|
var err error
|
||||||
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
if options.CertificateProvider != nil {
|
||||||
|
certificateProvider, err = newCertificateProvider(ctx, options.CertificateProvider)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConfig = &tls.Config{
|
||||||
|
GetCertificate: certificateProvider.GetCertificate,
|
||||||
|
}
|
||||||
|
if options.Insecure {
|
||||||
|
return nil, errInsecureUnused
|
||||||
|
}
|
||||||
|
} else if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
||||||
|
logger.Warn("inline acme configuration is deprecated, use certificate_provider with an ACME service instead")
|
||||||
//nolint:staticcheck
|
//nolint:staticcheck
|
||||||
tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME))
|
tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -272,7 +390,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
|||||||
certificate []byte
|
certificate []byte
|
||||||
key []byte
|
key []byte
|
||||||
)
|
)
|
||||||
if acmeService == nil {
|
if certificateProvider == nil && acmeService == nil {
|
||||||
if len(options.Certificate) > 0 {
|
if len(options.Certificate) > 0 {
|
||||||
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
||||||
} else if options.CertificatePath != "" {
|
} else if options.CertificatePath != "" {
|
||||||
@@ -360,6 +478,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
|||||||
serverConfig := &STDServerConfig{
|
serverConfig := &STDServerConfig{
|
||||||
config: tlsConfig,
|
config: tlsConfig,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
certificateProvider: certificateProvider,
|
||||||
acmeService: acmeService,
|
acmeService: acmeService,
|
||||||
certificate: certificate,
|
certificate: certificate,
|
||||||
key: key,
|
key: key,
|
||||||
@@ -387,3 +506,19 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
|||||||
}
|
}
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newCertificateProvider(ctx context.Context, options *option.CertificateProviderOptions) (managedCertificateProvider, error) {
|
||||||
|
switch options.Type {
|
||||||
|
case C.TypeACME:
|
||||||
|
serviceTag := options.ACMEOptions.Service
|
||||||
|
if serviceTag == "" {
|
||||||
|
return nil, E.New("missing ACME service tag in certificate_provider")
|
||||||
|
}
|
||||||
|
return &acmeServiceCertificateProvider{
|
||||||
|
ctx: ctx,
|
||||||
|
serviceTag: serviceTag,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return nil, E.New("unknown certificate provider type: ", options.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const (
|
|||||||
TypeCCM = "ccm"
|
TypeCCM = "ccm"
|
||||||
TypeOCM = "ocm"
|
TypeOCM = "ocm"
|
||||||
TypeOOMKiller = "oom-killer"
|
TypeOOMKiller = "oom-killer"
|
||||||
|
TypeACME = "acme"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
3
constant/tls.go
Normal file
3
constant/tls.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
const ACMETLS1Protocol = "acme-tls/1"
|
||||||
@@ -2,6 +2,79 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
|
#### 1.14.0-alpha.2
|
||||||
|
|
||||||
|
* Add OpenWrt and Alpine APK packages to release **1**
|
||||||
|
* Backport to macOS 10.13 High Sierra **2**
|
||||||
|
* OCM service: Add WebSocket support for Responses API **3**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
|
||||||
|
|
||||||
|
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
|
||||||
|
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
|
||||||
|
macOS 10.13 High Sierra, built using Go 1.25 with patches
|
||||||
|
from [SagerNet/go](https://github.com/SagerNet/go).
|
||||||
|
|
||||||
|
**3**:
|
||||||
|
|
||||||
|
See [OCM](/configuration/service/ocm).
|
||||||
|
|
||||||
|
#### 1.13.3-beta.1
|
||||||
|
|
||||||
|
* Add OpenWrt and Alpine APK packages to release **1**
|
||||||
|
* Backport to macOS 10.13 High Sierra **2**
|
||||||
|
* OCM service: Add WebSocket support for Responses API **3**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
|
||||||
|
|
||||||
|
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
|
||||||
|
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
|
||||||
|
macOS 10.13 High Sierra, built using Go 1.25 with patches
|
||||||
|
from [SagerNet/go](https://github.com/SagerNet/go).
|
||||||
|
|
||||||
|
**3**:
|
||||||
|
|
||||||
|
See [OCM](/configuration/service/ocm).
|
||||||
|
|
||||||
|
#### 1.14.0-alpha.1
|
||||||
|
|
||||||
|
* Add `source_mac_address` and `source_hostname` rule items **1**
|
||||||
|
* Add `include_mac_address` and `exclude_mac_address` TUN options **2**
|
||||||
|
* Update NaiveProxy to 145.0.7632.159 **3**
|
||||||
|
* Fixes and improvements
|
||||||
|
|
||||||
|
**1**:
|
||||||
|
|
||||||
|
New rule items for matching LAN devices by MAC address and hostname via neighbor resolution.
|
||||||
|
Supported on Linux, macOS, or in graphical clients on Android and macOS.
|
||||||
|
|
||||||
|
See [Route Rule](/configuration/route/rule/#source_mac_address), [DNS Rule](/configuration/dns/rule/#source_mac_address) and [Neighbor Resolution](/configuration/shared/neighbor/).
|
||||||
|
|
||||||
|
**2**:
|
||||||
|
|
||||||
|
Limit or exclude devices from TUN routing by MAC address.
|
||||||
|
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
|
||||||
|
|
||||||
|
See [TUN](/configuration/inbound/tun/#include_mac_address).
|
||||||
|
|
||||||
|
**3**:
|
||||||
|
|
||||||
|
This is not an official update from NaiveProxy. Instead, it's a Chromium codebase update maintained by Project S.
|
||||||
|
|
||||||
#### 1.13.2
|
#### 1.13.2
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
icon: material/alert-decagram
|
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"
|
!!! quote "Changes in sing-box 1.13.0"
|
||||||
|
|
||||||
:material-plus: [interface_address](#interface_address)
|
:material-plus: [interface_address](#interface_address)
|
||||||
@@ -149,6 +154,12 @@ icon: material/alert-decagram
|
|||||||
"default_interface_address": [
|
"default_interface_address": [
|
||||||
"2000::/3"
|
"2000::/3"
|
||||||
],
|
],
|
||||||
|
"source_mac_address": [
|
||||||
|
"00:11:22:33:44:55"
|
||||||
|
],
|
||||||
|
"source_hostname": [
|
||||||
|
"my-device"
|
||||||
|
],
|
||||||
"wifi_ssid": [
|
"wifi_ssid": [
|
||||||
"My WIFI"
|
"My WIFI"
|
||||||
],
|
],
|
||||||
@@ -408,6 +419,26 @@ Matches network interface (same values as `network_type`) address.
|
|||||||
|
|
||||||
Match default interface address.
|
Match default interface address.
|
||||||
|
|
||||||
|
#### source_mac_address
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.14.0"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||||
|
|
||||||
|
Match source device MAC address.
|
||||||
|
|
||||||
|
#### source_hostname
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.14.0"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||||
|
|
||||||
|
Match source device hostname from DHCP leases.
|
||||||
|
|
||||||
#### wifi_ssid
|
#### wifi_ssid
|
||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
icon: material/alert-decagram
|
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 中的更改"
|
!!! quote "sing-box 1.13.0 中的更改"
|
||||||
|
|
||||||
:material-plus: [interface_address](#interface_address)
|
:material-plus: [interface_address](#interface_address)
|
||||||
@@ -149,6 +154,12 @@ icon: material/alert-decagram
|
|||||||
"default_interface_address": [
|
"default_interface_address": [
|
||||||
"2000::/3"
|
"2000::/3"
|
||||||
],
|
],
|
||||||
|
"source_mac_address": [
|
||||||
|
"00:11:22:33:44:55"
|
||||||
|
],
|
||||||
|
"source_hostname": [
|
||||||
|
"my-device"
|
||||||
|
],
|
||||||
"wifi_ssid": [
|
"wifi_ssid": [
|
||||||
"My WIFI"
|
"My WIFI"
|
||||||
],
|
],
|
||||||
@@ -407,6 +418,26 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
|||||||
|
|
||||||
匹配默认接口地址。
|
匹配默认接口地址。
|
||||||
|
|
||||||
|
#### source_mac_address
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.14.0 起"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||||
|
|
||||||
|
匹配源设备 MAC 地址。
|
||||||
|
|
||||||
|
#### source_hostname
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.14.0 起"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||||
|
|
||||||
|
匹配源设备从 DHCP 租约获取的主机名。
|
||||||
|
|
||||||
#### wifi_ssid
|
#### wifi_ssid
|
||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.14.0"
|
||||||
|
|
||||||
|
:material-plus: [include_mac_address](#include_mac_address)
|
||||||
|
:material-plus: [exclude_mac_address](#exclude_mac_address)
|
||||||
|
|
||||||
|
!!! quote "Changes in sing-box 1.13.3"
|
||||||
|
|
||||||
|
:material-alert: [strict_route](#strict_route)
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.13.0"
|
!!! quote "Changes in sing-box 1.13.0"
|
||||||
|
|
||||||
:material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark)
|
:material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark)
|
||||||
@@ -125,6 +134,12 @@ icon: material/new-box
|
|||||||
"exclude_package": [
|
"exclude_package": [
|
||||||
"com.android.captiveportallogin"
|
"com.android.captiveportallogin"
|
||||||
],
|
],
|
||||||
|
"include_mac_address": [
|
||||||
|
"00:11:22:33:44:55"
|
||||||
|
],
|
||||||
|
"exclude_mac_address": [
|
||||||
|
"66:77:88:99:aa:bb"
|
||||||
|
],
|
||||||
"platform": {
|
"platform": {
|
||||||
"http_proxy": {
|
"http_proxy": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
@@ -348,6 +363,9 @@ Enforce strict routing rules when `auto_route` is enabled:
|
|||||||
|
|
||||||
* Let unsupported network unreachable
|
* Let unsupported network unreachable
|
||||||
* For legacy reasons, when neither `strict_route` nor `auto_redirect` are enabled, all ICMP traffic will not go through TUN.
|
* For legacy reasons, when neither `strict_route` nor `auto_redirect` are enabled, all ICMP traffic will not go through TUN.
|
||||||
|
* When `auto_redirect` is enabled, `strict_route` also affects `SO_BINDTODEVICE` traffic:
|
||||||
|
* Enabled: `SO_BINDTODEVICE` traffic is redirected through sing-box.
|
||||||
|
* Disabled: `SO_BINDTODEVICE` traffic bypasses sing-box.
|
||||||
|
|
||||||
*In Windows*:
|
*In Windows*:
|
||||||
|
|
||||||
@@ -548,6 +566,30 @@ Limit android packages in route.
|
|||||||
|
|
||||||
Exclude 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
|
||||||
|
|
||||||
Platform-specific settings, provided by client applications.
|
Platform-specific settings, provided by client applications.
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
icon: material/new-box
|
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)
|
||||||
|
|
||||||
!!! quote "sing-box 1.13.0 中的更改"
|
!!! quote "sing-box 1.13.0 中的更改"
|
||||||
|
|
||||||
:material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark)
|
:material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark)
|
||||||
@@ -126,6 +135,12 @@ icon: material/new-box
|
|||||||
"exclude_package": [
|
"exclude_package": [
|
||||||
"com.android.captiveportallogin"
|
"com.android.captiveportallogin"
|
||||||
],
|
],
|
||||||
|
"include_mac_address": [
|
||||||
|
"00:11:22:33:44:55"
|
||||||
|
],
|
||||||
|
"exclude_mac_address": [
|
||||||
|
"66:77:88:99:aa:bb"
|
||||||
|
],
|
||||||
"platform": {
|
"platform": {
|
||||||
"http_proxy": {
|
"http_proxy": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
@@ -347,6 +362,9 @@ tun 接口的 IPv6 前缀。
|
|||||||
|
|
||||||
* 使不支持的网络不可达。
|
* 使不支持的网络不可达。
|
||||||
* 出于历史遗留原因,当未启用 `strict_route` 或 `auto_redirect` 时,所有 ICMP 流量将不会通过 TUN。
|
* 出于历史遗留原因,当未启用 `strict_route` 或 `auto_redirect` 时,所有 ICMP 流量将不会通过 TUN。
|
||||||
|
* 当启用 `auto_redirect` 时,`strict_route` 也影响 `SO_BINDTODEVICE` 流量:
|
||||||
|
* 启用:`SO_BINDTODEVICE` 流量被重定向通过 sing-box。
|
||||||
|
* 禁用:`SO_BINDTODEVICE` 流量绕过 sing-box。
|
||||||
|
|
||||||
*在 Windows 中*:
|
*在 Windows 中*:
|
||||||
|
|
||||||
@@ -536,6 +554,30 @@ TCP/IP 栈。
|
|||||||
|
|
||||||
排除路由的 Android 应用包名。
|
排除路由的 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
|
#### platform
|
||||||
|
|
||||||
平台特定的设置,由客户端应用提供。
|
平台特定的设置,由客户端应用提供。
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ icon: material/alert-decagram
|
|||||||
|
|
||||||
# Route
|
# 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"
|
!!! quote "Changes in sing-box 1.12.0"
|
||||||
|
|
||||||
:material-plus: [default_domain_resolver](#default_domain_resolver)
|
:material-plus: [default_domain_resolver](#default_domain_resolver)
|
||||||
@@ -35,6 +40,9 @@ icon: material/alert-decagram
|
|||||||
"override_android_vpn": false,
|
"override_android_vpn": false,
|
||||||
"default_interface": "",
|
"default_interface": "",
|
||||||
"default_mark": 0,
|
"default_mark": 0,
|
||||||
|
"find_process": false,
|
||||||
|
"find_neighbor": false,
|
||||||
|
"dhcp_lease_files": [],
|
||||||
"default_domain_resolver": "", // or {}
|
"default_domain_resolver": "", // or {}
|
||||||
"default_network_strategy": "",
|
"default_network_strategy": "",
|
||||||
"default_network_type": [],
|
"default_network_type": [],
|
||||||
@@ -107,6 +115,38 @@ Set routing mark by default.
|
|||||||
|
|
||||||
Takes no effect if `outbound.routing_mark` is set.
|
Takes no effect if `outbound.routing_mark` is set.
|
||||||
|
|
||||||
|
#### find_process
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
Only supported on Linux, Windows, and macOS.
|
||||||
|
|
||||||
|
Enable process search for logging when no `process_name`, `process_path`, `package_name`, `user` or `user_id` rules exist.
|
||||||
|
|
||||||
|
#### find_neighbor
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.14.0"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
Only supported on Linux and macOS.
|
||||||
|
|
||||||
|
Enable neighbor resolution for logging when no `source_mac_address` or `source_hostname` rules exist.
|
||||||
|
|
||||||
|
See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||||
|
|
||||||
|
#### dhcp_lease_files
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.14.0"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
Only supported on Linux and macOS.
|
||||||
|
|
||||||
|
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
|
#### default_domain_resolver
|
||||||
|
|
||||||
!!! question "Since sing-box 1.12.0"
|
!!! 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 中的更改"
|
!!! quote "sing-box 1.12.0 中的更改"
|
||||||
|
|
||||||
:material-plus: [default_domain_resolver](#default_domain_resolver)
|
:material-plus: [default_domain_resolver](#default_domain_resolver)
|
||||||
@@ -37,6 +42,9 @@ icon: material/alert-decagram
|
|||||||
"override_android_vpn": false,
|
"override_android_vpn": false,
|
||||||
"default_interface": "",
|
"default_interface": "",
|
||||||
"default_mark": 0,
|
"default_mark": 0,
|
||||||
|
"find_process": false,
|
||||||
|
"find_neighbor": false,
|
||||||
|
"dhcp_lease_files": [],
|
||||||
"default_network_strategy": "",
|
"default_network_strategy": "",
|
||||||
"default_fallback_delay": ""
|
"default_fallback_delay": ""
|
||||||
}
|
}
|
||||||
@@ -106,6 +114,38 @@ icon: material/alert-decagram
|
|||||||
|
|
||||||
如果设置了 `outbound.routing_mark` 设置,则不生效。
|
如果设置了 `outbound.routing_mark` 设置,则不生效。
|
||||||
|
|
||||||
|
#### find_process
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
仅支持 Linux、Windows 和 macOS。
|
||||||
|
|
||||||
|
在没有 `process_name`、`process_path`、`package_name`、`user` 或 `user_id` 规则时启用进程搜索以输出日志。
|
||||||
|
|
||||||
|
#### find_neighbor
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.14.0 起"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
仅支持 Linux 和 macOS。
|
||||||
|
|
||||||
|
在没有 `source_mac_address` 或 `source_hostname` 规则时启用邻居解析以输出日志。
|
||||||
|
|
||||||
|
参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||||
|
|
||||||
|
#### dhcp_lease_files
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.14.0 起"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
仅支持 Linux 和 macOS。
|
||||||
|
|
||||||
|
用于主机名和 MAC 地址解析的自定义 DHCP 租约文件路径。
|
||||||
|
|
||||||
|
为空时自动从常见 DHCP 服务器(dnsmasq、odhcpd、ISC dhcpd、Kea)检测。
|
||||||
|
|
||||||
#### default_domain_resolver
|
#### default_domain_resolver
|
||||||
|
|
||||||
!!! question "自 sing-box 1.12.0 起"
|
!!! question "自 sing-box 1.12.0 起"
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
icon: material/new-box
|
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"
|
!!! quote "Changes in sing-box 1.13.0"
|
||||||
|
|
||||||
:material-plus: [interface_address](#interface_address)
|
:material-plus: [interface_address](#interface_address)
|
||||||
@@ -159,6 +164,12 @@ icon: material/new-box
|
|||||||
"tailscale",
|
"tailscale",
|
||||||
"wireguard"
|
"wireguard"
|
||||||
],
|
],
|
||||||
|
"source_mac_address": [
|
||||||
|
"00:11:22:33:44:55"
|
||||||
|
],
|
||||||
|
"source_hostname": [
|
||||||
|
"my-device"
|
||||||
|
],
|
||||||
"rule_set": [
|
"rule_set": [
|
||||||
"geoip-cn",
|
"geoip-cn",
|
||||||
"geosite-cn"
|
"geosite-cn"
|
||||||
@@ -449,6 +460,26 @@ Match specified outbounds' preferred routes.
|
|||||||
| `tailscale` | Match MagicDNS domains and peers' allowed IPs |
|
| `tailscale` | Match MagicDNS domains and peers' allowed IPs |
|
||||||
| `wireguard` | Match peers's allowed IPs |
|
| `wireguard` | Match peers's allowed IPs |
|
||||||
|
|
||||||
|
#### source_mac_address
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.14.0"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||||
|
|
||||||
|
Match source device MAC address.
|
||||||
|
|
||||||
|
#### source_hostname
|
||||||
|
|
||||||
|
!!! question "Since sing-box 1.14.0"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||||
|
|
||||||
|
Match source device hostname from DHCP leases.
|
||||||
|
|
||||||
#### rule_set
|
#### rule_set
|
||||||
|
|
||||||
!!! question "Since sing-box 1.8.0"
|
!!! question "Since sing-box 1.8.0"
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
icon: material/new-box
|
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 中的更改"
|
!!! quote "sing-box 1.13.0 中的更改"
|
||||||
|
|
||||||
:material-plus: [interface_address](#interface_address)
|
:material-plus: [interface_address](#interface_address)
|
||||||
@@ -156,6 +161,12 @@ icon: material/new-box
|
|||||||
"tailscale",
|
"tailscale",
|
||||||
"wireguard"
|
"wireguard"
|
||||||
],
|
],
|
||||||
|
"source_mac_address": [
|
||||||
|
"00:11:22:33:44:55"
|
||||||
|
],
|
||||||
|
"source_hostname": [
|
||||||
|
"my-device"
|
||||||
|
],
|
||||||
"rule_set": [
|
"rule_set": [
|
||||||
"geoip-cn",
|
"geoip-cn",
|
||||||
"geosite-cn"
|
"geosite-cn"
|
||||||
@@ -446,6 +457,26 @@ icon: material/new-box
|
|||||||
| `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs |
|
| `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs |
|
||||||
| `wireguard` | 匹配对端的 allowed IPs |
|
| `wireguard` | 匹配对端的 allowed IPs |
|
||||||
|
|
||||||
|
#### source_mac_address
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.14.0 起"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||||
|
|
||||||
|
匹配源设备 MAC 地址。
|
||||||
|
|
||||||
|
#### source_hostname
|
||||||
|
|
||||||
|
!!! question "自 sing-box 1.14.0 起"
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||||
|
|
||||||
|
匹配源设备从 DHCP 租约获取的主机名。
|
||||||
|
|
||||||
#### rule_set
|
#### rule_set
|
||||||
|
|
||||||
!!! question "自 sing-box 1.8.0 起"
|
!!! question "自 sing-box 1.8.0 起"
|
||||||
|
|||||||
49
docs/configuration/shared/neighbor.md
Normal file
49
docs/configuration/shared/neighbor.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
icon: material/lan
|
||||||
|
---
|
||||||
|
|
||||||
|
# Neighbor Resolution
|
||||||
|
|
||||||
|
Match LAN devices by MAC address and hostname using
|
||||||
|
[`source_mac_address`](/configuration/route/rule/#source_mac_address) and
|
||||||
|
[`source_hostname`](/configuration/route/rule/#source_hostname) rule items.
|
||||||
|
|
||||||
|
Neighbor resolution is automatically enabled when these rule items exist.
|
||||||
|
Use [`route.find_neighbor`](/configuration/route/#find_neighbor) to force enable it for logging without rules.
|
||||||
|
|
||||||
|
## Linux
|
||||||
|
|
||||||
|
Works natively. No special setup required.
|
||||||
|
|
||||||
|
Hostname resolution requires DHCP lease files,
|
||||||
|
automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea).
|
||||||
|
Custom paths can be set via [`route.dhcp_lease_files`](/configuration/route/#dhcp_lease_files).
|
||||||
|
|
||||||
|
## Android
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
Only supported in graphical clients.
|
||||||
|
|
||||||
|
Requires Android 11 or above and ROOT.
|
||||||
|
|
||||||
|
Must use [VPNHotspot](https://github.com/Mygod/VPNHotspot) to share the VPN connection.
|
||||||
|
ROM built-in features like "Use VPN for connected devices" can share VPN
|
||||||
|
but cannot provide MAC address or hostname information.
|
||||||
|
|
||||||
|
Set **IP Masquerade Mode** to **None** in VPNHotspot settings.
|
||||||
|
|
||||||
|
Only route/DNS rules are supported. TUN include/exclude routes are not supported.
|
||||||
|
|
||||||
|
### Hostname Visibility
|
||||||
|
|
||||||
|
Hostname is only visible in sing-box if it is visible in VPNHotspot.
|
||||||
|
For Apple devices, change **Private Wi-Fi Address** from **Rotating** to **Fixed** in the Wi-Fi settings
|
||||||
|
of the connected network. Non-Apple devices are always visible.
|
||||||
|
|
||||||
|
## macOS
|
||||||
|
|
||||||
|
Requires the standalone version (macOS system extension).
|
||||||
|
The App Store version can share the VPN as a hotspot but does not support MAC address or hostname reading.
|
||||||
|
|
||||||
|
See [VPN Hotspot](/manual/misc/vpn-hotspot/#macos) for Internet Sharing setup.
|
||||||
49
docs/configuration/shared/neighbor.zh.md
Normal file
49
docs/configuration/shared/neighbor.zh.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
---
|
||||||
|
icon: material/lan
|
||||||
|
---
|
||||||
|
|
||||||
|
# 邻居解析
|
||||||
|
|
||||||
|
通过
|
||||||
|
[`source_mac_address`](/configuration/route/rule/#source_mac_address) 和
|
||||||
|
[`source_hostname`](/configuration/route/rule/#source_hostname) 规则项匹配局域网设备的 MAC 地址和主机名。
|
||||||
|
|
||||||
|
当这些规则项存在时,邻居解析自动启用。
|
||||||
|
使用 [`route.find_neighbor`](/configuration/route/#find_neighbor) 可在没有规则时强制启用以输出日志。
|
||||||
|
|
||||||
|
## Linux
|
||||||
|
|
||||||
|
原生支持,无需特殊设置。
|
||||||
|
|
||||||
|
主机名解析需要 DHCP 租约文件,
|
||||||
|
自动从常见 DHCP 服务器(dnsmasq、odhcpd、ISC dhcpd、Kea)检测。
|
||||||
|
可通过 [`route.dhcp_lease_files`](/configuration/route/#dhcp_lease_files) 设置自定义路径。
|
||||||
|
|
||||||
|
## Android
|
||||||
|
|
||||||
|
!!! quote ""
|
||||||
|
|
||||||
|
仅在图形客户端中支持。
|
||||||
|
|
||||||
|
需要 Android 11 或以上版本和 ROOT。
|
||||||
|
|
||||||
|
必须使用 [VPNHotspot](https://github.com/Mygod/VPNHotspot) 共享 VPN 连接。
|
||||||
|
ROM 自带的「通过 VPN 共享连接」等功能可以共享 VPN,
|
||||||
|
但无法提供 MAC 地址或主机名信息。
|
||||||
|
|
||||||
|
在 VPNHotspot 设置中将 **IP 遮掩模式** 设为 **无**。
|
||||||
|
|
||||||
|
仅支持路由/DNS 规则。不支持 TUN 的 include/exclude 路由。
|
||||||
|
|
||||||
|
### 设备可见性
|
||||||
|
|
||||||
|
MAC 地址和主机名仅在 VPNHotspot 中可见时 sing-box 才能读取。
|
||||||
|
对于 Apple 设备,需要在所连接网络的 Wi-Fi 设置中将**私有无线局域网地址**从**轮替**改为**固定**。
|
||||||
|
非 Apple 设备始终可见。
|
||||||
|
|
||||||
|
## macOS
|
||||||
|
|
||||||
|
需要独立版本(macOS 系统扩展)。
|
||||||
|
App Store 版本可以共享 VPN 热点但不支持 MAC 地址或主机名读取。
|
||||||
|
|
||||||
|
参阅 [VPN 热点](/manual/misc/vpn-hotspot/#macos) 了解互联网共享设置。
|
||||||
@@ -144,6 +144,18 @@ func (s *platformInterfaceStub) SendNotification(notification *adapter.Notificat
|
|||||||
return nil
|
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 {
|
func (s *platformInterfaceStub) UsePlatformLocalDNSTransport() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
53
experimental/libbox/neighbor.go
Normal file
53
experimental/libbox/neighbor.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package libbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NeighborEntry struct {
|
||||||
|
Address string
|
||||||
|
MacAddress string
|
||||||
|
Hostname string
|
||||||
|
}
|
||||||
|
|
||||||
|
type NeighborEntryIterator interface {
|
||||||
|
Next() *NeighborEntry
|
||||||
|
HasNext() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type NeighborSubscription struct {
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NeighborSubscription) Close() {
|
||||||
|
close(s.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
123
experimental/libbox/neighbor_darwin.go
Normal file
123
experimental/libbox/neighbor_darwin.go
Normal file
@@ -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}
|
||||||
|
}
|
||||||
88
experimental/libbox/neighbor_linux.go
Normal file
88
experimental/libbox/neighbor_linux.go
Normal file
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
experimental/libbox/neighbor_stub.go
Normal file
9
experimental/libbox/neighbor_stub.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
//go:build !linux && !darwin
|
||||||
|
|
||||||
|
package libbox
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
func SubscribeNeighborTable(_ NeighborUpdateListener) (*NeighborSubscription, error) {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
@@ -21,6 +21,13 @@ type PlatformInterface interface {
|
|||||||
SystemCertificates() StringIterator
|
SystemCertificates() StringIterator
|
||||||
ClearDNSCache()
|
ClearDNSCache()
|
||||||
SendNotification(notification *Notification) error
|
SendNotification(notification *Notification) error
|
||||||
|
StartNeighborMonitor(listener NeighborUpdateListener) error
|
||||||
|
CloseNeighborMonitor(listener NeighborUpdateListener) error
|
||||||
|
RegisterMyInterface(name string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NeighborUpdateListener interface {
|
||||||
|
UpdateNeighborTable(entries NeighborEntryIterator)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConnectionOwner struct {
|
type ConnectionOwner struct {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,6 +221,46 @@ func (w *platformInterfaceWrapper) SendNotification(notification *adapter.Notifi
|
|||||||
return w.iif.SendNotification((*Notification)(notification))
|
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()
|
||||||
|
if entry == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
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) {
|
func AvailablePort(startPort int32) (int32, error) {
|
||||||
for port := int(startPort); ; port++ {
|
for port := int(startPort); ; port++ {
|
||||||
if port > 65535 {
|
if port > 65535 {
|
||||||
|
|||||||
12
go.mod
12
go.mod
@@ -14,11 +14,13 @@ require (
|
|||||||
github.com/godbus/dbus/v5 v5.2.2
|
github.com/godbus/dbus/v5 v5.2.2
|
||||||
github.com/gofrs/uuid/v5 v5.4.0
|
github.com/gofrs/uuid/v5 v5.4.0
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91
|
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/keybase/go-keychain v0.0.1
|
||||||
github.com/libdns/acmedns v0.5.0
|
github.com/libdns/acmedns v0.5.0
|
||||||
github.com/libdns/alidns v1.0.6
|
github.com/libdns/alidns v1.0.6
|
||||||
github.com/libdns/cloudflare v0.2.2
|
github.com/libdns/cloudflare v0.2.2
|
||||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||||
|
github.com/mdlayher/netlink v1.9.0
|
||||||
github.com/metacubex/utls v1.8.4
|
github.com/metacubex/utls v1.8.4
|
||||||
github.com/mholt/acmez/v3 v3.1.6
|
github.com/mholt/acmez/v3 v3.1.6
|
||||||
github.com/miekg/dns v1.1.72
|
github.com/miekg/dns v1.1.72
|
||||||
@@ -27,8 +29,8 @@ require (
|
|||||||
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1
|
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1
|
||||||
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
|
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
|
||||||
github.com/sagernet/cors v1.2.1
|
github.com/sagernet/cors v1.2.1
|
||||||
github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9
|
github.com/sagernet/cronet-go v0.0.0-20260309100020-c128886ff3fc
|
||||||
github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9
|
github.com/sagernet/cronet-go/all v0.0.0-20260309100020-c128886ff3fc
|
||||||
github.com/sagernet/fswatch v0.1.1
|
github.com/sagernet/fswatch v0.1.1
|
||||||
github.com/sagernet/gomobile v0.1.12
|
github.com/sagernet/gomobile v0.1.12
|
||||||
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
|
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1
|
||||||
@@ -39,10 +41,10 @@ require (
|
|||||||
github.com/sagernet/sing-shadowsocks v0.2.8
|
github.com/sagernet/sing-shadowsocks v0.2.8
|
||||||
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
github.com/sagernet/sing-shadowsocks2 v0.2.1
|
||||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
|
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11
|
||||||
github.com/sagernet/sing-tun v0.8.2
|
github.com/sagernet/sing-tun v0.8.3-0.20260311132553-5485872f601f
|
||||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
|
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/smux v1.5.50-sing-box-mod.1
|
||||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310162543-0c2de366d4de
|
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e
|
||||||
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c
|
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c
|
||||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
||||||
github.com/spf13/cobra v1.10.2
|
github.com/spf13/cobra v1.10.2
|
||||||
@@ -92,11 +94,9 @@ require (
|
|||||||
github.com/hashicorp/yamux v0.1.2 // indirect
|
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.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/compress v1.18.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/libdns/libdns v1.1.1 // 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/mdlayher/socket v0.5.1 // indirect
|
||||||
github.com/mitchellh/go-ps v1.0.0 // indirect
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||||
|
|||||||
16
go.sum
16
go.sum
@@ -162,10 +162,10 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk
|
|||||||
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
|
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
|
||||||
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
|
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
|
||||||
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
|
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
|
||||||
github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9 h1:xq5Yr10jXEppD3cnGjE3WENaB6D0YsZu6KptZ8d3054=
|
github.com/sagernet/cronet-go v0.0.0-20260309100020-c128886ff3fc h1:YK7PwJT0irRAEui9ASdXSxcE2BOVQipWMF/A1Ogt+7c=
|
||||||
github.com/sagernet/cronet-go v0.0.0-20260309102448-2fef65f9dba9/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw=
|
github.com/sagernet/cronet-go v0.0.0-20260309100020-c128886ff3fc/go.mod h1:hwFHBEjjthyEquDULbr4c4ucMedp8Drb6Jvm2kt/0Bw=
|
||||||
github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9 h1:uxQyy6Y/boOuecVA66tf79JgtoRGfeDJcfYZZLKVA5E=
|
github.com/sagernet/cronet-go/all v0.0.0-20260309100020-c128886ff3fc h1:EJPHOqk23IuBsTjXK9OXqkNxPbKOBWKRmviQoCcriAs=
|
||||||
github.com/sagernet/cronet-go/all v0.0.0-20260309102448-2fef65f9dba9/go.mod h1:Xm6cCvs0/twozC1JYNq0sVlOVmcSGzV7YON1XGcD97w=
|
github.com/sagernet/cronet-go/all v0.0.0-20260309100020-c128886ff3fc/go.mod h1:8aty0RW96DrJSMWXO6bRPMBJEjuqq5JWiOIi4bCRzFA=
|
||||||
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Qi0IKBpoPP3qZqIXuOKMsT2dv+l/MLWMyBHDMLRw2EA=
|
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9 h1:Qi0IKBpoPP3qZqIXuOKMsT2dv+l/MLWMyBHDMLRw2EA=
|
||||||
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw=
|
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260309101654-0cbdcfddded9/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw=
|
||||||
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:p+wCMjOhj46SpSD/AJeTGgkCcbyA76FyH631XZatyU8=
|
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260309101654-0cbdcfddded9 h1:p+wCMjOhj46SpSD/AJeTGgkCcbyA76FyH631XZatyU8=
|
||||||
@@ -248,14 +248,14 @@ 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-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 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
|
||||||
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
|
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
|
||||||
github.com/sagernet/sing-tun v0.8.2 h1:rQr/x3eQCHh3oleIaoJdPdJwqzZp4+QWcJLT0Wz2xKY=
|
github.com/sagernet/sing-tun v0.8.3-0.20260311132553-5485872f601f h1:uj3rzedphq1AiL0PpuVoob5RtKsPBcMRd8aqo+q0rqA=
|
||||||
github.com/sagernet/sing-tun v0.8.2/go.mod h1:pLCo4o+LacXEzz0bhwhJkKBjLlKOGPBNOAZ97ZVZWzs=
|
github.com/sagernet/sing-tun v0.8.3-0.20260311132553-5485872f601f/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 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
|
||||||
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
|
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=
|
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
|
||||||
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
|
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
|
||||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310162543-0c2de366d4de h1:wsJ0COxUOIvBE+hUho0C/DbMeUe9jtwfh6dECAiTk94=
|
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e h1:Sv1qUhJIidjSTc24XEknovDZnbmVSlAXj8wNVgIfgGo=
|
||||||
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260310162543-0c2de366d4de/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc=
|
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.6.0.20260311131347-f88b27eeb76e/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc=
|
||||||
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c h1:f9cXNB+IOOPnR8DOLMTpr42jf7naxh5Un5Y09BBf5Cg=
|
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c h1:f9cXNB+IOOPnR8DOLMTpr42jf7naxh5Un5Y09BBf5Cg=
|
||||||
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0=
|
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0=
|
||||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
|
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
|
||||||
|
|||||||
12
include/acme.go
Normal file
12
include/acme.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build with_acme
|
||||||
|
|
||||||
|
package include
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-box/adapter/service"
|
||||||
|
"github.com/sagernet/sing-box/service/acme"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerACMEService(registry *service.Registry) {
|
||||||
|
acme.RegisterService(registry)
|
||||||
|
}
|
||||||
20
include/acme_stub.go
Normal file
20
include/acme_stub.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//go:build !with_acme
|
||||||
|
|
||||||
|
package include
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/adapter/service"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerACMEService(registry *service.Registry) {
|
||||||
|
service.Register[option.ACMEServiceOptions](registry, C.TypeACME, func(ctx context.Context, logger log.ContextLogger, tag string, options option.ACMEServiceOptions) (adapter.Service, error) {
|
||||||
|
return nil, E.New(`ACME is not included in this build, rebuild with -tags with_acme`)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -130,6 +130,7 @@ func ServiceRegistry() *service.Registry {
|
|||||||
|
|
||||||
resolved.RegisterService(registry)
|
resolved.RegisterService(registry)
|
||||||
ssmapi.RegisterService(registry)
|
ssmapi.RegisterService(registry)
|
||||||
|
registerACMEService(registry)
|
||||||
|
|
||||||
registerDERPService(registry)
|
registerDERPService(registry)
|
||||||
registerCCMService(registry)
|
registerCCMService(registry)
|
||||||
|
|||||||
@@ -129,6 +129,7 @@ nav:
|
|||||||
- UDP over TCP: configuration/shared/udp-over-tcp.md
|
- UDP over TCP: configuration/shared/udp-over-tcp.md
|
||||||
- TCP Brutal: configuration/shared/tcp-brutal.md
|
- TCP Brutal: configuration/shared/tcp-brutal.md
|
||||||
- Wi-Fi State: configuration/shared/wifi-state.md
|
- Wi-Fi State: configuration/shared/wifi-state.md
|
||||||
|
- Neighbor Resolution: configuration/shared/neighbor.md
|
||||||
- Endpoint:
|
- Endpoint:
|
||||||
- configuration/endpoint/index.md
|
- configuration/endpoint/index.md
|
||||||
- WireGuard: configuration/endpoint/wireguard.md
|
- WireGuard: configuration/endpoint/wireguard.md
|
||||||
|
|||||||
17
option/acme.go
Normal file
17
option/acme.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
import "github.com/sagernet/sing/common/json/badoption"
|
||||||
|
|
||||||
|
type ACMEServiceOptions struct {
|
||||||
|
Domain badoption.Listable[string] `json:"domain,omitempty"`
|
||||||
|
DataDirectory string `json:"data_directory,omitempty"`
|
||||||
|
DefaultServerName string `json:"default_server_name,omitempty"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
DisableHTTPChallenge bool `json:"disable_http_challenge,omitempty"`
|
||||||
|
DisableTLSALPNChallenge bool `json:"disable_tls_alpn_challenge,omitempty"`
|
||||||
|
AlternativeHTTPPort uint16 `json:"alternative_http_port,omitempty"`
|
||||||
|
AlternativeTLSPort uint16 `json:"alternative_tls_port,omitempty"`
|
||||||
|
ExternalAccount *ACMEExternalAccountOptions `json:"external_account,omitempty"`
|
||||||
|
DNS01Challenge *ACMEDNS01ChallengeOptions `json:"dns01_challenge,omitempty"`
|
||||||
|
}
|
||||||
53
option/certificate_provider.go
Normal file
53
option/certificate_provider.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
import (
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
"github.com/sagernet/sing/common/json/badjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type _CertificateProviderOptions struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
ACMEOptions CertificateProviderACMEOptions `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CertificateProviderOptions _CertificateProviderOptions
|
||||||
|
|
||||||
|
func (o CertificateProviderOptions) MarshalJSON() ([]byte, error) {
|
||||||
|
var v any
|
||||||
|
switch o.Type {
|
||||||
|
case C.TypeACME:
|
||||||
|
v = o.ACMEOptions
|
||||||
|
case "":
|
||||||
|
return nil, E.New("missing certificate provider type")
|
||||||
|
default:
|
||||||
|
return nil, E.New("unknown certificate provider type: ", o.Type)
|
||||||
|
}
|
||||||
|
return badjson.MarshallObjects((_CertificateProviderOptions)(o), v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *CertificateProviderOptions) UnmarshalJSON(bytes []byte) error {
|
||||||
|
err := json.Unmarshal(bytes, (*_CertificateProviderOptions)(o))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var v any
|
||||||
|
switch o.Type {
|
||||||
|
case C.TypeACME:
|
||||||
|
v = &o.ACMEOptions
|
||||||
|
case "":
|
||||||
|
return E.New("missing certificate provider type")
|
||||||
|
default:
|
||||||
|
return E.New("unknown certificate provider type: ", o.Type)
|
||||||
|
}
|
||||||
|
err = badjson.UnmarshallExcluded(bytes, (*_CertificateProviderOptions)(o), v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type CertificateProviderACMEOptions struct {
|
||||||
|
Service string `json:"service"`
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ type RouteOptions struct {
|
|||||||
RuleSet []RuleSet `json:"rule_set,omitempty"`
|
RuleSet []RuleSet `json:"rule_set,omitempty"`
|
||||||
Final string `json:"final,omitempty"`
|
Final string `json:"final,omitempty"`
|
||||||
FindProcess bool `json:"find_process,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"`
|
AutoDetectInterface bool `json:"auto_detect_interface,omitempty"`
|
||||||
OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"`
|
OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"`
|
||||||
DefaultInterface string `json:"default_interface,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"`
|
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"`
|
NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"`
|
||||||
DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_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"`
|
PreferredBy badoption.Listable[string] `json:"preferred_by,omitempty"`
|
||||||
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
|
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
|
||||||
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,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"`
|
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"`
|
NetworkInterfaceAddress *badjson.TypedMap[InterfaceType, badoption.Listable[*badoption.Prefixable]] `json:"network_interface_address,omitempty"`
|
||||||
DefaultInterfaceAddress badoption.Listable[*badoption.Prefixable] `json:"default_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"`
|
RuleSet badoption.Listable[string] `json:"rule_set,omitempty"`
|
||||||
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
|
RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"`
|
||||||
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"`
|
RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"`
|
||||||
|
|||||||
@@ -28,9 +28,12 @@ type InboundTLSOptions struct {
|
|||||||
KeyPath string `json:"key_path,omitempty"`
|
KeyPath string `json:"key_path,omitempty"`
|
||||||
KernelTx bool `json:"kernel_tx,omitempty"`
|
KernelTx bool `json:"kernel_tx,omitempty"`
|
||||||
KernelRx bool `json:"kernel_rx,omitempty"`
|
KernelRx bool `json:"kernel_rx,omitempty"`
|
||||||
ACME *InboundACMEOptions `json:"acme,omitempty"`
|
CertificateProvider *CertificateProviderOptions `json:"certificate_provider,omitempty"`
|
||||||
ECH *InboundECHOptions `json:"ech,omitempty"`
|
|
||||||
Reality *InboundRealityOptions `json:"reality,omitempty"`
|
// Deprecated: use certificate_provider
|
||||||
|
ACME *InboundACMEOptions `json:"acme,omitempty"`
|
||||||
|
ECH *InboundECHOptions `json:"ech,omitempty"`
|
||||||
|
Reality *InboundRealityOptions `json:"reality,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientAuthType tls.ClientAuthType
|
type ClientAuthType tls.ClientAuthType
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ type TunInboundOptions struct {
|
|||||||
IncludeAndroidUser badoption.Listable[int] `json:"include_android_user,omitempty"`
|
IncludeAndroidUser badoption.Listable[int] `json:"include_android_user,omitempty"`
|
||||||
IncludePackage badoption.Listable[string] `json:"include_package,omitempty"`
|
IncludePackage badoption.Listable[string] `json:"include_package,omitempty"`
|
||||||
ExcludePackage badoption.Listable[string] `json:"exclude_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"`
|
UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"`
|
||||||
Stack string `json:"stack,omitempty"`
|
Stack string `json:"stack,omitempty"`
|
||||||
Platform *TunPlatformOptions `json:"platform,omitempty"`
|
Platform *TunPlatformOptions `json:"platform,omitempty"`
|
||||||
|
|||||||
@@ -333,9 +333,6 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
|
|||||||
t.systemTun = systemTun
|
t.systemTun = systemTun
|
||||||
t.systemDialer = systemDialer
|
t.systemDialer = systemDialer
|
||||||
t.server.TunDevice = wgTunDevice
|
t.server.TunDevice = wgTunDevice
|
||||||
t.server.RouterWrapper = func(inner router.Router) router.Router {
|
|
||||||
return &addressOnlyRouter{Router: inner}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if mark := t.network.AutoRedirectOutputMark(); mark > 0 {
|
if mark := t.network.AutoRedirectOutputMark(); mark > 0 {
|
||||||
controlFunc := t.network.AutoRedirectOutputMarkFunc()
|
controlFunc := t.network.AutoRedirectOutputMarkFunc()
|
||||||
@@ -480,11 +477,12 @@ func (t *Endpoint) Close() error {
|
|||||||
t.fallbackTCPCloser()
|
t.fallbackTCPCloser()
|
||||||
t.fallbackTCPCloser = nil
|
t.fallbackTCPCloser = nil
|
||||||
}
|
}
|
||||||
|
err := common.Close(common.PtrOrNil(t.server))
|
||||||
if t.systemTun != nil {
|
if t.systemTun != nil {
|
||||||
_ = t.systemTun.Close()
|
t.systemTun.Close()
|
||||||
t.systemTun = nil
|
t.systemTun = nil
|
||||||
}
|
}
|
||||||
return common.Close(common.PtrOrNil(t.server))
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Endpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
func (t *Endpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
@@ -850,15 +848,3 @@ func (c *dnsConfigurtor) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type addressOnlyRouter struct {
|
|
||||||
router.Router
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *addressOnlyRouter) Set(config *router.Config) error {
|
|
||||||
if config != nil {
|
|
||||||
config = &router.Config{
|
|
||||||
LocalAddrs: config.LocalAddrs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return r.Router.Set(config)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -156,6 +156,22 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||||||
if nfQueue == 0 {
|
if nfQueue == 0 {
|
||||||
nfQueue = tun.DefaultAutoRedirectNFQueue
|
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)
|
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
||||||
multiPendingPackets := C.IsDarwin && ((options.Stack == "gvisor" && tunMTU < 32768) || (options.Stack != "gvisor" && options.MTU <= 9000))
|
multiPendingPackets := C.IsDarwin && ((options.Stack == "gvisor" && tunMTU < 32768) || (options.Stack != "gvisor" && options.MTU <= 9000))
|
||||||
inbound := &Inbound{
|
inbound := &Inbound{
|
||||||
@@ -193,6 +209,8 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo
|
|||||||
IncludeAndroidUser: options.IncludeAndroidUser,
|
IncludeAndroidUser: options.IncludeAndroidUser,
|
||||||
IncludePackage: options.IncludePackage,
|
IncludePackage: options.IncludePackage,
|
||||||
ExcludePackage: options.ExcludePackage,
|
ExcludePackage: options.ExcludePackage,
|
||||||
|
IncludeMACAddress: includeMACAddress,
|
||||||
|
ExcludeMACAddress: excludeMACAddress,
|
||||||
InterfaceMonitor: networkManager.InterfaceMonitor(),
|
InterfaceMonitor: networkManager.InterfaceMonitor(),
|
||||||
EXP_MultiPendingPackets: multiPendingPackets,
|
EXP_MultiPendingPackets: multiPendingPackets,
|
||||||
},
|
},
|
||||||
|
|||||||
239
route/neighbor_resolver_darwin.go
Normal file
239
route/neighbor_resolver_darwin.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
386
route/neighbor_resolver_lease.go
Normal file
386
route/neighbor_resolver_lease.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
224
route/neighbor_resolver_linux.go
Normal file
224
route/neighbor_resolver_linux.go
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package route
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/fswatch"
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
|
||||||
|
"github.com/jsimonetti/rtnetlink"
|
||||||
|
"github.com/mdlayher/netlink"
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultLeaseFiles = []string{
|
||||||
|
"/tmp/dhcp.leases",
|
||||||
|
"/var/lib/dhcp/dhcpd.leases",
|
||||||
|
"/var/lib/dhcpd/dhcpd.leases",
|
||||||
|
"/var/lib/kea/kea-leases4.csv",
|
||||||
|
"/var/lib/kea/kea-leases6.csv",
|
||||||
|
}
|
||||||
|
|
||||||
|
type neighborResolver struct {
|
||||||
|
logger logger.ContextLogger
|
||||||
|
leaseFiles []string
|
||||||
|
access sync.RWMutex
|
||||||
|
neighborIPToMAC map[netip.Addr]net.HardwareAddr
|
||||||
|
leaseIPToMAC map[netip.Addr]net.HardwareAddr
|
||||||
|
ipToHostname map[netip.Addr]string
|
||||||
|
macToHostname map[string]string
|
||||||
|
watcher *fswatch.Watcher
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNeighborResolver(resolverLogger logger.ContextLogger, leaseFiles []string) (adapter.NeighborResolver, error) {
|
||||||
|
if len(leaseFiles) == 0 {
|
||||||
|
for _, path := range defaultLeaseFiles {
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err == nil && info.Size() > 0 {
|
||||||
|
leaseFiles = append(leaseFiles, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &neighborResolver{
|
||||||
|
logger: resolverLogger,
|
||||||
|
leaseFiles: leaseFiles,
|
||||||
|
neighborIPToMAC: make(map[netip.Addr]net.HardwareAddr),
|
||||||
|
leaseIPToMAC: make(map[netip.Addr]net.HardwareAddr),
|
||||||
|
ipToHostname: make(map[netip.Addr]string),
|
||||||
|
macToHostname: make(map[string]string),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *neighborResolver) Start() error {
|
||||||
|
err := r.loadNeighborTable()
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Warn(E.Cause(err, "load neighbor table"))
|
||||||
|
}
|
||||||
|
r.doReloadLeaseFiles()
|
||||||
|
go r.subscribeNeighborUpdates()
|
||||||
|
if len(r.leaseFiles) > 0 {
|
||||||
|
watcher, err := fswatch.NewWatcher(fswatch.Options{
|
||||||
|
Path: r.leaseFiles,
|
||||||
|
Logger: r.logger,
|
||||||
|
Callback: func(_ string) {
|
||||||
|
r.doReloadLeaseFiles()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Warn(E.Cause(err, "create lease file watcher"))
|
||||||
|
} else {
|
||||||
|
r.watcher = watcher
|
||||||
|
err = watcher.Start()
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Warn(E.Cause(err, "start lease file watcher"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *neighborResolver) Close() error {
|
||||||
|
close(r.done)
|
||||||
|
if r.watcher != nil {
|
||||||
|
return r.watcher.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *neighborResolver) LookupMAC(address netip.Addr) (net.HardwareAddr, bool) {
|
||||||
|
r.access.RLock()
|
||||||
|
defer r.access.RUnlock()
|
||||||
|
mac, found := r.neighborIPToMAC[address]
|
||||||
|
if found {
|
||||||
|
return mac, true
|
||||||
|
}
|
||||||
|
mac, found = r.leaseIPToMAC[address]
|
||||||
|
if found {
|
||||||
|
return mac, true
|
||||||
|
}
|
||||||
|
mac, found = extractMACFromEUI64(address)
|
||||||
|
if found {
|
||||||
|
return mac, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *neighborResolver) LookupHostname(address netip.Addr) (string, bool) {
|
||||||
|
r.access.RLock()
|
||||||
|
defer r.access.RUnlock()
|
||||||
|
hostname, found := r.ipToHostname[address]
|
||||||
|
if found {
|
||||||
|
return hostname, true
|
||||||
|
}
|
||||||
|
mac, macFound := r.neighborIPToMAC[address]
|
||||||
|
if !macFound {
|
||||||
|
mac, macFound = r.leaseIPToMAC[address]
|
||||||
|
}
|
||||||
|
if !macFound {
|
||||||
|
mac, macFound = extractMACFromEUI64(address)
|
||||||
|
}
|
||||||
|
if macFound {
|
||||||
|
hostname, found = r.macToHostname[mac.String()]
|
||||||
|
if found {
|
||||||
|
return hostname, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *neighborResolver) loadNeighborTable() error {
|
||||||
|
connection, err := rtnetlink.Dial(nil)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "dial rtnetlink")
|
||||||
|
}
|
||||||
|
defer connection.Close()
|
||||||
|
neighbors, err := connection.Neigh.List()
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "list neighbors")
|
||||||
|
}
|
||||||
|
r.access.Lock()
|
||||||
|
defer r.access.Unlock()
|
||||||
|
for _, neigh := range neighbors {
|
||||||
|
if neigh.Attributes == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if neigh.Attributes.LLAddress == nil || len(neigh.Attributes.Address) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
address, ok := netip.AddrFromSlice(neigh.Attributes.Address)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r.neighborIPToMAC[address] = slices.Clone(neigh.Attributes.LLAddress)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *neighborResolver) subscribeNeighborUpdates() {
|
||||||
|
connection, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{
|
||||||
|
Groups: 1 << (unix.RTNLGRP_NEIGH - 1),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Warn(E.Cause(err, "subscribe neighbor updates"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer connection.Close()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-r.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
err = connection.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||||
|
if err != nil {
|
||||||
|
r.logger.Warn(E.Cause(err, "set netlink read deadline"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
messages, err := connection.Receive()
|
||||||
|
if err != nil {
|
||||||
|
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-r.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
r.logger.Warn(E.Cause(err, "receive neighbor update"))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, message := range messages {
|
||||||
|
address, mac, isDelete, ok := ParseNeighborMessage(message)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r.access.Lock()
|
||||||
|
if isDelete {
|
||||||
|
delete(r.neighborIPToMAC, address)
|
||||||
|
} else {
|
||||||
|
r.neighborIPToMAC[address] = mac
|
||||||
|
}
|
||||||
|
r.access.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *neighborResolver) doReloadLeaseFiles() {
|
||||||
|
leaseIPToMAC, ipToHostname, macToHostname := ReloadLeaseFiles(r.leaseFiles)
|
||||||
|
r.access.Lock()
|
||||||
|
r.leaseIPToMAC = leaseIPToMAC
|
||||||
|
r.ipToHostname = ipToHostname
|
||||||
|
r.macToHostname = macToHostname
|
||||||
|
r.access.Unlock()
|
||||||
|
}
|
||||||
50
route/neighbor_resolver_parse.go
Normal file
50
route/neighbor_resolver_parse.go
Normal 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)
|
||||||
|
}
|
||||||
84
route/neighbor_resolver_platform.go
Normal file
84
route/neighbor_resolver_platform.go
Normal 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")
|
||||||
|
}
|
||||||
14
route/neighbor_resolver_stub.go
Normal file
14
route/neighbor_resolver_stub.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//go:build !linux && !darwin
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
104
route/neighbor_table_darwin.go
Normal file
104
route/neighbor_table_darwin.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
68
route/neighbor_table_linux.go
Normal file
68
route/neighbor_table_linux.go
Normal 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
|
||||||
|
}
|
||||||
@@ -439,6 +439,23 @@ func (r *Router) matchRule(
|
|||||||
metadata.ProcessInfo = processInfo
|
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) {
|
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)
|
domain, loaded := r.dnsTransport.FakeIP().Store().Lookup(metadata.Destination.Addr)
|
||||||
if !loaded {
|
if !loaded {
|
||||||
|
|||||||
@@ -31,9 +31,12 @@ type Router struct {
|
|||||||
network adapter.NetworkManager
|
network adapter.NetworkManager
|
||||||
rules []adapter.Rule
|
rules []adapter.Rule
|
||||||
needFindProcess bool
|
needFindProcess bool
|
||||||
|
needFindNeighbor bool
|
||||||
|
leaseFiles []string
|
||||||
ruleSets []adapter.RuleSet
|
ruleSets []adapter.RuleSet
|
||||||
ruleSetMap map[string]adapter.RuleSet
|
ruleSetMap map[string]adapter.RuleSet
|
||||||
processSearcher process.Searcher
|
processSearcher process.Searcher
|
||||||
|
neighborResolver adapter.NeighborResolver
|
||||||
pauseManager pause.Manager
|
pauseManager pause.Manager
|
||||||
trackers []adapter.ConnectionTracker
|
trackers []adapter.ConnectionTracker
|
||||||
platformInterface adapter.PlatformInterface
|
platformInterface adapter.PlatformInterface
|
||||||
@@ -53,6 +56,8 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route
|
|||||||
rules: make([]adapter.Rule, 0, len(options.Rules)),
|
rules: make([]adapter.Rule, 0, len(options.Rules)),
|
||||||
ruleSetMap: make(map[string]adapter.RuleSet),
|
ruleSetMap: make(map[string]adapter.RuleSet),
|
||||||
needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess,
|
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),
|
pauseManager: service.FromContext[pause.Manager](ctx),
|
||||||
platformInterface: service.FromContext[adapter.PlatformInterface](ctx),
|
platformInterface: service.FromContext[adapter.PlatformInterface](ctx),
|
||||||
}
|
}
|
||||||
@@ -112,6 +117,7 @@ func (r *Router) Start(stage adapter.StartStage) error {
|
|||||||
}
|
}
|
||||||
r.network.Initialize(r.ruleSets)
|
r.network.Initialize(r.ruleSets)
|
||||||
needFindProcess := r.needFindProcess
|
needFindProcess := r.needFindProcess
|
||||||
|
needFindNeighbor := r.needFindNeighbor
|
||||||
for _, ruleSet := range r.ruleSets {
|
for _, ruleSet := range r.ruleSets {
|
||||||
metadata := ruleSet.Metadata()
|
metadata := ruleSet.Metadata()
|
||||||
if metadata.ContainsProcessRule {
|
if metadata.ContainsProcessRule {
|
||||||
@@ -141,6 +147,36 @@ func (r *Router) Start(stage adapter.StartStage) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
r.needFindNeighbor = needFindNeighbor
|
||||||
|
if needFindNeighbor {
|
||||||
|
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.Error(E.Cause(err, "start neighbor resolver"))
|
||||||
|
} else {
|
||||||
|
r.neighborResolver = resolver
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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:
|
case adapter.StartStatePostStart:
|
||||||
for i, rule := range r.rules {
|
for i, rule := range r.rules {
|
||||||
monitor.Start("initialize rule[", i, "]")
|
monitor.Start("initialize rule[", i, "]")
|
||||||
@@ -172,6 +208,13 @@ func (r *Router) Start(stage adapter.StartStage) error {
|
|||||||
func (r *Router) Close() error {
|
func (r *Router) Close() error {
|
||||||
monitor := taskmonitor.New(r.logger, C.StopTimeout)
|
monitor := taskmonitor.New(r.logger, C.StopTimeout)
|
||||||
var err error
|
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 {
|
for i, rule := range r.rules {
|
||||||
monitor.Start("close rule[", i, "]")
|
monitor.Start("close rule[", i, "]")
|
||||||
err = E.Append(err, rule.Close(), func(err error) error {
|
err = E.Append(err, rule.Close(), func(err error) error {
|
||||||
@@ -206,6 +249,14 @@ func (r *Router) NeedFindProcess() bool {
|
|||||||
return r.needFindProcess
|
return r.needFindProcess
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Router) NeedFindNeighbor() bool {
|
||||||
|
return r.needFindNeighbor
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) NeighborResolver() adapter.NeighborResolver {
|
||||||
|
return r.neighborResolver
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Router) ResetNetwork() {
|
func (r *Router) ResetNetwork() {
|
||||||
r.network.ResetNetwork()
|
r.network.ResetNetwork()
|
||||||
r.dns.ResetNetwork()
|
r.dns.ResetNetwork()
|
||||||
|
|||||||
@@ -260,6 +260,16 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
|
|||||||
rule.items = append(rule.items, item)
|
rule.items = append(rule.items, item)
|
||||||
rule.allItems = append(rule.allItems, 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 {
|
if len(options.PreferredBy) > 0 {
|
||||||
item := NewPreferredByItem(ctx, options.PreferredBy)
|
item := NewPreferredByItem(ctx, options.PreferredBy)
|
||||||
rule.items = append(rule.items, item)
|
rule.items = append(rule.items, item)
|
||||||
|
|||||||
@@ -261,6 +261,16 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
|
|||||||
rule.items = append(rule.items, item)
|
rule.items = append(rule.items, item)
|
||||||
rule.allItems = append(rule.allItems, 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 {
|
if len(options.RuleSet) > 0 {
|
||||||
//nolint:staticcheck
|
//nolint:staticcheck
|
||||||
if options.Deprecated_RulesetIPCIDRMatchSource {
|
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
|
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 {
|
func isWIFIRule(rule option.DefaultRule) bool {
|
||||||
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
|
return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0
|
||||||
}
|
}
|
||||||
|
|||||||
43
service/acme/logger.go
Normal file
43
service/acme/logger.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
//go:build with_acme
|
||||||
|
|
||||||
|
package acme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
type logWriter struct {
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *logWriter) Write(p []byte) (n int, err error) {
|
||||||
|
logLine := strings.ReplaceAll(string(p), " ", ": ")
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(logLine, "error: "):
|
||||||
|
w.logger.Error(logLine[7:])
|
||||||
|
case strings.HasPrefix(logLine, "warn: "):
|
||||||
|
w.logger.Warn(logLine[6:])
|
||||||
|
case strings.HasPrefix(logLine, "info: "):
|
||||||
|
w.logger.Info(logLine[6:])
|
||||||
|
case strings.HasPrefix(logLine, "debug: "):
|
||||||
|
w.logger.Debug(logLine[7:])
|
||||||
|
default:
|
||||||
|
w.logger.Debug(logLine)
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *logWriter) Sync() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encoderConfig() zapcore.EncoderConfig {
|
||||||
|
config := zap.NewProductionEncoderConfig()
|
||||||
|
config.TimeKey = zapcore.OmitKey
|
||||||
|
return config
|
||||||
|
}
|
||||||
158
service/acme/service.go
Normal file
158
service/acme/service.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
//go:build with_acme
|
||||||
|
|
||||||
|
package acme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
"github.com/libdns/acmedns"
|
||||||
|
"github.com/libdns/alidns"
|
||||||
|
"github.com/libdns/cloudflare"
|
||||||
|
"github.com/mholt/acmez/v3/acme"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterService(registry *boxService.Registry) {
|
||||||
|
boxService.Register[option.ACMEServiceOptions](registry, C.TypeACME, NewService)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ adapter.ACMECertificateProvider = (*Service)(nil)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
boxService.Adapter
|
||||||
|
ctx context.Context
|
||||||
|
logger log.ContextLogger
|
||||||
|
config *certmagic.Config
|
||||||
|
cache *certmagic.Cache
|
||||||
|
domain []string
|
||||||
|
nextProtos []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.ACMEServiceOptions) (adapter.Service, error) {
|
||||||
|
var acmeServer string
|
||||||
|
switch options.Provider {
|
||||||
|
case "", "letsencrypt":
|
||||||
|
acmeServer = certmagic.LetsEncryptProductionCA
|
||||||
|
case "zerossl":
|
||||||
|
acmeServer = certmagic.ZeroSSLProductionCA
|
||||||
|
default:
|
||||||
|
if !strings.HasPrefix(options.Provider, "https://") {
|
||||||
|
return nil, E.New("unsupported ACME provider: ", options.Provider)
|
||||||
|
}
|
||||||
|
acmeServer = options.Provider
|
||||||
|
}
|
||||||
|
var storage certmagic.Storage
|
||||||
|
if options.DataDirectory != "" {
|
||||||
|
storage = &certmagic.FileStorage{
|
||||||
|
Path: options.DataDirectory,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
storage = certmagic.Default.Storage
|
||||||
|
}
|
||||||
|
zapLogger := zap.New(zapcore.NewCore(
|
||||||
|
zapcore.NewConsoleEncoder(encoderConfig()),
|
||||||
|
&logWriter{logger: logger},
|
||||||
|
zap.DebugLevel,
|
||||||
|
))
|
||||||
|
config := &certmagic.Config{
|
||||||
|
DefaultServerName: options.DefaultServerName,
|
||||||
|
Storage: storage,
|
||||||
|
Logger: zapLogger,
|
||||||
|
}
|
||||||
|
acmeIssuer := certmagic.ACMEIssuer{
|
||||||
|
CA: acmeServer,
|
||||||
|
Email: options.Email,
|
||||||
|
Agreed: true,
|
||||||
|
DisableHTTPChallenge: options.DisableHTTPChallenge,
|
||||||
|
DisableTLSALPNChallenge: options.DisableTLSALPNChallenge,
|
||||||
|
AltHTTPPort: int(options.AlternativeHTTPPort),
|
||||||
|
AltTLSALPNPort: int(options.AlternativeTLSPort),
|
||||||
|
Logger: zapLogger,
|
||||||
|
}
|
||||||
|
if dnsOptions := options.DNS01Challenge; dnsOptions != nil && dnsOptions.Provider != "" {
|
||||||
|
var solver certmagic.DNS01Solver
|
||||||
|
switch dnsOptions.Provider {
|
||||||
|
case C.DNSProviderAliDNS:
|
||||||
|
solver.DNSProvider = &alidns.Provider{
|
||||||
|
CredentialInfo: alidns.CredentialInfo{
|
||||||
|
AccessKeyID: dnsOptions.AliDNSOptions.AccessKeyID,
|
||||||
|
AccessKeySecret: dnsOptions.AliDNSOptions.AccessKeySecret,
|
||||||
|
RegionID: dnsOptions.AliDNSOptions.RegionID,
|
||||||
|
SecurityToken: dnsOptions.AliDNSOptions.SecurityToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
case C.DNSProviderCloudflare:
|
||||||
|
solver.DNSProvider = &cloudflare.Provider{
|
||||||
|
APIToken: dnsOptions.CloudflareOptions.APIToken,
|
||||||
|
ZoneToken: dnsOptions.CloudflareOptions.ZoneToken,
|
||||||
|
}
|
||||||
|
case C.DNSProviderACMEDNS:
|
||||||
|
solver.DNSProvider = &acmedns.Provider{
|
||||||
|
Username: dnsOptions.ACMEDNSOptions.Username,
|
||||||
|
Password: dnsOptions.ACMEDNSOptions.Password,
|
||||||
|
Subdomain: dnsOptions.ACMEDNSOptions.Subdomain,
|
||||||
|
ServerURL: dnsOptions.ACMEDNSOptions.ServerURL,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, E.New("unsupported ACME DNS01 provider type: ", dnsOptions.Provider)
|
||||||
|
}
|
||||||
|
acmeIssuer.DNS01Solver = &solver
|
||||||
|
}
|
||||||
|
if options.ExternalAccount != nil && options.ExternalAccount.KeyID != "" {
|
||||||
|
acmeIssuer.ExternalAccount = (*acme.EAB)(options.ExternalAccount)
|
||||||
|
}
|
||||||
|
config.Issuers = []certmagic.Issuer{certmagic.NewACMEIssuer(config, acmeIssuer)}
|
||||||
|
cache := certmagic.NewCache(certmagic.CacheOptions{
|
||||||
|
GetConfigForCert: func(certificate certmagic.Certificate) (*certmagic.Config, error) {
|
||||||
|
return config, nil
|
||||||
|
},
|
||||||
|
Logger: zapLogger,
|
||||||
|
})
|
||||||
|
config = certmagic.New(cache, *config)
|
||||||
|
var nextProtos []string
|
||||||
|
if !acmeIssuer.DisableTLSALPNChallenge && acmeIssuer.DNS01Solver == nil {
|
||||||
|
nextProtos = []string{C.ACMETLS1Protocol}
|
||||||
|
}
|
||||||
|
return &Service{
|
||||||
|
Adapter: boxService.NewAdapter(C.TypeACME, tag),
|
||||||
|
ctx: ctx,
|
||||||
|
logger: logger,
|
||||||
|
config: config,
|
||||||
|
cache: cache,
|
||||||
|
domain: options.Domain,
|
||||||
|
nextProtos: nextProtos,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Start(stage adapter.StartStage) error {
|
||||||
|
if stage != adapter.StartStateStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s.config.ManageAsync(s.ctx, s.domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Close() error {
|
||||||
|
if s.cache != nil {
|
||||||
|
s.cache.Stop()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
return s.config.GetCertificate(hello)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetACMENextProtos() []string {
|
||||||
|
return s.nextProtos
|
||||||
|
}
|
||||||
3
service/acme/stub.go
Normal file
3
service/acme/stub.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
//go:build !with_acme
|
||||||
|
|
||||||
|
package acme
|
||||||
Reference in New Issue
Block a user