Add L3 forwarding support
This commit is contained in:
+146
-138
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user