sing-box/protocol/bridge/backend_linux.go
2026-08-04 21:50:10 +08:00

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
}