Add bridge outbound
This commit is contained in:
@@ -0,0 +1,461 @@
|
||||
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
|
||||
bridgeWriteBatchSize = 32
|
||||
)
|
||||
|
||||
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,
|
||||
AutoRoute: false,
|
||||
InterfaceMonitor: b.networkManager.InterfaceMonitor(),
|
||||
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 {
|
||||
// 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,
|
||||
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")
|
||||
}
|
||||
b.closed = make(chan struct{})
|
||||
b.readDone = make(chan struct{})
|
||||
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
|
||||
}
|
||||
|
||||
// Zero tells the dispatcher not to clamp the TCP MSS or fragment; the host kernel
|
||||
// does both on the forwarding path instead (see setupBridgeClampRules).
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user