mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-08-04 14:36:07 +00:00
472 lines
12 KiB
Go
472 lines
12 KiB
Go
package bridge
|
|
|
|
import (
|
|
"context"
|
|
"net/netip"
|
|
"sync"
|
|
|
|
"github.com/sagernet/netlink"
|
|
"github.com/sagernet/sing-box/adapter"
|
|
"github.com/sagernet/sing-box/option"
|
|
"github.com/sagernet/sing-tun"
|
|
"github.com/sagernet/sing/common/control"
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
"github.com/sagernet/sing/common/logger"
|
|
"github.com/sagernet/sing/service"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
const (
|
|
defaultBridgeRuleIndex = 100
|
|
defaultBridgeTableIndexBase = 2200
|
|
)
|
|
|
|
type backendLinux struct {
|
|
backendBase
|
|
|
|
nftTableName string
|
|
routeTable int
|
|
ruleIndex int
|
|
|
|
platform adapter.PlatformInterface
|
|
|
|
batchTUN tun.LinuxTUN
|
|
|
|
writeAccess sync.Mutex
|
|
writeHeadroom int
|
|
writeBuffers [][]byte
|
|
|
|
clampMTU int
|
|
}
|
|
|
|
func newBackend(ctx context.Context, logger logger.ContextLogger, networkManager adapter.NetworkManager, tag string, options option.BridgeOutboundOptions) (Backend, error) {
|
|
instance := &backendLinux{}
|
|
err := instance.init(ctx, logger, networkManager, tag, options)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
|
|
if platformInterface != nil && platformInterface.UsePlatformBridge() {
|
|
instance.platform = platformInterface
|
|
}
|
|
instance.ruleIndex = options.IPRoute2RuleIndex
|
|
if instance.ruleIndex == 0 {
|
|
instance.ruleIndex = defaultBridgeRuleIndex
|
|
}
|
|
if instance.boundInterface != "" || instance.platform != nil {
|
|
instance.routeTable = options.IPRoute2TableIndex
|
|
if instance.routeTable == 0 {
|
|
instance.routeTable = defaultBridgeTableIndexBase + int(instance.index)
|
|
}
|
|
}
|
|
return instance, nil
|
|
}
|
|
|
|
func (b *backendLinux) Start(stage adapter.StartStage) error {
|
|
if stage != adapter.StartStateStart {
|
|
return nil
|
|
}
|
|
err := b.start()
|
|
if err != nil {
|
|
b.Close()
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *backendLinux) start() error {
|
|
if b.platform != nil {
|
|
return b.startPlatform()
|
|
}
|
|
b.tunName = tun.CalculateInterfaceName(b.bridgeName)
|
|
b.nftTableName = "sing-box-" + b.tunName
|
|
tunInterface, err := tun.New(tun.Options{
|
|
Name: b.tunName,
|
|
MTU: bridgeTunMTU,
|
|
GSO: true,
|
|
InterfaceMonitor: b.networkManager.InterfaceMonitor(),
|
|
Logger: b.logger,
|
|
EXP_ExternalConfiguration: true,
|
|
})
|
|
if err != nil {
|
|
return E.Cause(err, "create bridge tun")
|
|
}
|
|
b.tunInterface = tunInterface
|
|
err = tunInterface.Start()
|
|
if err != nil {
|
|
return E.Cause(err, "start bridge tun")
|
|
}
|
|
linuxTUN := tunInterface.(tun.LinuxTUN)
|
|
if linuxTUN.BatchSize() > 1 {
|
|
b.batchTUN = linuxTUN
|
|
b.writeHeadroom = linuxTUN.FrontHeadroom()
|
|
b.writeBuffers = make([][]byte, bridgeWriteBatchSize)
|
|
for i := range b.writeBuffers {
|
|
// handleGRO coalesces same-flow packets by appending into the first
|
|
// packet's buffer capacity, up to the 0xffff total length limit.
|
|
b.writeBuffers[i] = make([]byte, b.writeHeadroom+maxPacketLength)
|
|
}
|
|
}
|
|
inet6Active, err := setupBridgeNetfilter(b.logger, b.nftTableName, b.tunName, b.inet6Port.IsValid())
|
|
if err != nil {
|
|
return E.Cause(err, "set up bridge netfilter")
|
|
}
|
|
if !inet6Active {
|
|
b.inet6Port = netip.Addr{}
|
|
}
|
|
b.forwardingRestore = enableBridgeForwarding(b.logger, b.tunName, b.inet4Port.IsValid(), b.inet6Port.IsValid())
|
|
if b.boundInterface != "" {
|
|
b.syncEgress()
|
|
}
|
|
err = setupBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET, b.inet4Port)
|
|
if err != nil {
|
|
return E.Cause(err, "set up bridge routing")
|
|
}
|
|
err = setupBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET6, b.inet6Port)
|
|
if err != nil {
|
|
b.logger.Debug(E.Cause(err, "IPv6 bridge routing unavailable, disabling IPv6 forwarding"))
|
|
removeBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET6, b.inet6Port)
|
|
b.inet6Port = netip.Addr{}
|
|
}
|
|
b.closed = make(chan struct{})
|
|
b.readDone = make(chan struct{})
|
|
if b.batchTUN != nil {
|
|
go b.batchReadLoop()
|
|
} else {
|
|
go b.readLoop()
|
|
}
|
|
egress := "auto"
|
|
if b.boundInterface != "" {
|
|
egress = b.boundInterface
|
|
monitor := b.networkManager.NetworkMonitor()
|
|
if monitor != nil {
|
|
element := monitor.RegisterCallback(func() { b.syncEgress() })
|
|
b.unregister = func() { monitor.UnregisterCallback(element) }
|
|
} else {
|
|
b.logger.Debug("network monitor unavailable, pinned egress will not track interface changes")
|
|
}
|
|
b.syncEgress()
|
|
} else {
|
|
monitor := b.networkManager.InterfaceMonitor()
|
|
if monitor != nil {
|
|
element := monitor.RegisterCallback(func(_ *control.Interface, _ int) { b.updateClamp() })
|
|
b.unregister = func() { monitor.UnregisterCallback(element) }
|
|
}
|
|
b.updateClamp()
|
|
}
|
|
natMode := "masquerade"
|
|
if fullConeSupported() {
|
|
natMode = "full-cone NAT"
|
|
}
|
|
b.logger.Info("bridge started at ", b.tunName, " (", natMode, ", egress ", egress, ")")
|
|
return nil
|
|
}
|
|
|
|
func (b *backendLinux) startPlatform() error {
|
|
session, err := b.platform.CreateBridge(adapter.BridgeOptions{
|
|
BridgeName: b.bridgeName,
|
|
MTU: bridgeTunMTU,
|
|
Inet4Port: b.inet4Port,
|
|
Inet6Port: b.inet6Port,
|
|
RuleIndex: b.ruleIndex,
|
|
RouteTable: b.routeTable,
|
|
})
|
|
if err != nil {
|
|
return E.Cause(err, "create bridge")
|
|
}
|
|
b.session = session
|
|
b.tunName = session.Name()
|
|
if !session.Inet6Active() {
|
|
b.inet6Port = netip.Addr{}
|
|
}
|
|
tunInterface, err := tun.New(tun.Options{
|
|
Name: b.tunName,
|
|
MTU: bridgeTunMTU,
|
|
GSO: true,
|
|
FileDescriptor: session.FileDescriptor(),
|
|
Logger: b.logger,
|
|
})
|
|
if err != nil {
|
|
return E.Cause(err, "create bridge tun")
|
|
}
|
|
b.tunInterface = tunInterface
|
|
err = tunInterface.Start()
|
|
if err != nil {
|
|
return E.Cause(err, "start bridge tun")
|
|
}
|
|
linuxTUN := tunInterface.(tun.LinuxTUN)
|
|
if linuxTUN.BatchSize() > 1 {
|
|
b.batchTUN = linuxTUN
|
|
b.writeHeadroom = linuxTUN.FrontHeadroom()
|
|
b.writeBuffers = make([][]byte, bridgeWriteBatchSize)
|
|
for i := range b.writeBuffers {
|
|
b.writeBuffers[i] = make([]byte, b.writeHeadroom+maxPacketLength)
|
|
}
|
|
}
|
|
b.closed = make(chan struct{})
|
|
b.readDone = make(chan struct{})
|
|
if b.batchTUN != nil {
|
|
go b.batchReadLoop()
|
|
} else {
|
|
go b.readLoop()
|
|
}
|
|
monitor := b.networkManager.InterfaceMonitor()
|
|
if monitor != nil {
|
|
element := monitor.RegisterCallback(func(_ *control.Interface, _ int) { b.syncSessionEgress() })
|
|
b.unregister = func() { monitor.UnregisterCallback(element) }
|
|
}
|
|
b.syncSessionEgress()
|
|
egress := "auto"
|
|
if b.boundInterface != "" {
|
|
egress = b.boundInterface
|
|
}
|
|
b.logger.Info("bridge started at ", b.tunName, " (platform, egress ", egress, ")")
|
|
return nil
|
|
}
|
|
|
|
func (b *backendLinux) Close() error {
|
|
b.closeOnce.Do(func() {
|
|
if b.closed != nil {
|
|
close(b.closed)
|
|
}
|
|
if b.unregister != nil {
|
|
b.unregister()
|
|
}
|
|
if b.tunInterface != nil {
|
|
b.tunInterface.Close()
|
|
}
|
|
if b.readDone != nil {
|
|
<-b.readDone
|
|
}
|
|
if b.session != nil {
|
|
_ = b.session.Close()
|
|
} else {
|
|
b.egressAccess.Lock()
|
|
if b.tunName != "" {
|
|
cleanupBridgeNetfilter(b.nftTableName)
|
|
removeBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET, b.inet4Port)
|
|
removeBridgeFamily(b.tunName, b.ruleIndex, b.routeTable, unix.AF_INET6, b.inet6Port)
|
|
}
|
|
if b.routeTable != 0 {
|
|
flushBridgeRouteTable(b.routeTable)
|
|
}
|
|
b.egressAccess.Unlock()
|
|
restoreBridgeForwarding(b.forwardingRestore)
|
|
b.forwardingRestore = nil
|
|
}
|
|
releaseBridgeIndex(b.index)
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (b *backendLinux) PortMTU() uint32 {
|
|
return 0
|
|
}
|
|
|
|
func (b *backendLinux) WritePackets(packets [][]byte) error {
|
|
if b.batchTUN == nil {
|
|
for _, packet := range packets {
|
|
if len(packet) == 0 {
|
|
continue
|
|
}
|
|
_, err := b.tunInterface.Write(packet)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
b.writeAccess.Lock()
|
|
defer b.writeAccess.Unlock()
|
|
for len(packets) > 0 {
|
|
chunk := packets
|
|
if len(chunk) > len(b.writeBuffers) {
|
|
chunk = chunk[:len(b.writeBuffers)]
|
|
}
|
|
packets = packets[len(chunk):]
|
|
batch := make([][]byte, 0, len(chunk))
|
|
for i, packet := range chunk {
|
|
if len(packet) == 0 || len(packet) > maxPacketLength {
|
|
continue
|
|
}
|
|
buffer := b.writeBuffers[i][:b.writeHeadroom+len(packet)]
|
|
copy(buffer[b.writeHeadroom:], packet)
|
|
batch = append(batch, buffer)
|
|
}
|
|
if len(batch) == 0 {
|
|
continue
|
|
}
|
|
_, err := b.batchTUN.BatchWrite(batch, b.writeHeadroom)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// BatchRead completes any kernel-deferred checksums while splitting GRO frames
|
|
// (virtio NEEDS_CSUM), so unlike readLoop no checksum fix is needed here.
|
|
func (b *backendLinux) batchReadLoop() {
|
|
defer close(b.readDone)
|
|
batchSize := b.batchTUN.BatchSize()
|
|
sizes := make([]int, batchSize)
|
|
batch := make([][]byte, 0, batchSize)
|
|
headroom := -1
|
|
var buffers [][]byte
|
|
for {
|
|
b.returnAccess.Lock()
|
|
returnPaths := b.returnPaths
|
|
b.returnAccess.Unlock()
|
|
pathHeadroom := 0
|
|
if len(returnPaths) > 0 {
|
|
pathHeadroom = returnPaths[0].ReturnHeadroom()
|
|
}
|
|
if pathHeadroom != headroom {
|
|
headroom = pathHeadroom
|
|
buffers = make([][]byte, batchSize)
|
|
for i := range buffers {
|
|
buffers[i] = make([]byte, headroom+bridgeTunMTU)
|
|
}
|
|
}
|
|
n, err := b.batchTUN.BatchRead(buffers, headroom, sizes)
|
|
if err != nil {
|
|
select {
|
|
case <-b.closed:
|
|
return
|
|
default:
|
|
}
|
|
if E.IsClosed(err) {
|
|
return
|
|
}
|
|
b.logger.Debug(E.Cause(err, "bridge tun read"))
|
|
continue
|
|
}
|
|
if n == 0 || len(returnPaths) == 0 {
|
|
continue
|
|
}
|
|
batch = batch[:0]
|
|
for i := range n {
|
|
if sizes[i] == 0 {
|
|
continue
|
|
}
|
|
batch = append(batch, buffers[i][:headroom+sizes[i]])
|
|
}
|
|
unconsumed := batch
|
|
currentHeadroom := headroom
|
|
for _, returnPath := range returnPaths {
|
|
if len(unconsumed) == 0 {
|
|
break
|
|
}
|
|
nextHeadroom := returnPath.ReturnHeadroom()
|
|
if nextHeadroom != currentHeadroom {
|
|
rebuffered := make([][]byte, 0, len(unconsumed))
|
|
for _, packet := range unconsumed {
|
|
payload := packet[currentHeadroom:]
|
|
buffer := make([]byte, nextHeadroom+len(payload))
|
|
copy(buffer[nextHeadroom:], payload)
|
|
rebuffered = append(rebuffered, buffer)
|
|
}
|
|
unconsumed = rebuffered
|
|
currentHeadroom = nextHeadroom
|
|
}
|
|
unconsumed = returnPath.ReturnPackets(unconsumed)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The policy rules default to priority 100/101, ahead of sing-tun auto_route's rules,
|
|
// so forwarded packets egress the physical interface instead of looping back into
|
|
// a tun.
|
|
func (b *backendLinux) syncEgress() {
|
|
b.egressAccess.Lock()
|
|
defer b.egressAccess.Unlock()
|
|
select {
|
|
case <-b.closed:
|
|
return
|
|
default:
|
|
}
|
|
b.updateClampLocked()
|
|
flushBridgeRouteTable(b.routeTable)
|
|
link, err := netlink.LinkByName(b.boundInterface)
|
|
if err != nil {
|
|
for _, family := range activeBridgeFamilies(b.inet6Port) {
|
|
blackholeBridgeDefault(b.routeTable, family)
|
|
}
|
|
b.logger.Debug("pinned egress ", b.boundInterface, " absent, dropping forwarded traffic")
|
|
return
|
|
}
|
|
for _, family := range activeBridgeFamilies(b.inet6Port) {
|
|
b.syncEgressFamily(family, link.Attrs().Index)
|
|
}
|
|
}
|
|
|
|
func (b *backendLinux) syncEgressFamily(family int, linkIndex int) {
|
|
connected, err := netlink.RouteListFiltered(family, &netlink.Route{
|
|
LinkIndex: linkIndex,
|
|
Table: unix.RT_TABLE_MAIN,
|
|
}, netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE)
|
|
if err == nil {
|
|
for _, route := range connected {
|
|
if route.Gw != nil || route.Dst == nil {
|
|
continue
|
|
}
|
|
pinned := route
|
|
pinned.Table = b.routeTable
|
|
pinned.ILinkIndex = 0
|
|
_ = netlink.RouteReplace(&pinned)
|
|
}
|
|
}
|
|
resolved, err := netlink.RouteGetWithOptions(probeAddress(family), &netlink.RouteGetOptions{Oif: b.boundInterface})
|
|
if err == nil && len(resolved) > 0 {
|
|
defaultRoute := &netlink.Route{
|
|
LinkIndex: linkIndex,
|
|
Table: b.routeTable,
|
|
Dst: defaultDestination(family),
|
|
}
|
|
if len(resolved[0].Gw) > 0 {
|
|
defaultRoute.Gw = resolved[0].Gw
|
|
}
|
|
err = netlink.RouteReplace(defaultRoute)
|
|
if err == nil {
|
|
return
|
|
}
|
|
}
|
|
blackholeBridgeDefault(b.routeTable, family)
|
|
}
|
|
|
|
func (b *backendLinux) updateClamp() {
|
|
b.egressAccess.Lock()
|
|
defer b.egressAccess.Unlock()
|
|
select {
|
|
case <-b.closed:
|
|
return
|
|
default:
|
|
}
|
|
b.updateClampLocked()
|
|
}
|
|
|
|
func (b *backendLinux) updateClampLocked() {
|
|
mtu := bridgeTunMTU
|
|
egress := b.resolveEgress()
|
|
if egress != "" {
|
|
mtu = b.egressMTU(egress)
|
|
}
|
|
if mtu == b.clampMTU {
|
|
return
|
|
}
|
|
err := setupBridgeClamp(b.nftTableName, b.tunName, b.inet4Port, b.inet6Port, mtu)
|
|
if err != nil {
|
|
b.logger.Debug(E.Cause(err, "update bridge MSS clamp"))
|
|
return
|
|
}
|
|
b.clampMTU = mtu
|
|
}
|
|
|
|
func (b *backendLinux) egressMTU(egress string) int {
|
|
iface, err := b.networkManager.InterfaceFinder().ByName(egress)
|
|
if err != nil || iface.MTU < 576 || iface.MTU > bridgeTunMTU {
|
|
return bridgeTunMTU
|
|
}
|
|
return iface.MTU
|
|
}
|