Add L3 forwarding support

This commit is contained in:
世界
2026-07-06 14:26:32 +08:00
parent 585d3e639e
commit e6419b945f
24 changed files with 763 additions and 646 deletions
+146 -138
View File
@@ -9,12 +9,11 @@ import (
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/sniff"
C "github.com/sagernet/sing-box/constant"
R "github.com/sagernet/sing-box/route/rule"
"github.com/sagernet/sing-mux"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/ping"
"github.com/sagernet/sing-vmess"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
@@ -95,7 +94,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad
if deadline.NeedAdditionalReadDeadline(conn) {
conn = deadline.NewConn(conn)
}
selectedRule, _, buffers, _, err := r.matchRule(ctx, &metadata, false, false, conn, nil)
selectedRule, _, buffers, _, err := r.matchRule(ctx, &metadata, conn, nil)
if err != nil {
return err
}
@@ -226,7 +225,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
if metadata.InboundType == C.TypeTun && metadata.Protocol == C.ProtocolDNS {
return r.hijackDNSPacket(ctx, conn, nil, metadata, onClose)
}
selectedRule, _, _, packetBuffers, err := r.matchRule(ctx, &metadata, false, false, nil, conn)
selectedRule, _, _, packetBuffers, err := r.matchRule(ctx, &metadata, nil, conn)
if err != nil {
return err
}
@@ -295,119 +294,157 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m
return nil
}
func (r *Router) PreMatch(metadata adapter.InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration, supportBypass bool) (tun.DirectRouteDestination, error) {
selectedRule, _, _, _, err := r.matchRule(r.ctx, &metadata, true, supportBypass, nil, nil)
if err != nil {
return nil, err
func (r *Router) PreMatch(metadata adapter.InboundContext) adapter.PreMatchResult {
continueResult := adapter.PreMatchResult{Action: adapter.PreMatchContinue}
packetDestination := metadata.Destination
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)
if !loaded || domain == "" {
return continueResult
}
metadata.OriginDestination = metadata.Destination
metadata.Destination = M.Socksaddr{
Fqdn: domain,
Port: metadata.Destination.Port,
}
metadata.FakeIP = true
}
var directRouteOutbound adapter.DirectRouteOutbound
if selectedRule != nil {
switch action := selectedRule.Action().(type) {
case *R.RuleActionReject:
switch metadata.Network {
case N.NetworkTCP:
if action.Method == C.RuleActionRejectMethodReply {
return nil, E.New("reject method `reply` is not supported for TCP connections")
}
case N.NetworkUDP:
if action.Method == C.RuleActionRejectMethodReply {
return nil, E.New("reject method `reply` is not supported for UDP connections")
}
if metadata.Destination.IsIPv4() {
metadata.IPVersion = 4
} else if metadata.Destination.IsIPv6() {
metadata.IPVersion = 6
}
for currentRuleIndex, currentRule := range r.rules {
metadata.ResetRuleCache()
if !currentRule.Match(&metadata) {
continue
}
switch action := currentRule.Action().(type) {
case *R.RuleActionSniff:
if metadata.Network == N.NetworkICMP {
continue
}
return nil, action.Error(context.Background())
case *R.RuleActionBypass:
if supportBypass {
return nil, &R.BypassedError{Cause: tun.ErrBypass}
}
if routeContext == nil {
return nil, nil
}
outbound, loaded := r.outbound.Outbound(action.Outbound)
if !loaded {
return nil, E.New("outbound not found: ", action.Outbound)
}
if !common.Contains(outbound.Network(), metadata.Network) {
return nil, E.New(metadata.Network, " is not supported by outbound: ", action.Outbound)
}
directRouteOutbound = outbound.(adapter.DirectRouteOutbound)
return continueResult
case *R.RuleActionRouteOptions:
applyRouteOptionsOverride(&metadata, action)
case *R.RuleActionRoute:
if routeContext == nil {
return nil, nil
applyRouteOptionsOverride(&metadata, &action.RuleActionRouteOptions)
r.logger.Debug("pre-match[", currentRuleIndex, "] ", currentRule, " => ", action)
return r.preMatchFlow(&metadata, packetDestination, action.Outbound)
case *R.RuleActionBypass:
applyRouteOptionsOverride(&metadata, &action.RuleActionRouteOptions)
r.logger.Debug("pre-match[", currentRuleIndex, "] ", currentRule, " => ", action)
if action.Outbound == "" {
if metadata.Destination.IsDomain() || metadata.Destination != packetDestination {
return continueResult
}
return adapter.PreMatchResult{Action: adapter.PreMatchBypass}
}
outbound, loaded := r.outbound.Outbound(action.Outbound)
if !loaded {
return nil, E.New("outbound not found: ", action.Outbound)
return r.preMatchFlow(&metadata, packetDestination, action.Outbound)
case *R.RuleActionReject:
r.logger.Debug("pre-match[", currentRuleIndex, "] ", currentRule, " => ", action)
rejectErr := action.Error(r.ctx)
if errors.Is(rejectErr, R.ErrDrop) {
return adapter.PreMatchResult{Action: adapter.PreMatchDrop}
}
if !common.Contains(outbound.Network(), metadata.Network) {
return nil, E.New(metadata.Network, " is not supported by outbound: ", action.Outbound)
return adapter.PreMatchResult{Action: adapter.PreMatchReject}
case *R.RuleActionResolve:
resolveErr := r.actionResolve(adapter.WithContext(r.ctx, &metadata), &metadata, action)
if resolveErr != nil {
r.logger.Debug("pre-match[", currentRuleIndex, "] ", currentRule, " => ", action, ": ", resolveErr)
return adapter.PreMatchResult{Action: adapter.PreMatchReject}
}
directRouteOutbound = outbound.(adapter.DirectRouteOutbound)
default:
return continueResult
}
}
if directRouteOutbound == nil {
if selectedRule != nil || metadata.Network != N.NetworkICMP {
return nil, nil
return r.preMatchFlow(&metadata, packetDestination, "")
}
func applyRouteOptionsOverride(metadata *adapter.InboundContext, routeOptions *R.RuleActionRouteOptions) {
if routeOptions.OverrideAddress.IsValid() {
metadata.Destination = M.Socksaddr{
Addr: routeOptions.OverrideAddress.Addr,
Port: metadata.Destination.Port,
Fqdn: routeOptions.OverrideAddress.Fqdn,
}
defaultOutbound := r.outbound.Default()
if !common.Contains(defaultOutbound.Network(), metadata.Network) {
return nil, E.New(metadata.Network, " is not supported by default outbound: ", defaultOutbound.Tag())
}
directRouteOutbound = defaultOutbound.(adapter.DirectRouteOutbound)
}
if routeOptions.OverridePort > 0 {
metadata.Destination = M.Socksaddr{
Addr: metadata.Destination.Addr,
Port: routeOptions.OverridePort,
Fqdn: metadata.Destination.Fqdn,
}
}
}
func (r *Router) preMatchFlow(metadata *adapter.InboundContext, packetDestination M.Socksaddr, outboundTag string) adapter.PreMatchResult {
continueResult := adapter.PreMatchResult{Action: adapter.PreMatchContinue}
var outbound adapter.Outbound
if outboundTag == "" {
outbound = r.outbound.Default()
} else {
var loaded bool
outbound, loaded = r.outbound.Outbound(outboundTag)
if !loaded {
return continueResult
}
}
for range 8 {
group, isGroup := outbound.(adapter.OutboundGroup)
if !isGroup {
break
}
selectedOutbound, selectedLoaded := r.outbound.Outbound(group.Now())
if !selectedLoaded {
return continueResult
}
outbound = selectedOutbound
}
if !common.Contains(outbound.Network(), metadata.Network) {
return continueResult
}
flowOutbound, isFlowOutbound := outbound.(adapter.FlowOutbound)
if !isFlowOutbound || !flowOutbound.SupportsFlow(metadata.Network) {
if outbound.Type() == C.TypeDirect {
directDialer, isDirectDialer := outbound.(dialer.DirectDialer)
if isDirectDialer && directDialer.IsEmpty() && !metadata.Destination.IsDomain() && metadata.Destination == packetDestination {
r.logger.Debug("pre-match bypass ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString())
return adapter.PreMatchResult{Action: adapter.PreMatchBypass, Outbound: outbound}
}
}
return continueResult
}
result := adapter.PreMatchResult{Action: adapter.PreMatchFlow, Outbound: outbound}
if metadata.Destination.IsDomain() {
if len(metadata.DestinationAddresses) == 0 {
var strategy C.DomainStrategy
if metadata.Source.IsIPv4() {
strategy = C.DomainStrategyIPv4Only
} else {
strategy = C.DomainStrategyIPv6Only
}
err = r.actionResolve(r.ctx, &metadata, &R.RuleActionResolve{
Strategy: strategy,
})
if err != nil {
return nil, err
}
if !metadata.FakeIP {
return continueResult
}
var newDestination netip.Addr
if metadata.Source.IsIPv4() {
for _, address := range metadata.DestinationAddresses {
if address.Is4() {
newDestination = address
break
}
}
} else {
for _, address := range metadata.DestinationAddresses {
if address.Is6() {
newDestination = address
break
}
for _, address := range metadata.DestinationAddresses {
if address.Is4() == packetDestination.IsIPv4() {
newDestination = address
break
}
}
if !newDestination.IsValid() {
if metadata.Source.IsIPv4() {
return nil, E.New("no IPv4 address found for domain: ", metadata.Destination.Fqdn)
if len(metadata.DestinationAddresses) == 0 {
r.logger.Warn("pre-match: reject ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to fake destination ", metadata.Destination.Fqdn, ": a resolve action is required before routing to outbound/", outbound.Type(), "[", outbound.Tag(), "]")
} else {
return nil, E.New("no IPv6 address found for domain: ", metadata.Destination.Fqdn)
r.logger.Debug("pre-match: reject ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to fake destination ", metadata.Destination.Fqdn, ": no resolved address for this address family")
}
return adapter.PreMatchResult{Action: adapter.PreMatchReject}
}
metadata.Destination = M.Socksaddr{
Addr: newDestination,
}
routeContext = ping.NewContextDestinationWriter(routeContext, metadata.OriginDestination.Addr)
var routeDestination tun.DirectRouteDestination
routeDestination, err = directRouteOutbound.NewDirectRouteConnection(metadata, routeContext, timeout)
if err != nil {
return nil, err
}
return ping.NewDestinationWriter(routeDestination, newDestination), nil
result.Destination = netip.AddrPortFrom(newDestination, metadata.Destination.Port)
} else if metadata.Destination != packetDestination {
result.Destination = metadata.Destination.AddrPort()
}
return directRouteOutbound.NewDirectRouteConnection(metadata, routeContext, timeout)
r.logger.Debug("pre-match forward ", metadata.Network, " connection from ", metadata.Source.AddrString(), " to ", metadata.Destination.AddrString(), " via outbound/", outbound.Type(), "[", outbound.Tag(), "]")
return result
}
func (r *Router) matchRule(
ctx context.Context, metadata *adapter.InboundContext, preMatch bool, supportBypass bool,
ctx context.Context, metadata *adapter.InboundContext,
inputConn net.Conn, inputPacketConn N.PacketConn,
) (
selectedRule adapter.Rule, selectedRuleIndex int,
@@ -465,23 +502,11 @@ match:
if !currentRule.Match(metadata) {
continue
}
if !preMatch {
ruleDescription := currentRule.String()
if ruleDescription != "" {
r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action())
} else {
r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] => ", currentRule.Action())
}
ruleDescription := currentRule.String()
if ruleDescription != "" {
r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action())
} else {
switch currentRule.Action().Type() {
case C.RuleActionTypeReject:
ruleDescription := currentRule.String()
if ruleDescription != "" {
r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action())
} else {
r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] => ", currentRule.Action())
}
}
r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] => ", currentRule.Action())
}
var routeOptions *R.RuleActionRouteOptions
switch action := currentRule.Action().(type) {
@@ -500,20 +525,9 @@ match:
metadata.RouteOriginalDestination = metadata.Destination
}
if routeOptions.OverrideAddress.IsValid() {
metadata.Destination = M.Socksaddr{
Addr: routeOptions.OverrideAddress.Addr,
Port: metadata.Destination.Port,
Fqdn: routeOptions.OverrideAddress.Fqdn,
}
metadata.DestinationAddresses = nil
}
if routeOptions.OverridePort > 0 {
metadata.Destination = M.Socksaddr{
Addr: metadata.Destination.Addr,
Port: routeOptions.OverridePort,
Fqdn: metadata.Destination.Fqdn,
}
}
applyRouteOptionsOverride(metadata, routeOptions)
if routeOptions.NetworkStrategy != nil {
metadata.NetworkStrategy = routeOptions.NetworkStrategy
}
@@ -549,21 +563,15 @@ match:
}
switch action := currentRule.Action().(type) {
case *R.RuleActionSniff:
if !preMatch {
newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn, buffers, packetBuffers)
if newBuffer != nil {
buffers = append(buffers, newBuffer)
} else if len(newPacketBuffers) > 0 {
packetBuffers = append(packetBuffers, newPacketBuffers...)
}
if newErr != nil {
fatalErr = newErr
return
}
} else if metadata.Network != N.NetworkICMP {
selectedRule = currentRule
selectedRuleIndex = currentRuleIndex
break match
newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn, buffers, packetBuffers)
if newBuffer != nil {
buffers = append(buffers, newBuffer)
} else if len(newPacketBuffers) > 0 {
packetBuffers = append(packetBuffers, newPacketBuffers...)
}
if newErr != nil {
fatalErr = newErr
return
}
case *R.RuleActionResolve:
fatalErr = r.actionResolve(ctx, metadata, action)
@@ -581,7 +589,7 @@ match:
}
if actionType == C.RuleActionTypeBypass {
bypassAction := currentRule.Action().(*R.RuleActionBypass)
if !supportBypass && bypassAction.Outbound == "" {
if bypassAction.Outbound == "" {
continue match
}
selectedRule = currentRule
+8 -4
View File
@@ -14,7 +14,6 @@ import (
"github.com/sagernet/sing-box/common/tlsspoof"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
@@ -388,6 +387,11 @@ func (r *RuleActionDirect) String() string {
return "direct" + r.description
}
var (
ErrReset = E.New("connection reset")
ErrDrop = E.New("packet dropped")
)
type RejectedError struct {
Cause error
}
@@ -445,9 +449,9 @@ func (r *RuleActionReject) Error(ctx context.Context) error {
var returnErr error
switch r.Method {
case C.RuleActionRejectMethodDefault:
returnErr = &RejectedError{tun.ErrReset}
returnErr = &RejectedError{ErrReset}
case C.RuleActionRejectMethodDrop:
return &RejectedError{tun.ErrDrop}
return &RejectedError{ErrDrop}
case C.RuleActionRejectMethodReply:
return nil
default:
@@ -467,7 +471,7 @@ func (r *RuleActionReject) Error(ctx context.Context) error {
if ctx != nil {
r.logger.DebugContext(ctx, "dropped due to flooding")
}
return &RejectedError{tun.ErrDrop}
return &RejectedError{ErrDrop}
}
return returnErr
}
+2 -4
View File
@@ -5,10 +5,8 @@ import (
"net"
"sync/atomic"
"testing"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-tun"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/x/list"
@@ -22,8 +20,8 @@ type ruleSetItemTestRouter struct {
func (r *ruleSetItemTestRouter) Start(adapter.StartStage) error { return nil }
func (r *ruleSetItemTestRouter) Close() error { return nil }
func (r *ruleSetItemTestRouter) PreMatch(adapter.InboundContext, tun.DirectRouteContext, time.Duration, bool) (tun.DirectRouteDestination, error) {
return nil, nil
func (r *ruleSetItemTestRouter) PreMatch(adapter.InboundContext) adapter.PreMatchResult {
return adapter.PreMatchResult{}
}
func (r *ruleSetItemTestRouter) RouteConnection(context.Context, net.Conn, adapter.InboundContext) error {