216 lines
5.8 KiB
Go
216 lines
5.8 KiB
Go
package bridge
|
|
|
|
import (
|
|
"net/netip"
|
|
"os"
|
|
"slices"
|
|
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
"github.com/sagernet/sing/common/logger"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
type ServiceOptions struct {
|
|
MTU int
|
|
Inet4Port netip.Addr
|
|
Inet6Port netip.Addr
|
|
Interface string
|
|
Logger logger.ContextLogger
|
|
}
|
|
|
|
type Service struct {
|
|
serviceBase
|
|
|
|
boundInterface string
|
|
inet4Local netip.Addr
|
|
inet6Local netip.Addr
|
|
anchorName string
|
|
pfDevice *pfDevice
|
|
pfToken uint64
|
|
currentRules []pfAnchorRule
|
|
}
|
|
|
|
func NewService(options ServiceOptions) (*Service, error) {
|
|
if !options.Inet4Port.IsValid() {
|
|
return nil, E.New("missing bridge IPv4 port address")
|
|
}
|
|
serviceLogger := options.Logger
|
|
if serviceLogger == nil {
|
|
serviceLogger = logger.NOP()
|
|
}
|
|
instance := &Service{
|
|
serviceBase: serviceBase{
|
|
logger: serviceLogger,
|
|
mtu: options.MTU,
|
|
inet4Port: options.Inet4Port,
|
|
inet6Port: options.Inet6Port,
|
|
tunFileDescriptor: -1,
|
|
},
|
|
boundInterface: options.Interface,
|
|
}
|
|
instance.applyEgress = instance.syncEgressLocked
|
|
index, err := bridgeIndexOf(options.Inet4Port)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
instance.inet4Local = addressAt(bridgeInet4LocalBase, index)
|
|
instance.inet6Local = addressAt(bridgeInet6LocalBase, index)
|
|
err = instance.start()
|
|
if err != nil {
|
|
instance.Close()
|
|
return nil, err
|
|
}
|
|
return instance, nil
|
|
}
|
|
|
|
func bridgeIndexOf(inet4Port netip.Addr) (uint32, error) {
|
|
for index := range uint32(bridgeMaxInstances) {
|
|
if addressAt(bridgeInet4Base, index) == inet4Port {
|
|
return index, nil
|
|
}
|
|
}
|
|
return 0, E.New("unexpected bridge IPv4 port address: ", inet4Port)
|
|
}
|
|
|
|
func (s *Service) start() error {
|
|
tunFileDescriptor, tunName, err := createBridgeTun(s.mtu)
|
|
if err != nil {
|
|
return E.Cause(err, "create bridge tun")
|
|
}
|
|
s.tunFileDescriptor = tunFileDescriptor
|
|
s.tunName = tunName
|
|
s.anchorName = bridgeAnchor(tunName)
|
|
s.forwardingRestore = enableDarwinForwarding(s.logger, s.inet4Port.IsValid(), s.inet6Port.IsValid())
|
|
err = assignBridgePortAddress(tunName, s.inet4Local, s.inet4Port)
|
|
if err != nil {
|
|
return E.Cause(err, "add bridge route")
|
|
}
|
|
err = assignBridgePortAddress(tunName, s.inet6Local, s.inet6Port)
|
|
if err != nil {
|
|
s.logger.Debug(E.Cause(err, "IPv6 bridge routing unavailable, disabling IPv6 forwarding"))
|
|
s.inet6Port = netip.Addr{}
|
|
}
|
|
device, err := openPfDevice()
|
|
if err != nil {
|
|
return E.Cause(err, "enable pf")
|
|
}
|
|
s.pfDevice = device
|
|
token, err := device.StartReference()
|
|
if err != nil {
|
|
return E.Cause(err, "enable pf")
|
|
}
|
|
s.pfToken = token
|
|
dropRules := bridgeDropRules(s.tunName, s.inet4Port, s.inet6Port)
|
|
err = s.pfDevice.LoadAnchor(s.anchorName, dropRules)
|
|
if err != nil {
|
|
return E.Cause(err, "initialize bridge pf rules")
|
|
}
|
|
s.currentRules = dropRules
|
|
s.startNetworkMonitor()
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) syncEgressLocked() error {
|
|
rules := bridgeDropRules(s.tunName, s.inet4Port, s.inet6Port)
|
|
var buildErr error
|
|
if s.egressName != "" {
|
|
rules, buildErr = buildBridgeAnchorRules(s.tunName, s.egressName, s.boundInterface, s.inet4Port, s.inet6Port)
|
|
}
|
|
if slices.Equal(rules, s.currentRules) {
|
|
return buildErr
|
|
}
|
|
err := s.pfDevice.LoadAnchor(s.anchorName, rules)
|
|
if err != nil {
|
|
return E.Cause(err, "apply bridge egress ", s.egressName)
|
|
}
|
|
s.currentRules = rules
|
|
if buildErr != nil || s.egressName == "" {
|
|
s.logger.Debug("bridge egress unavailable, dropping forwarded traffic")
|
|
} else {
|
|
s.logger.Debug("bridge egress ", s.egressName)
|
|
}
|
|
return buildErr
|
|
}
|
|
|
|
func (s *Service) Close() error {
|
|
if !s.beginClose() {
|
|
return nil
|
|
}
|
|
s.access.Lock()
|
|
defer s.access.Unlock()
|
|
if s.pfDevice != nil {
|
|
// anchorName is set before pfDevice is opened, so a non-nil pfDevice means
|
|
// it holds the intended target (the sub-anchor on macOS, "" on iOS).
|
|
_ = s.pfDevice.LoadAnchor(s.anchorName, nil)
|
|
if s.pfToken != 0 {
|
|
_ = s.pfDevice.StopReference(s.pfToken)
|
|
}
|
|
_ = s.pfDevice.Close()
|
|
s.pfDevice = nil
|
|
}
|
|
restoreDarwinForwarding(s.forwardingRestore)
|
|
s.forwardingRestore = nil
|
|
if s.tunFileDescriptor >= 0 {
|
|
_ = unix.Close(s.tunFileDescriptor)
|
|
s.tunFileDescriptor = -1
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// The stock macOS /etc/pf.conf ends its main ruleset with wildcard
|
|
// nat/rdr/scrub/anchor references to "com.apple/*", so rules loaded into a
|
|
// sub-anchor below it are evaluated without editing the main ruleset. iOS ships
|
|
// no /etc/pf.conf and no such references, leaving the main ruleset empty and
|
|
// pf-unused; there an anchor is never traversed, so we own the main ruleset
|
|
// directly (anchor "") instead.
|
|
func bridgeAnchor(tunName string) string {
|
|
_, err := os.Stat("/etc/pf.conf")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return "com.apple/sing-box-" + tunName
|
|
}
|
|
|
|
func createBridgeTun(mtu int) (int, string, error) {
|
|
tunFd, err := unix.Socket(unix.AF_SYSTEM, unix.SOCK_DGRAM, 2)
|
|
if err != nil {
|
|
return -1, "", os.NewSyscallError("socket", err)
|
|
}
|
|
ctlInfo := &unix.CtlInfo{}
|
|
copy(ctlInfo.Name[:], "com.apple.net.utun_control")
|
|
err = unix.IoctlCtlInfo(tunFd, ctlInfo)
|
|
if err != nil {
|
|
unix.Close(tunFd)
|
|
return -1, "", os.NewSyscallError("IoctlCtlInfo", err)
|
|
}
|
|
err = unix.Connect(tunFd, &unix.SockaddrCtl{ID: ctlInfo.Id, Unit: 0})
|
|
if err != nil {
|
|
unix.Close(tunFd)
|
|
return -1, "", os.NewSyscallError("Connect", err)
|
|
}
|
|
name, err := unix.GetsockoptString(
|
|
tunFd,
|
|
2, /* #define SYSPROTO_CONTROL 2 */
|
|
2, /* #define UTUN_OPT_IFNAME 2 */
|
|
)
|
|
if err != nil {
|
|
unix.Close(tunFd)
|
|
return -1, "", os.NewSyscallError("GetsockoptString", err)
|
|
}
|
|
socketFd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
|
if err != nil {
|
|
unix.Close(tunFd)
|
|
return -1, "", os.NewSyscallError("socket", err)
|
|
}
|
|
ifr := unix.IfreqMTU{MTU: int32(mtu)}
|
|
copy(ifr.Name[:], name)
|
|
err = unix.IoctlSetIfreqMTU(socketFd, &ifr)
|
|
unix.Close(socketFd)
|
|
if err != nil {
|
|
unix.Close(tunFd)
|
|
return -1, "", os.NewSyscallError("IoctlSetIfreqMTU", err)
|
|
}
|
|
return tunFd, name, nil
|
|
}
|