From 96a8de95483db0e34b513a39786941a106b074ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 21 Oct 2024 23:38:34 +0800 Subject: [PATCH 01/49] Crazy sekai overturns the small pond --- adapter/conn_router.go | 104 ---- adapter/handler.go | 26 + adapter/inbound.go | 35 +- adapter/outbound.go | 5 - adapter/router.go | 35 +- adapter/rule.go | 38 ++ adapter/upstream.go | 177 ++++-- adapter/upstream_legacy.go | 216 +++++++ adapter/v2ray.go | 4 +- cmd/sing-box/cmd_rule_set_match.go | 4 +- .../convertor/adguard/convertor_test.go | 8 +- common/mux/router.go | 17 +- common/mux/v2ray_legacy.go | 32 - common/sniff/sniff.go | 2 +- common/uot/router.go | 39 +- constant/rule.go | 15 + experimental/clashapi/rules.go | 3 +- .../clashapi/trafficontrol/tracker.go | 35 +- go.mod | 12 +- go.sum | 24 +- inbound/default.go | 51 +- inbound/default_tcp.go | 18 +- inbound/default_udp.go | 87 +-- inbound/direct.go | 49 +- inbound/http.go | 39 +- inbound/mixed.go | 26 +- inbound/naive.go | 26 +- inbound/redirect.go | 9 +- inbound/shadowsocks.go | 39 +- inbound/shadowsocks_multi.go | 33 +- inbound/shadowsocks_relay.go | 31 +- inbound/shadowtls.go | 9 + inbound/socks.go | 19 +- inbound/tproxy.go | 43 +- inbound/trojan.go | 26 +- inbound/tun.go | 88 +-- inbound/vless.go | 26 +- inbound/vmess.go | 26 +- include/quic_stub.go | 3 +- option/inbound.go | 1 + option/rule.go | 48 +- option/rule_action.go | 166 +++++ option/rule_dns.go | 57 +- option/types.go | 26 +- outbound/block.go | 2 + outbound/default.go | 10 +- outbound/direct.go | 14 +- outbound/dns.go | 2 + outbound/http.go | 8 - outbound/hysteria.go | 8 - outbound/hysteria2.go | 8 - outbound/proxy.go | 3 + outbound/selector.go | 16 +- outbound/shadowsocks.go | 8 - outbound/shadowtls.go | 8 - outbound/socks.go | 4 + outbound/ssh.go | 8 - outbound/tor.go | 8 - outbound/trojan.go | 8 - outbound/tuic.go | 8 - outbound/urltest.go | 4 + outbound/vless.go | 8 - outbound/vmess.go | 8 - outbound/wireguard.go | 4 + ...uter_geo_resources.go => geo_resources.go} | 3 +- route/route.go | 583 ++++++++++++++++++ route/{router_dns.go => route_dns.go} | 105 ++-- route/router.go | 394 +----------- route/{ => rule}/rule_abstract.go | 20 +- route/rule/rule_action.go | 228 +++++++ route/{ => rule}/rule_default.go | 48 +- route/{ => rule}/rule_dns.go | 63 +- route/{ => rule}/rule_headless.go | 2 +- route/{ => rule}/rule_item_adguard.go | 2 +- route/{ => rule}/rule_item_auth_user.go | 2 +- route/{ => rule}/rule_item_cidr.go | 2 +- route/{ => rule}/rule_item_clash_mode.go | 2 +- route/{ => rule}/rule_item_client.go | 2 +- route/{ => rule}/rule_item_domain.go | 2 +- route/{ => rule}/rule_item_domain_keyword.go | 2 +- route/{ => rule}/rule_item_domain_regex.go | 2 +- route/{ => rule}/rule_item_geoip.go | 2 +- route/{ => rule}/rule_item_geosite.go | 2 +- route/{ => rule}/rule_item_inbound.go | 2 +- route/{ => rule}/rule_item_ip_is_private.go | 2 +- route/{ => rule}/rule_item_ipversion.go | 2 +- route/{ => rule}/rule_item_network.go | 2 +- route/{ => rule}/rule_item_outbound.go | 2 +- route/{ => rule}/rule_item_package_name.go | 2 +- route/{ => rule}/rule_item_port.go | 2 +- route/{ => rule}/rule_item_port_range.go | 2 +- route/{ => rule}/rule_item_process_name.go | 2 +- route/{ => rule}/rule_item_process_path.go | 2 +- .../rule_item_process_path_regex.go | 2 +- route/{ => rule}/rule_item_protocol.go | 2 +- route/{ => rule}/rule_item_query_type.go | 2 +- route/{ => rule}/rule_item_rule_set.go | 2 +- route/{ => rule}/rule_item_user.go | 2 +- route/{ => rule}/rule_item_user_id.go | 2 +- route/{ => rule}/rule_item_wifi_bssid.go | 2 +- route/{ => rule}/rule_item_wifi_ssid.go | 2 +- route/{ => rule}/rule_set.go | 30 +- route/{ => rule}/rule_set_local.go | 2 +- route/{ => rule}/rule_set_remote.go | 2 +- route/{router_rule.go => rule_conds.go} | 24 - test/brutal_test.go | 52 +- test/clash_test.go | 12 +- test/direct_test.go | 13 +- test/docker_test.go | 9 +- test/domain_inbound_test.go | 13 +- test/ech_test.go | 38 +- test/go.mod | 87 +-- test/go.sum | 202 +++--- test/http_test.go | 13 +- test/hysteria2_test.go | 12 +- test/hysteria_test.go | 13 +- test/inbound_detour_test.go | 13 +- test/mux_cool_test.go | 13 +- test/mux_test.go | 26 +- test/shadowsocks_test.go | 39 +- test/shadowtls_test.go | 63 +- test/tfo_test.go | 13 +- test/tls_test.go | 13 +- test/trojan_test.go | 26 +- test/tuic_test.go | 13 +- test/v2ray_transport_test.go | 52 +- test/vmess_test.go | 12 +- transport/v2ray/grpc.go | 7 +- transport/v2ray/grpc_lite.go | 5 +- transport/v2ray/quic.go | 5 +- transport/v2ray/transport.go | 15 +- transport/v2raygrpc/client.go | 2 +- transport/v2raygrpc/conn.go | 10 +- transport/v2raygrpc/server.go | 25 +- transport/v2raygrpclite/server.go | 32 +- transport/v2rayhttp/server.go | 21 +- transport/v2rayhttpupgrade/server.go | 14 +- transport/v2rayquic/server.go | 12 +- transport/v2raywebsocket/server.go | 18 +- 139 files changed, 2854 insertions(+), 1543 deletions(-) delete mode 100644 adapter/conn_router.go create mode 100644 adapter/rule.go create mode 100644 adapter/upstream_legacy.go delete mode 100644 common/mux/v2ray_legacy.go create mode 100644 option/rule_action.go rename route/{router_geo_resources.go => geo_resources.go} (98%) create mode 100644 route/route.go rename route/{router_dns.go => route_dns.go} (75%) rename route/{ => rule}/rule_abstract.go (94%) create mode 100644 route/rule/rule_action.go rename route/{ => rule}/rule_default.go (88%) rename route/{ => rule}/rule_dns.go (89%) rename route/{ => rule}/rule_headless.go (99%) rename route/{ => rule}/rule_item_adguard.go (98%) rename route/{ => rule}/rule_item_auth_user.go (98%) rename route/{ => rule}/rule_item_cidr.go (99%) rename route/{ => rule}/rule_item_clash_mode.go (97%) rename route/{ => rule}/rule_item_client.go (98%) rename route/{ => rule}/rule_item_domain.go (99%) rename route/{ => rule}/rule_item_domain_keyword.go (98%) rename route/{ => rule}/rule_item_domain_regex.go (99%) rename route/{ => rule}/rule_item_geoip.go (99%) rename route/{ => rule}/rule_item_geosite.go (98%) rename route/{ => rule}/rule_item_inbound.go (98%) rename route/{ => rule}/rule_item_ip_is_private.go (98%) rename route/{ => rule}/rule_item_ipversion.go (97%) rename route/{ => rule}/rule_item_network.go (98%) rename route/{ => rule}/rule_item_outbound.go (98%) rename route/{ => rule}/rule_item_package_name.go (98%) rename route/{ => rule}/rule_item_port.go (98%) rename route/{ => rule}/rule_item_port_range.go (99%) rename route/{ => rule}/rule_item_process_name.go (98%) rename route/{ => rule}/rule_item_process_path.go (98%) rename route/{ => rule}/rule_item_process_path_regex.go (98%) rename route/{ => rule}/rule_item_protocol.go (98%) rename route/{ => rule}/rule_item_query_type.go (98%) rename route/{ => rule}/rule_item_rule_set.go (99%) rename route/{ => rule}/rule_item_user.go (98%) rename route/{ => rule}/rule_item_user_id.go (98%) rename route/{ => rule}/rule_item_wifi_bssid.go (98%) rename route/{ => rule}/rule_item_wifi_ssid.go (98%) rename route/{ => rule}/rule_set.go (60%) rename route/{ => rule}/rule_set_local.go (99%) rename route/{ => rule}/rule_set_remote.go (99%) rename route/{router_rule.go => rule_conds.go} (79%) diff --git a/adapter/conn_router.go b/adapter/conn_router.go deleted file mode 100644 index a87c45e8c5..0000000000 --- a/adapter/conn_router.go +++ /dev/null @@ -1,104 +0,0 @@ -package adapter - -import ( - "context" - "net" - - "github.com/sagernet/sing/common/logger" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -type ConnectionRouter interface { - RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error - RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error -} - -func NewRouteHandler( - metadata InboundContext, - router ConnectionRouter, - logger logger.ContextLogger, -) UpstreamHandlerAdapter { - return &routeHandlerWrapper{ - metadata: metadata, - router: router, - logger: logger, - } -} - -func NewRouteContextHandler( - router ConnectionRouter, - logger logger.ContextLogger, -) UpstreamHandlerAdapter { - return &routeContextHandlerWrapper{ - router: router, - logger: logger, - } -} - -var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil) - -type routeHandlerWrapper struct { - metadata InboundContext - router ConnectionRouter - logger logger.ContextLogger -} - -func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { - myMetadata := w.metadata - if metadata.Source.IsValid() { - myMetadata.Source = metadata.Source - } - if metadata.Destination.IsValid() { - myMetadata.Destination = metadata.Destination - } - return w.router.RouteConnection(ctx, conn, myMetadata) -} - -func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { - myMetadata := w.metadata - if metadata.Source.IsValid() { - myMetadata.Source = metadata.Source - } - if metadata.Destination.IsValid() { - myMetadata.Destination = metadata.Destination - } - return w.router.RoutePacketConnection(ctx, conn, myMetadata) -} - -func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) { - w.logger.ErrorContext(ctx, err) -} - -var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil) - -type routeContextHandlerWrapper struct { - router ConnectionRouter - logger logger.ContextLogger -} - -func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { - myMetadata := ContextFrom(ctx) - if metadata.Source.IsValid() { - myMetadata.Source = metadata.Source - } - if metadata.Destination.IsValid() { - myMetadata.Destination = metadata.Destination - } - return w.router.RouteConnection(ctx, conn, *myMetadata) -} - -func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { - myMetadata := ContextFrom(ctx) - if metadata.Source.IsValid() { - myMetadata.Source = metadata.Source - } - if metadata.Destination.IsValid() { - myMetadata.Destination = metadata.Destination - } - return w.router.RoutePacketConnection(ctx, conn, *myMetadata) -} - -func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) { - w.logger.ErrorContext(ctx, err) -} diff --git a/adapter/handler.go b/adapter/handler.go index bc5bcfbb02..6da158ab20 100644 --- a/adapter/handler.go +++ b/adapter/handler.go @@ -6,27 +6,53 @@ import ( "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) +// Deprecated type ConnectionHandler interface { NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error } +type ConnectionHandlerEx interface { + NewConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) +} + +// Deprecated: use PacketHandlerEx instead type PacketHandler interface { NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata InboundContext) error } +type PacketHandlerEx interface { + NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) +} + +// Deprecated: use OOBPacketHandlerEx instead type OOBPacketHandler interface { NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata InboundContext) error } +type OOBPacketHandlerEx interface { + NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr) +} + +// Deprecated type PacketConnectionHandler interface { NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error } +type PacketConnectionHandlerEx interface { + NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) +} + type UpstreamHandlerAdapter interface { N.TCPConnectionHandler N.UDPConnectionHandler E.Handler } + +type UpstreamHandlerAdapterEx interface { + N.TCPConnectionHandlerEx + N.UDPConnectionHandlerEx +} diff --git a/adapter/inbound.go b/adapter/inbound.go index 82909d0112..f4d5802f31 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -2,13 +2,11 @@ package adapter import ( "context" - "net" "net/netip" "github.com/sagernet/sing-box/common/process" "github.com/sagernet/sing-box/option" M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" ) type Inbound interface { @@ -17,11 +15,14 @@ type Inbound interface { Tag() string } -type InjectableInbound interface { +type TCPInjectableInbound interface { Inbound - Network() []string - NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error - NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error + ConnectionHandlerEx +} + +type UDPInjectableInbound interface { + Inbound + PacketConnectionHandlerEx } type InboundContext struct { @@ -43,16 +44,18 @@ type InboundContext struct { // cache - InboundDetour string - LastInbound string - OriginDestination M.Socksaddr - InboundOptions option.InboundOptions - DestinationAddresses []netip.Addr - SourceGeoIPCode string - GeoIPCode string - ProcessInfo *process.Info - QueryType uint16 - FakeIP bool + InboundDetour string + LastInbound string + OriginDestination M.Socksaddr + // Deprecated + InboundOptions option.InboundOptions + UDPDisableDomainUnmapping bool + DestinationAddresses []netip.Addr + SourceGeoIPCode string + GeoIPCode string + ProcessInfo *process.Info + QueryType uint16 + FakeIP bool // rule cache diff --git a/adapter/outbound.go b/adapter/outbound.go index b6980fb97c..312cdf3ed0 100644 --- a/adapter/outbound.go +++ b/adapter/outbound.go @@ -1,9 +1,6 @@ package adapter import ( - "context" - "net" - N "github.com/sagernet/sing/common/network" ) @@ -15,6 +12,4 @@ type Outbound interface { Network() []string Dependencies() []string N.Dialer - NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error - NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error } diff --git a/adapter/router.go b/adapter/router.go index 5dc4de538a..134c944208 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -34,6 +34,7 @@ type Router interface { FakeIPStore() FakeIPStore ConnectionRouter + ConnectionRouterEx GeoIPReader() *geoip.Reader LoadGeosite(code string) (Rule, error) @@ -70,34 +71,24 @@ type Router interface { ResetNetwork() error } -func ContextWithRouter(ctx context.Context, router Router) context.Context { - return service.ContextWith(ctx, router) +// Deprecated: Use ConnectionRouterEx instead. +type ConnectionRouter interface { + RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error + RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error } -func RouterFromContext(ctx context.Context) Router { - return service.FromContext[Router](ctx) -} - -type HeadlessRule interface { - Match(metadata *InboundContext) bool - String() string +type ConnectionRouterEx interface { + ConnectionRouter + RouteConnectionEx(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) + RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) } -type Rule interface { - HeadlessRule - Service - Type() string - UpdateGeosite() error - Outbound() string +func ContextWithRouter(ctx context.Context, router Router) context.Context { + return service.ContextWith(ctx, router) } -type DNSRule interface { - Rule - DisableCache() bool - RewriteTTL() *uint32 - ClientSubnet() *netip.Prefix - WithAddressLimit() bool - MatchAddressLimit(metadata *InboundContext) bool +func RouterFromContext(ctx context.Context) Router { + return service.FromContext[Router](ctx) } type RuleSet interface { diff --git a/adapter/rule.go b/adapter/rule.go new file mode 100644 index 0000000000..f3737a25de --- /dev/null +++ b/adapter/rule.go @@ -0,0 +1,38 @@ +package adapter + +import ( + C "github.com/sagernet/sing-box/constant" +) + +type HeadlessRule interface { + Match(metadata *InboundContext) bool + String() string +} + +type Rule interface { + HeadlessRule + Service + Type() string + UpdateGeosite() error + Action() RuleAction +} + +type DNSRule interface { + Rule + WithAddressLimit() bool + MatchAddressLimit(metadata *InboundContext) bool +} + +type RuleAction interface { + Type() string + String() string +} + +func IsFinalAction(action RuleAction) bool { + switch action.Type() { + case C.RuleActionTypeSniff, C.RuleActionTypeResolve: + return false + default: + return true + } +} diff --git a/adapter/upstream.go b/adapter/upstream.go index 95dd98ea1b..8b0d7c035b 100644 --- a/adapter/upstream.go +++ b/adapter/upstream.go @@ -4,112 +4,165 @@ import ( "context" "net" - E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) type ( - ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error - PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error + ConnectionHandlerFuncEx = func(ctx context.Context, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) + PacketConnectionHandlerFuncEx = func(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) ) -func NewUpstreamHandler( +func NewUpstreamHandlerEx( metadata InboundContext, - connectionHandler ConnectionHandlerFunc, - packetHandler PacketConnectionHandlerFunc, - errorHandler E.Handler, -) UpstreamHandlerAdapter { - return &myUpstreamHandlerWrapper{ + connectionHandler ConnectionHandlerFuncEx, + packetHandler PacketConnectionHandlerFuncEx, +) UpstreamHandlerAdapterEx { + return &myUpstreamHandlerWrapperEx{ metadata: metadata, connectionHandler: connectionHandler, packetHandler: packetHandler, - errorHandler: errorHandler, } } -var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil) +var _ UpstreamHandlerAdapterEx = (*myUpstreamHandlerWrapperEx)(nil) -type myUpstreamHandlerWrapper struct { +type myUpstreamHandlerWrapperEx struct { metadata InboundContext - connectionHandler ConnectionHandlerFunc - packetHandler PacketConnectionHandlerFunc - errorHandler E.Handler + connectionHandler ConnectionHandlerFuncEx + packetHandler PacketConnectionHandlerFuncEx } -func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { +func (w *myUpstreamHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { myMetadata := w.metadata - if metadata.Source.IsValid() { - myMetadata.Source = metadata.Source + if source.IsValid() { + myMetadata.Source = source } - if metadata.Destination.IsValid() { - myMetadata.Destination = metadata.Destination + if destination.IsValid() { + myMetadata.Destination = destination } - return w.connectionHandler(ctx, conn, myMetadata) + w.connectionHandler(ctx, conn, myMetadata, onClose) } -func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { +func (w *myUpstreamHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { myMetadata := w.metadata - if metadata.Source.IsValid() { - myMetadata.Source = metadata.Source + if source.IsValid() { + myMetadata.Source = source } - if metadata.Destination.IsValid() { - myMetadata.Destination = metadata.Destination + if destination.IsValid() { + myMetadata.Destination = destination } - return w.packetHandler(ctx, conn, myMetadata) + w.packetHandler(ctx, conn, myMetadata, onClose) } -func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) { - w.errorHandler.NewError(ctx, err) -} - -func UpstreamMetadata(metadata InboundContext) M.Metadata { - return M.Metadata{ - Source: metadata.Source, - Destination: metadata.Destination, - } -} +var _ UpstreamHandlerAdapterEx = (*myUpstreamContextHandlerWrapperEx)(nil) -type myUpstreamContextHandlerWrapper struct { - connectionHandler ConnectionHandlerFunc - packetHandler PacketConnectionHandlerFunc - errorHandler E.Handler +type myUpstreamContextHandlerWrapperEx struct { + connectionHandler ConnectionHandlerFuncEx + packetHandler PacketConnectionHandlerFuncEx } -func NewUpstreamContextHandler( - connectionHandler ConnectionHandlerFunc, - packetHandler PacketConnectionHandlerFunc, - errorHandler E.Handler, -) UpstreamHandlerAdapter { - return &myUpstreamContextHandlerWrapper{ +func NewUpstreamContextHandlerEx( + connectionHandler ConnectionHandlerFuncEx, + packetHandler PacketConnectionHandlerFuncEx, +) UpstreamHandlerAdapterEx { + return &myUpstreamContextHandlerWrapperEx{ connectionHandler: connectionHandler, packetHandler: packetHandler, - errorHandler: errorHandler, } } -func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { +func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { myMetadata := ContextFrom(ctx) - if metadata.Source.IsValid() { - myMetadata.Source = metadata.Source + if source.IsValid() { + myMetadata.Source = source } - if metadata.Destination.IsValid() { - myMetadata.Destination = metadata.Destination + if destination.IsValid() { + myMetadata.Destination = destination } - return w.connectionHandler(ctx, conn, *myMetadata) + w.connectionHandler(ctx, conn, *myMetadata, onClose) } -func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { +func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { myMetadata := ContextFrom(ctx) - if metadata.Source.IsValid() { - myMetadata.Source = metadata.Source + if source.IsValid() { + myMetadata.Source = source + } + if destination.IsValid() { + myMetadata.Destination = destination + } + w.packetHandler(ctx, conn, *myMetadata, onClose) +} + +func NewRouteHandlerEx( + metadata InboundContext, + router ConnectionRouterEx, +) UpstreamHandlerAdapterEx { + return &routeHandlerWrapperEx{ + metadata: metadata, + router: router, + } +} + +var _ UpstreamHandlerAdapterEx = (*routeHandlerWrapperEx)(nil) + +type routeHandlerWrapperEx struct { + metadata InboundContext + router ConnectionRouterEx +} + +func (r *routeHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + if source.IsValid() { + r.metadata.Source = source } - if metadata.Destination.IsValid() { - myMetadata.Destination = metadata.Destination + if destination.IsValid() { + r.metadata.Destination = destination } - return w.packetHandler(ctx, conn, *myMetadata) + r.router.RouteConnectionEx(ctx, conn, r.metadata, onClose) } -func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) { - w.errorHandler.NewError(ctx, err) +func (r *routeHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + if source.IsValid() { + r.metadata.Source = source + } + if destination.IsValid() { + r.metadata.Destination = destination + } + r.router.RoutePacketConnectionEx(ctx, conn, r.metadata, onClose) +} + +func NewRouteContextHandlerEx( + router ConnectionRouterEx, +) UpstreamHandlerAdapterEx { + return &routeContextHandlerWrapperEx{ + router: router, + } +} + +var _ UpstreamHandlerAdapterEx = (*routeContextHandlerWrapperEx)(nil) + +type routeContextHandlerWrapperEx struct { + router ConnectionRouterEx +} + +func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + metadata := ContextFrom(ctx) + if source.IsValid() { + metadata.Source = source + } + if destination.IsValid() { + metadata.Destination = destination + } + r.router.RouteConnectionEx(ctx, conn, *metadata, onClose) +} + +func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + metadata := ContextFrom(ctx) + if source.IsValid() { + metadata.Source = source + } + if destination.IsValid() { + metadata.Destination = destination + } + r.router.RoutePacketConnectionEx(ctx, conn, *metadata, onClose) } diff --git a/adapter/upstream_legacy.go b/adapter/upstream_legacy.go new file mode 100644 index 0000000000..1c6c15c017 --- /dev/null +++ b/adapter/upstream_legacy.go @@ -0,0 +1,216 @@ +package adapter + +import ( + "context" + "net" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type ( + // Deprecated + ConnectionHandlerFunc = func(ctx context.Context, conn net.Conn, metadata InboundContext) error + // Deprecated + PacketConnectionHandlerFunc = func(ctx context.Context, conn N.PacketConn, metadata InboundContext) error +) + +// Deprecated +func NewUpstreamHandler( + metadata InboundContext, + connectionHandler ConnectionHandlerFunc, + packetHandler PacketConnectionHandlerFunc, + errorHandler E.Handler, +) UpstreamHandlerAdapter { + return &myUpstreamHandlerWrapper{ + metadata: metadata, + connectionHandler: connectionHandler, + packetHandler: packetHandler, + errorHandler: errorHandler, + } +} + +var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil) + +// Deprecated +type myUpstreamHandlerWrapper struct { + metadata InboundContext + connectionHandler ConnectionHandlerFunc + packetHandler PacketConnectionHandlerFunc + errorHandler E.Handler +} + +func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + myMetadata := w.metadata + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.connectionHandler(ctx, conn, myMetadata) +} + +func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + myMetadata := w.metadata + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.packetHandler(ctx, conn, myMetadata) +} + +func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) { + w.errorHandler.NewError(ctx, err) +} + +// Deprecated +func UpstreamMetadata(metadata InboundContext) M.Metadata { + return M.Metadata{ + Source: metadata.Source, + Destination: metadata.Destination, + } +} + +// Deprecated +type myUpstreamContextHandlerWrapper struct { + connectionHandler ConnectionHandlerFunc + packetHandler PacketConnectionHandlerFunc + errorHandler E.Handler +} + +// Deprecated +func NewUpstreamContextHandler( + connectionHandler ConnectionHandlerFunc, + packetHandler PacketConnectionHandlerFunc, + errorHandler E.Handler, +) UpstreamHandlerAdapter { + return &myUpstreamContextHandlerWrapper{ + connectionHandler: connectionHandler, + packetHandler: packetHandler, + errorHandler: errorHandler, + } +} + +func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + myMetadata := ContextFrom(ctx) + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.connectionHandler(ctx, conn, *myMetadata) +} + +func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + myMetadata := ContextFrom(ctx) + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.packetHandler(ctx, conn, *myMetadata) +} + +func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) { + w.errorHandler.NewError(ctx, err) +} + +// Deprecated: Use ConnectionRouterEx instead. +func NewRouteHandler( + metadata InboundContext, + router ConnectionRouter, + logger logger.ContextLogger, +) UpstreamHandlerAdapter { + return &routeHandlerWrapper{ + metadata: metadata, + router: router, + logger: logger, + } +} + +// Deprecated: Use ConnectionRouterEx instead. +func NewRouteContextHandler( + router ConnectionRouter, + logger logger.ContextLogger, +) UpstreamHandlerAdapter { + return &routeContextHandlerWrapper{ + router: router, + logger: logger, + } +} + +var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil) + +// Deprecated: Use ConnectionRouterEx instead. +type routeHandlerWrapper struct { + metadata InboundContext + router ConnectionRouter + logger logger.ContextLogger +} + +func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + myMetadata := w.metadata + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.router.RouteConnection(ctx, conn, myMetadata) +} + +func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + myMetadata := w.metadata + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.router.RoutePacketConnection(ctx, conn, myMetadata) +} + +func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) { + w.logger.ErrorContext(ctx, err) +} + +var _ UpstreamHandlerAdapter = (*routeContextHandlerWrapper)(nil) + +// Deprecated: Use ConnectionRouterEx instead. +type routeContextHandlerWrapper struct { + router ConnectionRouter + logger logger.ContextLogger +} + +func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { + myMetadata := ContextFrom(ctx) + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.router.RouteConnection(ctx, conn, *myMetadata) +} + +func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { + myMetadata := ContextFrom(ctx) + if metadata.Source.IsValid() { + myMetadata.Source = metadata.Source + } + if metadata.Destination.IsValid() { + myMetadata.Destination = metadata.Destination + } + return w.router.RoutePacketConnection(ctx, conn, *myMetadata) +} + +func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) { + w.logger.ErrorContext(ctx, err) +} diff --git a/adapter/v2ray.go b/adapter/v2ray.go index 5a98d2e55a..d9370807fb 100644 --- a/adapter/v2ray.go +++ b/adapter/v2ray.go @@ -4,7 +4,6 @@ import ( "context" "net" - E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" ) @@ -16,8 +15,7 @@ type V2RayServerTransport interface { } type V2RayServerTransportHandler interface { - N.TCPConnectionHandler - E.Handler + N.TCPConnectionHandlerEx } type V2RayClientTransport interface { diff --git a/cmd/sing-box/cmd_rule_set_match.go b/cmd/sing-box/cmd_rule_set_match.go index 7cded86ecf..b96ff22cf9 100644 --- a/cmd/sing-box/cmd_rule_set_match.go +++ b/cmd/sing-box/cmd_rule_set_match.go @@ -10,7 +10,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing-box/route" + "github.com/sagernet/sing-box/route/rule" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" @@ -83,7 +83,7 @@ func ruleSetMatch(sourcePath string, domain string) error { } for i, ruleOptions := range plainRuleSet.Rules { var currentRule adapter.HeadlessRule - currentRule, err = route.NewHeadlessRule(nil, ruleOptions) + currentRule, err = rule.NewHeadlessRule(nil, ruleOptions) if err != nil { return E.Cause(err, "parse rule_set.rules.[", i, "]") } diff --git a/cmd/sing-box/internal/convertor/adguard/convertor_test.go b/cmd/sing-box/internal/convertor/adguard/convertor_test.go index 7da8a22841..6098485cf0 100644 --- a/cmd/sing-box/internal/convertor/adguard/convertor_test.go +++ b/cmd/sing-box/internal/convertor/adguard/convertor_test.go @@ -5,7 +5,7 @@ import ( "testing" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/route" + "github.com/sagernet/sing-box/route/rule" "github.com/stretchr/testify/require" ) @@ -26,7 +26,7 @@ example.arpa `)) require.NoError(t, err) require.Len(t, rules, 1) - rule, err := route.NewHeadlessRule(nil, rules[0]) + rule, err := rule.NewHeadlessRule(nil, rules[0]) require.NoError(t, err) matchDomain := []string{ "example.org", @@ -85,7 +85,7 @@ func TestHosts(t *testing.T) { `)) require.NoError(t, err) require.Len(t, rules, 1) - rule, err := route.NewHeadlessRule(nil, rules[0]) + rule, err := rule.NewHeadlessRule(nil, rules[0]) require.NoError(t, err) matchDomain := []string{ "google.com", @@ -115,7 +115,7 @@ www.example.org `)) require.NoError(t, err) require.Len(t, rules, 1) - rule, err := route.NewHeadlessRule(nil, rules[0]) + rule, err := rule.NewHeadlessRule(nil, rules[0]) require.NoError(t, err) matchDomain := []string{ "example.com", diff --git a/common/mux/router.go b/common/mux/router.go index 8a2296852b..2d4e7705d9 100644 --- a/common/mux/router.go +++ b/common/mux/router.go @@ -15,11 +15,11 @@ import ( ) type Router struct { - router adapter.ConnectionRouter + router adapter.ConnectionRouterEx service *mux.Service } -func NewRouterWithOptions(router adapter.ConnectionRouter, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouter, error) { +func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.ContextLogger, options option.InboundMultiplexOptions) (adapter.ConnectionRouterEx, error) { if !options.Enabled { return router, nil } @@ -54,6 +54,7 @@ func NewRouterWithOptions(router adapter.ConnectionRouter, logger logger.Context func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if metadata.Destination == mux.Destination { + // TODO: check if WithContext is necessary return r.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata)) } else { return r.router.RouteConnection(ctx, conn, metadata) @@ -63,3 +64,15 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { return r.router.RoutePacketConnection(ctx, conn, metadata) } + +func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + if metadata.Destination == mux.Destination { + r.service.NewConnectionEx(adapter.WithContext(ctx, &metadata), conn, metadata.Source, metadata.Destination, onClose) + return + } + r.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + r.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/common/mux/v2ray_legacy.go b/common/mux/v2ray_legacy.go deleted file mode 100644 index f53aff2db4..0000000000 --- a/common/mux/v2ray_legacy.go +++ /dev/null @@ -1,32 +0,0 @@ -package mux - -import ( - "context" - "net" - - "github.com/sagernet/sing-box/adapter" - vmess "github.com/sagernet/sing-vmess" - "github.com/sagernet/sing/common/logger" - N "github.com/sagernet/sing/common/network" -) - -type V2RayLegacyRouter struct { - router adapter.ConnectionRouter - logger logger.ContextLogger -} - -func NewV2RayLegacyRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) adapter.ConnectionRouter { - return &V2RayLegacyRouter{router, logger} -} - -func (r *V2RayLegacyRouter) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - if metadata.Destination.Fqdn == vmess.MuxDestination.Fqdn { - r.logger.InfoContext(ctx, "inbound legacy multiplex connection") - return vmess.HandleMuxConnection(ctx, conn, adapter.NewRouteHandler(metadata, r.router, r.logger)) - } - return r.router.RouteConnection(ctx, conn, metadata) -} - -func (r *V2RayLegacyRouter) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return r.router.RoutePacketConnection(ctx, conn, metadata) -} diff --git a/common/sniff/sniff.go b/common/sniff/sniff.go index 80bb2984f9..81fc0a2794 100644 --- a/common/sniff/sniff.go +++ b/common/sniff/sniff.go @@ -18,7 +18,7 @@ type ( PacketSniffer = func(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error ) -func Skip(metadata adapter.InboundContext) bool { +func Skip(metadata *adapter.InboundContext) bool { // skip server first protocols switch metadata.Destination.Port { case 25, 465, 587: diff --git a/common/uot/router.go b/common/uot/router.go index fb2d23d557..98c6d608f3 100644 --- a/common/uot/router.go +++ b/common/uot/router.go @@ -13,14 +13,14 @@ import ( "github.com/sagernet/sing/common/uot" ) -var _ adapter.ConnectionRouter = (*Router)(nil) +var _ adapter.ConnectionRouterEx = (*Router)(nil) type Router struct { - router adapter.ConnectionRouter + router adapter.ConnectionRouterEx logger logger.ContextLogger } -func NewRouter(router adapter.ConnectionRouter, logger logger.ContextLogger) *Router { +func NewRouter(router adapter.ConnectionRouterEx, logger logger.ContextLogger) *Router { return &Router{router, logger} } @@ -51,3 +51,36 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { return r.router.RoutePacketConnection(ctx, conn, metadata) } + +func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + switch metadata.Destination.Fqdn { + case uot.MagicAddress: + request, err := uot.ReadRequest(conn) + if err != nil { + err = E.Cause(err, "UoT read request") + r.logger.ErrorContext(ctx, "process connection from ", metadata.Source, ": ", err) + N.CloseOnHandshakeFailure(conn, onClose, err) + return + } + if request.IsConnect { + r.logger.InfoContext(ctx, "inbound UoT connect connection to ", request.Destination) + } else { + r.logger.InfoContext(ctx, "inbound UoT connection to ", request.Destination) + } + metadata.Domain = metadata.Destination.Fqdn + metadata.Destination = request.Destination + r.router.RoutePacketConnectionEx(ctx, uot.NewConn(conn, *request), metadata, onClose) + return + case uot.LegacyMagicAddress: + r.logger.InfoContext(ctx, "inbound legacy UoT connection") + metadata.Domain = metadata.Destination.Fqdn + metadata.Destination = M.Socksaddr{Addr: netip.IPv4Unspecified()} + r.RoutePacketConnectionEx(ctx, uot.NewConn(conn, uot.Request{}), metadata, onClose) + return + } + r.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + r.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/constant/rule.go b/constant/rule.go index 086b95a015..ba74ec63f7 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -23,3 +23,18 @@ const ( RuleSetVersion2 RuleSetVersionCurrent = RuleSetVersion2 ) + +const ( + RuleActionTypeRoute = "route" + RuleActionTypeReturn = "return" + RuleActionTypeReject = "reject" + RuleActionTypeHijackDNS = "hijack-dns" + RuleActionTypeSniff = "sniff" + RuleActionTypeResolve = "resolve" +) + +const ( + RuleActionRejectMethodDefault = "default" + RuleActionRejectMethodPortUnreachable = "port-unreachable" + RuleActionRejectMethodDrop = "drop" +) diff --git a/experimental/clashapi/rules.go b/experimental/clashapi/rules.go index 6ab5dda108..bc8fbb2bba 100644 --- a/experimental/clashapi/rules.go +++ b/experimental/clashapi/rules.go @@ -30,10 +30,9 @@ func getRules(router adapter.Router) func(w http.ResponseWriter, r *http.Request rules = append(rules, Rule{ Type: rule.Type(), Payload: rule.String(), - Proxy: rule.Outbound(), + Proxy: rule.Action().String(), }) } - render.JSON(w, r, render.M{ "rules": rules, }) diff --git a/experimental/clashapi/trafficontrol/tracker.go b/experimental/clashapi/trafficontrol/tracker.go index 73c28e69bd..9c18abebf6 100644 --- a/experimental/clashapi/trafficontrol/tracker.go +++ b/experimental/clashapi/trafficontrol/tracker.go @@ -5,6 +5,7 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + R "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common/bufio" @@ -60,7 +61,7 @@ func (t TrackerMetadata) MarshalJSON() ([]byte, error) { } var rule string if t.Rule != nil { - rule = F.ToString(t.Rule, " => ", t.Rule.Outbound()) + rule = F.ToString(t.Rule, " => ", t.Rule.Action()) } else { rule = "final" } @@ -131,19 +132,21 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundCont outbound string outboundType string ) - if rule == nil { - if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { - next = defaultOutbound.Tag() - } - } else { - next = rule.Outbound() + var action adapter.RuleAction + if rule != nil { + action = rule.Action() + } + if routeAction, isRouteAction := action.(*R.RuleActionRoute); isRouteAction { + next = routeAction.Outbound + } else if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { + next = defaultOutbound.Tag() } for { - chain = append(chain, next) detour, loaded := router.Outbound(next) if !loaded { break } + chain = append(chain, next) outbound = detour.Tag() outboundType = detour.Type() group, isGroup := detour.(adapter.OutboundGroup) @@ -218,19 +221,21 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.Inbound outbound string outboundType string ) - if rule == nil { - if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil { - next = defaultOutbound.Tag() - } - } else { - next = rule.Outbound() + var action adapter.RuleAction + if rule != nil { + action = rule.Action() + } + if routeAction, isRouteAction := action.(*R.RuleActionRoute); isRouteAction { + next = routeAction.Outbound + } else if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil { + next = defaultOutbound.Tag() } for { - chain = append(chain, next) detour, loaded := router.Outbound(next) if !loaded { break } + chain = append(chain, next) outbound = detour.Tag() outboundType = detour.Type() group, isGroup := detour.(adapter.OutboundGroup) diff --git a/go.mod b/go.mod index febd338ce4..da517b17ba 100644 --- a/go.mod +++ b/go.mod @@ -28,14 +28,14 @@ require ( github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff github.com/sagernet/quic-go v0.48.2-beta.1 github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 - github.com/sagernet/sing v0.5.1 - github.com/sagernet/sing-dns v0.3.0 - github.com/sagernet/sing-mux v0.2.1 - github.com/sagernet/sing-quic v0.3.1 + github.com/sagernet/sing v0.6.0-beta.9 + github.com/sagernet/sing-dns v0.4.0-beta.1 + github.com/sagernet/sing-mux v0.3.0-alpha.1 + github.com/sagernet/sing-quic v0.4.0-alpha.4 github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks2 v0.2.0 - github.com/sagernet/sing-shadowtls v0.1.5 - github.com/sagernet/sing-tun v0.4.5 + github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 + github.com/sagernet/sing-tun v0.6.0-beta.7 github.com/sagernet/sing-vmess v0.1.13 github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 github.com/sagernet/utls v1.6.7 diff --git a/go.sum b/go.sum index e4f1b71956..2671007554 100644 --- a/go.sum +++ b/go.sum @@ -124,22 +124,22 @@ github.com/sagernet/quic-go v0.48.2-beta.1/go.mod h1:1WgdDIVD1Gybp40JTWketeSfKA/ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= -github.com/sagernet/sing v0.5.1 h1:mhL/MZVq0TjuvHcpYcFtmSD1BFOxZ/+8ofbNZcg1k1Y= -github.com/sagernet/sing v0.5.1/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= -github.com/sagernet/sing-dns v0.3.0 h1:uHCIlbCwBxALJwXcEK1d75d7t3vzCSVEQsPfZR1cxQE= -github.com/sagernet/sing-dns v0.3.0/go.mod h1:TqLIelI+FAbVEdiTRolhGLOwvhVjY7oT+wezlOJUQ7M= -github.com/sagernet/sing-mux v0.2.1 h1:N/3MHymfnFZRd29tE3TaXwPUVVgKvxhtOkiCMLp9HVo= -github.com/sagernet/sing-mux v0.2.1/go.mod h1:dm3BWL6NvES9pbib7llpylrq7Gq+LjlzG+0RacdxcyE= -github.com/sagernet/sing-quic v0.3.1 h1:kLg2n4JPnuzUPg7myJGbfGVJGeXiccXfV+PhXIlkSEc= -github.com/sagernet/sing-quic v0.3.1/go.mod h1:g8b5Fj88KRM0H9lpKAxJj0EpkL/Yk06qXJAG7FuZd2I= +github.com/sagernet/sing v0.6.0-beta.9 h1:P8lKa5hN53fRNAVCIKy5cWd6/kLO5c4slhdsfehSmHs= +github.com/sagernet/sing v0.6.0-beta.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing-dns v0.4.0-beta.1 h1:W1XkdhigwxDOMgMDVB+9kdomCpb7ExsZfB4acPcTZFY= +github.com/sagernet/sing-dns v0.4.0-beta.1/go.mod h1:8wuFcoFkWM4vJuQyg8e97LyvDwe0/Vl7G839WLcKDs8= +github.com/sagernet/sing-mux v0.3.0-alpha.1 h1:IgNX5bJBpL41gGbp05pdDOvh/b5eUQ6cv9240+Ngipg= +github.com/sagernet/sing-mux v0.3.0-alpha.1/go.mod h1:FTcImmdfW38Lz7b+HQ+mxxOth1lz4ao8uEnz+MwIJQE= +github.com/sagernet/sing-quic v0.4.0-alpha.4 h1:P9xAx3nIfcqb9M8jfgs0uLm+VxCcaY++FCqaBfHY3dQ= +github.com/sagernet/sing-quic v0.4.0-alpha.4/go.mod h1:h5RkKTmUhudJKzK7c87FPXD5w1bJjVyxMN9+opZcctA= github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE= github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg= github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= -github.com/sagernet/sing-shadowtls v0.1.5 h1:uXxmq/HXh8DIiBGLzpMjCbWnzIAFs+lIxiTOjdgG5qo= -github.com/sagernet/sing-shadowtls v0.1.5/go.mod h1:tvrDPTGLrSM46Wnf7mSr+L8NHvgvF8M4YnJF790rZX4= -github.com/sagernet/sing-tun v0.4.5 h1:GgZR9pNRem0TigZw1AgdrCE/v2oRexjmyAytvp89kW0= -github.com/sagernet/sing-tun v0.4.5/go.mod h1:1WQVMelJQjrtlzhzHwwPTSa7n41b3zSWP2DeJqWxruk= +github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 h1:RPrpgAdkP5td0vLfS5ldvYosFjSsZtRPxiyLV6jyKg0= +github.com/sagernet/sing-shadowtls v0.2.0-alpha.2/go.mod h1:0j5XlzKxaWRIEjc1uiSKmVoWb0k+L9QgZVb876+thZA= +github.com/sagernet/sing-tun v0.6.0-beta.7 h1:FCSX8oGBqb0H57AAvfGeeH/jMGYWCOg6XWkN/oeES+0= +github.com/sagernet/sing-tun v0.6.0-beta.7/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE= github.com/sagernet/sing-vmess v0.1.13 h1:/GSfD1Rt6/mVfE80WFHNBykNT7KJNWWmvcMP9DoElEs= github.com/sagernet/sing-vmess v0.1.13/go.mod h1:D+g+lhv4iOk1Pn08pd3MtUd72khL5+wgEE3xVH8J+ow= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= diff --git a/inbound/default.go b/inbound/default.go index 44c580deb9..880dd26f61 100644 --- a/inbound/default.go +++ b/inbound/default.go @@ -22,13 +22,13 @@ type myInboundAdapter struct { protocol string network []string ctx context.Context - router adapter.ConnectionRouter + router adapter.ConnectionRouterEx logger log.ContextLogger tag string listenOptions option.ListenOptions - connHandler adapter.ConnectionHandler - packetHandler adapter.PacketHandler - oobPacketHandler adapter.OOBPacketHandler + connHandler adapter.ConnectionHandlerEx + packetHandler adapter.PacketHandlerEx + oobPacketHandler adapter.OOBPacketHandlerEx packetUpstream any // http mixed @@ -55,10 +55,6 @@ func (a *myInboundAdapter) Tag() string { return a.tag } -func (a *myInboundAdapter) Network() []string { - return a.network -} - func (a *myInboundAdapter) Start() error { var err error if common.Contains(a.network, N.NetworkTCP) { @@ -150,6 +146,31 @@ func (a *myInboundAdapter) newPacketConnection(ctx context.Context, conn N.Packe return a.router.RoutePacketConnection(ctx, conn, metadata) } +func (a *myInboundAdapter) upstreamHandlerEx(metadata adapter.InboundContext) adapter.UpstreamHandlerAdapterEx { + return adapter.NewUpstreamHandlerEx(metadata, a.newConnectionEx, a.streamPacketConnectionEx) +} + +func (a *myInboundAdapter) upstreamContextHandlerEx() adapter.UpstreamHandlerAdapterEx { + return adapter.NewUpstreamContextHandlerEx(a.newConnectionEx, a.newPacketConnectionEx) +} + +func (a *myInboundAdapter) newConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + a.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + a.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (a *myInboundAdapter) newPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + ctx = log.ContextWithNewID(ctx) + a.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) + a.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + a.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + +func (a *myInboundAdapter) streamPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + a.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + a.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + func (a *myInboundAdapter) createMetadata(conn net.Conn, metadata adapter.InboundContext) adapter.InboundContext { metadata.Inbound = a.tag metadata.InboundType = a.protocol @@ -167,25 +188,17 @@ func (a *myInboundAdapter) createMetadata(conn net.Conn, metadata adapter.Inboun return metadata } -func (a *myInboundAdapter) createPacketMetadata(conn N.PacketConn, metadata adapter.InboundContext) adapter.InboundContext { - metadata.Inbound = a.tag - metadata.InboundType = a.protocol - metadata.InboundDetour = a.listenOptions.Detour - metadata.InboundOptions = a.listenOptions.InboundOptions - if !metadata.Destination.IsValid() { - metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() - } - return metadata -} - +// Deprecated: don't use func (a *myInboundAdapter) newError(err error) { a.logger.Error(err) } +// Deprecated: don't use func (a *myInboundAdapter) NewError(ctx context.Context, err error) { NewError(a.logger, ctx, err) } +// Deprecated: don't use func NewError(logger log.ContextLogger, ctx context.Context, err error) { common.Close(err) if E.IsClosedOrCanceled(err) { diff --git a/inbound/default_tcp.go b/inbound/default_tcp.go index d680c6951a..d38f96fe6e 100644 --- a/inbound/default_tcp.go +++ b/inbound/default_tcp.go @@ -71,18 +71,14 @@ func (a *myInboundAdapter) injectTCP(conn net.Conn, metadata adapter.InboundCont ctx := log.ContextWithNewID(a.ctx) metadata = a.createMetadata(conn, metadata) a.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) - hErr := a.connHandler.NewConnection(ctx, conn, metadata) - if hErr != nil { - conn.Close() - a.NewError(ctx, E.Cause(hErr, "process connection from ", metadata.Source)) - } + a.connHandler.NewConnectionEx(ctx, conn, metadata, nil) } -func (a *myInboundAdapter) routeTCP(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) { +func (a *myInboundAdapter) routeTCP(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + metadata := a.createMetadata(conn, adapter.InboundContext{ + Source: source, + Destination: destination, + }) a.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) - hErr := a.newConnection(ctx, conn, metadata) - if hErr != nil { - conn.Close() - a.NewError(ctx, E.Cause(hErr, "process connection from ", metadata.Source)) - } + a.connHandler.NewConnectionEx(ctx, conn, metadata, onClose) } diff --git a/inbound/default_udp.go b/inbound/default_udp.go index 8e39471ac7..6bcde79dae 100644 --- a/inbound/default_udp.go +++ b/inbound/default_udp.go @@ -42,7 +42,6 @@ func (a *myInboundAdapter) loopUDPIn() { defer buffer.Release() buffer.IncRef() defer buffer.DecRef() - packetService := (*myInboundPacketAdapter)(a) for { buffer.Reset() n, addr, err := a.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes()) @@ -50,16 +49,7 @@ func (a *myInboundAdapter) loopUDPIn() { return } buffer.Truncate(n) - var metadata adapter.InboundContext - metadata.Inbound = a.tag - metadata.InboundType = a.protocol - metadata.InboundOptions = a.listenOptions.InboundOptions - metadata.Source = M.SocksaddrFromNetIP(addr).Unwrap() - metadata.OriginDestination = a.udpAddr - err = a.packetHandler.NewPacket(a.ctx, packetService, buffer, metadata) - if err != nil { - a.newError(E.Cause(err, "process packet from ", metadata.Source)) - } + a.packetHandler.NewPacketEx(buffer, M.SocksaddrFromNetIP(addr).Unwrap()) } } @@ -69,7 +59,6 @@ func (a *myInboundAdapter) loopUDPOOBIn() { defer buffer.Release() buffer.IncRef() defer buffer.DecRef() - packetService := (*myInboundPacketAdapter)(a) oob := make([]byte, 1024) for { buffer.Reset() @@ -78,22 +67,12 @@ func (a *myInboundAdapter) loopUDPOOBIn() { return } buffer.Truncate(n) - var metadata adapter.InboundContext - metadata.Inbound = a.tag - metadata.InboundType = a.protocol - metadata.InboundOptions = a.listenOptions.InboundOptions - metadata.Source = M.SocksaddrFromNetIP(addr).Unwrap() - metadata.OriginDestination = a.udpAddr - err = a.oobPacketHandler.NewPacket(a.ctx, packetService, buffer, oob[:oobN], metadata) - if err != nil { - a.newError(E.Cause(err, "process packet from ", metadata.Source)) - } + a.oobPacketHandler.NewPacketEx(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap()) } } func (a *myInboundAdapter) loopUDPInThreadSafe() { defer close(a.packetOutboundClosed) - packetService := (*myInboundPacketAdapter)(a) for { buffer := buf.NewPacket() n, addr, err := a.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes()) @@ -102,23 +81,12 @@ func (a *myInboundAdapter) loopUDPInThreadSafe() { return } buffer.Truncate(n) - var metadata adapter.InboundContext - metadata.Inbound = a.tag - metadata.InboundType = a.protocol - metadata.InboundOptions = a.listenOptions.InboundOptions - metadata.Source = M.SocksaddrFromNetIP(addr).Unwrap() - metadata.OriginDestination = a.udpAddr - err = a.packetHandler.NewPacket(a.ctx, packetService, buffer, metadata) - if err != nil { - buffer.Release() - a.newError(E.Cause(err, "process packet from ", metadata.Source)) - } + a.packetHandler.NewPacketEx(buffer, M.SocksaddrFromNetIP(addr).Unwrap()) } } func (a *myInboundAdapter) loopUDPOOBInThreadSafe() { defer close(a.packetOutboundClosed) - packetService := (*myInboundPacketAdapter)(a) oob := make([]byte, 1024) for { buffer := buf.NewPacket() @@ -128,17 +96,7 @@ func (a *myInboundAdapter) loopUDPOOBInThreadSafe() { return } buffer.Truncate(n) - var metadata adapter.InboundContext - metadata.Inbound = a.tag - metadata.InboundType = a.protocol - metadata.InboundOptions = a.listenOptions.InboundOptions - metadata.Source = M.SocksaddrFromNetIP(addr).Unwrap() - metadata.OriginDestination = a.udpAddr - err = a.oobPacketHandler.NewPacket(a.ctx, packetService, buffer, oob[:oobN], metadata) - if err != nil { - buffer.Release() - a.newError(E.Cause(err, "process packet from ", metadata.Source)) - } + a.oobPacketHandler.NewPacketEx(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap()) } } @@ -148,7 +106,7 @@ func (a *myInboundAdapter) loopUDPOut() { case packet := <-a.packetOutbound: err := a.writePacket(packet.buffer, packet.destination) if err != nil && !E.IsClosed(err) { - a.newError(E.New("write back udp: ", err)) + a.logger.Error(E.New("write back udp: ", err)) } continue case <-a.packetOutboundClosed: @@ -164,15 +122,36 @@ func (a *myInboundAdapter) loopUDPOut() { } } +func (a *myInboundAdapter) packetConn() N.PacketConn { + return (*myInboundPacketAdapter)(a) +} + +func (a *myInboundAdapter) createPacketMetadata(conn N.PacketConn, metadata adapter.InboundContext) adapter.InboundContext { + metadata.Inbound = a.tag + metadata.InboundType = a.protocol + metadata.InboundDetour = a.listenOptions.Detour + metadata.InboundOptions = a.listenOptions.InboundOptions + if !metadata.Destination.IsValid() { + metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() + } + metadata.OriginDestination = a.udpAddr + return metadata +} + +func (a *myInboundAdapter) createPacketMetadataEx(source M.Socksaddr, destination M.Socksaddr) adapter.InboundContext { + var metadata adapter.InboundContext + metadata.Inbound = a.tag + metadata.InboundType = a.protocol + metadata.InboundDetour = a.listenOptions.Detour + metadata.InboundOptions = a.listenOptions.InboundOptions + metadata.Source = source + metadata.Destination = destination + metadata.OriginDestination = a.udpAddr + return metadata +} + func (a *myInboundAdapter) writePacket(buffer *buf.Buffer, destination M.Socksaddr) error { defer buffer.Release() - if destination.IsFqdn() { - udpAddr, err := net.ResolveUDPAddr(N.NetworkUDP, destination.String()) - if err != nil { - return err - } - return common.Error(a.udpConn.WriteTo(buffer.Bytes(), udpAddr)) - } return common.Error(a.udpConn.WriteToUDPAddrPort(buffer.Bytes(), destination.AddrPort())) } diff --git a/inbound/direct.go b/inbound/direct.go index 7079a9f24f..b9a99f12cc 100644 --- a/inbound/direct.go +++ b/inbound/direct.go @@ -3,7 +3,6 @@ package inbound import ( "context" "net" - "net/netip" "time" "github.com/sagernet/sing-box/adapter" @@ -13,14 +12,14 @@ import ( "github.com/sagernet/sing/common/buf" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/udpnat" + "github.com/sagernet/sing/common/udpnat2" ) var _ adapter.Inbound = (*Direct)(nil) type Direct struct { myInboundAdapter - udpNat *udpnat.Service[netip.AddrPort] + udpNat *udpnat.Service overrideOption int overrideDestination M.Socksaddr } @@ -54,10 +53,9 @@ func NewDirect(ctx context.Context, router adapter.Router, logger log.ContextLog } else { udpTimeout = C.UDPTimeout } - inbound.udpNat = udpnat.New[netip.AddrPort](int64(udpTimeout.Seconds()), adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound)) + inbound.udpNat = udpnat.New(inbound, inbound.preparePacketConnection, udpTimeout, false) inbound.connHandler = inbound inbound.packetHandler = inbound - inbound.packetUpstream = inbound.udpNat return inbound } @@ -76,29 +74,38 @@ func (d *Direct) NewConnection(ctx context.Context, conn net.Conn, metadata adap return d.router.RouteConnection(ctx, conn, metadata) } -func (d *Direct) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error { +func (d *Direct) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { + var destination M.Socksaddr switch d.overrideOption { case 1: - metadata.Destination = d.overrideDestination + destination = d.overrideDestination case 2: - destination := d.overrideDestination - destination.Port = metadata.Destination.Port - metadata.Destination = destination + destination = d.overrideDestination + destination.Port = source.Port case 3: - metadata.Destination.Port = d.overrideDestination.Port + destination = source + destination.Port = d.overrideDestination.Port } - d.udpNat.NewContextPacket(ctx, metadata.Source.AddrPort(), buffer, adapter.UpstreamMetadata(metadata), func(natConn N.PacketConn) (context.Context, N.PacketWriter) { - return adapter.WithContext(log.ContextWithNewID(ctx), &metadata), &udpnat.DirectBackWriter{Source: conn, Nat: natConn} - }) - return nil + d.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, destination, nil) } -func (d *Direct) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return d.router.RouteConnection(ctx, conn, metadata) +func (d *Direct) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + d.newConnectionEx(ctx, conn, metadata, onClose) +} + +func (d *Direct) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + d.newPacketConnectionEx(ctx, conn, d.createPacketMetadataEx(source, destination), onClose) +} + +func (d *Direct) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) { + return true, d.ctx, &directPacketWriter{d.packetConn(), source}, nil +} + +type directPacketWriter struct { + writer N.PacketWriter + source M.Socksaddr } -func (d *Direct) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - ctx = log.ContextWithNewID(ctx) - d.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) - return d.router.RoutePacketConnection(ctx, conn, metadata) +func (w *directPacketWriter) WritePacket(buffer *buf.Buffer, addr M.Socksaddr) error { + return w.writer.WritePacket(buffer, w.source) } diff --git a/inbound/http.go b/inbound/http.go index b466331995..20c8f6903d 100644 --- a/inbound/http.go +++ b/inbound/http.go @@ -4,7 +4,6 @@ import ( std_bufio "bufio" "context" "net" - "os" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" @@ -20,8 +19,8 @@ import ( ) var ( - _ adapter.Inbound = (*HTTP)(nil) - _ adapter.InjectableInbound = (*HTTP)(nil) + _ adapter.Inbound = (*HTTP)(nil) + _ adapter.TCPInjectableInbound = (*HTTP)(nil) ) type HTTP struct { @@ -72,7 +71,15 @@ func (h *HTTP) Close() error { ) } -func (h *HTTP) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *HTTP) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.newConnection(ctx, conn, metadata, onClose) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } +} + +func (h *HTTP) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { var err error if h.tlsConfig != nil { conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig) @@ -80,35 +87,33 @@ func (h *HTTP) NewConnection(ctx context.Context, conn net.Conn, metadata adapte return err } } - return http.HandleConnection(ctx, conn, std_bufio.NewReader(conn), h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata)) -} - -func (h *HTTP) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return os.ErrInvalid + return http.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, h.upstreamUserHandlerEx(metadata), metadata.Source, onClose) } -func (a *myInboundAdapter) upstreamUserHandler(metadata adapter.InboundContext) adapter.UpstreamHandlerAdapter { - return adapter.NewUpstreamHandler(metadata, a.newUserConnection, a.streamUserPacketConnection, a) +func (a *myInboundAdapter) upstreamUserHandlerEx(metadata adapter.InboundContext) adapter.UpstreamHandlerAdapterEx { + return adapter.NewUpstreamHandlerEx(metadata, a.newUserConnection, a.streamUserPacketConnection) } -func (a *myInboundAdapter) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (a *myInboundAdapter) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { user, loaded := auth.UserFromContext[string](ctx) if !loaded { a.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) - return a.router.RouteConnection(ctx, conn, metadata) + a.router.RouteConnectionEx(ctx, conn, metadata, onClose) + return } metadata.User = user a.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) - return a.router.RouteConnection(ctx, conn, metadata) + a.router.RouteConnectionEx(ctx, conn, metadata, onClose) } -func (a *myInboundAdapter) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (a *myInboundAdapter) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { user, loaded := auth.UserFromContext[string](ctx) if !loaded { a.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) - return a.router.RoutePacketConnection(ctx, conn, metadata) + a.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) + return } metadata.User = user a.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) - return a.router.RoutePacketConnection(ctx, conn, metadata) + a.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } diff --git a/inbound/mixed.go b/inbound/mixed.go index 3933f7af05..81f6a43a09 100644 --- a/inbound/mixed.go +++ b/inbound/mixed.go @@ -4,7 +4,6 @@ import ( std_bufio "bufio" "context" "net" - "os" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/uot" @@ -12,6 +11,7 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/protocol/http" "github.com/sagernet/sing/protocol/socks" @@ -20,8 +20,8 @@ import ( ) var ( - _ adapter.Inbound = (*Mixed)(nil) - _ adapter.InjectableInbound = (*Mixed)(nil) + _ adapter.Inbound = (*Mixed)(nil) + _ adapter.TCPInjectableInbound = (*Mixed)(nil) ) type Mixed struct { @@ -47,20 +47,24 @@ func NewMixed(ctx context.Context, router adapter.Router, logger log.ContextLogg return inbound } -func (h *Mixed) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Mixed) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.newConnection(ctx, conn, metadata, onClose) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } +} + +func (h *Mixed) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { reader := std_bufio.NewReader(conn) headerBytes, err := reader.Peek(1) if err != nil { - return err + return E.Cause(err, "peek first byte") } switch headerBytes[0] { case socks4.Version, socks5.Version: - return socks.HandleConnection0(ctx, conn, reader, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata)) + return socks.HandleConnectionEx(ctx, conn, reader, h.authenticator, nil, h.upstreamUserHandlerEx(metadata), metadata.Source, metadata.Destination, onClose) default: - return http.HandleConnection(ctx, conn, reader, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata)) + return http.HandleConnectionEx(ctx, conn, reader, h.authenticator, nil, h.upstreamUserHandlerEx(metadata), metadata.Source, onClose) } } - -func (h *Mixed) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return os.ErrInvalid -} diff --git a/inbound/naive.go b/inbound/naive.go index 07328c09f4..498e823cec 100644 --- a/inbound/naive.go +++ b/inbound/naive.go @@ -17,6 +17,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2rayhttp" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/buf" @@ -164,13 +165,13 @@ func (n *Naive) ServeHTTP(writer http.ResponseWriter, request *http.Request) { n.badRequest(ctx, request, E.New("hijack failed")) return } - n.newConnection(ctx, &naiveH1Conn{Conn: conn}, userName, source, destination) + n.newConnection(ctx, false, &naiveH1Conn{Conn: conn}, userName, source, destination) } else { - n.newConnection(ctx, &naiveH2Conn{reader: request.Body, writer: writer, flusher: writer.(http.Flusher)}, userName, source, destination) + n.newConnection(ctx, true, &naiveH2Conn{reader: request.Body, writer: writer, flusher: writer.(http.Flusher)}, userName, source, destination) } } -func (n *Naive) newConnection(ctx context.Context, conn net.Conn, userName string, source, destination M.Socksaddr) { +func (n *Naive) newConnection(ctx context.Context, waitForClose bool, conn net.Conn, userName string, source M.Socksaddr, destination M.Socksaddr) { if userName != "" { n.logger.InfoContext(ctx, "[", userName, "] inbound connection from ", source) n.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", destination) @@ -178,19 +179,26 @@ func (n *Naive) newConnection(ctx context.Context, conn net.Conn, userName strin n.logger.InfoContext(ctx, "inbound connection from ", source) n.logger.InfoContext(ctx, "inbound connection to ", destination) } - hErr := n.router.RouteConnection(ctx, conn, n.createMetadata(conn, adapter.InboundContext{ + metadata := n.createMetadata(conn, adapter.InboundContext{ Source: source, Destination: destination, User: userName, - })) - if hErr != nil { - conn.Close() - n.NewError(ctx, E.Cause(hErr, "process connection from ", source)) + }) + if !waitForClose { + n.router.RouteConnectionEx(ctx, conn, metadata, nil) + } else { + done := make(chan struct{}) + wrapper := v2rayhttp.NewHTTP2Wrapper(conn) + n.router.RouteConnectionEx(ctx, conn, metadata, N.OnceClose(func(it error) { + close(done) + })) + <-done + wrapper.CloseWrapper() } } func (n *Naive) badRequest(ctx context.Context, request *http.Request, err error) { - n.NewError(ctx, E.Cause(err, "process connection from ", request.RemoteAddr)) + n.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", request.RemoteAddr)) } func rejectHTTP(writer http.ResponseWriter, statusCode int) { diff --git a/inbound/redirect.go b/inbound/redirect.go index 4c7cf1d585..c4c6faf341 100644 --- a/inbound/redirect.go +++ b/inbound/redirect.go @@ -9,7 +9,6 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) @@ -34,11 +33,13 @@ func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextL return redirect } -func (r *Redirect) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (r *Redirect) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { destination, err := redir.GetOriginalDestination(conn) if err != nil { - return E.Cause(err, "get redirect destination") + conn.Close() + r.logger.ErrorContext(ctx, "process connection from ", conn.RemoteAddr(), ": get redirect destination: ", err) + return } metadata.Destination = M.SocksaddrFromNetIP(destination) - return r.newConnection(ctx, conn, metadata) + r.newConnectionEx(ctx, conn, metadata, onClose) } diff --git a/inbound/shadowsocks.go b/inbound/shadowsocks.go index ca15b8d8e8..3fff231d8f 100644 --- a/inbound/shadowsocks.go +++ b/inbound/shadowsocks.go @@ -3,7 +3,6 @@ package inbound import ( "context" "net" - "os" "time" "github.com/sagernet/sing-box/adapter" @@ -18,6 +17,7 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ntp" ) @@ -36,8 +36,8 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte } var ( - _ adapter.Inbound = (*Shadowsocks)(nil) - _ adapter.InjectableInbound = (*Shadowsocks)(nil) + _ adapter.Inbound = (*Shadowsocks)(nil) + _ adapter.TCPInjectableInbound = (*Shadowsocks)(nil) ) type Shadowsocks struct { @@ -74,11 +74,11 @@ func newShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte } switch { case options.Method == shadowsocks.MethodNone: - inbound.service = shadowsocks.NewNoneService(int64(udpTimeout.Seconds()), inbound.upstreamContextHandler()) + inbound.service = shadowsocks.NewNoneService(int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound)) case common.Contains(shadowaead.List, options.Method): - inbound.service, err = shadowaead.NewService(options.Method, nil, options.Password, int64(udpTimeout.Seconds()), inbound.upstreamContextHandler()) + inbound.service, err = shadowaead.NewService(options.Method, nil, options.Password, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound)) case common.Contains(shadowaead_2022.List, options.Method): - inbound.service, err = shadowaead_2022.NewServiceWithPassword(options.Method, options.Password, int64(udpTimeout.Seconds()), inbound.upstreamContextHandler(), ntp.TimeFuncFromContext(ctx)) + inbound.service, err = shadowaead_2022.NewServiceWithPassword(options.Method, options.Password, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ntp.TimeFuncFromContext(ctx)) default: err = E.New("unsupported method: ", options.Method) } @@ -86,14 +86,29 @@ func newShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte return inbound, err } -func (h *Shadowsocks) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata)) +func (h *Shadowsocks) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } +} + +func (h *Shadowsocks) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { + err := h.service.NewPacket(h.ctx, h.packetConn(), buffer, M.Metadata{Source: source}) + if err != nil { + h.logger.Error(E.Cause(err, "process packet from ", source)) + } } -func (h *Shadowsocks) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error { - return h.service.NewPacket(adapter.WithContext(ctx, &metadata), conn, buffer, adapter.UpstreamMetadata(metadata)) +func (h *Shadowsocks) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + return h.router.RouteConnection(ctx, conn, h.createMetadata(conn, metadata)) } -func (h *Shadowsocks) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return os.ErrInvalid +func (h *Shadowsocks) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + ctx = log.ContextWithNewID(ctx) + h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + return h.router.RoutePacketConnection(ctx, conn, h.createPacketMetadata(conn, metadata)) } diff --git a/inbound/shadowsocks_multi.go b/inbound/shadowsocks_multi.go index a291af4acb..2953419490 100644 --- a/inbound/shadowsocks_multi.go +++ b/inbound/shadowsocks_multi.go @@ -20,13 +20,14 @@ import ( "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ntp" ) var ( - _ adapter.Inbound = (*ShadowsocksMulti)(nil) - _ adapter.InjectableInbound = (*ShadowsocksMulti)(nil) + _ adapter.Inbound = (*ShadowsocksMulti)(nil) + _ adapter.TCPInjectableInbound = (*ShadowsocksMulti)(nil) ) type ShadowsocksMulti struct { @@ -66,14 +67,15 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log. options.Method, options.Password, int64(udpTimeout.Seconds()), - adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound), + adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ntp.TimeFuncFromContext(ctx), ) } else if common.Contains(shadowaead.List, options.Method) { service, err = shadowaead.NewMultiService[int]( options.Method, int64(udpTimeout.Seconds()), - adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound)) + adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), + ) } else { return nil, E.New("unsupported method: " + options.Method) } @@ -94,16 +96,19 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log. return inbound, err } -func (h *ShadowsocksMulti) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata)) -} - -func (h *ShadowsocksMulti) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error { - return h.service.NewPacket(adapter.WithContext(ctx, &metadata), conn, buffer, adapter.UpstreamMetadata(metadata)) +func (h *ShadowsocksMulti) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } } -func (h *ShadowsocksMulti) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return os.ErrInvalid +func (h *ShadowsocksMulti) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { + err := h.service.NewPacket(h.ctx, h.packetConn(), buffer, M.Metadata{Source: source}) + if err != nil { + h.logger.Error(E.Cause(err, "process packet from ", source)) + } } func (h *ShadowsocksMulti) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { @@ -118,7 +123,7 @@ func (h *ShadowsocksMulti) newConnection(ctx context.Context, conn net.Conn, met metadata.User = user } h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) - return h.router.RouteConnection(ctx, conn, metadata) + return h.router.RouteConnection(ctx, conn, h.createMetadata(conn, metadata)) } func (h *ShadowsocksMulti) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { @@ -135,5 +140,5 @@ func (h *ShadowsocksMulti) newPacketConnection(ctx context.Context, conn N.Packe ctx = log.ContextWithNewID(ctx) h.logger.InfoContext(ctx, "[", user, "] inbound packet connection from ", metadata.Source) h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) - return h.router.RoutePacketConnection(ctx, conn, metadata) + return h.router.RoutePacketConnection(ctx, conn, h.createPacketMetadata(conn, metadata)) } diff --git a/inbound/shadowsocks_relay.go b/inbound/shadowsocks_relay.go index fbc2838ade..02246a3f3c 100644 --- a/inbound/shadowsocks_relay.go +++ b/inbound/shadowsocks_relay.go @@ -16,13 +16,15 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) var ( - _ adapter.Inbound = (*ShadowsocksRelay)(nil) - _ adapter.InjectableInbound = (*ShadowsocksRelay)(nil) + _ adapter.Inbound = (*ShadowsocksRelay)(nil) + _ adapter.TCPInjectableInbound = (*ShadowsocksRelay)(nil) ) type ShadowsocksRelay struct { @@ -61,7 +63,7 @@ func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log. options.Method, options.Password, int64(udpTimeout.Seconds()), - adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound), + adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ) if err != nil { return nil, err @@ -79,16 +81,19 @@ func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log. return inbound, err } -func (h *ShadowsocksRelay) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata)) -} - -func (h *ShadowsocksRelay) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext) error { - return h.service.NewPacket(adapter.WithContext(ctx, &metadata), conn, buffer, adapter.UpstreamMetadata(metadata)) +func (h *ShadowsocksRelay) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } } -func (h *ShadowsocksRelay) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return os.ErrInvalid +func (h *ShadowsocksRelay) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { + err := h.service.NewPacket(h.ctx, h.packetConn(), buffer, M.Metadata{Source: source}) + if err != nil { + h.logger.Error(E.Cause(err, "process packet from ", source)) + } } func (h *ShadowsocksRelay) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { @@ -103,7 +108,7 @@ func (h *ShadowsocksRelay) newConnection(ctx context.Context, conn net.Conn, met metadata.User = destination } h.logger.InfoContext(ctx, "[", destination, "] inbound connection to ", metadata.Destination) - return h.router.RouteConnection(ctx, conn, metadata) + return h.router.RouteConnection(ctx, conn, h.createMetadata(conn, metadata)) } func (h *ShadowsocksRelay) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { @@ -120,5 +125,5 @@ func (h *ShadowsocksRelay) newPacketConnection(ctx context.Context, conn N.Packe ctx = log.ContextWithNewID(ctx) h.logger.InfoContext(ctx, "[", destination, "] inbound packet connection from ", metadata.Source) h.logger.InfoContext(ctx, "[", destination, "] inbound packet connection to ", metadata.Destination) - return h.router.RoutePacketConnection(ctx, conn, metadata) + return h.router.RoutePacketConnection(ctx, conn, h.createPacketMetadata(conn, metadata)) } diff --git a/inbound/shadowtls.go b/inbound/shadowtls.go index 358564f221..ca14228635 100644 --- a/inbound/shadowtls.go +++ b/inbound/shadowtls.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing-shadowtls" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" ) @@ -91,3 +92,11 @@ func (h *ShadowTLS) newConnection(ctx context.Context, conn net.Conn, metadata a } return h.router.RouteConnection(ctx, conn, metadata) } + +func (h *ShadowTLS) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.NewConnection(ctx, conn, metadata) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } +} diff --git a/inbound/socks.go b/inbound/socks.go index 7c5183c9f7..04b0a77dc7 100644 --- a/inbound/socks.go +++ b/inbound/socks.go @@ -1,9 +1,9 @@ package inbound import ( + std_bufio "bufio" "context" "net" - "os" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/uot" @@ -11,13 +11,14 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/protocol/socks" ) var ( - _ adapter.Inbound = (*Socks)(nil) - _ adapter.InjectableInbound = (*Socks)(nil) + _ adapter.Inbound = (*Socks)(nil) + _ adapter.TCPInjectableInbound = (*Socks)(nil) ) type Socks struct { @@ -42,10 +43,10 @@ func NewSocks(ctx context.Context, router adapter.Router, logger log.ContextLogg return inbound } -func (h *Socks) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return socks.HandleConnection(ctx, conn, h.authenticator, h.upstreamUserHandler(metadata), adapter.UpstreamMetadata(metadata)) -} - -func (h *Socks) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return os.ErrInvalid +func (h *Socks) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, h.upstreamUserHandlerEx(metadata), metadata.Source, metadata.Destination, onClose) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } } diff --git a/inbound/tproxy.go b/inbound/tproxy.go index a074eb495b..40653c797b 100644 --- a/inbound/tproxy.go +++ b/inbound/tproxy.go @@ -18,12 +18,12 @@ import ( E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/udpnat" + "github.com/sagernet/sing/common/udpnat2" ) type TProxy struct { myInboundAdapter - udpNat *udpnat.Service[netip.AddrPort] + udpNat *udpnat.Service } func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TProxyInboundOptions) *TProxy { @@ -46,8 +46,7 @@ func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLog } tproxy.connHandler = tproxy tproxy.oobPacketHandler = tproxy - tproxy.udpNat = udpnat.New[netip.AddrPort](int64(udpTimeout.Seconds()), tproxy.upstreamContextHandler()) - tproxy.packetUpstream = tproxy.udpNat + tproxy.udpNat = udpnat.New(tproxy, tproxy.preparePacketConnection, udpTimeout, false) return tproxy } @@ -75,35 +74,43 @@ func (t *TProxy) Start() error { return nil } -func (t *TProxy) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (t *TProxy) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() - return t.newConnection(ctx, conn, metadata) + t.newConnectionEx(ctx, conn, metadata, onClose) } -func (t *TProxy) NewPacket(ctx context.Context, conn N.PacketConn, buffer *buf.Buffer, oob []byte, metadata adapter.InboundContext) error { +func (t *TProxy) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + t.newPacketConnectionEx(ctx, conn, t.createPacketMetadataEx(source, destination), onClose) +} + +func (t *TProxy) NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr) { destination, err := redir.GetOriginalDestinationFromOOB(oob) if err != nil { - return E.Cause(err, "get tproxy destination") + t.logger.Warn("process packet from ", source, ": get tproxy destination: ", err) + return } - metadata.Destination = M.SocksaddrFromNetIP(destination).Unwrap() - t.udpNat.NewContextPacket(ctx, metadata.Source.AddrPort(), buffer, adapter.UpstreamMetadata(metadata), func(natConn N.PacketConn) (context.Context, N.PacketWriter) { - return adapter.WithContext(log.ContextWithNewID(ctx), &metadata), &tproxyPacketWriter{ctx: ctx, source: natConn, destination: metadata.Destination} - }) - return nil + t.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, M.SocksaddrFromNetIP(destination), nil) } type tproxyPacketWriter struct { ctx context.Context - source N.PacketConn + source netip.AddrPort destination M.Socksaddr conn *net.UDPConn } +func (t *TProxy) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) { + writer := &tproxyPacketWriter{ctx: t.ctx, source: source.AddrPort(), destination: destination} + return true, t.ctx, writer, func(it error) { + common.Close(common.PtrOrNil(writer.conn)) + } +} + func (w *tproxyPacketWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { defer buffer.Release() conn := w.conn if w.destination == destination && conn != nil { - _, err := conn.WriteToUDPAddrPort(buffer.Bytes(), M.AddrPortFromNet(w.source.LocalAddr())) + _, err := conn.WriteToUDPAddrPort(buffer.Bytes(), w.source) if err != nil { w.conn = nil } @@ -122,9 +129,5 @@ func (w *tproxyPacketWriter) WritePacket(buffer *buf.Buffer, destination M.Socks } else { defer udpConn.Close() } - return common.Error(udpConn.WriteToUDPAddrPort(buffer.Bytes(), M.AddrPortFromNet(w.source.LocalAddr()))) -} - -func (w *tproxyPacketWriter) Close() error { - return common.Close(common.PtrOrNil(w.conn)) + return common.Error(udpConn.WriteToUDPAddrPort(buffer.Bytes(), w.source)) } diff --git a/inbound/trojan.go b/inbound/trojan.go index 203b6d3927..ce003dda70 100644 --- a/inbound/trojan.go +++ b/inbound/trojan.go @@ -22,8 +22,8 @@ import ( ) var ( - _ adapter.Inbound = (*Trojan)(nil) - _ adapter.InjectableInbound = (*Trojan)(nil) + _ adapter.Inbound = (*Trojan)(nil) + _ adapter.TCPInjectableInbound = (*Trojan)(nil) ) type Trojan struct { @@ -90,7 +90,7 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog return nil, err } if options.Transport != nil { - inbound.transport, err = v2ray.NewServerTransport(ctx, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*trojanTransportHandler)(inbound)) + inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*trojanTransportHandler)(inbound)) if err != nil { return nil, E.Cause(err, "create server transport: ", options.Transport.Type) } @@ -149,11 +149,6 @@ func (h *Trojan) Close() error { ) } -func (h *Trojan) newTransportConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - h.injectTCP(conn, metadata) - return nil -} - func (h *Trojan) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { var err error if h.tlsConfig != nil && h.transport == nil { @@ -165,8 +160,12 @@ func (h *Trojan) NewConnection(ctx context.Context, conn net.Conn, metadata adap return h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata)) } -func (h *Trojan) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return os.ErrInvalid +func (h *Trojan) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.NewConnection(ctx, conn, metadata) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } } func (h *Trojan) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { @@ -226,9 +225,6 @@ var _ adapter.V2RayServerTransportHandler = (*trojanTransportHandler)(nil) type trojanTransportHandler Trojan -func (t *trojanTransportHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { - return (*Trojan)(t).newTransportConnection(ctx, conn, adapter.InboundContext{ - Source: metadata.Source, - Destination: metadata.Destination, - }) +func (t *trojanTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + (*Trojan)(t).routeTCP(ctx, conn, source, destination, onClose) } diff --git a/inbound/tun.go b/inbound/tun.go index 721112d779..0d856419ef 100644 --- a/inbound/tun.go +++ b/inbound/tun.go @@ -28,17 +28,18 @@ import ( "go4.org/netipx" ) -var _ adapter.Inbound = (*Tun)(nil) +var _ adapter.Inbound = (*TUN)(nil) -type Tun struct { - tag string - ctx context.Context - router adapter.Router - logger log.ContextLogger +type TUN struct { + tag string + ctx context.Context + router adapter.Router + logger log.ContextLogger + // Deprecated inboundOptions option.InboundOptions tunOptions tun.Options endpointIndependentNat bool - udpTimeout int64 + udpTimeout time.Duration stack string tunIf tun.Tun tunStack tun.Stack @@ -53,7 +54,7 @@ type Tun struct { routeExcludeAddressSet []*netipx.IPSet } -func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*Tun, error) { +func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*TUN, error) { address := options.Address var deprecatedAddressUsed bool //nolint:staticcheck @@ -162,7 +163,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger outputMark = tun.DefaultAutoRedirectOutputMark } - inbound := &Tun{ + inbound := &TUN{ tag: tag, ctx: ctx, router: router, @@ -194,7 +195,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger InterfaceMonitor: router.InterfaceMonitor(), }, endpointIndependentNat: options.EndpointIndependentNat, - udpTimeout: int64(udpTimeout.Seconds()), + udpTimeout: udpTimeout, stack: options.Stack, platformInterface: platformInterface, platformOptions: common.PtrValueOrDefault(options.Platform), @@ -207,7 +208,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger inbound.autoRedirect, err = tun.NewAutoRedirect(tun.AutoRedirectOptions{ TunOptions: &inbound.tunOptions, Context: ctx, - Handler: inbound, + Handler: (*autoRedirectHandler)(inbound), Logger: logger, NetworkMonitor: router.NetworkMonitor(), InterfaceFinder: router.InterfaceFinder(), @@ -283,17 +284,17 @@ func parseRange(uidRanges []ranges.Range[uint32], rangeList []string) ([]ranges. return uidRanges, nil } -func (t *Tun) Type() string { +func (t *TUN) Type() string { return C.TypeTun } -func (t *Tun) Tag() string { +func (t *TUN) Tag() string { return t.tag } -func (t *Tun) Start() error { +func (t *TUN) Start() error { if C.IsAndroid && t.platformInterface == nil { - t.tunOptions.BuildAndroidRules(t.router.PackageManager(), t) + t.tunOptions.BuildAndroidRules(t.router.PackageManager()) } if t.tunOptions.Name == "" { t.tunOptions.Name = tun.CalculateInterfaceName("") @@ -327,7 +328,6 @@ func (t *Tun) Start() error { Context: t.ctx, Tun: tunInterface, TunOptions: t.tunOptions, - EndpointIndependentNat: t.endpointIndependentNat, UDPTimeout: t.udpTimeout, Handler: t, Logger: t.logger, @@ -349,7 +349,7 @@ func (t *Tun) Start() error { return nil } -func (t *Tun) PostStart() error { +func (t *TUN) PostStart() error { monitor := taskmonitor.New(t.logger, C.StartTimeout) if t.autoRedirect != nil { t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) @@ -388,7 +388,7 @@ func (t *Tun) PostStart() error { return nil } -func (t *Tun) updateRouteAddressSet(it adapter.RuleSet) { +func (t *TUN) updateRouteAddressSet(it adapter.RuleSet) { t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet) t.autoRedirect.UpdateRouteAddressSet() @@ -396,7 +396,7 @@ func (t *Tun) updateRouteAddressSet(it adapter.RuleSet) { t.routeExcludeAddressSet = nil } -func (t *Tun) Close() error { +func (t *TUN) Close() error { return common.Close( t.tunStack, t.tunIf, @@ -404,44 +404,48 @@ func (t *Tun) Close() error { ) } -func (t *Tun) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata M.Metadata) error { +func (t *TUN) PrepareConnection(source M.Socksaddr, destination M.Socksaddr) error { + // TODO: implement rejects + return nil +} + +func (t *TUN) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) var metadata adapter.InboundContext metadata.Inbound = t.tag metadata.InboundType = C.TypeTun - metadata.Source = upstreamMetadata.Source - metadata.Destination = upstreamMetadata.Destination + metadata.Source = source + metadata.Destination = destination metadata.InboundOptions = t.inboundOptions - if upstreamMetadata.Protocol != "" { - t.logger.InfoContext(ctx, "inbound ", upstreamMetadata.Protocol, " connection from ", metadata.Source) - } else { - t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) - } + t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) - err := t.router.RouteConnection(ctx, conn, metadata) - if err != nil { - t.NewError(ctx, err) - } - return nil + t.router.RouteConnectionEx(ctx, conn, metadata, onClose) } -func (t *Tun) NewPacketConnection(ctx context.Context, conn N.PacketConn, upstreamMetadata M.Metadata) error { +func (t *TUN) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) var metadata adapter.InboundContext metadata.Inbound = t.tag metadata.InboundType = C.TypeTun - metadata.Source = upstreamMetadata.Source - metadata.Destination = upstreamMetadata.Destination + metadata.Source = source + metadata.Destination = destination metadata.InboundOptions = t.inboundOptions t.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) t.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) - err := t.router.RoutePacketConnection(ctx, conn, metadata) - if err != nil { - t.NewError(ctx, err) - } - return nil + t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } -func (t *Tun) NewError(ctx context.Context, err error) { - NewError(t.logger, ctx, err) +type autoRedirectHandler TUN + +func (t *autoRedirectHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + ctx = log.ContextWithNewID(ctx) + var metadata adapter.InboundContext + metadata.Inbound = t.tag + metadata.InboundType = C.TypeTun + metadata.Source = source + metadata.Destination = destination + metadata.InboundOptions = t.inboundOptions + t.logger.InfoContext(ctx, "inbound redirect connection from ", metadata.Source) + t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + t.router.RouteConnectionEx(ctx, conn, metadata, onClose) } diff --git a/inbound/vless.go b/inbound/vless.go index 567475647a..ec26bd8859 100644 --- a/inbound/vless.go +++ b/inbound/vless.go @@ -25,8 +25,8 @@ import ( ) var ( - _ adapter.Inbound = (*VLESS)(nil) - _ adapter.InjectableInbound = (*VLESS)(nil) + _ adapter.Inbound = (*VLESS)(nil) + _ adapter.TCPInjectableInbound = (*VLESS)(nil) ) type VLESS struct { @@ -73,7 +73,7 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg } } if options.Transport != nil { - inbound.transport, err = v2ray.NewServerTransport(ctx, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*vlessTransportHandler)(inbound)) + inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*vlessTransportHandler)(inbound)) if err != nil { return nil, E.Cause(err, "create server transport: ", options.Transport.Type) } @@ -128,11 +128,6 @@ func (h *VLESS) Close() error { ) } -func (h *VLESS) newTransportConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - h.injectTCP(conn, metadata) - return nil -} - func (h *VLESS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { var err error if h.tlsConfig != nil && h.transport == nil { @@ -144,8 +139,12 @@ func (h *VLESS) NewConnection(ctx context.Context, conn net.Conn, metadata adapt return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata)) } -func (h *VLESS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return os.ErrInvalid +func (h *VLESS) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.NewConnection(ctx, conn, metadata) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } } func (h *VLESS) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { @@ -188,9 +187,6 @@ var _ adapter.V2RayServerTransportHandler = (*vlessTransportHandler)(nil) type vlessTransportHandler VLESS -func (t *vlessTransportHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { - return (*VLESS)(t).newTransportConnection(ctx, conn, adapter.InboundContext{ - Source: metadata.Source, - Destination: metadata.Destination, - }) +func (t *vlessTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + t.routeTCP(ctx, conn, source, destination, onClose) } diff --git a/inbound/vmess.go b/inbound/vmess.go index 154512751c..9099bd6293 100644 --- a/inbound/vmess.go +++ b/inbound/vmess.go @@ -25,8 +25,8 @@ import ( ) var ( - _ adapter.Inbound = (*VMess)(nil) - _ adapter.InjectableInbound = (*VMess)(nil) + _ adapter.Inbound = (*VMess)(nil) + _ adapter.TCPInjectableInbound = (*VMess)(nil) ) type VMess struct { @@ -83,7 +83,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg } } if options.Transport != nil { - inbound.transport, err = v2ray.NewServerTransport(ctx, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*vmessTransportHandler)(inbound)) + inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*vmessTransportHandler)(inbound)) if err != nil { return nil, E.Cause(err, "create server transport: ", options.Transport.Type) } @@ -142,11 +142,6 @@ func (h *VMess) Close() error { ) } -func (h *VMess) newTransportConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - h.injectTCP(conn, metadata) - return nil -} - func (h *VMess) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { var err error if h.tlsConfig != nil && h.transport == nil { @@ -158,8 +153,12 @@ func (h *VMess) NewConnection(ctx context.Context, conn net.Conn, metadata adapt return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata)) } -func (h *VMess) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return os.ErrInvalid +func (h *VMess) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.NewConnection(ctx, conn, metadata) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } } func (h *VMess) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { @@ -202,9 +201,6 @@ var _ adapter.V2RayServerTransportHandler = (*vmessTransportHandler)(nil) type vmessTransportHandler VMess -func (t *vmessTransportHandler) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { - return (*VMess)(t).newTransportConnection(ctx, conn, adapter.InboundContext{ - Source: metadata.Source, - Destination: metadata.Destination, - }) +func (t *vmessTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + (*VMess)(t).routeTCP(ctx, conn, source, destination, onClose) } diff --git a/include/quic_stub.go b/include/quic_stub.go index ddf9723f9a..43aa58d996 100644 --- a/include/quic_stub.go +++ b/include/quic_stub.go @@ -11,6 +11,7 @@ import ( "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2ray" "github.com/sagernet/sing-dns" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) @@ -20,7 +21,7 @@ func init() { return nil, C.ErrQUICNotIncluded }) v2ray.RegisterQUICConstructor( - func(ctx context.Context, options option.V2RayQUICOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { + func(ctx context.Context, logger logger.ContextLogger, options option.V2RayQUICOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { return nil, C.ErrQUICNotIncluded }, func(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayQUICOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { diff --git a/option/inbound.go b/option/inbound.go index 54e8bab84a..d38799048f 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -98,6 +98,7 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error { return nil } +// Deprecated: Use rule action instead type InboundOptions struct { SniffEnabled bool `json:"sniff,omitempty"` SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"` diff --git a/option/rule.go b/option/rule.go index 0c66ab1d53..0b11cbdda1 100644 --- a/option/rule.go +++ b/option/rule.go @@ -64,7 +64,7 @@ func (r Rule) IsValid() bool { } } -type DefaultRule struct { +type RawDefaultRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` Network Listable[string] `json:"network,omitempty"` @@ -98,26 +98,58 @@ type DefaultRule struct { RuleSet Listable[string] `json:"rule_set,omitempty"` RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` // Deprecated: renamed to rule_set_ip_cidr_match_source Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` } +type DefaultRule struct { + RawDefaultRule + RuleAction +} + +func (r *DefaultRule) MarshalJSON() ([]byte, error) { + return MarshallObjects(r.RawDefaultRule, r.RuleAction) +} + +func (r *DefaultRule) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, &r.RawDefaultRule) + if err != nil { + return err + } + return UnmarshallExcluded(data, &r.RawDefaultRule, &r.RuleAction) +} + func (r *DefaultRule) IsValid() bool { var defaultValue DefaultRule defaultValue.Invert = r.Invert - defaultValue.Outbound = r.Outbound + defaultValue.Action = r.Action return !reflect.DeepEqual(r, defaultValue) } +type _LogicalRule struct { + Mode string `json:"mode"` + Rules []Rule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` +} + type LogicalRule struct { - Mode string `json:"mode"` - Rules []Rule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Outbound string `json:"outbound,omitempty"` + _LogicalRule + RuleAction +} + +func (r *LogicalRule) MarshalJSON() ([]byte, error) { + return MarshallObjects(r._LogicalRule, r.RuleAction) +} + +func (r *LogicalRule) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, &r._LogicalRule) + if err != nil { + return err + } + return UnmarshallExcluded(data, &r._LogicalRule, &r.RuleAction) } -func (r LogicalRule) IsValid() bool { +func (r *LogicalRule) IsValid() bool { return len(r.Rules) > 0 && common.All(r.Rules, Rule.IsValid) } diff --git a/option/rule_action.go b/option/rule_action.go new file mode 100644 index 0000000000..4f0ec177b8 --- /dev/null +++ b/option/rule_action.go @@ -0,0 +1,166 @@ +package option + +import ( + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" +) + +type _RuleAction struct { + Action string `json:"action,omitempty"` + RouteOptions RouteActionOptions `json:"-"` + RejectOptions RejectActionOptions `json:"-"` + SniffOptions RouteActionSniff `json:"-"` + ResolveOptions RouteActionResolve `json:"-"` +} + +type RuleAction _RuleAction + +func (r RuleAction) MarshalJSON() ([]byte, error) { + var v any + switch r.Action { + case C.RuleActionTypeRoute: + r.Action = "" + v = r.RouteOptions + case C.RuleActionTypeReturn: + v = nil + case C.RuleActionTypeReject: + v = r.RejectOptions + case C.RuleActionTypeHijackDNS: + v = nil + case C.RuleActionTypeSniff: + v = r.SniffOptions + case C.RuleActionTypeResolve: + v = r.ResolveOptions + default: + return nil, E.New("unknown rule action: " + r.Action) + } + if v == nil { + return MarshallObjects((_RuleAction)(r)) + } + return MarshallObjects((_RuleAction)(r), v) +} + +func (r *RuleAction) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, (*_RuleAction)(r)) + if err != nil { + return err + } + var v any + switch r.Action { + case "", C.RuleActionTypeRoute: + r.Action = C.RuleActionTypeRoute + v = &r.RouteOptions + case C.RuleActionTypeReturn: + v = nil + case C.RuleActionTypeReject: + v = &r.RejectOptions + case C.RuleActionTypeHijackDNS: + v = nil + case C.RuleActionTypeSniff: + v = &r.SniffOptions + case C.RuleActionTypeResolve: + v = &r.ResolveOptions + default: + return E.New("unknown rule action: " + r.Action) + } + if v == nil { + // check unknown fields + return json.UnmarshalDisallowUnknownFields(data, &_RuleAction{}) + } + return UnmarshallExcluded(data, (*_RuleAction)(r), v) +} + +type _DNSRuleAction struct { + Action string `json:"action,omitempty"` + RouteOptions DNSRouteActionOptions `json:"-"` + RejectOptions RejectActionOptions `json:"-"` + SniffOptions RouteActionSniff `json:"-"` + ResolveOptions RouteActionResolve `json:"-"` +} + +type DNSRuleAction _DNSRuleAction + +func (r DNSRuleAction) MarshalJSON() ([]byte, error) { + var v any + switch r.Action { + case C.RuleActionTypeRoute: + r.Action = "" + v = r.RouteOptions + case C.RuleActionTypeReturn: + v = nil + case C.RuleActionTypeReject: + v = r.RejectOptions + default: + return nil, E.New("unknown DNS rule action: " + r.Action) + } + if v == nil { + return MarshallObjects((_DNSRuleAction)(r)) + } + return MarshallObjects((_DNSRuleAction)(r), v) +} + +func (r *DNSRuleAction) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, (*_DNSRuleAction)(r)) + if err != nil { + return err + } + var v any + switch r.Action { + case "", C.RuleActionTypeRoute: + r.Action = C.RuleActionTypeRoute + v = &r.RouteOptions + case C.RuleActionTypeReturn: + v = nil + case C.RuleActionTypeReject: + v = &r.RejectOptions + default: + return E.New("unknown DNS rule action: " + r.Action) + } + if v == nil { + // check unknown fields + return json.UnmarshalDisallowUnknownFields(data, &_DNSRuleAction{}) + } + return UnmarshallExcluded(data, (*_DNSRuleAction)(r), v) +} + +type RouteActionOptions struct { + Outbound string `json:"outbound"` + UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` +} + +type DNSRouteActionOptions struct { + Server string `json:"server"` + DisableCache bool `json:"disable_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"` +} + +type RejectActionOptions struct { + Method RejectMethod `json:"method,omitempty"` +} + +type RejectMethod string + +func (m *RejectMethod) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*string)(m)) + if err != nil { + return err + } + switch *m { + case C.RuleActionRejectMethodDefault, C.RuleActionRejectMethodPortUnreachable, C.RuleActionRejectMethodDrop: + return nil + default: + return E.New("unknown reject method: " + *m) + } +} + +type RouteActionSniff struct { + Sniffer Listable[string] `json:"sniffer,omitempty"` + Timeout Duration `json:"timeout,omitempty"` +} + +type RouteActionResolve struct { + Strategy DomainStrategy `json:"strategy,omitempty"` + Server string `json:"server,omitempty"` +} diff --git a/option/rule_dns.go b/option/rule_dns.go index 2e52d6c561..b328c45c01 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -64,7 +64,7 @@ func (r DNSRule) IsValid() bool { } } -type DefaultDNSRule struct { +type RawDefaultDNSRule struct { Inbound Listable[string] `json:"inbound,omitempty"` IPVersion int `json:"ip_version,omitempty"` QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` @@ -100,35 +100,58 @@ type DefaultDNSRule struct { RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` - ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"` // Deprecated: renamed to rule_set_ip_cidr_match_source Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` } +type DefaultDNSRule struct { + RawDefaultDNSRule + DNSRuleAction +} + +func (r *DefaultDNSRule) MarshalJSON() ([]byte, error) { + return MarshallObjects(r.RawDefaultDNSRule, r.DNSRuleAction) +} + +func (r *DefaultDNSRule) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, &r.RawDefaultDNSRule) + if err != nil { + return err + } + return UnmarshallExcluded(data, &r.RawDefaultDNSRule, &r.DNSRuleAction) +} + func (r *DefaultDNSRule) IsValid() bool { var defaultValue DefaultDNSRule defaultValue.Invert = r.Invert - defaultValue.Server = r.Server - defaultValue.DisableCache = r.DisableCache - defaultValue.RewriteTTL = r.RewriteTTL - defaultValue.ClientSubnet = r.ClientSubnet + defaultValue.DNSRuleAction = r.DNSRuleAction return !reflect.DeepEqual(r, defaultValue) } +type _LogicalDNSRule struct { + Mode string `json:"mode"` + Rules []DNSRule `json:"rules,omitempty"` + Invert bool `json:"invert,omitempty"` +} + type LogicalDNSRule struct { - Mode string `json:"mode"` - Rules []DNSRule `json:"rules,omitempty"` - Invert bool `json:"invert,omitempty"` - Server string `json:"server,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` - ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"` + _LogicalDNSRule + DNSRuleAction +} + +func (r *LogicalDNSRule) MarshalJSON() ([]byte, error) { + return MarshallObjects(r._LogicalDNSRule, r.DNSRuleAction) +} + +func (r *LogicalDNSRule) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, &r._LogicalDNSRule) + if err != nil { + return err + } + return UnmarshallExcluded(data, &r._LogicalDNSRule, &r.DNSRuleAction) } -func (r LogicalDNSRule) IsValid() bool { +func (r *LogicalDNSRule) IsValid() bool { return len(r.Rules) > 0 && common.All(r.Rules, DNSRule.IsValid) } diff --git a/option/types.go b/option/types.go index b17f222248..bb6485491e 100644 --- a/option/types.go +++ b/option/types.go @@ -81,8 +81,11 @@ func (a *AddrPrefix) UnmarshalJSON(content []byte) error { return prefixErr } -func (a AddrPrefix) Build() netip.Prefix { - return netip.Prefix(a) +func (a *AddrPrefix) Build() netip.Prefix { + if a == nil { + return netip.Prefix{} + } + return netip.Prefix(*a) } type NetworkList string @@ -143,12 +146,29 @@ func (l *Listable[T]) UnmarshalJSON(content []byte) error { type DomainStrategy dns.DomainStrategy +func (s DomainStrategy) String() string { + switch dns.DomainStrategy(s) { + case dns.DomainStrategyAsIS: + return "" + case dns.DomainStrategyPreferIPv4: + return "prefer_ipv4" + case dns.DomainStrategyPreferIPv6: + return "prefer_ipv6" + case dns.DomainStrategyUseIPv4: + return "ipv4_only" + case dns.DomainStrategyUseIPv6: + return "ipv6_only" + default: + panic(E.New("unknown domain strategy: ", s)) + } +} + func (s DomainStrategy) MarshalJSON() ([]byte, error) { var value string switch dns.DomainStrategy(s) { case dns.DomainStrategyAsIS: value = "" - // value = "AsIS" + // value = "as_is" case dns.DomainStrategyPreferIPv4: value = "prefer_ipv4" case dns.DomainStrategyPreferIPv6: diff --git a/outbound/block.go b/outbound/block.go index e73c44228b..b6ccefe2be 100644 --- a/outbound/block.go +++ b/outbound/block.go @@ -39,12 +39,14 @@ func (h *Block) ListenPacket(ctx context.Context, destination M.Socksaddr) (net. return nil, io.EOF } +// Deprecated func (h *Block) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { conn.Close() h.logger.InfoContext(ctx, "blocked connection to ", metadata.Destination) return nil } +// Deprecated func (h *Block) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { conn.Close() h.logger.InfoContext(ctx, "blocked packet connection to ", metadata.Destination) diff --git a/outbound/default.go b/outbound/default.go index 972aca9479..a34ac97af1 100644 --- a/outbound/default.go +++ b/outbound/default.go @@ -69,7 +69,7 @@ func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata a if err != nil { return N.ReportHandshakeFailure(conn, err) } - err = N.ReportHandshakeSuccess(conn) + err = N.ReportConnHandshakeSuccess(conn, outConn) if err != nil { outConn.Close() return err @@ -96,7 +96,7 @@ func NewDirectConnection(ctx context.Context, router adapter.Router, this N.Dial if err != nil { return N.ReportHandshakeFailure(conn, err) } - err = N.ReportHandshakeSuccess(conn) + err = N.ReportConnHandshakeSuccess(conn, outConn) if err != nil { outConn.Close() return err @@ -117,14 +117,14 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, if err != nil { return N.ReportHandshakeFailure(conn, err) } - err = N.ReportHandshakeSuccess(conn) + err = N.ReportPacketConnHandshakeSuccess(conn, outConn) if err != nil { outConn.Close() return err } if destinationAddress.IsValid() { if metadata.Destination.IsFqdn() { - if metadata.InboundOptions.UDPDisableDomainUnmapping { + if metadata.UDPDisableDomainUnmapping { outConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) } else { outConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) @@ -165,7 +165,7 @@ func NewDirectPacketConnection(ctx context.Context, router adapter.Router, this if err != nil { return N.ReportHandshakeFailure(conn, err) } - err = N.ReportHandshakeSuccess(conn) + err = N.ReportPacketConnHandshakeSuccess(conn, outConn) if err != nil { outConn.Close() return err diff --git a/outbound/direct.go b/outbound/direct.go index c873941c11..415a72f370 100644 --- a/outbound/direct.go +++ b/outbound/direct.go @@ -30,7 +30,7 @@ type Direct struct { fallbackDelay time.Duration overrideOption int overrideDestination M.Socksaddr - loopBack *loopBackDetector + // loopBack *loopBackDetector } func NewDirect(router adapter.Router, logger log.ContextLogger, tag string, options option.DirectOutboundOptions) (*Direct, error) { @@ -51,7 +51,7 @@ func NewDirect(router adapter.Router, logger log.ContextLogger, tag string, opti domainStrategy: dns.DomainStrategy(options.DomainStrategy), fallbackDelay: time.Duration(options.FallbackDelay), dialer: outboundDialer, - loopBack: newLoopBackDetector(router), + // loopBack: newLoopBackDetector(router), } if options.ProxyProtocol != 0 { return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0") @@ -90,11 +90,12 @@ func (h *Direct) DialContext(ctx context.Context, network string, destination M. case N.NetworkUDP: h.logger.InfoContext(ctx, "outbound packet connection to ", destination) } - conn, err := h.dialer.DialContext(ctx, network, destination) + /*conn, err := h.dialer.DialContext(ctx, network, destination) if err != nil { return nil, err } - return h.loopBack.NewConn(conn), nil + return h.loopBack.NewConn(conn), nil*/ + return h.dialer.DialContext(ctx, network, destination) } func (h *Direct) DialParallel(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr) (net.Conn, error) { @@ -148,14 +149,14 @@ func (h *Direct) ListenPacket(ctx context.Context, destination M.Socksaddr) (net if err != nil { return nil, err } - conn = h.loopBack.NewPacketConn(bufio.NewPacketConn(conn), destination) + // conn = h.loopBack.NewPacketConn(bufio.NewPacketConn(conn), destination) if originDestination != destination { conn = bufio.NewNATPacketConn(bufio.NewPacketConn(conn), destination, originDestination) } return conn, nil } -func (h *Direct) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +/*func (h *Direct) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if h.loopBack.CheckConn(metadata.Source.AddrPort(), M.AddrPortFromNet(conn.LocalAddr())) { return E.New("reject loopback connection to ", metadata.Destination) } @@ -168,3 +169,4 @@ func (h *Direct) NewPacketConnection(ctx context.Context, conn N.PacketConn, met } return NewPacketConnection(ctx, h, conn, metadata) } +*/ diff --git a/outbound/dns.go b/outbound/dns.go index 6ad4a0f8ce..08661a99aa 100644 --- a/outbound/dns.go +++ b/outbound/dns.go @@ -45,6 +45,7 @@ func (d *DNS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.Pa return nil, os.ErrInvalid } +// Deprecated func (d *DNS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { metadata.Destination = M.Socksaddr{} defer conn.Close() @@ -97,6 +98,7 @@ func (d *DNS) handleConnection(ctx context.Context, conn net.Conn, metadata adap return nil } +// Deprecated func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { metadata.Destination = M.Socksaddr{} var reader N.PacketReader = conn diff --git a/outbound/http.go b/outbound/http.go index cbcbce075a..6f15afb5dc 100644 --- a/outbound/http.go +++ b/outbound/http.go @@ -64,11 +64,3 @@ func (h *HTTP) DialContext(ctx context.Context, network string, destination M.So func (h *HTTP) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return nil, os.ErrInvalid } - -func (h *HTTP) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return NewConnection(ctx, h, conn, metadata) -} - -func (h *HTTP) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return os.ErrInvalid -} diff --git a/outbound/hysteria.go b/outbound/hysteria.go index 50383af5d4..f3c30739cf 100644 --- a/outbound/hysteria.go +++ b/outbound/hysteria.go @@ -122,14 +122,6 @@ func (h *Hysteria) ListenPacket(ctx context.Context, destination M.Socksaddr) (n return h.client.ListenPacket(ctx, destination) } -func (h *Hysteria) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return NewConnection(ctx, h, conn, metadata) -} - -func (h *Hysteria) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return NewPacketConnection(ctx, h, conn, metadata) -} - func (h *Hysteria) InterfaceUpdated() { h.client.CloseWithError(E.New("network changed")) } diff --git a/outbound/hysteria2.go b/outbound/hysteria2.go index 9079e40308..5e46f6a866 100644 --- a/outbound/hysteria2.go +++ b/outbound/hysteria2.go @@ -108,14 +108,6 @@ func (h *Hysteria2) ListenPacket(ctx context.Context, destination M.Socksaddr) ( return h.client.ListenPacket(ctx) } -func (h *Hysteria2) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return NewConnection(ctx, h, conn, metadata) -} - -func (h *Hysteria2) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return NewPacketConnection(ctx, h, conn, metadata) -} - func (h *Hysteria2) InterfaceUpdated() { h.client.CloseWithError(E.New("network changed")) } diff --git a/outbound/proxy.go b/outbound/proxy.go index fbc4848180..38c1845392 100644 --- a/outbound/proxy.go +++ b/outbound/proxy.go @@ -94,6 +94,9 @@ func (l *ProxyListener) acceptLoop() { } } +// TODO: migrate to new api +// +//nolint:staticcheck func (l *ProxyListener) accept(ctx context.Context, conn *net.TCPConn) error { return socks.HandleConnection(ctx, conn, l.authenticator, l, M.Metadata{}) } diff --git a/outbound/selector.go b/outbound/selector.go index 7326f58f0f..64e6a2f936 100644 --- a/outbound/selector.go +++ b/outbound/selector.go @@ -146,14 +146,26 @@ func (s *Selector) ListenPacket(ctx context.Context, destination M.Socksaddr) (n return s.interruptGroup.NewPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil } +// TODO +// Deprecated func (s *Selector) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { ctx = interrupt.ContextWithIsExternalConnection(ctx) - return s.selected.NewConnection(ctx, conn, metadata) + if legacyHandler, ok := s.selected.(adapter.ConnectionHandler); ok { + return legacyHandler.NewConnection(ctx, conn, metadata) + } else { + return NewConnection(ctx, s.selected, conn, metadata) + } } +// TODO +// Deprecated func (s *Selector) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { ctx = interrupt.ContextWithIsExternalConnection(ctx) - return s.selected.NewPacketConnection(ctx, conn, metadata) + if legacyHandler, ok := s.selected.(adapter.PacketConnectionHandler); ok { + return legacyHandler.NewPacketConnection(ctx, conn, metadata) + } else { + return NewPacketConnection(ctx, s.selected, conn, metadata) + } } func RealTag(detour adapter.Outbound) string { diff --git a/outbound/shadowsocks.go b/outbound/shadowsocks.go index 9f8c1cbdb2..153542742a 100644 --- a/outbound/shadowsocks.go +++ b/outbound/shadowsocks.go @@ -125,14 +125,6 @@ func (h *Shadowsocks) ListenPacket(ctx context.Context, destination M.Socksaddr) } } -func (h *Shadowsocks) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return NewConnection(ctx, h, conn, metadata) -} - -func (h *Shadowsocks) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return NewPacketConnection(ctx, h, conn, metadata) -} - func (h *Shadowsocks) InterfaceUpdated() { if h.multiplexDialer != nil { h.multiplexDialer.Reset() diff --git a/outbound/shadowtls.go b/outbound/shadowtls.go index 38de8c0baf..ff1b9d6c45 100644 --- a/outbound/shadowtls.go +++ b/outbound/shadowtls.go @@ -106,11 +106,3 @@ func (h *ShadowTLS) DialContext(ctx context.Context, network string, destination func (h *ShadowTLS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return nil, os.ErrInvalid } - -func (h *ShadowTLS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return NewConnection(ctx, h, conn, metadata) -} - -func (h *ShadowTLS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return os.ErrInvalid -} diff --git a/outbound/socks.go b/outbound/socks.go index f4757467c5..575d6eb3dd 100644 --- a/outbound/socks.go +++ b/outbound/socks.go @@ -113,6 +113,8 @@ func (h *Socks) ListenPacket(ctx context.Context, destination M.Socksaddr) (net. return h.client.ListenPacket(ctx, destination) } +// TODO +// Deprecated func (h *Socks) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if h.resolve { return NewDirectConnection(ctx, h.router, h, conn, metadata, dns.DomainStrategyUseIPv4) @@ -121,6 +123,8 @@ func (h *Socks) NewConnection(ctx context.Context, conn net.Conn, metadata adapt } } +// TODO +// Deprecated func (h *Socks) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { if h.resolve { return NewDirectPacketConnection(ctx, h.router, h, conn, metadata, dns.DomainStrategyUseIPv4) diff --git a/outbound/ssh.go b/outbound/ssh.go index ce62b00481..28abe9a5e3 100644 --- a/outbound/ssh.go +++ b/outbound/ssh.go @@ -199,11 +199,3 @@ func (s *SSH) DialContext(ctx context.Context, network string, destination M.Soc func (s *SSH) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return nil, os.ErrInvalid } - -func (s *SSH) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return NewConnection(ctx, s, conn, metadata) -} - -func (s *SSH) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return os.ErrInvalid -} diff --git a/outbound/tor.go b/outbound/tor.go index 8ae73a66fa..ccc0c0cfe5 100644 --- a/outbound/tor.go +++ b/outbound/tor.go @@ -211,11 +211,3 @@ func (t *Tor) DialContext(ctx context.Context, network string, destination M.Soc func (t *Tor) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return nil, os.ErrInvalid } - -func (t *Tor) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return NewConnection(ctx, t, conn, metadata) -} - -func (t *Tor) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return os.ErrInvalid -} diff --git a/outbound/trojan.go b/outbound/trojan.go index bde251d00a..ee0b2a4b9e 100644 --- a/outbound/trojan.go +++ b/outbound/trojan.go @@ -99,14 +99,6 @@ func (h *Trojan) ListenPacket(ctx context.Context, destination M.Socksaddr) (net } } -func (h *Trojan) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return NewConnection(ctx, h, conn, metadata) -} - -func (h *Trojan) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return NewPacketConnection(ctx, h, conn, metadata) -} - func (h *Trojan) InterfaceUpdated() { if h.transport != nil { h.transport.Close() diff --git a/outbound/tuic.go b/outbound/tuic.go index c098332375..aaf998b16e 100644 --- a/outbound/tuic.go +++ b/outbound/tuic.go @@ -136,14 +136,6 @@ func (h *TUIC) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.P } } -func (h *TUIC) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return NewConnection(ctx, h, conn, metadata) -} - -func (h *TUIC) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return NewPacketConnection(ctx, h, conn, metadata) -} - func (h *TUIC) InterfaceUpdated() { _ = h.client.CloseWithError(E.New("network changed")) } diff --git a/outbound/urltest.go b/outbound/urltest.go index c6e38ec5a8..564a0ddc3a 100644 --- a/outbound/urltest.go +++ b/outbound/urltest.go @@ -167,11 +167,15 @@ func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (ne return nil, err } +// TODO +// Deprecated func (s *URLTest) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { ctx = interrupt.ContextWithIsExternalConnection(ctx) return NewConnection(ctx, s, conn, metadata) } +// TODO +// Deprecated func (s *URLTest) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { ctx = interrupt.ContextWithIsExternalConnection(ctx) return NewPacketConnection(ctx, s, conn, metadata) diff --git a/outbound/vless.go b/outbound/vless.go index a81678f015..536a1e8fba 100644 --- a/outbound/vless.go +++ b/outbound/vless.go @@ -118,14 +118,6 @@ func (h *VLESS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net. } } -func (h *VLESS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return NewConnection(ctx, h, conn, metadata) -} - -func (h *VLESS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return NewPacketConnection(ctx, h, conn, metadata) -} - func (h *VLESS) InterfaceUpdated() { if h.transport != nil { h.transport.Close() diff --git a/outbound/vmess.go b/outbound/vmess.go index 3149729c00..126d2fd05f 100644 --- a/outbound/vmess.go +++ b/outbound/vmess.go @@ -146,14 +146,6 @@ func (h *VMess) ListenPacket(ctx context.Context, destination M.Socksaddr) (net. } } -func (h *VMess) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return NewConnection(ctx, h, conn, metadata) -} - -func (h *VMess) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return NewPacketConnection(ctx, h, conn, metadata) -} - type vmessDialer VMess func (h *vmessDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { diff --git a/outbound/wireguard.go b/outbound/wireguard.go index 6ecf536dc1..e6e4b8f8e4 100644 --- a/outbound/wireguard.go +++ b/outbound/wireguard.go @@ -241,10 +241,14 @@ func (w *WireGuard) ListenPacket(ctx context.Context, destination M.Socksaddr) ( return w.tunDevice.ListenPacket(ctx, destination) } +// TODO +// Deprecated func (w *WireGuard) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { return NewDirectConnection(ctx, w.router, w, conn, metadata, dns.DomainStrategyAsIS) } +// TODO +// Deprecated func (w *WireGuard) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { return NewDirectPacketConnection(ctx, w.router, w, conn, metadata, dns.DomainStrategyAsIS) } diff --git a/route/router_geo_resources.go b/route/geo_resources.go similarity index 98% rename from route/router_geo_resources.go rename to route/geo_resources.go index d4f65cc801..91c06796bc 100644 --- a/route/router_geo_resources.go +++ b/route/geo_resources.go @@ -13,6 +13,7 @@ import ( "github.com/sagernet/sing-box/common/geosite" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/deprecated" + R "github.com/sagernet/sing-box/route/rule" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/rw" @@ -32,7 +33,7 @@ func (r *Router) LoadGeosite(code string) (adapter.Rule, error) { if err != nil { return nil, err } - rule, err = NewDefaultRule(r.ctx, r, nil, geosite.Compile(items)) + rule, err = R.NewDefaultRule(r.ctx, r, nil, geosite.Compile(items)) if err != nil { return nil, err } diff --git a/route/route.go b/route/route.go new file mode 100644 index 0000000000..86d4d95cc9 --- /dev/null +++ b/route/route.go @@ -0,0 +1,583 @@ +package route + +import ( + "context" + "errors" + "net" + "net/netip" + "os" + "os/user" + "strings" + "syscall" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/conntrack" + "github.com/sagernet/sing-box/common/process" + "github.com/sagernet/sing-box/common/sniff" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/outbound" + "github.com/sagernet/sing-box/route/rule" + "github.com/sagernet/sing-dns" + "github.com/sagernet/sing-mux" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-vmess" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/bufio/deadline" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/uot" +) + +// Deprecated: use RouteConnectionEx instead. +func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + return r.routeConnection(ctx, conn, metadata, nil) +} + +func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := r.routeConnection(ctx, conn, metadata, onClose) + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + if E.IsClosedOrCanceled(err) { + r.logger.DebugContext(ctx, "connection closed: ", err) + } else { + r.logger.ErrorContext(ctx, err) + } + } +} + +func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { + if r.pauseManager.IsDevicePaused() { + return E.New("reject connection to ", metadata.Destination, " while device paused") + } + + if metadata.InboundDetour != "" { + if metadata.LastInbound == metadata.InboundDetour { + return E.New("routing loop on detour: ", metadata.InboundDetour) + } + detour := r.inboundByTag[metadata.InboundDetour] + if detour == nil { + return E.New("inbound detour not found: ", metadata.InboundDetour) + } + injectable, isInjectable := detour.(adapter.TCPInjectableInbound) + if !isInjectable { + return E.New("inbound detour is not TCP injectable: ", metadata.InboundDetour) + } + metadata.LastInbound = metadata.Inbound + metadata.Inbound = metadata.InboundDetour + metadata.InboundDetour = "" + injectable.NewConnectionEx(ctx, conn, metadata, onClose) + return nil + } + conntrack.KillerCheck() + metadata.Network = N.NetworkTCP + switch metadata.Destination.Fqdn { + case mux.Destination.Fqdn: + return E.New("global multiplex is deprecated since sing-box v1.7.0, enable multiplex in inbound options instead.") + case vmess.MuxDestination.Fqdn: + return E.New("global multiplex (v2ray legacy) not supported since sing-box v1.7.0.") + case uot.MagicAddress: + return E.New("global UoT not supported since sing-box v1.7.0.") + case uot.LegacyMagicAddress: + return E.New("global UoT (legacy) not supported since sing-box v1.7.0.") + } + if deadline.NeedAdditionalReadDeadline(conn) { + conn = deadline.NewConn(conn) + } + selectedRule, _, buffers, err := r.matchRule(ctx, &metadata, conn, nil, -1) + if err != nil { + return err + } + var selectedOutbound adapter.Outbound + var selectReturn bool + if selectedRule != nil { + switch action := selectedRule.Action().(type) { + case *rule.RuleActionRoute: + var loaded bool + selectedOutbound, loaded = r.Outbound(action.Outbound) + if !loaded { + buf.ReleaseMulti(buffers) + return E.New("outbound not found: ", action.Outbound) + } + case *rule.RuleActionReturn: + selectReturn = true + case *rule.RuleActionReject: + buf.ReleaseMulti(buffers) + var rejectErr error + switch action.Method { + case C.RuleActionRejectMethodDefault: + rejectErr = os.ErrClosed + case C.RuleActionRejectMethodPortUnreachable: + rejectErr = syscall.ECONNREFUSED + case C.RuleActionRejectMethodDrop: + rejectErr = tun.ErrDrop + } + N.CloseOnHandshakeFailure(conn, onClose, rejectErr) + return nil + } + } + if selectedRule == nil || selectReturn { + if r.defaultOutboundForConnection == nil { + buf.ReleaseMulti(buffers) + return E.New("missing default outbound with TCP support") + } + selectedOutbound = r.defaultOutboundForConnection + } + if !common.Contains(selectedOutbound.Network(), N.NetworkTCP) { + buf.ReleaseMulti(buffers) + return E.New("TCP is not supported by outbound: ", selectedOutbound.Tag()) + } + for _, buffer := range buffers { + conn = bufio.NewCachedConn(conn, buffer) + } + if r.clashServer != nil { + trackerConn, tracker := r.clashServer.RoutedConnection(ctx, conn, metadata, selectedRule) + defer tracker.Leave() + conn = trackerConn + } + if r.v2rayServer != nil { + if statsService := r.v2rayServer.StatsService(); statsService != nil { + conn = statsService.RoutedConnection(metadata.Inbound, selectedOutbound.Tag(), metadata.User, conn) + } + } + legacyOutbound, isLegacy := selectedOutbound.(adapter.ConnectionHandler) + if isLegacy { + err = legacyOutbound.NewConnection(ctx, conn, metadata) + if err != nil { + conn.Close() + if onClose != nil { + onClose(err) + } + return E.Cause(err, "outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]") + } else { + if onClose != nil { + onClose(nil) + } + } + return nil + } + // TODO + err = outbound.NewConnection(ctx, selectedOutbound, conn, metadata) + if err != nil { + conn.Close() + if onClose != nil { + onClose(err) + } + return E.Cause(err, "outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]") + } else { + if onClose != nil { + onClose(nil) + } + } + return nil +} + +func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + err := r.routePacketConnection(ctx, conn, metadata, nil) + if err != nil { + conn.Close() + if E.IsClosedOrCanceled(err) { + r.logger.DebugContext(ctx, "connection closed: ", err) + } else { + r.logger.ErrorContext(ctx, err) + } + } + return nil +} + +func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := r.routePacketConnection(ctx, conn, metadata, onClose) + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + if E.IsClosedOrCanceled(err) { + r.logger.DebugContext(ctx, "connection closed: ", err) + } else { + r.logger.ErrorContext(ctx, err) + } + } else if onClose != nil { + onClose(nil) + } +} + +func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { + if r.pauseManager.IsDevicePaused() { + return E.New("reject packet connection to ", metadata.Destination, " while device paused") + } + if metadata.InboundDetour != "" { + if metadata.LastInbound == metadata.InboundDetour { + return E.New("routing loop on detour: ", metadata.InboundDetour) + } + detour := r.inboundByTag[metadata.InboundDetour] + if detour == nil { + return E.New("inbound detour not found: ", metadata.InboundDetour) + } + injectable, isInjectable := detour.(adapter.UDPInjectableInbound) + if !isInjectable { + return E.New("inbound detour is not UDP injectable: ", metadata.InboundDetour) + } + metadata.LastInbound = metadata.Inbound + metadata.Inbound = metadata.InboundDetour + metadata.InboundDetour = "" + injectable.NewPacketConnectionEx(ctx, conn, metadata, onClose) + return nil + } + conntrack.KillerCheck() + + // TODO: move to UoT + metadata.Network = N.NetworkUDP + + // Currently we don't have deadline usages for UDP connections + /*if deadline.NeedAdditionalReadDeadline(conn) { + conn = deadline.NewPacketConn(bufio.NewNetPacketConn(conn)) + }*/ + + selectedRule, _, buffers, err := r.matchRule(ctx, &metadata, nil, conn, -1) + if err != nil { + return err + } + var selectedOutbound adapter.Outbound + var selectReturn bool + if selectedRule != nil { + switch action := selectedRule.Action().(type) { + case *rule.RuleActionRoute: + var loaded bool + selectedOutbound, loaded = r.Outbound(action.Outbound) + if !loaded { + buf.ReleaseMulti(buffers) + return E.New("outbound not found: ", action.Outbound) + } + metadata.UDPDisableDomainUnmapping = action.UDPDisableDomainUnmapping + case *rule.RuleActionReturn: + selectReturn = true + case *rule.RuleActionReject: + buf.ReleaseMulti(buffers) + N.CloseOnHandshakeFailure(conn, onClose, syscall.ECONNREFUSED) + return nil + } + } + if selectedRule == nil || selectReturn { + if r.defaultOutboundForPacketConnection == nil { + buf.ReleaseMulti(buffers) + return E.New("missing default outbound with UDP support") + } + selectedOutbound = r.defaultOutboundForPacketConnection + } + if !common.Contains(selectedOutbound.Network(), N.NetworkUDP) { + buf.ReleaseMulti(buffers) + return E.New("UDP is not supported by outbound: ", selectedOutbound.Tag()) + } + for _, buffer := range buffers { + // TODO: check if metadata.Destination == packet destination + conn = bufio.NewCachedPacketConn(conn, buffer, metadata.Destination) + } + if r.clashServer != nil { + trackerConn, tracker := r.clashServer.RoutedPacketConnection(ctx, conn, metadata, selectedRule) + defer tracker.Leave() + conn = trackerConn + } + if r.v2rayServer != nil { + if statsService := r.v2rayServer.StatsService(); statsService != nil { + conn = statsService.RoutedPacketConnection(metadata.Inbound, selectedOutbound.Tag(), metadata.User, conn) + } + } + if metadata.FakeIP { + conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination) + } + legacyOutbound, isLegacy := selectedOutbound.(adapter.PacketConnectionHandler) + if isLegacy { + err = legacyOutbound.NewPacketConnection(ctx, conn, metadata) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + return E.Cause(err, "outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]") + } + return nil + } + // TODO + err = outbound.NewPacketConnection(ctx, selectedOutbound, conn, metadata) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + return E.Cause(err, "outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]") + } + return nil +} + +func (r *Router) matchRule( + ctx context.Context, metadata *adapter.InboundContext, + inputConn net.Conn, inputPacketConn N.PacketConn, ruleIndex int, +) (selectedRule adapter.Rule, selectedRuleIndex int, buffers []*buf.Buffer, fatalErr error) { + if r.processSearcher != nil && metadata.ProcessInfo == nil { + var originDestination netip.AddrPort + if metadata.OriginDestination.IsValid() { + originDestination = metadata.OriginDestination.AddrPort() + } else if metadata.Destination.IsIP() { + originDestination = metadata.Destination.AddrPort() + } + processInfo, fErr := process.FindProcessInfo(r.processSearcher, ctx, metadata.Network, metadata.Source.AddrPort(), originDestination) + if fErr != nil { + r.logger.InfoContext(ctx, "failed to search process: ", fErr) + } else { + if processInfo.ProcessPath != "" { + r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath) + } else if processInfo.PackageName != "" { + r.logger.InfoContext(ctx, "found package name: ", processInfo.PackageName) + } else if processInfo.UserId != -1 { + if /*needUserName &&*/ true { + osUser, _ := user.LookupId(F.ToString(processInfo.UserId)) + if osUser != nil { + processInfo.User = osUser.Username + } + } + if processInfo.User != "" { + r.logger.InfoContext(ctx, "found user: ", processInfo.User) + } else { + r.logger.InfoContext(ctx, "found user id: ", processInfo.UserId) + } + } + metadata.ProcessInfo = processInfo + } + } + if r.fakeIPStore != nil && r.fakeIPStore.Contains(metadata.Destination.Addr) { + domain, loaded := r.fakeIPStore.Lookup(metadata.Destination.Addr) + if !loaded { + fatalErr = E.New("missing fakeip record, try to configure experimental.cache_file") + return + } + metadata.OriginDestination = metadata.Destination + metadata.Destination = M.Socksaddr{ + Fqdn: domain, + Port: metadata.Destination.Port, + } + metadata.FakeIP = true + r.logger.DebugContext(ctx, "found fakeip domain: ", domain) + } + if r.dnsReverseMapping != nil && metadata.Domain == "" { + domain, loaded := r.dnsReverseMapping.Query(metadata.Destination.Addr) + if loaded { + metadata.Domain = domain + r.logger.DebugContext(ctx, "found reserve mapped domain: ", metadata.Domain) + } + } + if metadata.Destination.IsIPv4() { + metadata.IPVersion = 4 + } else if metadata.Destination.IsIPv6() { + metadata.IPVersion = 6 + } + + //nolint:staticcheck + if metadata.InboundOptions != common.DefaultValue[option.InboundOptions]() { + if metadata.InboundOptions.SniffEnabled { + newBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{ + OverrideDestination: metadata.InboundOptions.SniffOverrideDestination, + Timeout: time.Duration(metadata.InboundOptions.SniffTimeout), + }, inputConn, inputPacketConn) + if newErr != nil { + fatalErr = newErr + return + } + buffers = append(buffers, newBuffers...) + } + if dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) != dns.DomainStrategyAsIS { + fatalErr = r.actionResolve(ctx, metadata, &rule.RuleActionResolve{ + Strategy: dns.DomainStrategy(metadata.InboundOptions.DomainStrategy), + }) + if fatalErr != nil { + return + } + } + if metadata.InboundOptions.UDPDisableDomainUnmapping { + metadata.UDPDisableDomainUnmapping = true + } + metadata.InboundOptions = option.InboundOptions{} + } + +match: + for ruleIndex < len(r.rules) { + rules := r.rules + if ruleIndex != -1 { + rules = rules[ruleIndex+1:] + } + var ( + currentRule adapter.Rule + currentRuleIndex int + matched bool + ) + for currentRuleIndex, currentRule = range rules { + if currentRule.Match(metadata) { + matched = true + break + } + } + if !matched { + break + } + r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) + switch action := currentRule.Action().(type) { + case *rule.RuleActionSniff: + newBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn) + if newErr != nil { + fatalErr = newErr + return + } + buffers = append(buffers, newBuffers...) + case *rule.RuleActionResolve: + fatalErr = r.actionResolve(ctx, metadata, action) + if fatalErr != nil { + return + } + default: + selectedRule = currentRule + selectedRuleIndex = currentRuleIndex + break match + } + ruleIndex = currentRuleIndex + } + if metadata.Destination.Addr.IsUnspecified() { + newBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{}, inputConn, inputPacketConn) + if newErr != nil { + fatalErr = newErr + return + } + buffers = append(buffers, newBuffers...) + } + return +} + +func (r *Router) actionSniff( + ctx context.Context, metadata *adapter.InboundContext, action *rule.RuleActionSniff, + inputConn net.Conn, inputPacketConn N.PacketConn, +) (buffers []*buf.Buffer, fatalErr error) { + if sniff.Skip(metadata) { + return + } else if inputConn != nil && len(action.StreamSniffers) > 0 { + buffer := buf.NewPacket() + err := sniff.PeekStream( + ctx, + metadata, + inputConn, + buffer, + action.Timeout, + action.StreamSniffers..., + ) + if err == nil { + //goland:noinspection GoDeprecation + if action.OverrideDestination && M.IsDomainName(metadata.Domain) { + metadata.Destination = M.Socksaddr{ + Fqdn: metadata.Domain, + Port: metadata.Destination.Port, + } + } + if metadata.Domain != "" && metadata.Client != "" { + r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol, ", domain: ", metadata.Domain, ", client: ", metadata.Client) + } else if metadata.Domain != "" { + r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol, ", domain: ", metadata.Domain) + } else { + r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol) + } + } + if !buffer.IsEmpty() { + buffers = append(buffers, buffer) + } else { + buffer.Release() + } + } else if inputPacketConn != nil && len(action.PacketSniffers) > 0 { + for { + var ( + buffer = buf.NewPacket() + destination M.Socksaddr + done = make(chan struct{}) + err error + ) + go func() { + sniffTimeout := C.ReadPayloadTimeout + if action.Timeout > 0 { + sniffTimeout = action.Timeout + } + inputPacketConn.SetReadDeadline(time.Now().Add(sniffTimeout)) + destination, err = inputPacketConn.ReadPacket(buffer) + inputPacketConn.SetReadDeadline(time.Time{}) + close(done) + }() + select { + case <-done: + case <-ctx.Done(): + inputPacketConn.Close() + fatalErr = ctx.Err() + return + } + if err != nil { + buffer.Release() + if !errors.Is(err, os.ErrDeadlineExceeded) { + fatalErr = err + return + } + } else { + // TODO: maybe always override destination + if metadata.Destination.Addr.IsUnspecified() { + metadata.Destination = destination + } + if len(buffers) > 0 { + err = sniff.PeekPacket( + ctx, + metadata, + buffer.Bytes(), + sniff.QUICClientHello, + ) + } else { + err = sniff.PeekPacket( + ctx, metadata, + buffer.Bytes(), + action.PacketSniffers..., + ) + } + buffers = append(buffers, buffer) + if E.IsMulti(err, sniff.ErrClientHelloFragmented) && len(buffers) == 0 { + r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello") + continue + } + if metadata.Protocol != "" { + //goland:noinspection GoDeprecation + if action.OverrideDestination && M.IsDomainName(metadata.Domain) { + metadata.Destination = M.Socksaddr{ + Fqdn: metadata.Domain, + Port: metadata.Destination.Port, + } + } + if metadata.Domain != "" && metadata.Client != "" { + r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain, ", client: ", metadata.Client) + } else if metadata.Domain != "" { + r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain) + } else if metadata.Client != "" { + r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", client: ", metadata.Client) + } else { + r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol) + } + } + } + break + } + } + return +} + +func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundContext, action *rule.RuleActionResolve) error { + if metadata.Destination.IsFqdn() { + // TODO: check if WithContext is necessary + addresses, err := r.Lookup(adapter.WithContext(ctx, metadata), metadata.Destination.Fqdn, action.Strategy) + if err != nil { + return err + } + metadata.DestinationAddresses = addresses + r.dnsLogger.DebugContext(ctx, "resolved [", strings.Join(F.MapToString(metadata.DestinationAddresses), " "), "]") + if metadata.Destination.IsIPv4() { + metadata.IPVersion = 4 + } else if metadata.Destination.IsIPv6() { + metadata.IPVersion = 6 + } + } + return nil +} diff --git a/route/router_dns.go b/route/route_dns.go similarity index 75% rename from route/router_dns.go rename to route/route_dns.go index ead8c28943..43eb61e6df 100644 --- a/route/router_dns.go +++ b/route/route_dns.go @@ -8,6 +8,7 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + R "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing-dns" "github.com/sagernet/sing/common/cache" E "github.com/sagernet/sing/common/exceptions" @@ -36,15 +37,16 @@ func (m *DNSReverseMapping) Query(address netip.Addr) (string, bool) { return domain, loaded } -func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, index int, isAddressQuery bool) (context.Context, dns.Transport, dns.DomainStrategy, adapter.DNSRule, int) { +func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, isAddressQuery bool) (dns.Transport, dns.QueryOptions, adapter.DNSRule, int) { metadata := adapter.ContextFrom(ctx) if metadata == nil { panic("no context") } - if index < len(r.dnsRules) { + var options dns.QueryOptions + if ruleIndex < len(r.dnsRules) { dnsRules := r.dnsRules - if index != -1 { - dnsRules = dnsRules[index+1:] + if ruleIndex != -1 { + dnsRules = dnsRules[ruleIndex+1:] } for currentRuleIndex, rule := range dnsRules { if rule.WithAddressLimit() && !isAddressQuery { @@ -52,43 +54,42 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, index int, isAd } metadata.ResetRuleCache() if rule.Match(metadata) { - detour := rule.Outbound() - transport, loaded := r.transportMap[detour] - if !loaded { - r.dnsLogger.ErrorContext(ctx, "transport not found: ", detour) - continue + displayRuleIndex := currentRuleIndex + if displayRuleIndex != -1 { + displayRuleIndex += displayRuleIndex + 1 } - _, isFakeIP := transport.(adapter.FakeIPTransport) - if isFakeIP && !allowFakeIP { - continue - } - ruleIndex := currentRuleIndex - if index != -1 { - ruleIndex += index + 1 - } - r.dnsLogger.DebugContext(ctx, "match[", ruleIndex, "] ", rule.String(), " => ", detour) - if isFakeIP || rule.DisableCache() { - ctx = dns.ContextWithDisableCache(ctx, true) - } - if rewriteTTL := rule.RewriteTTL(); rewriteTTL != nil { - ctx = dns.ContextWithRewriteTTL(ctx, *rewriteTTL) - } - if clientSubnet := rule.ClientSubnet(); clientSubnet != nil { - ctx = dns.ContextWithClientSubnet(ctx, *clientSubnet) - } - if domainStrategy, dsLoaded := r.transportDomainStrategy[transport]; dsLoaded { - return ctx, transport, domainStrategy, rule, ruleIndex + if routeAction, isRoute := rule.Action().(*R.RuleActionDNSRoute); isRoute { + transport, loaded := r.transportMap[routeAction.Server] + if !loaded { + r.dnsLogger.ErrorContext(ctx, "transport not found: ", routeAction.Server) + continue + } + _, isFakeIP := transport.(adapter.FakeIPTransport) + if isFakeIP && !allowFakeIP { + continue + } + options.DisableCache = isFakeIP || routeAction.DisableCache + options.RewriteTTL = routeAction.RewriteTTL + options.ClientSubnet = routeAction.ClientSubnet + if domainStrategy, dsLoaded := r.transportDomainStrategy[transport]; dsLoaded { + options.Strategy = domainStrategy + } else { + options.Strategy = r.defaultDomainStrategy + } + r.dnsLogger.DebugContext(ctx, "match[", displayRuleIndex, "] ", rule.String(), " => ", rule.Action()) + return transport, options, rule, currentRuleIndex } else { - return ctx, transport, r.defaultDomainStrategy, rule, ruleIndex + return nil, options, rule, currentRuleIndex } } } } if domainStrategy, dsLoaded := r.transportDomainStrategy[r.defaultTransport]; dsLoaded { - return ctx, r.defaultTransport, domainStrategy, nil, -1 + options.Strategy = domainStrategy } else { - return ctx, r.defaultTransport, r.defaultDomainStrategy, nil, -1 + options.Strategy = r.defaultDomainStrategy } + return r.defaultTransport, options, nil, -1 } func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { @@ -117,21 +118,18 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er metadata.Domain = fqdnToDomain(message.Question[0].Name) } var ( - strategy dns.DomainStrategy + options dns.QueryOptions rule adapter.DNSRule ruleIndex int ) ruleIndex = -1 for { - var ( - dnsCtx context.Context - addressLimit bool - ) - dnsCtx, transport, strategy, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message)) - dnsCtx = adapter.OverrideContext(dnsCtx) + dnsCtx := adapter.OverrideContext(ctx) + var addressLimit bool + transport, options, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message)) if rule != nil && rule.WithAddressLimit() { addressLimit = true - response, err = r.dnsClient.ExchangeWithResponseCheck(dnsCtx, transport, message, strategy, func(response *mDNS.Msg) bool { + response, err = r.dnsClient.ExchangeWithResponseCheck(dnsCtx, transport, message, options, func(response *mDNS.Msg) bool { addresses, addrErr := dns.MessageToAddresses(response) if addrErr != nil { return false @@ -141,7 +139,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er }) } else { addressLimit = false - response, err = r.dnsClient.Exchange(dnsCtx, transport, message, strategy) + response, err = r.dnsClient.Exchange(dnsCtx, transport, message, options) } var rejected bool if err != nil { @@ -199,31 +197,28 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS metadata.Destination = M.Socksaddr{} metadata.Domain = domain var ( - transport dns.Transport - transportStrategy dns.DomainStrategy - rule adapter.DNSRule - ruleIndex int + transport dns.Transport + options dns.QueryOptions + rule adapter.DNSRule + ruleIndex int ) ruleIndex = -1 for { - var ( - dnsCtx context.Context - addressLimit bool - ) - dnsCtx, transport, transportStrategy, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true) - dnsCtx = adapter.OverrideContext(dnsCtx) - if strategy == dns.DomainStrategyAsIS { - strategy = transportStrategy + dnsCtx := adapter.OverrideContext(ctx) + var addressLimit bool + transport, options, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true) + if strategy != dns.DomainStrategyAsIS { + options.Strategy = strategy } if rule != nil && rule.WithAddressLimit() { addressLimit = true - responseAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, strategy, func(responseAddrs []netip.Addr) bool { + responseAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, options, func(responseAddrs []netip.Addr) bool { metadata.DestinationAddresses = responseAddrs return rule.MatchAddressLimit(metadata) }) } else { addressLimit = false - responseAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, strategy) + responseAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, options) } if err != nil { if errors.Is(err, dns.ErrResponseRejectedCached) { diff --git a/route/router.go b/route/router.go index 110809f0d2..05349485d2 100644 --- a/route/router.go +++ b/route/router.go @@ -3,11 +3,9 @@ package route import ( "context" "errors" - "net" "net/netip" "net/url" "os" - "os/user" "runtime" "strings" "syscall" @@ -19,22 +17,16 @@ import ( "github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-box/common/geosite" "github.com/sagernet/sing-box/common/process" - "github.com/sagernet/sing-box/common/sniff" "github.com/sagernet/sing-box/common/taskmonitor" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing-box/outbound" + R "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing-box/transport/fakeip" "github.com/sagernet/sing-dns" - "github.com/sagernet/sing-mux" "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-vmess" "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" - "github.com/sagernet/sing/common/bufio/deadline" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" @@ -42,7 +34,6 @@ import ( N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/task" - "github.com/sagernet/sing/common/uot" "github.com/sagernet/sing/common/winpowrprof" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" @@ -154,14 +145,14 @@ func NewRouter( Logger: router.dnsLogger, }) for i, ruleOptions := range options.Rules { - routeRule, err := NewRule(ctx, router, router.logger, ruleOptions, true) + routeRule, err := R.NewRule(ctx, router, router.logger, ruleOptions, true) if err != nil { return nil, E.Cause(err, "parse rule[", i, "]") } router.rules = append(router.rules, routeRule) } for i, dnsRuleOptions := range dnsOptions.Rules { - dnsRule, err := NewDNSRule(ctx, router, router.logger, dnsRuleOptions, true) + dnsRule, err := R.NewDNSRule(ctx, router, router.logger, dnsRuleOptions, true) if err != nil { return nil, E.Cause(err, "parse dns rule[", i, "]") } @@ -171,7 +162,7 @@ func NewRouter( if _, exists := router.ruleSetMap[ruleSetOptions.Tag]; exists { return nil, E.New("duplicate rule-set tag: ", ruleSetOptions.Tag) } - ruleSet, err := NewRuleSet(ctx, router, router.logger, ruleSetOptions) + ruleSet, err := R.NewRuleSet(ctx, router, router.logger, ruleSetOptions) if err != nil { return nil, E.Cause(err, "parse rule-set[", i, "]") } @@ -437,8 +428,12 @@ func (r *Router) Initialize(inbounds []adapter.Inbound, outbounds []adapter.Outb r.defaultOutboundForPacketConnection = defaultOutboundForPacketConnection r.outboundByTag = outboundByTag for i, rule := range r.rules { - if _, loaded := outboundByTag[rule.Outbound()]; !loaded { - return E.New("outbound not found for rule[", i, "]: ", rule.Outbound()) + routeAction, isRoute := rule.Action().(*R.RuleActionRoute) + if !isRoute { + continue + } + if _, loaded := outboundByTag[routeAction.Outbound]; !loaded { + return E.New("outbound not found for rule[", i, "]: ", routeAction.Outbound) } } return nil @@ -804,375 +799,6 @@ func (r *Router) NeedWIFIState() bool { return r.needWIFIState } -func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - if r.pauseManager.IsDevicePaused() { - return E.New("reject connection to ", metadata.Destination, " while device paused") - } - - if metadata.InboundDetour != "" { - if metadata.LastInbound == metadata.InboundDetour { - return E.New("routing loop on detour: ", metadata.InboundDetour) - } - detour := r.inboundByTag[metadata.InboundDetour] - if detour == nil { - return E.New("inbound detour not found: ", metadata.InboundDetour) - } - injectable, isInjectable := detour.(adapter.InjectableInbound) - if !isInjectable { - return E.New("inbound detour is not injectable: ", metadata.InboundDetour) - } - if !common.Contains(injectable.Network(), N.NetworkTCP) { - return E.New("inject: TCP unsupported") - } - metadata.LastInbound = metadata.Inbound - metadata.Inbound = metadata.InboundDetour - metadata.InboundDetour = "" - err := injectable.NewConnection(ctx, conn, metadata) - if err != nil { - return E.Cause(err, "inject ", detour.Tag()) - } - return nil - } - conntrack.KillerCheck() - metadata.Network = N.NetworkTCP - switch metadata.Destination.Fqdn { - case mux.Destination.Fqdn: - return E.New("global multiplex is deprecated since sing-box v1.7.0, enable multiplex in inbound options instead.") - case vmess.MuxDestination.Fqdn: - return E.New("global multiplex (v2ray legacy) not supported since sing-box v1.7.0.") - case uot.MagicAddress: - return E.New("global UoT not supported since sing-box v1.7.0.") - case uot.LegacyMagicAddress: - return E.New("global UoT (legacy) not supported since sing-box v1.7.0.") - } - - if r.fakeIPStore != nil && r.fakeIPStore.Contains(metadata.Destination.Addr) { - domain, loaded := r.fakeIPStore.Lookup(metadata.Destination.Addr) - if !loaded { - return E.New("missing fakeip context") - } - metadata.OriginDestination = metadata.Destination - metadata.Destination = M.Socksaddr{ - Fqdn: domain, - Port: metadata.Destination.Port, - } - metadata.FakeIP = true - r.logger.DebugContext(ctx, "found fakeip domain: ", domain) - } - - if deadline.NeedAdditionalReadDeadline(conn) { - conn = deadline.NewConn(conn) - } - - if metadata.InboundOptions.SniffEnabled && !sniff.Skip(metadata) { - buffer := buf.NewPacket() - err := sniff.PeekStream( - ctx, - &metadata, - conn, - buffer, - time.Duration(metadata.InboundOptions.SniffTimeout), - sniff.TLSClientHello, - sniff.HTTPHost, - sniff.StreamDomainNameQuery, - sniff.SSH, - sniff.BitTorrent, - ) - if err == nil { - if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) { - metadata.Destination = M.Socksaddr{ - Fqdn: metadata.Domain, - Port: metadata.Destination.Port, - } - } - if metadata.Domain != "" { - r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol, ", domain: ", metadata.Domain) - } else { - r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol) - } - } - if !buffer.IsEmpty() { - conn = bufio.NewCachedConn(conn, buffer) - } else { - buffer.Release() - } - } - - if r.dnsReverseMapping != nil && metadata.Domain == "" { - domain, loaded := r.dnsReverseMapping.Query(metadata.Destination.Addr) - if loaded { - metadata.Domain = domain - r.logger.DebugContext(ctx, "found reserve mapped domain: ", metadata.Domain) - } - } - - if metadata.Destination.IsFqdn() && dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) != dns.DomainStrategyAsIS { - addresses, err := r.Lookup(adapter.WithContext(ctx, &metadata), metadata.Destination.Fqdn, dns.DomainStrategy(metadata.InboundOptions.DomainStrategy)) - if err != nil { - return err - } - metadata.DestinationAddresses = addresses - r.dnsLogger.DebugContext(ctx, "resolved [", strings.Join(F.MapToString(metadata.DestinationAddresses), " "), "]") - } - if metadata.Destination.IsIPv4() { - metadata.IPVersion = 4 - } else if metadata.Destination.IsIPv6() { - metadata.IPVersion = 6 - } - ctx, matchedRule, detour, err := r.match(ctx, &metadata, r.defaultOutboundForConnection) - if err != nil { - return err - } - if !common.Contains(detour.Network(), N.NetworkTCP) { - return E.New("missing supported outbound, closing connection") - } - if r.clashServer != nil { - trackerConn, tracker := r.clashServer.RoutedConnection(ctx, conn, metadata, matchedRule) - defer tracker.Leave() - conn = trackerConn - } - if r.v2rayServer != nil { - if statsService := r.v2rayServer.StatsService(); statsService != nil { - conn = statsService.RoutedConnection(metadata.Inbound, detour.Tag(), metadata.User, conn) - } - } - return detour.NewConnection(ctx, conn, metadata) -} - -func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - if r.pauseManager.IsDevicePaused() { - return E.New("reject packet connection to ", metadata.Destination, " while device paused") - } - if metadata.InboundDetour != "" { - if metadata.LastInbound == metadata.InboundDetour { - return E.New("routing loop on detour: ", metadata.InboundDetour) - } - detour := r.inboundByTag[metadata.InboundDetour] - if detour == nil { - return E.New("inbound detour not found: ", metadata.InboundDetour) - } - injectable, isInjectable := detour.(adapter.InjectableInbound) - if !isInjectable { - return E.New("inbound detour is not injectable: ", metadata.InboundDetour) - } - if !common.Contains(injectable.Network(), N.NetworkUDP) { - return E.New("inject: UDP unsupported") - } - metadata.LastInbound = metadata.Inbound - metadata.Inbound = metadata.InboundDetour - metadata.InboundDetour = "" - err := injectable.NewPacketConnection(ctx, conn, metadata) - if err != nil { - return E.Cause(err, "inject ", detour.Tag()) - } - return nil - } - conntrack.KillerCheck() - metadata.Network = N.NetworkUDP - - if r.fakeIPStore != nil && r.fakeIPStore.Contains(metadata.Destination.Addr) { - domain, loaded := r.fakeIPStore.Lookup(metadata.Destination.Addr) - if !loaded { - return E.New("missing fakeip context") - } - metadata.OriginDestination = metadata.Destination - metadata.Destination = M.Socksaddr{ - Fqdn: domain, - Port: metadata.Destination.Port, - } - metadata.FakeIP = true - r.logger.DebugContext(ctx, "found fakeip domain: ", domain) - } - - // Currently we don't have deadline usages for UDP connections - /*if deadline.NeedAdditionalReadDeadline(conn) { - conn = deadline.NewPacketConn(bufio.NewNetPacketConn(conn)) - }*/ - - if metadata.InboundOptions.SniffEnabled || metadata.Destination.Addr.IsUnspecified() { - var bufferList []*buf.Buffer - for { - var ( - buffer = buf.NewPacket() - destination M.Socksaddr - done = make(chan struct{}) - err error - ) - go func() { - sniffTimeout := C.ReadPayloadTimeout - if metadata.InboundOptions.SniffTimeout > 0 { - sniffTimeout = time.Duration(metadata.InboundOptions.SniffTimeout) - } - conn.SetReadDeadline(time.Now().Add(sniffTimeout)) - destination, err = conn.ReadPacket(buffer) - conn.SetReadDeadline(time.Time{}) - close(done) - }() - select { - case <-done: - case <-ctx.Done(): - conn.Close() - return ctx.Err() - } - if err != nil { - buffer.Release() - if !errors.Is(err, os.ErrDeadlineExceeded) { - return err - } - } else { - if metadata.Destination.Addr.IsUnspecified() { - metadata.Destination = destination - } - if metadata.InboundOptions.SniffEnabled { - if len(bufferList) > 0 { - err = sniff.PeekPacket( - ctx, - &metadata, - buffer.Bytes(), - sniff.QUICClientHello, - ) - } else { - err = sniff.PeekPacket( - ctx, &metadata, - buffer.Bytes(), - sniff.DomainNameQuery, - sniff.QUICClientHello, - sniff.STUNMessage, - sniff.UTP, - sniff.UDPTracker, - sniff.DTLSRecord) - } - if E.IsMulti(err, sniff.ErrClientHelloFragmented) && len(bufferList) == 0 { - bufferList = append(bufferList, buffer) - r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello") - continue - } - if metadata.Protocol != "" { - if metadata.InboundOptions.SniffOverrideDestination && M.IsDomainName(metadata.Domain) { - metadata.Destination = M.Socksaddr{ - Fqdn: metadata.Domain, - Port: metadata.Destination.Port, - } - } - if metadata.Domain != "" && metadata.Client != "" { - r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain, ", client: ", metadata.Client) - } else if metadata.Domain != "" { - r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", domain: ", metadata.Domain) - } else if metadata.Client != "" { - r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol, ", client: ", metadata.Client) - } else { - r.logger.DebugContext(ctx, "sniffed packet protocol: ", metadata.Protocol) - } - } - } - conn = bufio.NewCachedPacketConn(conn, buffer, destination) - } - for _, cachedBuffer := range common.Reverse(bufferList) { - conn = bufio.NewCachedPacketConn(conn, cachedBuffer, destination) - } - break - } - } - if r.dnsReverseMapping != nil && metadata.Domain == "" { - domain, loaded := r.dnsReverseMapping.Query(metadata.Destination.Addr) - if loaded { - metadata.Domain = domain - r.logger.DebugContext(ctx, "found reserve mapped domain: ", metadata.Domain) - } - } - if metadata.Destination.IsFqdn() && dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) != dns.DomainStrategyAsIS { - addresses, err := r.Lookup(adapter.WithContext(ctx, &metadata), metadata.Destination.Fqdn, dns.DomainStrategy(metadata.InboundOptions.DomainStrategy)) - if err != nil { - return err - } - metadata.DestinationAddresses = addresses - r.dnsLogger.DebugContext(ctx, "resolved [", strings.Join(F.MapToString(metadata.DestinationAddresses), " "), "]") - } - if metadata.Destination.IsIPv4() { - metadata.IPVersion = 4 - } else if metadata.Destination.IsIPv6() { - metadata.IPVersion = 6 - } - ctx, matchedRule, detour, err := r.match(ctx, &metadata, r.defaultOutboundForPacketConnection) - if err != nil { - return err - } - if !common.Contains(detour.Network(), N.NetworkUDP) { - return E.New("missing supported outbound, closing packet connection") - } - if r.clashServer != nil { - trackerConn, tracker := r.clashServer.RoutedPacketConnection(ctx, conn, metadata, matchedRule) - defer tracker.Leave() - conn = trackerConn - } - if r.v2rayServer != nil { - if statsService := r.v2rayServer.StatsService(); statsService != nil { - conn = statsService.RoutedPacketConnection(metadata.Inbound, detour.Tag(), metadata.User, conn) - } - } - if metadata.FakeIP { - conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination) - } - return detour.NewPacketConnection(ctx, conn, metadata) -} - -func (r *Router) match(ctx context.Context, metadata *adapter.InboundContext, defaultOutbound adapter.Outbound) (context.Context, adapter.Rule, adapter.Outbound, error) { - matchRule, matchOutbound := r.match0(ctx, metadata, defaultOutbound) - if contextOutbound, loaded := outbound.TagFromContext(ctx); loaded { - if contextOutbound == matchOutbound.Tag() { - return nil, nil, nil, E.New("connection loopback in outbound/", matchOutbound.Type(), "[", matchOutbound.Tag(), "]") - } - } - ctx = outbound.ContextWithTag(ctx, matchOutbound.Tag()) - return ctx, matchRule, matchOutbound, nil -} - -func (r *Router) match0(ctx context.Context, metadata *adapter.InboundContext, defaultOutbound adapter.Outbound) (adapter.Rule, adapter.Outbound) { - if r.processSearcher != nil { - var originDestination netip.AddrPort - if metadata.OriginDestination.IsValid() { - originDestination = metadata.OriginDestination.AddrPort() - } else if metadata.Destination.IsIP() { - originDestination = metadata.Destination.AddrPort() - } - processInfo, err := process.FindProcessInfo(r.processSearcher, ctx, metadata.Network, metadata.Source.AddrPort(), originDestination) - if err != nil { - r.logger.InfoContext(ctx, "failed to search process: ", err) - } else { - if processInfo.ProcessPath != "" { - r.logger.InfoContext(ctx, "found process path: ", processInfo.ProcessPath) - } else if processInfo.PackageName != "" { - r.logger.InfoContext(ctx, "found package name: ", processInfo.PackageName) - } else if processInfo.UserId != -1 { - if /*needUserName &&*/ true { - osUser, _ := user.LookupId(F.ToString(processInfo.UserId)) - if osUser != nil { - processInfo.User = osUser.Username - } - } - if processInfo.User != "" { - r.logger.InfoContext(ctx, "found user: ", processInfo.User) - } else { - r.logger.InfoContext(ctx, "found user id: ", processInfo.UserId) - } - } - metadata.ProcessInfo = processInfo - } - } - for i, rule := range r.rules { - metadata.ResetRuleCache() - if rule.Match(metadata) { - detour := rule.Outbound() - r.logger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour) - if outbound, loaded := r.Outbound(detour); loaded { - return rule, outbound - } - r.logger.ErrorContext(ctx, "outbound not found: ", detour) - } - } - return nil, defaultOutbound -} - func (r *Router) InterfaceFinder() control.InterfaceFinder { return r.interfaceFinder } diff --git a/route/rule_abstract.go b/route/rule/rule_abstract.go similarity index 94% rename from route/rule_abstract.go rename to route/rule/rule_abstract.go index 9ef2e93277..6a56934188 100644 --- a/route/rule_abstract.go +++ b/route/rule/rule_abstract.go @@ -1,4 +1,4 @@ -package route +package rule import ( "io" @@ -20,7 +20,7 @@ type abstractDefaultRule struct { allItems []RuleItem ruleSetItem RuleItem invert bool - outbound string + action adapter.RuleAction } func (r *abstractDefaultRule) Type() string { @@ -150,8 +150,8 @@ func (r *abstractDefaultRule) Match(metadata *adapter.InboundContext) bool { return !r.invert } -func (r *abstractDefaultRule) Outbound() string { - return r.outbound +func (r *abstractDefaultRule) Action() adapter.RuleAction { + return r.action } func (r *abstractDefaultRule) String() string { @@ -163,10 +163,10 @@ func (r *abstractDefaultRule) String() string { } type abstractLogicalRule struct { - rules []adapter.HeadlessRule - mode string - invert bool - outbound string + rules []adapter.HeadlessRule + mode string + invert bool + action adapter.RuleAction } func (r *abstractLogicalRule) Type() string { @@ -231,8 +231,8 @@ func (r *abstractLogicalRule) Match(metadata *adapter.InboundContext) bool { } } -func (r *abstractLogicalRule) Outbound() string { - return r.outbound +func (r *abstractLogicalRule) Action() adapter.RuleAction { + return r.action } func (r *abstractLogicalRule) String() string { diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go new file mode 100644 index 0000000000..e85fc76381 --- /dev/null +++ b/route/rule/rule_action.go @@ -0,0 +1,228 @@ +package rule + +import ( + "net/netip" + "strings" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/sniff" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-dns" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" +) + +func NewRuleAction(action option.RuleAction) (adapter.RuleAction, error) { + switch action.Action { + case C.RuleActionTypeRoute: + return &RuleActionRoute{ + Outbound: action.RouteOptions.Outbound, + UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping, + }, nil + case C.RuleActionTypeReturn: + return &RuleActionReject{}, nil + case C.RuleActionTypeReject: + return &RuleActionReject{ + Method: string(action.RejectOptions.Method), + }, nil + case C.RuleActionTypeHijackDNS: + return &RuleActionHijackDNS{}, nil + case C.RuleActionTypeSniff: + sniffAction := &RuleActionSniff{ + snifferNames: action.SniffOptions.Sniffer, + Timeout: time.Duration(action.SniffOptions.Timeout), + } + return sniffAction, sniffAction.build() + case C.RuleActionTypeResolve: + return &RuleActionResolve{ + Strategy: dns.DomainStrategy(action.ResolveOptions.Strategy), + Server: action.ResolveOptions.Server, + }, nil + default: + panic(F.ToString("unknown rule action: ", action.Action)) + } +} + +func NewDNSRuleAction(action option.DNSRuleAction) adapter.RuleAction { + switch action.Action { + case C.RuleActionTypeRoute: + return &RuleActionDNSRoute{ + Server: action.RouteOptions.Server, + DisableCache: action.RouteOptions.DisableCache, + RewriteTTL: action.RouteOptions.RewriteTTL, + ClientSubnet: action.RouteOptions.ClientSubnet.Build(), + } + case C.RuleActionTypeReturn: + return &RuleActionReturn{} + case C.RuleActionTypeReject: + return &RuleActionReject{ + Method: string(action.RejectOptions.Method), + } + default: + panic(F.ToString("unknown rule action: ", action.Action)) + } +} + +type RuleActionRoute struct { + Outbound string + UDPDisableDomainUnmapping bool +} + +func (r *RuleActionRoute) Type() string { + return C.RuleActionTypeRoute +} + +func (r *RuleActionRoute) String() string { + return F.ToString("route(", r.Outbound, ")") +} + +type RuleActionDNSRoute struct { + Server string + DisableCache bool + RewriteTTL *uint32 + ClientSubnet netip.Prefix +} + +func (r *RuleActionDNSRoute) Type() string { + return C.RuleActionTypeRoute +} + +func (r *RuleActionDNSRoute) String() string { + return F.ToString("route(", r.Server, ")") +} + +type RuleActionReturn struct{} + +func (r *RuleActionReturn) Type() string { + return C.RuleActionTypeReturn +} + +func (r *RuleActionReturn) String() string { + return "return" +} + +type RuleActionReject struct { + Method string +} + +func (r *RuleActionReject) Type() string { + return C.RuleActionTypeReject +} + +func (r *RuleActionReject) String() string { + if r.Method == C.RuleActionRejectMethodDefault { + return "reject" + } + return F.ToString("reject(", r.Method, ")") +} + +type RuleActionHijackDNS struct{} + +func (r *RuleActionHijackDNS) Type() string { + return C.RuleActionTypeHijackDNS +} + +func (r *RuleActionHijackDNS) String() string { + return "hijack-dns" +} + +type RuleActionSniff struct { + snifferNames []string + StreamSniffers []sniff.StreamSniffer + PacketSniffers []sniff.PacketSniffer + Timeout time.Duration + // Deprecated + OverrideDestination bool +} + +func (r *RuleActionSniff) Type() string { + return C.RuleActionTypeSniff +} + +func (r *RuleActionSniff) build() error { + if len(r.StreamSniffers) > 0 || len(r.PacketSniffers) > 0 { + return nil + } + if len(r.snifferNames) > 0 { + for _, name := range r.snifferNames { + switch name { + case C.ProtocolTLS: + r.StreamSniffers = append(r.StreamSniffers, sniff.TLSClientHello) + case C.ProtocolHTTP: + r.StreamSniffers = append(r.StreamSniffers, sniff.HTTPHost) + case C.ProtocolQUIC: + r.PacketSniffers = append(r.PacketSniffers, sniff.QUICClientHello) + case C.ProtocolDNS: + r.StreamSniffers = append(r.StreamSniffers, sniff.StreamDomainNameQuery) + r.PacketSniffers = append(r.PacketSniffers, sniff.DomainNameQuery) + case C.ProtocolSTUN: + r.PacketSniffers = append(r.PacketSniffers, sniff.STUNMessage) + case C.ProtocolBitTorrent: + r.StreamSniffers = append(r.StreamSniffers, sniff.BitTorrent) + r.PacketSniffers = append(r.PacketSniffers, sniff.UTP) + r.PacketSniffers = append(r.PacketSniffers, sniff.UDPTracker) + case C.ProtocolDTLS: + r.PacketSniffers = append(r.PacketSniffers, sniff.DTLSRecord) + case C.ProtocolSSH: + r.StreamSniffers = append(r.StreamSniffers, sniff.SSH) + case C.ProtocolRDP: + r.StreamSniffers = append(r.StreamSniffers, sniff.RDP) + default: + return E.New("unknown sniffer: ", name) + } + } + } else { + r.StreamSniffers = []sniff.StreamSniffer{ + sniff.TLSClientHello, + sniff.HTTPHost, + sniff.StreamDomainNameQuery, + sniff.BitTorrent, + sniff.SSH, + sniff.RDP, + } + r.PacketSniffers = []sniff.PacketSniffer{ + sniff.DomainNameQuery, + sniff.QUICClientHello, + sniff.STUNMessage, + sniff.UTP, + sniff.UDPTracker, + sniff.DTLSRecord, + } + } + return nil +} + +func (r *RuleActionSniff) String() string { + if len(r.snifferNames) == 0 && r.Timeout == 0 { + return "sniff" + } else if len(r.snifferNames) > 0 && r.Timeout == 0 { + return F.ToString("sniff(", strings.Join(r.snifferNames, ","), ")") + } else if len(r.snifferNames) == 0 && r.Timeout > 0 { + return F.ToString("sniff(", r.Timeout.String(), ")") + } else { + return F.ToString("sniff(", strings.Join(r.snifferNames, ","), ",", r.Timeout.String(), ")") + } +} + +type RuleActionResolve struct { + Strategy dns.DomainStrategy + Server string +} + +func (r *RuleActionResolve) Type() string { + return C.RuleActionTypeResolve +} + +func (r *RuleActionResolve) String() string { + if r.Strategy == dns.DomainStrategyAsIS && r.Server == "" { + return F.ToString("resolve") + } else if r.Strategy != dns.DomainStrategyAsIS && r.Server == "" { + return F.ToString("resolve(", option.DomainStrategy(r.Strategy).String(), ")") + } else if r.Strategy == dns.DomainStrategyAsIS && r.Server != "" { + return F.ToString("resolve(", r.Server, ")") + } else { + return F.ToString("resolve(", option.DomainStrategy(r.Strategy).String(), ",", r.Server, ")") + } +} diff --git a/route/rule_default.go b/route/rule/rule_default.go similarity index 88% rename from route/rule_default.go rename to route/rule/rule_default.go index fb4f6d82be..4f5d1e8a3c 100644 --- a/route/rule_default.go +++ b/route/rule/rule_default.go @@ -1,4 +1,4 @@ -package route +package rule import ( "context" @@ -17,16 +17,22 @@ func NewRule(ctx context.Context, router adapter.Router, logger log.ContextLogge if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } - if options.DefaultOptions.Outbound == "" && checkOutbound { - return nil, E.New("missing outbound field") + switch options.DefaultOptions.Action { + case "", C.RuleActionTypeRoute: + if options.DefaultOptions.RouteOptions.Outbound == "" && checkOutbound { + return nil, E.New("missing outbound field") + } } return NewDefaultRule(ctx, router, logger, options.DefaultOptions) case C.RuleTypeLogical: if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } - if options.LogicalOptions.Outbound == "" && checkOutbound { - return nil, E.New("missing outbound field") + switch options.LogicalOptions.Action { + case "", C.RuleActionTypeRoute: + if options.LogicalOptions.RouteOptions.Outbound == "" && checkOutbound { + return nil, E.New("missing outbound field") + } } return NewLogicalRule(ctx, router, logger, options.LogicalOptions) default: @@ -46,10 +52,14 @@ type RuleItem interface { } func NewDefaultRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.DefaultRule) (*DefaultRule, error) { + action, err := NewRuleAction(options.RuleAction) + if err != nil { + return nil, E.Cause(err, "action") + } rule := &DefaultRule{ abstractDefaultRule{ - invert: options.Invert, - outbound: options.Outbound, + invert: options.Invert, + action: action, }, } if len(options.Inbound) > 0 { @@ -244,27 +254,31 @@ type LogicalRule struct { } func NewLogicalRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { - r := &LogicalRule{ + action, err := NewRuleAction(options.RuleAction) + if err != nil { + return nil, E.Cause(err, "action") + } + rule := &LogicalRule{ abstractLogicalRule{ - rules: make([]adapter.HeadlessRule, len(options.Rules)), - invert: options.Invert, - outbound: options.Outbound, + rules: make([]adapter.HeadlessRule, len(options.Rules)), + invert: options.Invert, + action: action, }, } switch options.Mode { case C.LogicalTypeAnd: - r.mode = C.LogicalTypeAnd + rule.mode = C.LogicalTypeAnd case C.LogicalTypeOr: - r.mode = C.LogicalTypeOr + rule.mode = C.LogicalTypeOr default: return nil, E.New("unknown logical mode: ", options.Mode) } - for i, subRule := range options.Rules { - rule, err := NewRule(ctx, router, logger, subRule, false) + for i, subOptions := range options.Rules { + subRule, err := NewRule(ctx, router, logger, subOptions, false) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } - r.rules[i] = rule + rule.rules[i] = subRule } - return r, nil + return rule, nil } diff --git a/route/rule_dns.go b/route/rule/rule_dns.go similarity index 89% rename from route/rule_dns.go rename to route/rule/rule_dns.go index 4740488e3d..6e57633d10 100644 --- a/route/rule_dns.go +++ b/route/rule/rule_dns.go @@ -1,8 +1,7 @@ -package route +package rule import ( "context" - "net/netip" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" @@ -19,16 +18,22 @@ func NewDNSRule(ctx context.Context, router adapter.Router, logger log.ContextLo if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } - if options.DefaultOptions.Server == "" && checkServer { - return nil, E.New("missing server field") + switch options.DefaultOptions.Action { + case "", C.RuleActionTypeRoute: + if options.DefaultOptions.RouteOptions.Server == "" && checkServer { + return nil, E.New("missing server field") + } } return NewDefaultDNSRule(ctx, router, logger, options.DefaultOptions) case C.RuleTypeLogical: if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } - if options.LogicalOptions.Server == "" && checkServer { - return nil, E.New("missing server field") + switch options.LogicalOptions.Action { + case "", C.RuleActionTypeRoute: + if options.LogicalOptions.RouteOptions.Server == "" && checkServer { + return nil, E.New("missing server field") + } } return NewLogicalDNSRule(ctx, router, logger, options.LogicalOptions) default: @@ -40,20 +45,14 @@ var _ adapter.DNSRule = (*DefaultDNSRule)(nil) type DefaultDNSRule struct { abstractDefaultRule - disableCache bool - rewriteTTL *uint32 - clientSubnet *netip.Prefix } func NewDefaultDNSRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) { rule := &DefaultDNSRule{ abstractDefaultRule: abstractDefaultRule{ - invert: options.Invert, - outbound: options.Server, + invert: options.Invert, + action: NewDNSRuleAction(options.DNSRuleAction), }, - disableCache: options.DisableCache, - rewriteTTL: options.RewriteTTL, - clientSubnet: (*netip.Prefix)(options.ClientSubnet), } if len(options.Inbound) > 0 { item := NewInboundRule(options.Inbound) @@ -245,16 +244,8 @@ func NewDefaultDNSRule(ctx context.Context, router adapter.Router, logger log.Co return rule, nil } -func (r *DefaultDNSRule) DisableCache() bool { - return r.disableCache -} - -func (r *DefaultDNSRule) RewriteTTL() *uint32 { - return r.rewriteTTL -} - -func (r *DefaultDNSRule) ClientSubnet() *netip.Prefix { - return r.clientSubnet +func (r *DefaultDNSRule) Action() adapter.RuleAction { + return r.action } func (r *DefaultDNSRule) WithAddressLimit() bool { @@ -289,21 +280,15 @@ var _ adapter.DNSRule = (*LogicalDNSRule)(nil) type LogicalDNSRule struct { abstractLogicalRule - disableCache bool - rewriteTTL *uint32 - clientSubnet *netip.Prefix } func NewLogicalDNSRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { r := &LogicalDNSRule{ abstractLogicalRule: abstractLogicalRule{ - rules: make([]adapter.HeadlessRule, len(options.Rules)), - invert: options.Invert, - outbound: options.Server, + rules: make([]adapter.HeadlessRule, len(options.Rules)), + invert: options.Invert, + action: NewDNSRuleAction(options.DNSRuleAction), }, - disableCache: options.DisableCache, - rewriteTTL: options.RewriteTTL, - clientSubnet: (*netip.Prefix)(options.ClientSubnet), } switch options.Mode { case C.LogicalTypeAnd: @@ -323,16 +308,8 @@ func NewLogicalDNSRule(ctx context.Context, router adapter.Router, logger log.Co return r, nil } -func (r *LogicalDNSRule) DisableCache() bool { - return r.disableCache -} - -func (r *LogicalDNSRule) RewriteTTL() *uint32 { - return r.rewriteTTL -} - -func (r *LogicalDNSRule) ClientSubnet() *netip.Prefix { - return r.clientSubnet +func (r *LogicalDNSRule) Action() adapter.RuleAction { + return r.action } func (r *LogicalDNSRule) WithAddressLimit() bool { diff --git a/route/rule_headless.go b/route/rule/rule_headless.go similarity index 99% rename from route/rule_headless.go rename to route/rule/rule_headless.go index 23a98c7237..9ea357af8c 100644 --- a/route/rule_headless.go +++ b/route/rule/rule_headless.go @@ -1,4 +1,4 @@ -package route +package rule import ( "github.com/sagernet/sing-box/adapter" diff --git a/route/rule_item_adguard.go b/route/rule/rule_item_adguard.go similarity index 98% rename from route/rule_item_adguard.go rename to route/rule/rule_item_adguard.go index bdbb3b75fd..84252e606d 100644 --- a/route/rule_item_adguard.go +++ b/route/rule/rule_item_adguard.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_auth_user.go b/route/rule/rule_item_auth_user.go similarity index 98% rename from route/rule_item_auth_user.go rename to route/rule/rule_item_auth_user.go index fbe053e6be..5799e3c718 100644 --- a/route/rule_item_auth_user.go +++ b/route/rule/rule_item_auth_user.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_cidr.go b/route/rule/rule_item_cidr.go similarity index 99% rename from route/rule_item_cidr.go rename to route/rule/rule_item_cidr.go index be0bb1369c..c823dcf30a 100644 --- a/route/rule_item_cidr.go +++ b/route/rule/rule_item_cidr.go @@ -1,4 +1,4 @@ -package route +package rule import ( "net/netip" diff --git a/route/rule_item_clash_mode.go b/route/rule/rule_item_clash_mode.go similarity index 97% rename from route/rule_item_clash_mode.go rename to route/rule/rule_item_clash_mode.go index 70141f1116..aa5126cbde 100644 --- a/route/rule_item_clash_mode.go +++ b/route/rule/rule_item_clash_mode.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_client.go b/route/rule/rule_item_client.go similarity index 98% rename from route/rule_item_client.go rename to route/rule/rule_item_client.go index eeab440240..63ff410355 100644 --- a/route/rule_item_client.go +++ b/route/rule/rule_item_client.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_domain.go b/route/rule/rule_item_domain.go similarity index 99% rename from route/rule_item_domain.go rename to route/rule/rule_item_domain.go index c77890df24..b7655a7942 100644 --- a/route/rule_item_domain.go +++ b/route/rule/rule_item_domain.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_domain_keyword.go b/route/rule/rule_item_domain_keyword.go similarity index 98% rename from route/rule_item_domain_keyword.go rename to route/rule/rule_item_domain_keyword.go index c6ca1e8c28..6e19a10ccd 100644 --- a/route/rule_item_domain_keyword.go +++ b/route/rule/rule_item_domain_keyword.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_domain_regex.go b/route/rule/rule_item_domain_regex.go similarity index 99% rename from route/rule_item_domain_regex.go rename to route/rule/rule_item_domain_regex.go index b3555168a2..b9752a45ad 100644 --- a/route/rule_item_domain_regex.go +++ b/route/rule/rule_item_domain_regex.go @@ -1,4 +1,4 @@ -package route +package rule import ( "regexp" diff --git a/route/rule_item_geoip.go b/route/rule/rule_item_geoip.go similarity index 99% rename from route/rule_item_geoip.go rename to route/rule/rule_item_geoip.go index 3611613a48..3c967fec5f 100644 --- a/route/rule_item_geoip.go +++ b/route/rule/rule_item_geoip.go @@ -1,4 +1,4 @@ -package route +package rule import ( "net/netip" diff --git a/route/rule_item_geosite.go b/route/rule/rule_item_geosite.go similarity index 98% rename from route/rule_item_geosite.go rename to route/rule/rule_item_geosite.go index 5fdbfe5962..9e5e03c819 100644 --- a/route/rule_item_geosite.go +++ b/route/rule/rule_item_geosite.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_inbound.go b/route/rule/rule_item_inbound.go similarity index 98% rename from route/rule_item_inbound.go rename to route/rule/rule_item_inbound.go index 7e28781f00..87e84740c9 100644 --- a/route/rule_item_inbound.go +++ b/route/rule/rule_item_inbound.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_ip_is_private.go b/route/rule/rule_item_ip_is_private.go similarity index 98% rename from route/rule_item_ip_is_private.go rename to route/rule/rule_item_ip_is_private.go index 6592a9d3fe..e185db1db4 100644 --- a/route/rule_item_ip_is_private.go +++ b/route/rule/rule_item_ip_is_private.go @@ -1,4 +1,4 @@ -package route +package rule import ( "net/netip" diff --git a/route/rule_item_ipversion.go b/route/rule/rule_item_ipversion.go similarity index 97% rename from route/rule_item_ipversion.go rename to route/rule/rule_item_ipversion.go index 3d8762b417..8ab6494270 100644 --- a/route/rule_item_ipversion.go +++ b/route/rule/rule_item_ipversion.go @@ -1,4 +1,4 @@ -package route +package rule import ( "github.com/sagernet/sing-box/adapter" diff --git a/route/rule_item_network.go b/route/rule/rule_item_network.go similarity index 98% rename from route/rule_item_network.go rename to route/rule/rule_item_network.go index fc54f425da..bfb334d310 100644 --- a/route/rule_item_network.go +++ b/route/rule/rule_item_network.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_outbound.go b/route/rule/rule_item_outbound.go similarity index 98% rename from route/rule_item_outbound.go rename to route/rule/rule_item_outbound.go index 4b3e16fc6b..3f37dee7fb 100644 --- a/route/rule_item_outbound.go +++ b/route/rule/rule_item_outbound.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_package_name.go b/route/rule/rule_item_package_name.go similarity index 98% rename from route/rule_item_package_name.go rename to route/rule/rule_item_package_name.go index d1ca09eb2d..0066735c30 100644 --- a/route/rule_item_package_name.go +++ b/route/rule/rule_item_package_name.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_port.go b/route/rule/rule_item_port.go similarity index 98% rename from route/rule_item_port.go rename to route/rule/rule_item_port.go index 6247893364..af166ee640 100644 --- a/route/rule_item_port.go +++ b/route/rule/rule_item_port.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_port_range.go b/route/rule/rule_item_port_range.go similarity index 99% rename from route/rule_item_port_range.go rename to route/rule/rule_item_port_range.go index f87575f2ce..980f7d2391 100644 --- a/route/rule_item_port_range.go +++ b/route/rule/rule_item_port_range.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strconv" diff --git a/route/rule_item_process_name.go b/route/rule/rule_item_process_name.go similarity index 98% rename from route/rule_item_process_name.go rename to route/rule/rule_item_process_name.go index ce051666d0..fa0f71651d 100644 --- a/route/rule_item_process_name.go +++ b/route/rule/rule_item_process_name.go @@ -1,4 +1,4 @@ -package route +package rule import ( "path/filepath" diff --git a/route/rule_item_process_path.go b/route/rule/rule_item_process_path.go similarity index 98% rename from route/rule_item_process_path.go rename to route/rule/rule_item_process_path.go index feae4b2756..75dee476d6 100644 --- a/route/rule_item_process_path.go +++ b/route/rule/rule_item_process_path.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_process_path_regex.go b/route/rule/rule_item_process_path_regex.go similarity index 98% rename from route/rule_item_process_path_regex.go rename to route/rule/rule_item_process_path_regex.go index 01b2723cc6..76cf67b9f5 100644 --- a/route/rule_item_process_path_regex.go +++ b/route/rule/rule_item_process_path_regex.go @@ -1,4 +1,4 @@ -package route +package rule import ( "regexp" diff --git a/route/rule_item_protocol.go b/route/rule/rule_item_protocol.go similarity index 98% rename from route/rule_item_protocol.go rename to route/rule/rule_item_protocol.go index 1988f8ade1..319b81d50d 100644 --- a/route/rule_item_protocol.go +++ b/route/rule/rule_item_protocol.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_query_type.go b/route/rule/rule_item_query_type.go similarity index 98% rename from route/rule_item_query_type.go rename to route/rule/rule_item_query_type.go index 7b6efdd044..36b615f3af 100644 --- a/route/rule_item_query_type.go +++ b/route/rule/rule_item_query_type.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_rule_set.go b/route/rule/rule_item_rule_set.go similarity index 99% rename from route/rule_item_rule_set.go rename to route/rule/rule_item_rule_set.go index b80fca995c..a0115a044a 100644 --- a/route/rule_item_rule_set.go +++ b/route/rule/rule_item_rule_set.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_user.go b/route/rule/rule_item_user.go similarity index 98% rename from route/rule_item_user.go rename to route/rule/rule_item_user.go index bed97fbaa6..d635fa165b 100644 --- a/route/rule_item_user.go +++ b/route/rule/rule_item_user.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_user_id.go b/route/rule/rule_item_user_id.go similarity index 98% rename from route/rule_item_user_id.go rename to route/rule/rule_item_user_id.go index 43ab704e3a..57372de003 100644 --- a/route/rule_item_user_id.go +++ b/route/rule/rule_item_user_id.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_wifi_bssid.go b/route/rule/rule_item_wifi_bssid.go similarity index 98% rename from route/rule_item_wifi_bssid.go rename to route/rule/rule_item_wifi_bssid.go index 3b1ff9c852..ae94bd6df2 100644 --- a/route/rule_item_wifi_bssid.go +++ b/route/rule/rule_item_wifi_bssid.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_item_wifi_ssid.go b/route/rule/rule_item_wifi_ssid.go similarity index 98% rename from route/rule_item_wifi_ssid.go rename to route/rule/rule_item_wifi_ssid.go index 62cf935eb0..3a928f77e5 100644 --- a/route/rule_item_wifi_ssid.go +++ b/route/rule/rule_item_wifi_ssid.go @@ -1,4 +1,4 @@ -package route +package rule import ( "strings" diff --git a/route/rule_set.go b/route/rule/rule_set.go similarity index 60% rename from route/rule_set.go rename to route/rule/rule_set.go index a2c6d0c192..cdd0fc0af2 100644 --- a/route/rule_set.go +++ b/route/rule/rule_set.go @@ -1,4 +1,4 @@ -package route +package rule import ( "context" @@ -41,3 +41,31 @@ func extractIPSetFromRule(rawRule adapter.HeadlessRule) []*netipx.IPSet { panic("unexpected rule type") } } + +func hasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool { + for _, rule := range rules { + switch rule.Type { + case C.RuleTypeDefault: + if cond(rule.DefaultOptions) { + return true + } + case C.RuleTypeLogical: + if hasHeadlessRule(rule.LogicalOptions.Rules, cond) { + return true + } + } + } + return false +} + +func isProcessHeadlessRule(rule option.DefaultHeadlessRule) bool { + return len(rule.ProcessName) > 0 || len(rule.ProcessPath) > 0 || len(rule.PackageName) > 0 +} + +func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool { + return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 +} + +func isIPCIDRHeadlessRule(rule option.DefaultHeadlessRule) bool { + return len(rule.IPCIDR) > 0 || rule.IPSet != nil +} diff --git a/route/rule_set_local.go b/route/rule/rule_set_local.go similarity index 99% rename from route/rule_set_local.go rename to route/rule/rule_set_local.go index c3ecf9ca28..9186454e68 100644 --- a/route/rule_set_local.go +++ b/route/rule/rule_set_local.go @@ -1,4 +1,4 @@ -package route +package rule import ( "context" diff --git a/route/rule_set_remote.go b/route/rule/rule_set_remote.go similarity index 99% rename from route/rule_set_remote.go rename to route/rule/rule_set_remote.go index 5045bf2dab..f055771827 100644 --- a/route/rule_set_remote.go +++ b/route/rule/rule_set_remote.go @@ -1,4 +1,4 @@ -package route +package rule import ( "bytes" diff --git a/route/router_rule.go b/route/rule_conds.go similarity index 79% rename from route/router_rule.go rename to route/rule_conds.go index 4c345b59a3..6944d23737 100644 --- a/route/router_rule.go +++ b/route/rule_conds.go @@ -38,22 +38,6 @@ func hasDNSRule(rules []option.DNSRule, cond func(rule option.DefaultDNSRule) bo return false } -func hasHeadlessRule(rules []option.HeadlessRule, cond func(rule option.DefaultHeadlessRule) bool) bool { - for _, rule := range rules { - switch rule.Type { - case C.RuleTypeDefault: - if cond(rule.DefaultOptions) { - return true - } - case C.RuleTypeLogical: - if hasHeadlessRule(rule.LogicalOptions.Rules, cond) { - return true - } - } - } - return false -} - func isGeoIPRule(rule option.DefaultRule) bool { return len(rule.SourceGeoIP) > 0 && common.Any(rule.SourceGeoIP, notPrivateNode) || len(rule.GeoIP) > 0 && common.Any(rule.GeoIP, notPrivateNode) } @@ -93,11 +77,3 @@ func isWIFIRule(rule option.DefaultRule) bool { func isWIFIDNSRule(rule option.DefaultDNSRule) bool { return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 } - -func isWIFIHeadlessRule(rule option.DefaultHeadlessRule) bool { - return len(rule.WIFISSID) > 0 || len(rule.WIFIBSSID) > 0 -} - -func isIPCIDRHeadlessRule(rule option.DefaultHeadlessRule) bool { - return len(rule.IPCIDR) > 0 || rule.IPSet != nil -} diff --git a/test/brutal_test.go b/test/brutal_test.go index bfe4d1fc02..18aae2e201 100644 --- a/test/brutal_test.go +++ b/test/brutal_test.go @@ -76,9 +76,18 @@ func TestBrutalShadowsocks(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "ss-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, }, }, }, @@ -165,9 +174,18 @@ func TestBrutalTrojan(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "ss-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, }, }, }, @@ -238,9 +256,18 @@ func TestBrutalVMess(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "ss-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, }, }, }, @@ -342,9 +369,18 @@ func TestBrutalVLESS(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "ss-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, }, }, }, diff --git a/test/clash_test.go b/test/clash_test.go index ffd3e10c78..bba7f3bebe 100644 --- a/test/clash_test.go +++ b/test/clash_test.go @@ -17,7 +17,7 @@ import ( "github.com/sagernet/sing/common/control" F "github.com/sagernet/sing/common/format" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -68,7 +68,7 @@ func init() { } defer dockerClient.Close() - list, err := dockerClient.ImageList(context.Background(), types.ImageListOptions{All: true}) + list, err := dockerClient.ImageList(context.Background(), image.ListOptions{All: true}) if err != nil { log.Warn(err) return @@ -85,13 +85,13 @@ func init() { return false } - for _, image := range allImages { - if imageExist(image) { + for _, i := range allImages { + if imageExist(i) { continue } - log.Info("pulling image: ", image) - imageStream, err := dockerClient.ImagePull(context.Background(), image, types.ImagePullOptions{}) + log.Info("pulling image: ", i) + imageStream, err := dockerClient.ImagePull(context.Background(), i, image.PullOptions{}) if err != nil { panic(err) } diff --git a/test/direct_test.go b/test/direct_test.go index ec3cf88c07..1dbf1de1b6 100644 --- a/test/direct_test.go +++ b/test/direct_test.go @@ -50,9 +50,18 @@ func _TestProxyProtocol(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "proxy-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "proxy-out", + }, + }, }, }, }, diff --git a/test/docker_test.go b/test/docker_test.go index ade813d75c..a85dd12cd9 100644 --- a/test/docker_test.go +++ b/test/docker_test.go @@ -11,7 +11,6 @@ import ( F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/rw" - "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" @@ -85,10 +84,10 @@ func startDockerContainer(t *testing.T, options DockerOptions) { cleanContainer(dockerContainer.ID) }) - require.NoError(t, dockerClient.ContainerStart(context.Background(), dockerContainer.ID, types.ContainerStartOptions{})) + require.NoError(t, dockerClient.ContainerStart(context.Background(), dockerContainer.ID, container.StartOptions{})) if writeStdin { - stdinAttach, err := dockerClient.ContainerAttach(context.Background(), dockerContainer.ID, types.ContainerAttachOptions{ + stdinAttach, err := dockerClient.ContainerAttach(context.Background(), dockerContainer.ID, container.AttachOptions{ Stdin: writeStdin, Stream: true, }) @@ -98,7 +97,7 @@ func startDockerContainer(t *testing.T, options DockerOptions) { stdinAttach.Close() } if debug.Enabled { - attach, err := dockerClient.ContainerAttach(context.Background(), dockerContainer.ID, types.ContainerAttachOptions{ + attach, err := dockerClient.ContainerAttach(context.Background(), dockerContainer.ID, container.AttachOptions{ Stdout: true, Stderr: true, Logs: true, @@ -118,5 +117,5 @@ func cleanContainer(id string) error { return err } defer dockerClient.Close() - return dockerClient.ContainerRemove(context.Background(), id, types.ContainerRemoveOptions{Force: true}) + return dockerClient.ContainerRemove(context.Background(), id, container.RemoveOptions{Force: true}) } diff --git a/test/domain_inbound_test.go b/test/domain_inbound_test.go index f22fe24982..1ca2121dae 100644 --- a/test/domain_inbound_test.go +++ b/test/domain_inbound_test.go @@ -75,9 +75,18 @@ func TestTUICDomainUDP(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "tuic-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "tuic-out", + }, + }, }, }, }, diff --git a/test/ech_test.go b/test/ech_test.go index 35d5d89158..90eae1f48c 100644 --- a/test/ech_test.go +++ b/test/ech_test.go @@ -85,9 +85,18 @@ func TestECH(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "trojan-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "trojan-out", + }, + }, }, }, }, @@ -166,9 +175,18 @@ func TestECHQUIC(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "tuic-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "tuic-out", + }, + }, }, }, }, @@ -249,8 +267,16 @@ func TestECHHysteria2(t *testing.T) { { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "hy2-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "hy2-out", + }, + }, }, }, }, diff --git a/test/go.mod b/test/go.mod index 6caf6240d4..458ff8800b 100644 --- a/test/go.mod +++ b/test/go.mod @@ -1,25 +1,27 @@ module test -go 1.20 +go 1.23 + +toolchain go1.23.2 require github.com/sagernet/sing-box v0.0.0 replace github.com/sagernet/sing-box => ../ require ( - github.com/docker/docker v24.0.7+incompatible - github.com/docker/go-connections v0.4.0 - github.com/gofrs/uuid/v5 v5.2.0 - github.com/sagernet/quic-go v0.45.1-beta.2 - github.com/sagernet/sing v0.4.2 - github.com/sagernet/sing-dns v0.2.3 - github.com/sagernet/sing-quic v0.2.0-beta.12 + github.com/docker/docker v27.3.1+incompatible + github.com/docker/go-connections v0.5.0 + github.com/gofrs/uuid/v5 v5.3.0 + github.com/sagernet/quic-go v0.48.2-beta.1 + github.com/sagernet/sing v0.6.0-beta.2 + github.com/sagernet/sing-dns v0.4.0-beta.1 + github.com/sagernet/sing-quic v0.4.0-alpha.4 github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks2 v0.2.0 - github.com/spyzhov/ajson v0.9.0 + github.com/spyzhov/ajson v0.9.4 github.com/stretchr/testify v1.9.0 go.uber.org/goleak v1.3.0 - golang.org/x/net v0.25.0 + golang.org/x/net v0.31.0 ) require ( @@ -28,30 +30,38 @@ require ( github.com/andybalholm/brotli v1.0.6 // indirect github.com/caddyserver/certmagic v0.20.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect + github.com/containerd/log v0.1.0 // indirect github.com/cretz/bine v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.5.0 // indirect - github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/gaukas/godicttls v0.0.4 // indirect - github.com/go-chi/chi/v5 v5.0.12 // indirect + github.com/go-chi/chi/v5 v5.1.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/btree v1.1.2 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect - github.com/hashicorp/yamux v0.1.1 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect + github.com/josharian/native v1.1.0 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/libdns/alidns v1.0.3 // indirect github.com/libdns/cloudflare v0.1.1 // indirect github.com/libdns/libdns v0.2.2 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect + github.com/mdlayher/netlink v1.7.2 // indirect + github.com/mdlayher/socket v0.4.1 // indirect + github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa // indirect github.com/mholt/acmez v1.2.0 // indirect - github.com/miekg/dns v1.1.59 // indirect + github.com/miekg/dns v1.1.62 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/onsi/ginkgo/v2 v2.9.7 // indirect @@ -65,34 +75,41 @@ require ( github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 // indirect - github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f // indirect - github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba // indirect + github.com/sagernet/fswatch v0.1.1 // indirect + github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff // indirect + github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect + github.com/sagernet/nftables v0.3.0-beta.4 // indirect github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect - github.com/sagernet/sing-mux v0.2.0 // indirect - github.com/sagernet/sing-shadowtls v0.1.4 // indirect - github.com/sagernet/sing-tun v0.3.2 // indirect + github.com/sagernet/sing-mux v0.3.0-alpha.1 // indirect + github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 // indirect + github.com/sagernet/sing-tun v0.6.0-beta.2 // indirect github.com/sagernet/sing-vmess v0.1.12 // indirect github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect - github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6 // indirect - github.com/sagernet/utls v1.5.4 // indirect + github.com/sagernet/utls v1.6.7 // indirect github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8 // indirect github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect - github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect + github.com/vishvananda/netns v0.0.4 // indirect github.com/zeebo/blake3 v0.2.3 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect + go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/sdk v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect - golang.org/x/crypto v0.23.0 // indirect - golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect - golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect - google.golang.org/grpc v1.63.2 // indirect - google.golang.org/protobuf v1.33.0 // indirect + golang.org/x/crypto v0.29.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/sync v0.9.0 // indirect + golang.org/x/sys v0.27.0 // indirect + golang.org/x/text v0.20.0 // indirect + golang.org/x/time v0.7.0 // indirect + golang.org/x/tools v0.24.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect + google.golang.org/grpc v1.67.1 // indirect + google.golang.org/protobuf v1.35.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.1 // indirect lukechampine.com/blake3 v1.3.0 // indirect diff --git a/test/go.sum b/test/go.sum index f482438e37..5fc5292c9c 100644 --- a/test/go.sum +++ b/test/go.sum @@ -1,14 +1,19 @@ berty.tech/go-libtor v1.0.385 h1:RWK94C3hZj6Z2GdvePpHJLnWYobFr3bY/OdUJ5aoEXw= berty.tech/go-libtor v1.0.385/go.mod h1:9swOOQVb+kmvuAlsgWUK/4c52pm69AdbJsxLzk+fJEw= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc= github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= @@ -17,21 +22,23 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= -github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= -github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= -github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= -github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= -github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= @@ -40,18 +47,26 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM= -github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= +github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= -github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk= github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= -github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= @@ -60,6 +75,7 @@ github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuOb github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ= github.com/libdns/alidns v1.0.3/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE= github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054= @@ -69,18 +85,28 @@ github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s= github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ= github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa h1:9mcjV+RGZVC3reJBNDjjNPyS8PmFG97zq56X7WNaFO4= +github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa/go.mod h1:4tLB5c8U0CxpkFM+AJJB77jEaVDbLH5XQvy42vAGsWw= github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30= github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE= -github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs= -github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk= +github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= +github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss= github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= +github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= github.com/ooni/go-libtor v1.1.8 h1:Wo3V3DVTxl5vZdxtQakqYP+DAHx7pPtAFSl1bnAa08w= github.com/ooni/go-libtor v1.1.8/go.mod h1:q1YyLwRD9GeMyeerVvwc0vJ2YgwDLTp2bdVcrh/JXyI= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -101,53 +127,65 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 h1:YbmpqPQEMdlk9oFSKYWRqVuu9qzNiOayIonKmv1gCXY= github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1/go.mod h1:J2yAxTFPDjrDPhuAi9aWFz2L3ox9it4qAluBBbN0H5k= -github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f h1:NkhuupzH5ch7b/Y/6ZHJWrnNLoiNnSJaow6DPb8VW2I= -github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f/go.mod h1:KXmw+ouSJNOsuRpg4wgwwCQuunrGz4yoAqQjsLjc6N0= -github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba h1:EY5AS7CCtfmARNv2zXUOrsEMPFDGYxaw65JzA2p51Vk= -github.com/sagernet/netlink v0.0.0-20240523065131-45e60152f9ba/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= -github.com/sagernet/quic-go v0.45.1-beta.2 h1:zkEeCbhdFFkrxKcuIRBtXNKci/1t2J/39QSG/sPvlmc= -github.com/sagernet/quic-go v0.45.1-beta.2/go.mod h1:+N3FqM9DAzOWfe64uxXuBejVJwX7DeW7BslzLO6N/xI= +github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= +github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= +github.com/sagernet/gvisor v0.0.0-20241021032506-a4324256e4a3 h1:RxEz7LhPNiF/gX/Hg+OXr5lqsM9iVAgmaK1L1vzlDRM= +github.com/sagernet/gvisor v0.0.0-20241021032506-a4324256e4a3/go.mod h1:ehZwnT2UpmOWAHFL48XdBhnd4Qu4hN2O3Ji0us3ZHMw= +github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff/go.mod h1:ehZwnT2UpmOWAHFL48XdBhnd4Qu4hN2O3Ji0us3ZHMw= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= +github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= +github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= +github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= +github.com/sagernet/quic-go v0.48.0-beta.1 h1:86hQZrmuoARI3BpDRkQaP0iAVpywA4YsRrzJPYuPKWg= +github.com/sagernet/quic-go v0.48.0-beta.1/go.mod h1:1WgdDIVD1Gybp40JTWketeSfKA/+or9YMLaG5VeTk4k= +github.com/sagernet/quic-go v0.48.2-beta.1/go.mod h1:1WgdDIVD1Gybp40JTWketeSfKA/+or9YMLaG5VeTk4k= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= -github.com/sagernet/sing v0.4.2 h1:jzGNJdZVRI0xlAfFugsIQUPvyB9SuWvbJK7zQCXc4QM= -github.com/sagernet/sing v0.4.2/go.mod h1:ieZHA/+Y9YZfXs2I3WtuwgyCZ6GPsIR7HdKb1SdEnls= -github.com/sagernet/sing-dns v0.2.3 h1:YzeBUn2tR38F7HtvGEQ0kLRLmZWMEgi/+7wqa4Twb1k= -github.com/sagernet/sing-dns v0.2.3/go.mod h1:BJpJv6XLnrUbSyIntOT6DG9FW0f4fETmPAHvNjOprLg= -github.com/sagernet/sing-mux v0.2.0 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo= -github.com/sagernet/sing-mux v0.2.0/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ= -github.com/sagernet/sing-quic v0.2.0-beta.12 h1:BhvA5mmrDFEyDUQB5eeu+9UhF+ieyuNJ5Rsb0dAG3QY= -github.com/sagernet/sing-quic v0.2.0-beta.12/go.mod h1:YVpLfVi8BvYM7NMrjmnvcRm3E8iMETf1gFQmTQDN9jI= +github.com/sagernet/sing v0.5.0-rc.4.0.20241022031908-cd17884118cb h1:3IhGq2UmcbQfAcuqyE8RYKFapqEEa3eItS/MrZr+5l8= +github.com/sagernet/sing v0.5.0-rc.4.0.20241022031908-cd17884118cb/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing v0.6.0-beta.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing-dns v0.3.0-rc.2.0.20241021154031-a59e0fbba3ce h1:OfpxE5qnXMyU/9LtNgX4M7bGP11lJx4s+KZ3Sijb0HE= +github.com/sagernet/sing-dns v0.3.0-rc.2.0.20241021154031-a59e0fbba3ce/go.mod h1:TqLIelI+FAbVEdiTRolhGLOwvhVjY7oT+wezlOJUQ7M= +github.com/sagernet/sing-dns v0.4.0-beta.1/go.mod h1:8wuFcoFkWM4vJuQyg8e97LyvDwe0/Vl7G839WLcKDs8= +github.com/sagernet/sing-mux v0.2.1-0.20241020175909-fe6153f7a9ec h1:6Fd/VsEsw9qIjaGi1IBTZSb4b4v5JYtNcoiBtGsQC48= +github.com/sagernet/sing-mux v0.2.1-0.20241020175909-fe6153f7a9ec/go.mod h1:RSwqqHwbtTOX3vs6ms8vMtBGH/0ZNyLm/uwt6TlmR84= +github.com/sagernet/sing-mux v0.3.0-alpha.1/go.mod h1:FTcImmdfW38Lz7b+HQ+mxxOth1lz4ao8uEnz+MwIJQE= +github.com/sagernet/sing-quic v0.3.0-rc.1 h1:SlzL1yfEAKJyRduub8vzOVtbyTLAX7RZEEBZxO5utts= +github.com/sagernet/sing-quic v0.3.0-rc.1/go.mod h1:uX+aUHA0fgIN6U3WViseDpSdTQriViZ7qz0Wbsf1mNQ= +github.com/sagernet/sing-quic v0.4.0-alpha.4/go.mod h1:h5RkKTmUhudJKzK7c87FPXD5w1bJjVyxMN9+opZcctA= github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE= github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg= github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k= github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4= -github.com/sagernet/sing-tun v0.3.2 h1:z0bLUT/YXH9RrJS9DsIpB0Bb9afl2hVJOmHd0zA3HJY= -github.com/sagernet/sing-tun v0.3.2/go.mod h1:DxLIyhjWU/HwGYoX0vNGg2c5QgTQIakphU1MuERR5tQ= +github.com/sagernet/sing-shadowtls v0.2.0-alpha.2/go.mod h1:0j5XlzKxaWRIEjc1uiSKmVoWb0k+L9QgZVb876+thZA= +github.com/sagernet/sing-tun v0.4.0-rc.4.0.20241021153919-9ae45181180d h1:zWcIQM3eAKJGzy7zhqkO7zm7ZI890OdR4vSwx2mevS0= +github.com/sagernet/sing-tun v0.4.0-rc.4.0.20241021153919-9ae45181180d/go.mod h1:Xhv+Mz2nE7HZTwResni8EtTa7AMJz/S6uQLT5lV23M8= +github.com/sagernet/sing-tun v0.6.0-beta.2/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE= github.com/sagernet/sing-vmess v0.1.12 h1:2gFD8JJb+eTFMoa8FIVMnknEi+vCSfaiTXTfEYAYAPg= github.com/sagernet/sing-vmess v0.1.12/go.mod h1:luTSsfyBGAc9VhtCqwjR+dt1QgqBhuYBCONB/POhF8I= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= -github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6 h1:z3SJQhVyU63FT26Wn/UByW6b7q8QKB0ZkPqsyqcz2PI= -github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6/go.mod h1:73xRZuxwkFk4aiLw28hG8W6o9cr2UPrGL9pdY2UTbvY= -github.com/sagernet/utls v1.5.4 h1:KmsEGbB2dKUtCNC+44NwAdNAqnqQ6GA4pTO0Yik56co= -github.com/sagernet/utls v1.5.4/go.mod h1:CTGxPWExIloRipK3XFpYv0OVyhO8kk3XCGW/ieyTh1s= +github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8= +github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM= github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8 h1:R0OMYAScomNAVpTfbHFpxqJpvwuhxSRi+g6z7gZhABs= github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8/go.mod h1:K4J7/npM+VAMUeUmTa2JaA02JmyheP0GpRBOUvn3ecc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= -github.com/spyzhov/ajson v0.9.0 h1:tF46gJGOenYVj+k9K1U1XpCxVWhmiyY5PsVCAs1+OJ0= -github.com/spyzhov/ajson v0.9.0/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spyzhov/ajson v0.9.4 h1:MVibcTCgO7DY4IlskdqIlCmDOsUOZ9P7oKj8ifdcf84= +github.com/spyzhov/ajson v0.9.4/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= -github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY= @@ -156,6 +194,22 @@ github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg= github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -169,31 +223,33 @@ golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaE golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -201,35 +257,41 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= -google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= -google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg= +google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/http_test.go b/test/http_test.go index 4b5fe70fc9..88385c2757 100644 --- a/test/http_test.go +++ b/test/http_test.go @@ -49,9 +49,18 @@ func TestHTTPSelf(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "http-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "http-out", + }, + }, }, }, }, diff --git a/test/hysteria2_test.go b/test/hysteria2_test.go index f549442811..9ca2f5d38d 100644 --- a/test/hysteria2_test.go +++ b/test/hysteria2_test.go @@ -94,8 +94,16 @@ func testHysteria2Self(t *testing.T, salamanderPassword string) { { Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "hy2-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "hy2-out", + }, + }, }, }, }, diff --git a/test/hysteria_test.go b/test/hysteria_test.go index 90ff62dd10..bde1b9fa73 100644 --- a/test/hysteria_test.go +++ b/test/hysteria_test.go @@ -75,9 +75,18 @@ func TestHysteriaSelf(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "hy-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "hy-out", + }, + }, }, }, }, diff --git a/test/inbound_detour_test.go b/test/inbound_detour_test.go index c2ef57a56c..9505c217ff 100644 --- a/test/inbound_detour_test.go +++ b/test/inbound_detour_test.go @@ -80,9 +80,18 @@ func TestChainedInbound(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "ss-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, }, }, }, diff --git a/test/mux_cool_test.go b/test/mux_cool_test.go index ef47695a50..81130fad9a 100644 --- a/test/mux_cool_test.go +++ b/test/mux_cool_test.go @@ -159,9 +159,18 @@ func TestMuxCoolSelf(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "vmess-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "vmess-out", + }, + }, }, }, }, diff --git a/test/mux_test.go b/test/mux_test.go index c02f270878..8d75518554 100644 --- a/test/mux_test.go +++ b/test/mux_test.go @@ -102,9 +102,18 @@ func testShadowsocksMux(t *testing.T, options option.OutboundMultiplexOptions) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "ss-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, }, }, }, @@ -166,9 +175,18 @@ func testVMessMux(t *testing.T, options option.OutboundMultiplexOptions) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "vmess-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "vmess-out", + }, + }, }, }, }, diff --git a/test/shadowsocks_test.go b/test/shadowsocks_test.go index 7d063d9a96..4ef1ee9d93 100644 --- a/test/shadowsocks_test.go +++ b/test/shadowsocks_test.go @@ -197,9 +197,18 @@ func testShadowsocksSelf(t *testing.T, method string, password string) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "ss-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, }, }, }, @@ -258,9 +267,18 @@ func TestShadowsocksUoT(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "ss-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, }, }, }, @@ -319,9 +337,18 @@ func testShadowsocks2022EIH(t *testing.T, method string, password string) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "ss-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, }, }, }, diff --git a/test/shadowtls_test.go b/test/shadowtls_test.go index 2f53d46ada..6f9ee1e566 100644 --- a/test/shadowtls_test.go +++ b/test/shadowtls_test.go @@ -118,12 +118,23 @@ func testShadowTLS(t *testing.T, version int, password string, utlsEanbled bool) }, }, Route: &option.RouteOptions{ - Rules: []option.Rule{{ - DefaultOptions: option.DefaultRule{ - Inbound: []string{"detour"}, - Outbound: "direct", + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"detour"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "direct", + }, + }, + }, }, - }}, + }, }, }) testTCP(t, clientPort, testPort) @@ -239,12 +250,23 @@ func TestShadowTLSInbound(t *testing.T) { }, }, Route: &option.RouteOptions{ - Rules: []option.Rule{{ - DefaultOptions: option.DefaultRule{ - Inbound: []string{"in"}, - Outbound: "out", + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "out", + }, + }, + }, }, - }}, + }, }, }) testTCP(t, clientPort, testPort) @@ -319,12 +341,23 @@ func TestShadowTLSOutbound(t *testing.T) { }, }, Route: &option.RouteOptions{ - Rules: []option.Rule{{ - DefaultOptions: option.DefaultRule{ - Inbound: []string{"detour"}, - Outbound: "direct", + Rules: []option.Rule{ + { + Type: C.RuleTypeDefault, + DefaultOptions: option.DefaultRule{ + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"detour"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "direct", + }, + }, + }, }, - }}, + }, }, }) testTCP(t, clientPort, testPort) diff --git a/test/tfo_test.go b/test/tfo_test.go index cc97e189d1..7bd34e2db7 100644 --- a/test/tfo_test.go +++ b/test/tfo_test.go @@ -60,9 +60,18 @@ func TestTCPSlowOpen(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "ss-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "ss-out", + }, + }, }, }, }, diff --git a/test/tls_test.go b/test/tls_test.go index da55faf5bf..cfc6c1a50b 100644 --- a/test/tls_test.go +++ b/test/tls_test.go @@ -76,9 +76,18 @@ func TestUTLS(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "trojan-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "trojan-out", + }, + }, }, }, }, diff --git a/test/trojan_test.go b/test/trojan_test.go index d8659b2e4f..f88ec88502 100644 --- a/test/trojan_test.go +++ b/test/trojan_test.go @@ -118,9 +118,18 @@ func TestTrojanSelf(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "trojan-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "trojan-out", + }, + }, }, }, }, @@ -177,9 +186,18 @@ func TestTrojanPlainSelf(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "trojan-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "trojan-out", + }, + }, }, }, }, diff --git a/test/tuic_test.go b/test/tuic_test.go index c2b71111f0..5b838f22f7 100644 --- a/test/tuic_test.go +++ b/test/tuic_test.go @@ -90,9 +90,18 @@ func testTUICSelf(t *testing.T, udpStream bool, zeroRTTHandshake bool) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "tuic-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "tuic-out", + }, + }, }, }, }, diff --git a/test/v2ray_transport_test.go b/test/v2ray_transport_test.go index 2f39d18aed..c7362f345f 100644 --- a/test/v2ray_transport_test.go +++ b/test/v2ray_transport_test.go @@ -108,9 +108,18 @@ func testVMessTransportSelf(t *testing.T, server *option.V2RayTransportOptions, Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "vmess-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "vmess-out", + }, + }, }, }, }, @@ -187,9 +196,18 @@ func testTrojanTransportSelf(t *testing.T, server *option.V2RayTransportOptions, Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "vmess-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "vmess-out", + }, + }, }, }, }, @@ -270,9 +288,18 @@ func TestVMessQUICSelf(t *testing.T) { Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "vmess-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "vmess-out", + }, + }, }, }, }, @@ -334,9 +361,18 @@ func testV2RayTransportNOTLSSelf(t *testing.T, transport *option.V2RayTransportO Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "vmess-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + + RouteOptions: option.RouteActionOptions{ + Outbound: "vmess-out", + }, + }, }, }, }, diff --git a/test/vmess_test.go b/test/vmess_test.go index cc7879ab84..fcf7bf8f47 100644 --- a/test/vmess_test.go +++ b/test/vmess_test.go @@ -315,9 +315,17 @@ func testVMessSelf(t *testing.T, security string, alterId int, globalPadding boo Route: &option.RouteOptions{ Rules: []option.Rule{ { + Type: C.RuleTypeDefault, DefaultOptions: option.DefaultRule{ - Inbound: []string{"mixed-in"}, - Outbound: "vmess-out", + RawDefaultRule: option.RawDefaultRule{ + Inbound: []string{"mixed-in"}, + }, + RuleAction: option.RuleAction{ + Action: C.RuleActionTypeRoute, + RouteOptions: option.RouteActionOptions{ + Outbound: "vmess-out", + }, + }, }, }, }, diff --git a/transport/v2ray/grpc.go b/transport/v2ray/grpc.go index 05bc5a2a4e..1b4250addf 100644 --- a/transport/v2ray/grpc.go +++ b/transport/v2ray/grpc.go @@ -10,15 +10,16 @@ import ( "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2raygrpc" "github.com/sagernet/sing-box/transport/v2raygrpclite" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) -func NewGRPCServer(ctx context.Context, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { +func NewGRPCServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { if options.ForceLite { - return v2raygrpclite.NewServer(ctx, options, tlsConfig, handler) + return v2raygrpclite.NewServer(ctx, logger, options, tlsConfig, handler) } - return v2raygrpc.NewServer(ctx, options, tlsConfig, handler) + return v2raygrpc.NewServer(ctx, logger, options, tlsConfig, handler) } func NewGRPCClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayGRPCOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { diff --git a/transport/v2ray/grpc_lite.go b/transport/v2ray/grpc_lite.go index 94f6fad13d..4f2814a726 100644 --- a/transport/v2ray/grpc_lite.go +++ b/transport/v2ray/grpc_lite.go @@ -9,12 +9,13 @@ import ( "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2raygrpclite" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) -func NewGRPCServer(ctx context.Context, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { - return v2raygrpclite.NewServer(ctx, options, tlsConfig, handler) +func NewGRPCServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { + return v2raygrpclite.NewServer(ctx, logger, options, tlsConfig, handler) } func NewGRPCClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayGRPCOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { diff --git a/transport/v2ray/quic.go b/transport/v2ray/quic.go index 5471157aff..4d3cdc6f8f 100644 --- a/transport/v2ray/quic.go +++ b/transport/v2ray/quic.go @@ -7,6 +7,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) @@ -21,11 +22,11 @@ func RegisterQUICConstructor(server ServerConstructor[option.V2RayQUICOptions], quicClientConstructor = client } -func NewQUICServer(ctx context.Context, options option.V2RayQUICOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { +func NewQUICServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayQUICOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { if quicServerConstructor == nil { return nil, os.ErrInvalid } - return quicServerConstructor(ctx, options, tlsConfig, handler) + return quicServerConstructor(ctx, logger, options, tlsConfig, handler) } func NewQUICClient(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options option.V2RayQUICOptions, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) { diff --git a/transport/v2ray/transport.go b/transport/v2ray/transport.go index deb8a7f0ee..ab52f55e8e 100644 --- a/transport/v2ray/transport.go +++ b/transport/v2ray/transport.go @@ -11,33 +11,34 @@ import ( "github.com/sagernet/sing-box/transport/v2rayhttpupgrade" "github.com/sagernet/sing-box/transport/v2raywebsocket" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) type ( - ServerConstructor[O any] func(ctx context.Context, options O, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) + ServerConstructor[O any] func(ctx context.Context, logger logger.ContextLogger, options O, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) ClientConstructor[O any] func(ctx context.Context, dialer N.Dialer, serverAddr M.Socksaddr, options O, tlsConfig tls.Config) (adapter.V2RayClientTransport, error) ) -func NewServerTransport(ctx context.Context, options option.V2RayTransportOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { +func NewServerTransport(ctx context.Context, logger logger.ContextLogger, options option.V2RayTransportOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { if options.Type == "" { return nil, nil } switch options.Type { case C.V2RayTransportTypeHTTP: - return v2rayhttp.NewServer(ctx, options.HTTPOptions, tlsConfig, handler) + return v2rayhttp.NewServer(ctx, logger, options.HTTPOptions, tlsConfig, handler) case C.V2RayTransportTypeWebsocket: - return v2raywebsocket.NewServer(ctx, options.WebsocketOptions, tlsConfig, handler) + return v2raywebsocket.NewServer(ctx, logger, options.WebsocketOptions, tlsConfig, handler) case C.V2RayTransportTypeQUIC: if tlsConfig == nil { return nil, C.ErrTLSRequired } - return NewQUICServer(ctx, options.QUICOptions, tlsConfig, handler) + return NewQUICServer(ctx, logger, options.QUICOptions, tlsConfig, handler) case C.V2RayTransportTypeGRPC: - return NewGRPCServer(ctx, options.GRPCOptions, tlsConfig, handler) + return NewGRPCServer(ctx, logger, options.GRPCOptions, tlsConfig, handler) case C.V2RayTransportTypeHTTPUpgrade: - return v2rayhttpupgrade.NewServer(ctx, options.HTTPUpgradeOptions, tlsConfig, handler) + return v2rayhttpupgrade.NewServer(ctx, logger, options.HTTPUpgradeOptions, tlsConfig, handler) default: return nil, E.New("unknown transport type: " + options.Type) } diff --git a/transport/v2raygrpc/client.go b/transport/v2raygrpc/client.go index 1e72040a15..af922b45c2 100644 --- a/transport/v2raygrpc/client.go +++ b/transport/v2raygrpc/client.go @@ -105,7 +105,7 @@ func (c *Client) DialContext(ctx context.Context) (net.Conn, error) { cancel(err) return nil, err } - return NewGRPCConn(stream, cancel), nil + return NewGRPCConn(stream), nil } func (c *Client) Close() error { diff --git a/transport/v2raygrpc/conn.go b/transport/v2raygrpc/conn.go index bc78f91e3c..0a0a627f73 100644 --- a/transport/v2raygrpc/conn.go +++ b/transport/v2raygrpc/conn.go @@ -5,7 +5,6 @@ import ( "os" "time" - "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/baderror" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" @@ -15,17 +14,15 @@ var _ net.Conn = (*GRPCConn)(nil) type GRPCConn struct { GunService - cancel common.ContextCancelCauseFunc - cache []byte + cache []byte } -func NewGRPCConn(service GunService, cancel common.ContextCancelCauseFunc) *GRPCConn { +func NewGRPCConn(service GunService) *GRPCConn { if client, isClient := service.(GunService_TunClient); isClient { service = &clientConnWrapper{client} } return &GRPCConn{ GunService: service, - cancel: cancel, } } @@ -38,7 +35,6 @@ func (c *GRPCConn) Read(b []byte) (n int, err error) { hunk, err := c.Recv() err = baderror.WrapGRPC(err) if err != nil { - c.cancel(err) return } n = copy(b, hunk.Data) @@ -51,14 +47,12 @@ func (c *GRPCConn) Read(b []byte) (n int, err error) { func (c *GRPCConn) Write(b []byte) (n int, err error) { err = baderror.WrapGRPC(c.Send(&Hunk{Data: b})) if err != nil { - c.cancel(err) return } return len(b), nil } func (c *GRPCConn) Close() error { - c.cancel(net.ErrClosed) return nil } diff --git a/transport/v2raygrpc/server.go b/transport/v2raygrpc/server.go index 15088b268c..b6b13f829d 100644 --- a/transport/v2raygrpc/server.go +++ b/transport/v2raygrpc/server.go @@ -9,8 +9,10 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" @@ -25,11 +27,12 @@ var _ adapter.V2RayServerTransport = (*Server)(nil) type Server struct { ctx context.Context - handler N.TCPConnectionHandler + logger logger.ContextLogger + handler adapter.V2RayServerTransportHandler server *grpc.Server } -func NewServer(ctx context.Context, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler N.TCPConnectionHandler) (*Server, error) { +func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { var serverOptions []grpc.ServerOption if tlsConfig != nil { if !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) { @@ -43,17 +46,16 @@ func NewServer(ctx context.Context, options option.V2RayGRPCOptions, tlsConfig t Timeout: time.Duration(options.PingTimeout), })) } - server := &Server{ctx, handler, grpc.NewServer(serverOptions...)} + server := &Server{ctx, logger, handler, grpc.NewServer(serverOptions...)} RegisterGunServiceCustomNameServer(server.server, server, options.ServiceName) return server, nil } func (s *Server) Tun(server GunService_TunServer) error { - ctx, cancel := common.ContextWithCancelCause(s.ctx) - conn := NewGRPCConn(server, cancel) - var metadata M.Metadata + conn := NewGRPCConn(server) + var source M.Socksaddr if remotePeer, loaded := peer.FromContext(server.Context()); loaded { - metadata.Source = M.SocksaddrFromNet(remotePeer.Addr) + source = M.SocksaddrFromNet(remotePeer.Addr) } if grpcMetadata, loaded := gM.FromIncomingContext(server.Context()); loaded { forwardFrom := strings.Join(grpcMetadata.Get("X-Forwarded-For"), ",") @@ -61,13 +63,16 @@ func (s *Server) Tun(server GunService_TunServer) error { for _, from := range strings.Split(forwardFrom, ",") { originAddr := M.ParseSocksaddr(from) if originAddr.IsValid() { - metadata.Source = originAddr.Unwrap() + source = originAddr.Unwrap() } } } } - go s.handler.NewConnection(ctx, conn, metadata) - <-ctx.Done() + done := make(chan struct{}) + go s.handler.NewConnectionEx(log.ContextWithNewID(s.ctx), conn, source, M.Socksaddr{}, N.OnceClose(func(it error) { + close(done) + })) + <-done return nil } diff --git a/transport/v2raygrpclite/server.go b/transport/v2raygrpclite/server.go index 6d3e42ebe5..622d785abd 100644 --- a/transport/v2raygrpclite/server.go +++ b/transport/v2raygrpclite/server.go @@ -10,10 +10,12 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/v2rayhttp" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" aTLS "github.com/sagernet/sing/common/tls" @@ -26,18 +28,19 @@ import ( var _ adapter.V2RayServerTransport = (*Server)(nil) type Server struct { - tlsConfig tls.ServerConfig - handler adapter.V2RayServerTransportHandler - errorHandler E.Handler - httpServer *http.Server - h2Server *http2.Server - h2cHandler http.Handler - path string + tlsConfig tls.ServerConfig + logger logger.ContextLogger + handler adapter.V2RayServerTransportHandler + httpServer *http.Server + h2Server *http2.Server + h2cHandler http.Handler + path string } -func NewServer(ctx context.Context, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { +func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayGRPCOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { server := &Server{ tlsConfig: tlsConfig, + logger: logger, handler: handler, path: "/" + options.ServiceName + "/Tun", h2Server: &http2.Server{ @@ -49,6 +52,9 @@ func NewServer(ctx context.Context, options option.V2RayGRPCOptions, tlsConfig t BaseContext: func(net.Listener) context.Context { return ctx }, + ConnContext: func(ctx context.Context, c net.Conn) context.Context { + return log.ContextWithNewID(ctx) + }, } server.h2cHandler = h2c.NewHandler(server, server.h2Server) return server, nil @@ -74,10 +80,12 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { writer.Header().Set("Content-Type", "application/grpc") writer.Header().Set("TE", "trailers") writer.WriteHeader(http.StatusOK) - var metadata M.Metadata - metadata.Source = sHttp.SourceAddress(request) + done := make(chan struct{}) conn := v2rayhttp.NewHTTP2Wrapper(newGunConn(request.Body, writer, writer.(http.Flusher))) - s.handler.NewConnection(request.Context(), conn, metadata) + s.handler.NewConnectionEx(request.Context(), conn, sHttp.SourceAddress(request), M.Socksaddr{}, N.OnceClose(func(it error) { + close(done) + })) + <-done conn.CloseWrapper() } @@ -85,7 +93,7 @@ func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Reques if statusCode > 0 { writer.WriteHeader(statusCode) } - s.handler.NewError(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) + s.logger.ErrorContext(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) } func (s *Server) Network() []string { diff --git a/transport/v2rayhttp/server.go b/transport/v2rayhttp/server.go index cad7d906b4..e0ee42a77e 100644 --- a/transport/v2rayhttp/server.go +++ b/transport/v2rayhttp/server.go @@ -11,11 +11,13 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" aTLS "github.com/sagernet/sing/common/tls" @@ -29,6 +31,7 @@ var _ adapter.V2RayServerTransport = (*Server)(nil) type Server struct { ctx context.Context + logger logger.ContextLogger tlsConfig tls.ServerConfig handler adapter.V2RayServerTransportHandler httpServer *http.Server @@ -40,7 +43,7 @@ type Server struct { headers http.Header } -func NewServer(ctx context.Context, options option.V2RayHTTPOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { +func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayHTTPOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { server := &Server{ ctx: ctx, tlsConfig: tlsConfig, @@ -63,6 +66,9 @@ func NewServer(ctx context.Context, options option.V2RayHTTPOptions, tlsConfig t BaseContext: func(net.Listener) context.Context { return ctx }, + ConnContext: func(ctx context.Context, c net.Conn) context.Context { + return log.ContextWithNewID(ctx) + }, } server.h2cHandler = h2c.NewHandler(server, server.h2Server) return server, nil @@ -95,8 +101,7 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { } } - var metadata M.Metadata - metadata.Source = sHttp.SourceAddress(request) + source := sHttp.SourceAddress(request) if h, ok := writer.(http.Hijacker); ok { var requestBody *buf.Buffer if contentLength := int(request.ContentLength); contentLength > 0 { @@ -127,14 +132,18 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { if requestBody != nil { conn = bufio.NewCachedConn(conn, requestBody) } - s.handler.NewConnection(request.Context(), conn, metadata) + s.handler.NewConnectionEx(request.Context(), conn, source, M.Socksaddr{}, nil) } else { writer.WriteHeader(http.StatusOK) + done := make(chan struct{}) conn := NewHTTP2Wrapper(&ServerHTTPConn{ NewHTTPConn(request.Body, writer), writer.(http.Flusher), }) - s.handler.NewConnection(request.Context(), conn, metadata) + s.handler.NewConnectionEx(request.Context(), conn, source, M.Socksaddr{}, N.OnceClose(func(it error) { + close(done) + })) + <-done conn.CloseWrapper() } } @@ -143,7 +152,7 @@ func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Reques if statusCode > 0 { writer.WriteHeader(statusCode) } - s.handler.NewError(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) + s.logger.ErrorContext(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) } func (s *Server) Network() []string { diff --git a/transport/v2rayhttpupgrade/server.go b/transport/v2rayhttpupgrade/server.go index a3b5d23ed9..6a42912e92 100644 --- a/transport/v2rayhttpupgrade/server.go +++ b/transport/v2rayhttpupgrade/server.go @@ -10,9 +10,11 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" aTLS "github.com/sagernet/sing/common/tls" @@ -23,6 +25,7 @@ var _ adapter.V2RayServerTransport = (*Server)(nil) type Server struct { ctx context.Context + logger logger.ContextLogger tlsConfig tls.ServerConfig handler adapter.V2RayServerTransportHandler httpServer *http.Server @@ -31,7 +34,7 @@ type Server struct { headers http.Header } -func NewServer(ctx context.Context, options option.V2RayHTTPUpgradeOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { +func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayHTTPUpgradeOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { server := &Server{ ctx: ctx, tlsConfig: tlsConfig, @@ -50,6 +53,9 @@ func NewServer(ctx context.Context, options option.V2RayHTTPUpgradeOptions, tlsC BaseContext: func(net.Listener) context.Context { return ctx }, + ConnContext: func(ctx context.Context, c net.Conn) context.Context { + return log.ContextWithNewID(ctx) + }, TLSNextProto: make(map[string]func(*http.Server, *tls.STDConn, http.Handler)), } return server, nil @@ -104,16 +110,14 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { s.invalidRequest(writer, request, http.StatusInternalServerError, E.Cause(err, "hijack failed")) return } - var metadata M.Metadata - metadata.Source = sHttp.SourceAddress(request) - s.handler.NewConnection(request.Context(), conn, metadata) + s.handler.NewConnectionEx(request.Context(), conn, sHttp.SourceAddress(request), M.Socksaddr{}, nil) } func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Request, statusCode int, err error) { if statusCode > 0 { writer.WriteHeader(statusCode) } - s.handler.NewError(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) + s.logger.ErrorContext(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) } func (s *Server) Network() []string { diff --git a/transport/v2rayquic/server.go b/transport/v2rayquic/server.go index f772103012..4c4397e6b6 100644 --- a/transport/v2rayquic/server.go +++ b/transport/v2rayquic/server.go @@ -15,6 +15,8 @@ import ( "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-quic" "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) @@ -23,6 +25,7 @@ var _ adapter.V2RayServerTransport = (*Server)(nil) type Server struct { ctx context.Context + logger logger.ContextLogger tlsConfig tls.ServerConfig quicConfig *quic.Config handler adapter.V2RayServerTransportHandler @@ -30,7 +33,7 @@ type Server struct { quicListener qtls.Listener } -func NewServer(ctx context.Context, options option.V2RayQUICOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { +func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayQUICOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (adapter.V2RayServerTransport, error) { quicConfig := &quic.Config{ DisablePathMTUDiscovery: !C.IsLinux && !C.IsWindows, } @@ -39,6 +42,7 @@ func NewServer(ctx context.Context, options option.V2RayQUICOptions, tlsConfig t } server := &Server{ ctx: ctx, + logger: logger, tlsConfig: tlsConfig, quicConfig: quicConfig, handler: handler, @@ -73,8 +77,8 @@ func (s *Server) acceptLoop() { } go func() { hErr := s.streamAcceptLoop(conn) - if hErr != nil { - s.handler.NewError(conn.Context(), hErr) + if hErr != nil && !E.IsClosedOrCanceled(hErr) { + s.logger.ErrorContext(conn.Context(), hErr) } }() } @@ -86,7 +90,7 @@ func (s *Server) streamAcceptLoop(conn quic.Connection) error { if err != nil { return err } - go s.handler.NewConnection(conn.Context(), &StreamWrapper{Conn: conn, Stream: stream}, M.Metadata{}) + go s.handler.NewConnectionEx(conn.Context(), &StreamWrapper{Conn: conn, Stream: stream}, M.SocksaddrFromNet(conn.RemoteAddr()), M.Socksaddr{}, nil) } } diff --git a/transport/v2raywebsocket/server.go b/transport/v2raywebsocket/server.go index 86f2de9cd3..ccabf086ad 100644 --- a/transport/v2raywebsocket/server.go +++ b/transport/v2raywebsocket/server.go @@ -11,11 +11,13 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" aTLS "github.com/sagernet/sing/common/tls" @@ -27,6 +29,7 @@ var _ adapter.V2RayServerTransport = (*Server)(nil) type Server struct { ctx context.Context + logger logger.ContextLogger tlsConfig tls.ServerConfig handler adapter.V2RayServerTransportHandler httpServer *http.Server @@ -36,9 +39,10 @@ type Server struct { upgrader ws.HTTPUpgrader } -func NewServer(ctx context.Context, options option.V2RayWebsocketOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { +func NewServer(ctx context.Context, logger logger.ContextLogger, options option.V2RayWebsocketOptions, tlsConfig tls.ServerConfig, handler adapter.V2RayServerTransportHandler) (*Server, error) { server := &Server{ ctx: ctx, + logger: logger, tlsConfig: tlsConfig, handler: handler, path: options.Path, @@ -59,6 +63,9 @@ func NewServer(ctx context.Context, options option.V2RayWebsocketOptions, tlsCon BaseContext: func(net.Listener) context.Context { return ctx }, + ConnContext: func(ctx context.Context, c net.Conn) context.Context { + return log.ContextWithNewID(ctx) + }, } return server, nil } @@ -102,20 +109,19 @@ func (s *Server) ServeHTTP(writer http.ResponseWriter, request *http.Request) { s.invalidRequest(writer, request, 0, E.Cause(err, "upgrade websocket connection")) return } - var metadata M.Metadata - metadata.Source = sHttp.SourceAddress(request) - conn = NewConn(wsConn, metadata.Source.TCPAddr(), ws.StateServerSide) + source := sHttp.SourceAddress(request) + conn = NewConn(wsConn, source, ws.StateServerSide) if len(earlyData) > 0 { conn = bufio.NewCachedConn(conn, buf.As(earlyData)) } - s.handler.NewConnection(request.Context(), conn, metadata) + s.handler.NewConnectionEx(request.Context(), conn, source, M.Socksaddr{}, nil) } func (s *Server) invalidRequest(writer http.ResponseWriter, request *http.Request, statusCode int, err error) { if statusCode > 0 { writer.WriteHeader(statusCode) } - s.handler.NewError(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) + s.logger.ErrorContext(request.Context(), E.Cause(err, "process connection from ", request.RemoteAddr)) } func (s *Server) Network() []string { From 7686503df8ecda9bffcc530ef974f4b1582007ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 22 Oct 2024 21:28:22 +0800 Subject: [PATCH 02/49] Implement TCP and ICMP rejects --- adapter/router.go | 1 + constant/rule.go | 9 ++++-- inbound/tun.go | 12 ++++++-- option/rule_action.go | 24 +++++++++------ route/route.go | 62 +++++++++++++++++++++++++-------------- route/rule/rule_action.go | 26 ++++++++++++++-- 6 files changed, 94 insertions(+), 40 deletions(-) diff --git a/adapter/router.go b/adapter/router.go index 134c944208..c9cd46e91a 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -34,6 +34,7 @@ type Router interface { FakeIPStore() FakeIPStore ConnectionRouter + PreMatch(metadata InboundContext) error ConnectionRouterEx GeoIPReader() *geoip.Reader diff --git a/constant/rule.go b/constant/rule.go index ba74ec63f7..c7717376c1 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -34,7 +34,10 @@ const ( ) const ( - RuleActionRejectMethodDefault = "default" - RuleActionRejectMethodPortUnreachable = "port-unreachable" - RuleActionRejectMethodDrop = "drop" + RuleActionRejectMethodDefault = "default" + RuleActionRejectMethodReset = "reset" + RuleActionRejectMethodNetworkUnreachable = "network-unreachable" + RuleActionRejectMethodHostUnreachable = "host-unreachable" + RuleActionRejectMethodPortUnreachable = "port-unreachable" + RuleActionRejectMethodDrop = "drop" ) diff --git a/inbound/tun.go b/inbound/tun.go index 0d856419ef..f04ae71b3e 100644 --- a/inbound/tun.go +++ b/inbound/tun.go @@ -404,9 +404,15 @@ func (t *TUN) Close() error { ) } -func (t *TUN) PrepareConnection(source M.Socksaddr, destination M.Socksaddr) error { - // TODO: implement rejects - return nil +func (t *TUN) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr) error { + return t.router.PreMatch(adapter.InboundContext{ + Inbound: t.tag, + InboundType: C.TypeTun, + Network: network, + Source: source, + Destination: destination, + InboundOptions: t.inboundOptions, + }) } func (t *TUN) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { diff --git a/option/rule_action.go b/option/rule_action.go index 4f0ec177b8..f446d81d5f 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -136,23 +136,29 @@ type DNSRouteActionOptions struct { ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"` } -type RejectActionOptions struct { - Method RejectMethod `json:"method,omitempty"` +type _RejectActionOptions struct { + Method string `json:"method,omitempty"` } -type RejectMethod string +type RejectActionOptions _RejectActionOptions -func (m *RejectMethod) UnmarshalJSON(bytes []byte) error { - err := json.Unmarshal(bytes, (*string)(m)) +func (r *RejectActionOptions) UnmarshalJSON(bytes []byte) error { + err := json.Unmarshal(bytes, (*_RejectActionOptions)(r)) if err != nil { return err } - switch *m { - case C.RuleActionRejectMethodDefault, C.RuleActionRejectMethodPortUnreachable, C.RuleActionRejectMethodDrop: - return nil + switch r.Method { + case "", C.RuleActionRejectMethodDefault: + r.Method = C.RuleActionRejectMethodDefault + case C.RuleActionRejectMethodReset, + C.RuleActionRejectMethodNetworkUnreachable, + C.RuleActionRejectMethodHostUnreachable, + C.RuleActionRejectMethodPortUnreachable, + C.RuleActionRejectMethodDrop: default: - return E.New("unknown reject method: " + *m) + return E.New("unknown reject method: " + r.Method) } + return nil } type RouteActionSniff struct { diff --git a/route/route.go b/route/route.go index 86d4d95cc9..cecd0f2ae9 100644 --- a/route/route.go +++ b/route/route.go @@ -21,7 +21,6 @@ import ( "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing-dns" "github.com/sagernet/sing-mux" - "github.com/sagernet/sing-tun" "github.com/sagernet/sing-vmess" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" @@ -89,7 +88,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, conn, nil, -1) + selectedRule, _, buffers, err := r.matchRule(ctx, &metadata, false, conn, nil, -1) if err != nil { return err } @@ -108,16 +107,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad selectReturn = true case *rule.RuleActionReject: buf.ReleaseMulti(buffers) - var rejectErr error - switch action.Method { - case C.RuleActionRejectMethodDefault: - rejectErr = os.ErrClosed - case C.RuleActionRejectMethodPortUnreachable: - rejectErr = syscall.ECONNREFUSED - case C.RuleActionRejectMethodDrop: - rejectErr = tun.ErrDrop - } - N.CloseOnHandshakeFailure(conn, onClose, rejectErr) + N.CloseOnHandshakeFailure(conn, onClose, action.Error()) return nil } } @@ -236,7 +226,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m conn = deadline.NewPacketConn(bufio.NewNetPacketConn(conn)) }*/ - selectedRule, _, buffers, err := r.matchRule(ctx, &metadata, nil, conn, -1) + selectedRule, _, buffers, err := r.matchRule(ctx, &metadata, false, nil, conn, -1) if err != nil { return err } @@ -306,8 +296,23 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m return nil } +func (r *Router) PreMatch(metadata adapter.InboundContext) error { + selectedRule, _, _, err := r.matchRule(r.ctx, &metadata, true, nil, nil, -1) + if err != nil { + return err + } + if selectedRule == nil { + return nil + } + rejectAction, isReject := selectedRule.Action().(*rule.RuleActionReject) + if !isReject { + return nil + } + return rejectAction.Error() +} + func (r *Router) matchRule( - ctx context.Context, metadata *adapter.InboundContext, + ctx context.Context, metadata *adapter.InboundContext, preMatch bool, inputConn net.Conn, inputPacketConn N.PacketConn, ruleIndex int, ) (selectedRule adapter.Rule, selectedRuleIndex int, buffers []*buf.Buffer, fatalErr error) { if r.processSearcher != nil && metadata.ProcessInfo == nil { @@ -370,7 +375,7 @@ func (r *Router) matchRule( //nolint:staticcheck if metadata.InboundOptions != common.DefaultValue[option.InboundOptions]() { - if metadata.InboundOptions.SniffEnabled { + if !preMatch && metadata.InboundOptions.SniffEnabled { newBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{ OverrideDestination: metadata.InboundOptions.SniffOverrideDestination, Timeout: time.Duration(metadata.InboundOptions.SniffTimeout), @@ -415,15 +420,28 @@ match: if !matched { break } - r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) + if !preMatch { + r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) + } else { + switch currentRule.Action().Type() { + case C.RuleActionTypeReject, C.RuleActionTypeResolve: + r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) + } + } switch action := currentRule.Action().(type) { case *rule.RuleActionSniff: - newBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn) - if newErr != nil { - fatalErr = newErr - return + if !preMatch { + newBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn) + if newErr != nil { + fatalErr = newErr + return + } + buffers = append(buffers, newBuffers...) + } else { + selectedRule = currentRule + selectedRuleIndex = currentRuleIndex + break match } - buffers = append(buffers, newBuffers...) case *rule.RuleActionResolve: fatalErr = r.actionResolve(ctx, metadata, action) if fatalErr != nil { @@ -436,7 +454,7 @@ match: } ruleIndex = currentRuleIndex } - if metadata.Destination.Addr.IsUnspecified() { + if !preMatch && metadata.Destination.Addr.IsUnspecified() { newBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{}, inputConn, inputPacketConn) if newErr != nil { fatalErr = newErr diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index e85fc76381..a157e94e52 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -2,7 +2,9 @@ package rule import ( "net/netip" + "os" "strings" + "syscall" "time" "github.com/sagernet/sing-box/adapter" @@ -10,6 +12,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-dns" + "github.com/sagernet/sing-tun" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" ) @@ -22,10 +25,10 @@ func NewRuleAction(action option.RuleAction) (adapter.RuleAction, error) { UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping, }, nil case C.RuleActionTypeReturn: - return &RuleActionReject{}, nil + return &RuleActionReturn{}, nil case C.RuleActionTypeReject: return &RuleActionReject{ - Method: string(action.RejectOptions.Method), + Method: action.RejectOptions.Method, }, nil case C.RuleActionTypeHijackDNS: return &RuleActionHijackDNS{}, nil @@ -58,7 +61,7 @@ func NewDNSRuleAction(action option.DNSRuleAction) adapter.RuleAction { return &RuleActionReturn{} case C.RuleActionTypeReject: return &RuleActionReject{ - Method: string(action.RejectOptions.Method), + Method: action.RejectOptions.Method, } default: panic(F.ToString("unknown rule action: ", action.Action)) @@ -118,6 +121,23 @@ func (r *RuleActionReject) String() string { return F.ToString("reject(", r.Method, ")") } +func (r *RuleActionReject) Error() error { + switch r.Method { + case C.RuleActionRejectMethodReset: + return os.ErrClosed + case C.RuleActionRejectMethodNetworkUnreachable: + return syscall.ENETUNREACH + case C.RuleActionRejectMethodHostUnreachable: + return syscall.EHOSTUNREACH + case C.RuleActionRejectMethodDefault, C.RuleActionRejectMethodPortUnreachable: + return syscall.ECONNREFUSED + case C.RuleActionRejectMethodDrop: + return tun.ErrDrop + default: + panic(F.ToString("unknown reject method: ", r.Method)) + } +} + type RuleActionHijackDNS struct{} func (r *RuleActionHijackDNS) Type() string { From 5a1c59ca888d654a287c9453e8e413563f339b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 22 Oct 2024 22:01:28 +0800 Subject: [PATCH 03/49] Implement resolve(server) --- adapter/inbound.go | 14 ++++---- route/route.go | 2 +- route/route_dns.go | 87 ++++++++++++++++++++++++++++------------------ 3 files changed, 62 insertions(+), 41 deletions(-) diff --git a/adapter/inbound.go b/adapter/inbound.go index f4d5802f31..300f57e3d9 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -50,12 +50,14 @@ type InboundContext struct { // Deprecated InboundOptions option.InboundOptions UDPDisableDomainUnmapping bool - DestinationAddresses []netip.Addr - SourceGeoIPCode string - GeoIPCode string - ProcessInfo *process.Info - QueryType uint16 - FakeIP bool + DNSServer string + + DestinationAddresses []netip.Addr + SourceGeoIPCode string + GeoIPCode string + ProcessInfo *process.Info + QueryType uint16 + FakeIP bool // rule cache diff --git a/route/route.go b/route/route.go index cecd0f2ae9..56493bd176 100644 --- a/route/route.go +++ b/route/route.go @@ -584,7 +584,7 @@ func (r *Router) actionSniff( func (r *Router) actionResolve(ctx context.Context, metadata *adapter.InboundContext, action *rule.RuleActionResolve) error { if metadata.Destination.IsFqdn() { - // TODO: check if WithContext is necessary + metadata.DNSServer = action.Server addresses, err := r.Lookup(adapter.WithContext(ctx, metadata), metadata.Destination.Fqdn, action.Strategy) if err != nil { return err diff --git a/route/route_dns.go b/route/route_dns.go index 43eb61e6df..60aff6a9ff 100644 --- a/route/route_dns.go +++ b/route/route_dns.go @@ -185,6 +185,20 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS cached bool err error ) + printResult := func() { + if err != nil { + if errors.Is(err, dns.ErrResponseRejectedCached) { + r.dnsLogger.DebugContext(ctx, "response rejected for ", domain, " (cached)") + } else if errors.Is(err, dns.ErrResponseRejected) { + r.dnsLogger.DebugContext(ctx, "response rejected for ", domain) + } else { + r.dnsLogger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain)) + } + } else if len(responseAddrs) == 0 { + r.dnsLogger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result") + err = dns.RCodeNameError + } + } responseAddrs, cached = r.dnsClient.LookupCache(ctx, domain, strategy) if cached { if len(responseAddrs) == 0 { @@ -196,46 +210,51 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS ctx, metadata := adapter.ExtendContext(ctx) metadata.Destination = M.Socksaddr{} metadata.Domain = domain - var ( - transport dns.Transport - options dns.QueryOptions - rule adapter.DNSRule - ruleIndex int - ) - ruleIndex = -1 - for { - dnsCtx := adapter.OverrideContext(ctx) - var addressLimit bool - transport, options, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true) - if strategy != dns.DomainStrategyAsIS { - options.Strategy = strategy - } - if rule != nil && rule.WithAddressLimit() { - addressLimit = true - responseAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, options, func(responseAddrs []netip.Addr) bool { - metadata.DestinationAddresses = responseAddrs - return rule.MatchAddressLimit(metadata) - }) - } else { - addressLimit = false - responseAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, options) + if metadata.DNSServer != "" { + transport, loaded := r.transportMap[metadata.DNSServer] + if !loaded { + return nil, E.New("transport not found: ", metadata.DNSServer) } - if err != nil { - if errors.Is(err, dns.ErrResponseRejectedCached) { - r.dnsLogger.DebugContext(ctx, "response rejected for ", domain, " (cached)") - } else if errors.Is(err, dns.ErrResponseRejected) { - r.dnsLogger.DebugContext(ctx, "response rejected for ", domain) + if strategy == dns.DomainStrategyAsIS { + if transportDomainStrategy, loaded := r.transportDomainStrategy[transport]; loaded { + strategy = transportDomainStrategy } else { - r.dnsLogger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain)) + strategy = r.defaultDomainStrategy } - } else if len(responseAddrs) == 0 { - r.dnsLogger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result") - err = dns.RCodeNameError } - if !addressLimit || err == nil { - break + responseAddrs, err = r.dnsClient.Lookup(ctx, transport, domain, dns.QueryOptions{Strategy: strategy}) + } else { + var ( + transport dns.Transport + options dns.QueryOptions + rule adapter.DNSRule + ruleIndex int + ) + ruleIndex = -1 + for { + dnsCtx := adapter.OverrideContext(ctx) + var addressLimit bool + transport, options, rule, ruleIndex = r.matchDNS(ctx, false, ruleIndex, true) + if strategy != dns.DomainStrategyAsIS { + options.Strategy = strategy + } + if rule != nil && rule.WithAddressLimit() { + addressLimit = true + responseAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, options, func(responseAddrs []netip.Addr) bool { + metadata.DestinationAddresses = responseAddrs + return rule.MatchAddressLimit(metadata) + }) + } else { + addressLimit = false + responseAddrs, err = r.dnsClient.Lookup(dnsCtx, transport, domain, options) + } + if !addressLimit || err == nil { + break + } + printResult() } } + printResult() if len(responseAddrs) > 0 { r.dnsLogger.InfoContext(ctx, "lookup succeed for ", domain, ": ", strings.Join(F.MapToString(responseAddrs), " ")) } From 15cc3b85eb9f12c535e9354835dff9abe63b87f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 23 Oct 2024 13:44:08 +0800 Subject: [PATCH 04/49] Implement dns-hijack --- cmd/sing-box/cmd_tools.go | 7 +- inbound/tun.go | 5 +- outbound/dns.go | 25 ++++--- route/dns.go | 93 +++++++++++++++++++++++++ route/route.go | 139 ++++++++++++++++++++++++++++---------- route/rule/rule_action.go | 72 +++++++------------- 6 files changed, 245 insertions(+), 96 deletions(-) create mode 100644 route/dns.go diff --git a/cmd/sing-box/cmd_tools.go b/cmd/sing-box/cmd_tools.go index c45f585576..86b9302e09 100644 --- a/cmd/sing-box/cmd_tools.go +++ b/cmd/sing-box/cmd_tools.go @@ -1,6 +1,9 @@ package main import ( + "errors" + "os" + "github.com/sagernet/sing-box" E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" @@ -23,7 +26,9 @@ func init() { func createPreStartedClient() (*box.Box, error) { options, err := readConfigAndMerge() if err != nil { - return nil, err + if !(errors.Is(err, os.ErrNotExist) && len(configDirectories) == 0 && len(configPaths) == 1) || configPaths[0] != "config.json" { + return nil, err + } } instance, err := box.New(box.Options{Options: options}) if err != nil { diff --git a/inbound/tun.go b/inbound/tun.go index f04ae71b3e..11b16428b9 100644 --- a/inbound/tun.go +++ b/inbound/tun.go @@ -36,8 +36,9 @@ type TUN struct { router adapter.Router logger log.ContextLogger // Deprecated - inboundOptions option.InboundOptions - tunOptions tun.Options + inboundOptions option.InboundOptions + tunOptions tun.Options + // Deprecated endpointIndependentNat bool udpTimeout time.Duration stack string diff --git a/outbound/dns.go b/outbound/dns.go index 08661a99aa..d9c92f19ec 100644 --- a/outbound/dns.go +++ b/outbound/dns.go @@ -5,6 +5,7 @@ import ( "encoding/binary" "net" "os" + "time" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" @@ -50,14 +51,15 @@ func (d *DNS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter metadata.Destination = M.Socksaddr{} defer conn.Close() for { - err := d.handleConnection(ctx, conn, metadata) + conn.SetReadDeadline(time.Now().Add(C.DNSTimeout)) + err := HandleStreamDNSRequest(ctx, d.router, conn, metadata) if err != nil { return err } } } -func (d *DNS) handleConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func HandleStreamDNSRequest(ctx context.Context, router adapter.Router, conn net.Conn, metadata adapter.InboundContext) error { var queryLength uint16 err := binary.Read(conn, binary.BigEndian, &queryLength) if err != nil { @@ -79,7 +81,7 @@ func (d *DNS) handleConnection(ctx context.Context, conn net.Conn, metadata adap } metadataInQuery := metadata go func() error { - response, err := d.router.Exchange(adapter.WithContext(ctx, &metadataInQuery), &message) + response, err := router.Exchange(adapter.WithContext(ctx, &metadataInQuery), &message) if err != nil { return err } @@ -100,10 +102,14 @@ func (d *DNS) handleConnection(ctx context.Context, conn net.Conn, metadata adap // Deprecated func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + return NewDNSPacketConnection(ctx, d.router, conn, nil, metadata) +} + +func NewDNSPacketConnection(ctx context.Context, router adapter.Router, conn N.PacketConn, cachedPackets []*N.PacketBuffer, metadata adapter.InboundContext) error { metadata.Destination = M.Socksaddr{} var reader N.PacketReader = conn var counters []N.CountFunc - var cachedPackets []*N.PacketBuffer + cachedPackets = common.Reverse(cachedPackets) for { reader, counters = N.UnwrapCountPacketReader(reader, counters) if cachedReader, isCached := reader.(N.CachedPacketReader); isCached { @@ -115,7 +121,7 @@ func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metada } if readWaiter, created := bufio.CreatePacketReadWaiter(reader); created { readWaiter.InitializeReadWaiter(N.ReadWaitOptions{}) - return d.newPacketConnection(ctx, conn, readWaiter, counters, cachedPackets, metadata) + return newDNSPacketConnection(ctx, router, conn, readWaiter, counters, cachedPackets, metadata) } break } @@ -161,7 +167,7 @@ func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metada } metadataInQuery := metadata go func() error { - response, err := d.router.Exchange(adapter.WithContext(ctx, &metadataInQuery), &message) + response, err := router.Exchange(adapter.WithContext(ctx, &metadataInQuery), &message) if err != nil { cancel(err) return err @@ -186,7 +192,7 @@ func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metada return group.Run(fastClose) } -func (d *DNS) newPacketConnection(ctx context.Context, conn N.PacketConn, readWaiter N.PacketReadWaiter, readCounters []N.CountFunc, cached []*N.PacketBuffer, metadata adapter.InboundContext) error { +func newDNSPacketConnection(ctx context.Context, router adapter.Router, conn N.PacketConn, readWaiter N.PacketReadWaiter, readCounters []N.CountFunc, cached []*N.PacketBuffer, metadata adapter.InboundContext) error { fastClose, cancel := common.ContextWithCancelCause(ctx) timeout := canceler.New(fastClose, cancel, C.DNSTimeout) var group task.Group @@ -206,11 +212,12 @@ func (d *DNS) newPacketConnection(ctx context.Context, conn N.PacketConn, readWa } err = message.Unpack(packet.Buffer.Bytes()) packet.Buffer.Release() + destination = packet.Destination + N.PutPacketBuffer(packet) if err != nil { cancel(err) return err } - destination = packet.Destination } else { buffer, destination, err = readWaiter.WaitReadPacket() if err != nil { @@ -230,7 +237,7 @@ func (d *DNS) newPacketConnection(ctx context.Context, conn N.PacketConn, readWa } metadataInQuery := metadata go func() error { - response, err := d.router.Exchange(adapter.WithContext(ctx, &metadataInQuery), &message) + response, err := router.Exchange(adapter.WithContext(ctx, &metadataInQuery), &message) if err != nil { cancel(err) return err diff --git a/route/dns.go b/route/dns.go new file mode 100644 index 0000000000..cc0760b851 --- /dev/null +++ b/route/dns.go @@ -0,0 +1,93 @@ +package route + +import ( + "context" + "errors" + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/outbound" + "github.com/sagernet/sing-dns" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/udpnat2" + + mDNS "github.com/miekg/dns" +) + +func (r *Router) hijackDNSStream(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + metadata.Destination = M.Socksaddr{} + for { + conn.SetReadDeadline(time.Now().Add(C.DNSTimeout)) + err := outbound.HandleStreamDNSRequest(ctx, r, conn, metadata) + if err != nil { + return err + } + } +} + +func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext) { + if uConn, isUDPNAT2 := conn.(*udpnat.Conn); isUDPNAT2 { + metadata.Destination = M.Socksaddr{} + for _, packet := range packetBuffers { + buffer := packet.Buffer + destination := packet.Destination + N.PutPacketBuffer(packet) + go ExchangeDNSPacket(ctx, r, uConn, buffer, metadata, destination) + } + uConn.SetHandler(&dnsHijacker{ + router: r, + conn: conn, + ctx: ctx, + metadata: metadata, + }) + return + } + err := outbound.NewDNSPacketConnection(ctx, r, conn, packetBuffers, metadata) + if err != nil && !E.IsClosedOrCanceled(err) { + r.dnsLogger.ErrorContext(ctx, E.Cause(err, "process packet connection")) + } +} + +func ExchangeDNSPacket(ctx context.Context, router *Router, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext, destination M.Socksaddr) { + err := exchangeDNSPacket(ctx, router, conn, buffer, metadata, destination) + if err != nil && !errors.Is(err, tun.ErrDrop) && !E.IsClosedOrCanceled(err) { + router.dnsLogger.ErrorContext(ctx, E.Cause(err, "process packet connection")) + } +} + +func exchangeDNSPacket(ctx context.Context, router *Router, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext, destination M.Socksaddr) error { + var message mDNS.Msg + err := message.Unpack(buffer.Bytes()) + buffer.Release() + if err != nil { + return E.Cause(err, "unpack request") + } + response, err := router.Exchange(adapter.WithContext(ctx, &metadata), &message) + if err != nil { + return err + } + responseBuffer, err := dns.TruncateDNSMessage(&message, response, 1024) + if err != nil { + return err + } + err = conn.WritePacket(responseBuffer, destination) + responseBuffer.Release() + return err +} + +type dnsHijacker struct { + router *Router + conn N.PacketConn + ctx context.Context + metadata adapter.InboundContext +} + +func (h *dnsHijacker) NewPacketEx(buffer *buf.Buffer, destination M.Socksaddr) { + go ExchangeDNSPacket(h.ctx, h.router, h.conn, buffer, h.metadata, destination) +} diff --git a/route/route.go b/route/route.go index 56493bd176..ebffddddbf 100644 --- a/route/route.go +++ b/route/route.go @@ -88,7 +88,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, conn, nil, -1) + selectedRule, _, buffers, _, err := r.matchRule(ctx, &metadata, false, conn, nil, -1) if err != nil { return err } @@ -109,6 +109,12 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad buf.ReleaseMulti(buffers) N.CloseOnHandshakeFailure(conn, onClose, action.Error()) return nil + case *rule.RuleActionHijackDNS: + for _, buffer := range buffers { + conn = bufio.NewCachedConn(conn, buffer) + } + r.hijackDNSStream(ctx, conn, metadata) + return nil } } if selectedRule == nil || selectReturn { @@ -226,7 +232,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m conn = deadline.NewPacketConn(bufio.NewNetPacketConn(conn)) }*/ - selectedRule, _, buffers, err := r.matchRule(ctx, &metadata, false, nil, conn, -1) + selectedRule, _, _, packetBuffers, err := r.matchRule(ctx, &metadata, false, nil, conn, -1) if err != nil { return err } @@ -238,32 +244,35 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m var loaded bool selectedOutbound, loaded = r.Outbound(action.Outbound) if !loaded { - buf.ReleaseMulti(buffers) + N.ReleaseMultiPacketBuffer(packetBuffers) return E.New("outbound not found: ", action.Outbound) } metadata.UDPDisableDomainUnmapping = action.UDPDisableDomainUnmapping case *rule.RuleActionReturn: selectReturn = true case *rule.RuleActionReject: - buf.ReleaseMulti(buffers) + N.ReleaseMultiPacketBuffer(packetBuffers) N.CloseOnHandshakeFailure(conn, onClose, syscall.ECONNREFUSED) return nil + case *rule.RuleActionHijackDNS: + r.hijackDNSPacket(ctx, conn, packetBuffers, metadata) + return nil } } if selectedRule == nil || selectReturn { if r.defaultOutboundForPacketConnection == nil { - buf.ReleaseMulti(buffers) + N.ReleaseMultiPacketBuffer(packetBuffers) return E.New("missing default outbound with UDP support") } selectedOutbound = r.defaultOutboundForPacketConnection } if !common.Contains(selectedOutbound.Network(), N.NetworkUDP) { - buf.ReleaseMulti(buffers) + N.ReleaseMultiPacketBuffer(packetBuffers) return E.New("UDP is not supported by outbound: ", selectedOutbound.Tag()) } - for _, buffer := range buffers { - // TODO: check if metadata.Destination == packet destination - conn = bufio.NewCachedPacketConn(conn, buffer, metadata.Destination) + for _, buffer := range packetBuffers { + conn = bufio.NewCachedPacketConn(conn, buffer.Buffer, buffer.Destination) + N.PutPacketBuffer(buffer) } if r.clashServer != nil { trackerConn, tracker := r.clashServer.RoutedPacketConnection(ctx, conn, metadata, selectedRule) @@ -297,7 +306,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m } func (r *Router) PreMatch(metadata adapter.InboundContext) error { - selectedRule, _, _, err := r.matchRule(r.ctx, &metadata, true, nil, nil, -1) + selectedRule, _, _, _, err := r.matchRule(r.ctx, &metadata, true, nil, nil, -1) if err != nil { return err } @@ -314,7 +323,10 @@ func (r *Router) PreMatch(metadata adapter.InboundContext) error { func (r *Router) matchRule( ctx context.Context, metadata *adapter.InboundContext, preMatch bool, inputConn net.Conn, inputPacketConn N.PacketConn, ruleIndex int, -) (selectedRule adapter.Rule, selectedRuleIndex int, buffers []*buf.Buffer, fatalErr error) { +) ( + selectedRule adapter.Rule, selectedRuleIndex int, + buffers []*buf.Buffer, packetBuffers []*N.PacketBuffer, fatalErr error, +) { if r.processSearcher != nil && metadata.ProcessInfo == nil { var originDestination netip.AddrPort if metadata.OriginDestination.IsValid() { @@ -376,7 +388,7 @@ func (r *Router) matchRule( //nolint:staticcheck if metadata.InboundOptions != common.DefaultValue[option.InboundOptions]() { if !preMatch && metadata.InboundOptions.SniffEnabled { - newBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{ + newBuffer, newPackerBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{ OverrideDestination: metadata.InboundOptions.SniffOverrideDestination, Timeout: time.Duration(metadata.InboundOptions.SniffTimeout), }, inputConn, inputPacketConn) @@ -384,7 +396,11 @@ func (r *Router) matchRule( fatalErr = newErr return } - buffers = append(buffers, newBuffers...) + if newBuffer != nil { + buffers = []*buf.Buffer{newBuffer} + } else if len(newPackerBuffers) > 0 { + packetBuffers = newPackerBuffers + } } if dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) != dns.DomainStrategyAsIS { fatalErr = r.actionResolve(ctx, metadata, &rule.RuleActionResolve{ @@ -421,22 +437,36 @@ match: break } if !preMatch { - r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) + ruleDescription := currentRule.String() + if ruleDescription != "" { + r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) + } else { + r.logger.DebugContext(ctx, "match[", currentRuleIndex, "] => ", currentRule.Action()) + } } else { switch currentRule.Action().Type() { case C.RuleActionTypeReject, C.RuleActionTypeResolve: - r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) + 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()) + } } } switch action := currentRule.Action().(type) { case *rule.RuleActionSniff: if !preMatch { - newBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn) + newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn) if newErr != nil { fatalErr = newErr return } - buffers = append(buffers, newBuffers...) + if newBuffer != nil { + buffers = append(buffers, newBuffer) + } else if len(newPacketBuffers) > 0 { + packetBuffers = append(packetBuffers, newPacketBuffers...) + } } else { selectedRule = currentRule selectedRuleIndex = currentRuleIndex @@ -455,12 +485,16 @@ match: ruleIndex = currentRuleIndex } if !preMatch && metadata.Destination.Addr.IsUnspecified() { - newBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{}, inputConn, inputPacketConn) + newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{}, inputConn, inputPacketConn) if newErr != nil { fatalErr = newErr return } - buffers = append(buffers, newBuffers...) + if newBuffer != nil { + buffers = append(buffers, newBuffer) + } else if len(newPacketBuffers) > 0 { + packetBuffers = append(packetBuffers, newPacketBuffers...) + } } return } @@ -468,18 +502,31 @@ match: func (r *Router) actionSniff( ctx context.Context, metadata *adapter.InboundContext, action *rule.RuleActionSniff, inputConn net.Conn, inputPacketConn N.PacketConn, -) (buffers []*buf.Buffer, fatalErr error) { +) (buffer *buf.Buffer, packetBuffers []*N.PacketBuffer, fatalErr error) { if sniff.Skip(metadata) { return - } else if inputConn != nil && len(action.StreamSniffers) > 0 { - buffer := buf.NewPacket() + } else if inputConn != nil { + sniffBuffer := buf.NewPacket() + var streamSniffers []sniff.StreamSniffer + if len(action.StreamSniffers) > 0 { + streamSniffers = action.StreamSniffers + } else { + streamSniffers = []sniff.StreamSniffer{ + sniff.TLSClientHello, + sniff.HTTPHost, + sniff.StreamDomainNameQuery, + sniff.BitTorrent, + sniff.SSH, + sniff.RDP, + } + } err := sniff.PeekStream( ctx, metadata, inputConn, - buffer, + sniffBuffer, action.Timeout, - action.StreamSniffers..., + streamSniffers..., ) if err == nil { //goland:noinspection GoDeprecation @@ -497,15 +544,15 @@ func (r *Router) actionSniff( r.logger.DebugContext(ctx, "sniffed protocol: ", metadata.Protocol) } } - if !buffer.IsEmpty() { - buffers = append(buffers, buffer) + if !sniffBuffer.IsEmpty() { + buffer = sniffBuffer } else { - buffer.Release() + sniffBuffer.Release() } - } else if inputPacketConn != nil && len(action.PacketSniffers) > 0 { + } else if inputPacketConn != nil { for { var ( - buffer = buf.NewPacket() + sniffBuffer = buf.NewPacket() destination M.Socksaddr done = make(chan struct{}) err error @@ -516,7 +563,7 @@ func (r *Router) actionSniff( sniffTimeout = action.Timeout } inputPacketConn.SetReadDeadline(time.Now().Add(sniffTimeout)) - destination, err = inputPacketConn.ReadPacket(buffer) + destination, err = inputPacketConn.ReadPacket(sniffBuffer) inputPacketConn.SetReadDeadline(time.Time{}) close(done) }() @@ -528,7 +575,7 @@ func (r *Router) actionSniff( return } if err != nil { - buffer.Release() + sniffBuffer.Release() if !errors.Is(err, os.ErrDeadlineExceeded) { fatalErr = err return @@ -538,22 +585,40 @@ func (r *Router) actionSniff( if metadata.Destination.Addr.IsUnspecified() { metadata.Destination = destination } - if len(buffers) > 0 { + if len(packetBuffers) > 0 { err = sniff.PeekPacket( ctx, metadata, - buffer.Bytes(), + sniffBuffer.Bytes(), sniff.QUICClientHello, ) } else { + var packetSniffers []sniff.PacketSniffer + if len(action.PacketSniffers) > 0 { + packetSniffers = action.PacketSniffers + } else { + packetSniffers = []sniff.PacketSniffer{ + sniff.DomainNameQuery, + sniff.QUICClientHello, + sniff.STUNMessage, + sniff.UTP, + sniff.UDPTracker, + sniff.DTLSRecord, + } + } err = sniff.PeekPacket( ctx, metadata, - buffer.Bytes(), - action.PacketSniffers..., + sniffBuffer.Bytes(), + packetSniffers..., ) } - buffers = append(buffers, buffer) - if E.IsMulti(err, sniff.ErrClientHelloFragmented) && len(buffers) == 0 { + packetBuffer := N.NewPacketBuffer() + *packetBuffer = N.PacketBuffer{ + Buffer: sniffBuffer, + Destination: destination, + } + packetBuffers = append(packetBuffers, packetBuffer) + if E.IsMulti(err, sniff.ErrClientHelloFragmented) && len(packetBuffers) == 0 { r.logger.DebugContext(ctx, "attempt to sniff fragmented QUIC client hello") continue } diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index a157e94e52..57b7364794 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -162,53 +162,31 @@ func (r *RuleActionSniff) Type() string { } func (r *RuleActionSniff) build() error { - if len(r.StreamSniffers) > 0 || len(r.PacketSniffers) > 0 { - return nil - } - if len(r.snifferNames) > 0 { - for _, name := range r.snifferNames { - switch name { - case C.ProtocolTLS: - r.StreamSniffers = append(r.StreamSniffers, sniff.TLSClientHello) - case C.ProtocolHTTP: - r.StreamSniffers = append(r.StreamSniffers, sniff.HTTPHost) - case C.ProtocolQUIC: - r.PacketSniffers = append(r.PacketSniffers, sniff.QUICClientHello) - case C.ProtocolDNS: - r.StreamSniffers = append(r.StreamSniffers, sniff.StreamDomainNameQuery) - r.PacketSniffers = append(r.PacketSniffers, sniff.DomainNameQuery) - case C.ProtocolSTUN: - r.PacketSniffers = append(r.PacketSniffers, sniff.STUNMessage) - case C.ProtocolBitTorrent: - r.StreamSniffers = append(r.StreamSniffers, sniff.BitTorrent) - r.PacketSniffers = append(r.PacketSniffers, sniff.UTP) - r.PacketSniffers = append(r.PacketSniffers, sniff.UDPTracker) - case C.ProtocolDTLS: - r.PacketSniffers = append(r.PacketSniffers, sniff.DTLSRecord) - case C.ProtocolSSH: - r.StreamSniffers = append(r.StreamSniffers, sniff.SSH) - case C.ProtocolRDP: - r.StreamSniffers = append(r.StreamSniffers, sniff.RDP) - default: - return E.New("unknown sniffer: ", name) - } - } - } else { - r.StreamSniffers = []sniff.StreamSniffer{ - sniff.TLSClientHello, - sniff.HTTPHost, - sniff.StreamDomainNameQuery, - sniff.BitTorrent, - sniff.SSH, - sniff.RDP, - } - r.PacketSniffers = []sniff.PacketSniffer{ - sniff.DomainNameQuery, - sniff.QUICClientHello, - sniff.STUNMessage, - sniff.UTP, - sniff.UDPTracker, - sniff.DTLSRecord, + for _, name := range r.snifferNames { + switch name { + case C.ProtocolTLS: + r.StreamSniffers = append(r.StreamSniffers, sniff.TLSClientHello) + case C.ProtocolHTTP: + r.StreamSniffers = append(r.StreamSniffers, sniff.HTTPHost) + case C.ProtocolQUIC: + r.PacketSniffers = append(r.PacketSniffers, sniff.QUICClientHello) + case C.ProtocolDNS: + r.StreamSniffers = append(r.StreamSniffers, sniff.StreamDomainNameQuery) + r.PacketSniffers = append(r.PacketSniffers, sniff.DomainNameQuery) + case C.ProtocolSTUN: + r.PacketSniffers = append(r.PacketSniffers, sniff.STUNMessage) + case C.ProtocolBitTorrent: + r.StreamSniffers = append(r.StreamSniffers, sniff.BitTorrent) + r.PacketSniffers = append(r.PacketSniffers, sniff.UTP) + r.PacketSniffers = append(r.PacketSniffers, sniff.UDPTracker) + case C.ProtocolDTLS: + r.PacketSniffers = append(r.PacketSniffers, sniff.DTLSRecord) + case C.ProtocolSSH: + r.StreamSniffers = append(r.StreamSniffers, sniff.SSH) + case C.ProtocolRDP: + r.StreamSniffers = append(r.StreamSniffers, sniff.RDP) + default: + return E.New("unknown sniffer: ", name) } } return nil From 63d8f6dc1c2018655c20b4e8e0140f67c2ada851 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 2 Nov 2024 00:39:02 +0800 Subject: [PATCH 05/49] refactor: Modular inbounds/outbounds --- adapter/inbound.go | 7 + adapter/inbound/adapter.go | 21 ++ adapter/inbound/registry.go | 68 +++++ adapter/outbound.go | 9 + adapter/outbound/adapter.go | 45 ++++ {outbound => adapter/outbound}/default.go | 47 ---- adapter/outbound/registry.go | 68 +++++ box.go | 73 ++++-- cmd/sing-box/cmd.go | 4 +- cmd/sing-box/cmd_format.go | 3 +- cmd/sing-box/cmd_merge.go | 23 +- cmd/sing-box/cmd_run.go | 6 +- cmd/sing-box/cmd_tools.go | 2 +- common/dialer/default.go | 2 +- common/dialer/wireguard.go | 4 + common/dialer/wireguard_control.go | 11 - common/dialer/wiregurad_stub.go | 9 - common/listener/listener.go | 136 ++++++++++ .../listener/listener_go121.go | 2 +- common/listener/listener_go123.go | 16 ++ .../listener/listener_nongo121.go | 2 +- common/listener/listener_nongo123.go | 15 ++ common/listener/listener_tcp.go | 85 ++++++ common/listener/listener_udp.go | 153 +++++++++++ experimental/clashapi/api_meta_group.go | 10 +- experimental/clashapi/proxies.go | 6 +- experimental/libbox/command_group.go | 22 +- experimental/libbox/command_select.go | 4 +- experimental/libbox/command_urltest.go | 4 +- experimental/libbox/config.go | 19 +- experimental/libbox/service.go | 8 +- experimental/libbox/setup.go | 1 - go.mod | 2 - go.sum | 8 - inbound/builder.go | 54 ---- inbound/default.go | 209 --------------- inbound/default_tcp.go | 84 ------ inbound/default_tcp_go1.20.go | 18 -- inbound/default_tcp_nongo1.20.go | 15 -- inbound/default_udp.go | 208 --------------- inbound/direct.go | 111 -------- inbound/http.go | 119 --------- inbound/hysteria_stub.go | 20 -- inbound/mixed.go | 70 ----- inbound/naive_quic.go | 47 ---- inbound/naive_quic_stub.go | 11 - inbound/redirect.go | 45 ---- inbound/shadowsocks.go | 114 -------- inbound/socks.go | 52 ---- inbound/tuic_stub.go | 16 -- include/quic.go | 18 ++ include/quic_stub.go | 34 +++ include/registry.go | 95 +++++++ include/wireguard.go | 12 + include/wireguard_stub.go | 20 ++ option/inbound.go | 122 +++------ option/json.go | 71 ----- option/{config.go => options.go} | 7 +- option/outbound.go | 103 ++------ option/rule.go | 13 +- option/rule_action.go | 13 +- option/rule_dns.go | 13 +- option/rule_set.go | 13 +- option/simple.go | 2 +- option/tls_acme.go | 5 +- option/v2ray_transport.go | 5 +- outbound/block.go | 54 ---- outbound/builder.go | 65 ----- outbound/hysteria_stub.go | 20 -- outbound/lookback.go | 14 - outbound/shadowsocksr.go | 18 -- outbound/shadowsocksr_stub.go | 16 -- outbound/tor_embed.go | 15 -- outbound/tor_embed_mobile.go | 15 -- outbound/tor_external.go | 9 - outbound/tuic_stub.go | 16 -- outbound/wireguard_stub.go | 16 -- protocol/block/outbound.go | 42 +++ protocol/direct/inbound.go | 139 ++++++++++ .../direct/loopback_detect.go | 2 +- .../direct.go => protocol/direct/outbound.go | 48 ++-- outbound/dns.go => protocol/dns/handle.go | 47 +--- protocol/dns/outbound.go | 61 +++++ {outbound => protocol/group}/selector.go | 41 +-- {outbound => protocol/group}/urltest.go | 29 +- protocol/http/inbound.go | 122 +++++++++ outbound/http.go => protocol/http/outbound.go | 35 ++- .../hysteria/inbound.go | 61 +++-- .../hysteria/outbound.go | 44 ++-- .../hysteria2/inbound.go | 61 +++-- .../hysteria2/outbound.go | 44 ++-- protocol/mixed/inbound.go | 109 ++++++++ protocol/naive/inbound.go | 248 ++++++++++++++++++ .../naive/inbound_conn.go | 219 +--------------- protocol/naive/quic/inbound_init.go | 52 ++++ protocol/redirect/redirect.go | 65 +++++ {inbound => protocol/redirect}/tproxy.go | 79 ++++-- protocol/shadowsocks/inbound.go | 179 +++++++++++++ .../shadowsocks/inbound_multi.go | 83 +++--- .../shadowsocks/inbound_relay.go | 79 ++++-- .../shadowsocks/outbound.go | 45 ++-- .../shadowtls/inbound.go | 58 ++-- .../shadowtls/outbound.go | 30 +-- protocol/socks/inbound.go | 91 +++++++ .../socks.go => protocol/socks/outbound.go | 51 ++-- outbound/ssh.go => protocol/ssh/outbound.go | 42 ++- outbound/tor.go => protocol/tor/outbound.go | 41 ++- {outbound => protocol/tor}/proxy.go | 7 +- .../trojan.go => protocol/trojan/inbound.go | 88 ++++--- .../trojan.go => protocol/trojan/outbound.go | 39 ++- inbound/tuic.go => protocol/tuic/inbound.go | 61 +++-- outbound/tuic.go => protocol/tuic/outbound.go | 42 ++- inbound/tun.go => protocol/tun/inbound.go | 36 +-- inbound/vless.go => protocol/vless/inbound.go | 101 ++++--- .../vless.go => protocol/vless/outbound.go | 41 ++- inbound/vmess.go => protocol/vmess/inbound.go | 101 ++++--- .../vmess.go => protocol/vmess/outbound.go | 41 ++- protocol/wireguard/init.go | 10 + .../wireguard/outbound.go | 62 +++-- route/dns.go | 6 +- route/route.go | 2 +- route/router.go | 22 +- test/brutal_test.go | 16 +- test/direct_test.go | 4 +- test/domain_inbound_test.go | 6 +- test/ech_test.go | 12 +- test/http_test.go | 4 +- test/hysteria2_test.go | 10 +- test/hysteria_test.go | 10 +- test/inbound_detour_test.go | 4 +- test/mux_cool_test.go | 10 +- test/mux_test.go | 8 +- test/naive_test.go | 6 +- test/shadowsocks_legacy_test.go | 4 +- test/shadowsocks_test.go | 18 +- test/shadowtls_test.go | 14 +- test/ss_plugin_test.go | 4 +- test/tfo_test.go | 4 +- test/tls_test.go | 4 +- test/trojan_test.go | 12 +- test/tuic_test.go | 10 +- test/v2ray_api_test.go | 4 +- test/v2ray_grpc_test.go | 6 +- test/v2ray_transport_test.go | 16 +- test/v2ray_ws_test.go | 6 +- test/vmess_test.go | 10 +- test/wireguard_test.go | 4 +- test/wrapper_test.go | 2 +- transport/trojan/mux.go | 9 +- transport/trojan/service.go | 8 +- transport/wireguard/client_bind.go | 11 +- transport/wireguard/resolve.go | 2 +- 152 files changed, 3113 insertions(+), 2923 deletions(-) create mode 100644 adapter/inbound/adapter.go create mode 100644 adapter/inbound/registry.go create mode 100644 adapter/outbound/adapter.go rename {outbound => adapter/outbound}/default.go (87%) create mode 100644 adapter/outbound/registry.go delete mode 100644 common/dialer/wireguard_control.go delete mode 100644 common/dialer/wiregurad_stub.go create mode 100644 common/listener/listener.go rename inbound/default_tcp_go1.21.go => common/listener/listener_go121.go (90%) create mode 100644 common/listener/listener_go123.go rename inbound/default_tcp_nongo1.21.go => common/listener/listener_nongo121.go (87%) create mode 100644 common/listener/listener_nongo123.go create mode 100644 common/listener/listener_tcp.go create mode 100644 common/listener/listener_udp.go delete mode 100644 inbound/builder.go delete mode 100644 inbound/default.go delete mode 100644 inbound/default_tcp.go delete mode 100644 inbound/default_tcp_go1.20.go delete mode 100644 inbound/default_tcp_nongo1.20.go delete mode 100644 inbound/default_udp.go delete mode 100644 inbound/direct.go delete mode 100644 inbound/http.go delete mode 100644 inbound/hysteria_stub.go delete mode 100644 inbound/mixed.go delete mode 100644 inbound/naive_quic.go delete mode 100644 inbound/naive_quic_stub.go delete mode 100644 inbound/redirect.go delete mode 100644 inbound/shadowsocks.go delete mode 100644 inbound/socks.go delete mode 100644 inbound/tuic_stub.go create mode 100644 include/registry.go create mode 100644 include/wireguard.go create mode 100644 include/wireguard_stub.go delete mode 100644 option/json.go rename option/{config.go => options.go} (84%) delete mode 100644 outbound/block.go delete mode 100644 outbound/builder.go delete mode 100644 outbound/hysteria_stub.go delete mode 100644 outbound/lookback.go delete mode 100644 outbound/shadowsocksr.go delete mode 100644 outbound/shadowsocksr_stub.go delete mode 100644 outbound/tor_embed.go delete mode 100644 outbound/tor_embed_mobile.go delete mode 100644 outbound/tor_external.go delete mode 100644 outbound/tuic_stub.go delete mode 100644 outbound/wireguard_stub.go create mode 100644 protocol/block/outbound.go create mode 100644 protocol/direct/inbound.go rename outbound/direct_loopback_detect.go => protocol/direct/loopback_detect.go (99%) rename outbound/direct.go => protocol/direct/outbound.go (76%) rename outbound/dns.go => protocol/dns/handle.go (83%) create mode 100644 protocol/dns/outbound.go rename {outbound => protocol/group}/selector.go (82%) rename {outbound => protocol/group}/urltest.go (94%) create mode 100644 protocol/http/inbound.go rename outbound/http.go => protocol/http/outbound.go (56%) rename inbound/hysteria.go => protocol/hysteria/inbound.go (71%) rename outbound/hysteria.go => protocol/hysteria/outbound.go (76%) rename inbound/hysteria2.go => protocol/hysteria2/inbound.go (74%) rename outbound/hysteria2.go => protocol/hysteria2/outbound.go (69%) create mode 100644 protocol/mixed/inbound.go create mode 100644 protocol/naive/inbound.go rename inbound/naive.go => protocol/naive/inbound_conn.go (58%) create mode 100644 protocol/naive/quic/inbound_init.go create mode 100644 protocol/redirect/redirect.go rename {inbound => protocol/redirect}/tproxy.go (60%) create mode 100644 protocol/shadowsocks/inbound.go rename inbound/shadowsocks_multi.go => protocol/shadowsocks/inbound_multi.go (59%) rename inbound/shadowsocks_relay.go => protocol/shadowsocks/inbound_relay.go (57%) rename outbound/shadowsocks.go => protocol/shadowsocks/outbound.go (80%) rename inbound/shadowtls.go => protocol/shadowtls/inbound.go (62%) rename outbound/shadowtls.go => protocol/shadowtls/outbound.go (74%) create mode 100644 protocol/socks/inbound.go rename outbound/socks.go => protocol/socks/outbound.go (65%) rename outbound/ssh.go => protocol/ssh/outbound.go (80%) rename outbound/tor.go => protocol/tor/outbound.go (83%) rename {outbound => protocol/tor}/proxy.go (94%) rename inbound/trojan.go => protocol/trojan/inbound.go (68%) rename outbound/trojan.go => protocol/trojan/outbound.go (79%) rename inbound/tuic.go => protocol/tuic/inbound.go (68%) rename outbound/tuic.go => protocol/tuic/outbound.go (76%) rename inbound/tun.go => protocol/tun/inbound.go (92%) rename inbound/vless.go => protocol/vless/inbound.go (60%) rename outbound/vless.go => protocol/vless/outbound.go (84%) rename inbound/vmess.go => protocol/vmess/inbound.go (62%) rename outbound/vmess.go => protocol/vmess/outbound.go (84%) create mode 100644 protocol/wireguard/init.go rename outbound/wireguard.go => protocol/wireguard/outbound.go (75%) diff --git a/adapter/inbound.go b/adapter/inbound.go index 300f57e3d9..ca3e9e5900 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -5,6 +5,7 @@ import ( "net/netip" "github.com/sagernet/sing-box/common/process" + "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" M "github.com/sagernet/sing/common/metadata" ) @@ -25,6 +26,11 @@ type UDPInjectableInbound interface { PacketConnectionHandlerEx } +type InboundRegistry interface { + option.InboundOptionsRegistry + CreateInbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Inbound, error) +} + type InboundContext struct { Inbound string InboundType string @@ -44,6 +50,7 @@ type InboundContext struct { // cache + // Deprecated: implement in rule action InboundDetour string LastInbound string OriginDestination M.Socksaddr diff --git a/adapter/inbound/adapter.go b/adapter/inbound/adapter.go new file mode 100644 index 0000000000..1426104afb --- /dev/null +++ b/adapter/inbound/adapter.go @@ -0,0 +1,21 @@ +package inbound + +type Adapter struct { + inboundType string + inboundTag string +} + +func NewAdapter(inboundType string, inboundTag string) Adapter { + return Adapter{ + inboundType: inboundType, + inboundTag: inboundTag, + } +} + +func (a *Adapter) Type() string { + return a.inboundType +} + +func (a *Adapter) Tag() string { + return a.inboundTag +} diff --git a/adapter/inbound/registry.go b/adapter/inbound/registry.go new file mode 100644 index 0000000000..9f678c90e7 --- /dev/null +++ b/adapter/inbound/registry.go @@ -0,0 +1,68 @@ +package inbound + +import ( + "context" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" +) + +type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Inbound, error) + +func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) { + registry.register(outboundType, func() any { + return new(Options) + }, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Inbound, error) { + return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options.(*Options))) + }) +} + +var _ adapter.InboundRegistry = (*Registry)(nil) + +type ( + optionsConstructorFunc func() any + constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Inbound, error) +) + +type Registry struct { + access sync.Mutex + optionsType map[string]optionsConstructorFunc + constructors map[string]constructorFunc +} + +func NewRegistry() *Registry { + return &Registry{ + optionsType: make(map[string]optionsConstructorFunc), + constructors: make(map[string]constructorFunc), + } +} + +func (r *Registry) CreateOptions(outboundType string) (any, bool) { + r.access.Lock() + defer r.access.Unlock() + optionsConstructor, loaded := r.optionsType[outboundType] + if !loaded { + return nil, false + } + return optionsConstructor(), true +} + +func (r *Registry) CreateInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) { + r.access.Lock() + defer r.access.Unlock() + constructor, loaded := r.constructors[outboundType] + if !loaded { + return nil, E.New("outbound type not found: " + outboundType) + } + return constructor(ctx, router, logger, tag, options) +} + +func (r *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) { + r.access.Lock() + defer r.access.Unlock() + r.optionsType[outboundType] = optionsConstructor + r.constructors[outboundType] = constructor +} diff --git a/adapter/outbound.go b/adapter/outbound.go index 312cdf3ed0..df11ed6132 100644 --- a/adapter/outbound.go +++ b/adapter/outbound.go @@ -1,6 +1,10 @@ package adapter import ( + "context" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" N "github.com/sagernet/sing/common/network" ) @@ -13,3 +17,8 @@ type Outbound interface { Dependencies() []string N.Dialer } + +type OutboundRegistry interface { + option.OutboundOptionsRegistry + CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error) +} diff --git a/adapter/outbound/adapter.go b/adapter/outbound/adapter.go new file mode 100644 index 0000000000..481bb6197d --- /dev/null +++ b/adapter/outbound/adapter.go @@ -0,0 +1,45 @@ +package outbound + +import ( + "github.com/sagernet/sing-box/option" +) + +type Adapter struct { + protocol string + network []string + tag string + dependencies []string +} + +func NewAdapter(protocol string, network []string, tag string, dependencies []string) Adapter { + return Adapter{ + protocol: protocol, + network: network, + tag: tag, + dependencies: dependencies, + } +} + +func NewAdapterWithDialerOptions(protocol string, network []string, tag string, dialOptions option.DialerOptions) Adapter { + var dependencies []string + if dialOptions.Detour != "" { + dependencies = []string{dialOptions.Detour} + } + return NewAdapter(protocol, network, tag, dependencies) +} + +func (a *Adapter) Type() string { + return a.protocol +} + +func (a *Adapter) Tag() string { + return a.tag +} + +func (a *Adapter) Network() []string { + return a.network +} + +func (a *Adapter) Dependencies() []string { + return a.dependencies +} diff --git a/outbound/default.go b/adapter/outbound/default.go similarity index 87% rename from outbound/default.go rename to adapter/outbound/default.go index a34ac97af1..78b9bfd8d6 100644 --- a/outbound/default.go +++ b/adapter/outbound/default.go @@ -9,8 +9,6 @@ import ( "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-dns" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" @@ -21,42 +19,6 @@ import ( N "github.com/sagernet/sing/common/network" ) -type myOutboundAdapter struct { - protocol string - network []string - router adapter.Router - logger log.ContextLogger - tag string - dependencies []string -} - -func (a *myOutboundAdapter) Type() string { - return a.protocol -} - -func (a *myOutboundAdapter) Tag() string { - return a.tag -} - -func (a *myOutboundAdapter) Network() []string { - return a.network -} - -func (a *myOutboundAdapter) Dependencies() []string { - return a.dependencies -} - -func (a *myOutboundAdapter) NewError(ctx context.Context, err error) { - NewError(a.logger, ctx, err) -} - -func withDialerDependency(options option.DialerOptions) []string { - if options.Detour != "" { - return []string{options.Detour} - } - return nil -} - func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext) error { ctx = adapter.WithContext(ctx, &metadata) var outConn net.Conn @@ -233,12 +195,3 @@ func CopyEarlyConn(ctx context.Context, conn net.Conn, serverConn net.Conn) erro } return bufio.CopyConn(ctx, conn, serverConn) } - -func NewError(logger log.ContextLogger, ctx context.Context, err error) { - common.Close(err) - if E.IsClosedOrCanceled(err) { - logger.DebugContext(ctx, "connection closed: ", err) - return - } - logger.ErrorContext(ctx, err) -} diff --git a/adapter/outbound/registry.go b/adapter/outbound/registry.go new file mode 100644 index 0000000000..f25631cf14 --- /dev/null +++ b/adapter/outbound/registry.go @@ -0,0 +1,68 @@ +package outbound + +import ( + "context" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" +) + +type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Outbound, error) + +func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) { + registry.register(outboundType, func() any { + return new(Options) + }, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Outbound, error) { + return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options.(*Options))) + }) +} + +var _ adapter.OutboundRegistry = (*Registry)(nil) + +type ( + optionsConstructorFunc func() any + constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Outbound, error) +) + +type Registry struct { + access sync.Mutex + optionsType map[string]optionsConstructorFunc + constructors map[string]constructorFunc +} + +func NewRegistry() *Registry { + return &Registry{ + optionsType: make(map[string]optionsConstructorFunc), + constructors: make(map[string]constructorFunc), + } +} + +func (r *Registry) CreateOptions(outboundType string) (any, bool) { + r.access.Lock() + defer r.access.Unlock() + optionsConstructor, loaded := r.optionsType[outboundType] + if !loaded { + return nil, false + } + return optionsConstructor(), true +} + +func (r *Registry) CreateOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Outbound, error) { + r.access.Lock() + defer r.access.Unlock() + constructor, loaded := r.constructors[outboundType] + if !loaded { + return nil, E.New("outbound type not found: " + outboundType) + } + return constructor(ctx, router, logger, tag, options) +} + +func (r *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) { + r.access.Lock() + defer r.access.Unlock() + r.optionsType[outboundType] = optionsConstructor + r.constructors[outboundType] = constructor +} diff --git a/box.go b/box.go index 716b1b093c..bc714e5a32 100644 --- a/box.go +++ b/box.go @@ -14,10 +14,9 @@ import ( "github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/experimental/cachefile" "github.com/sagernet/sing-box/experimental/libbox/platform" - "github.com/sagernet/sing-box/inbound" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing-box/outbound" + "github.com/sagernet/sing-box/protocol/direct" "github.com/sagernet/sing-box/route" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" @@ -44,16 +43,37 @@ type Box struct { type Options struct { option.Options Context context.Context - PlatformInterface platform.Interface PlatformLogWriter log.PlatformWriter } +func Context(ctx context.Context, inboundRegistry adapter.InboundRegistry, outboundRegistry adapter.OutboundRegistry) context.Context { + if service.FromContext[option.InboundOptionsRegistry](ctx) == nil || + service.FromContext[adapter.InboundRegistry](ctx) == nil { + ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry) + ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry) + } + if service.FromContext[option.OutboundOptionsRegistry](ctx) == nil || + service.FromContext[adapter.OutboundRegistry](ctx) == nil { + ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry) + ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry) + } + return ctx +} + func New(options Options) (*Box, error) { createdAt := time.Now() ctx := options.Context if ctx == nil { ctx = context.Background() } + inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx) + if inboundRegistry == nil { + return nil, E.New("missing inbound registry in context") + } + outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx) + if outboundRegistry == nil { + return nil, E.New("missing outbound registry in context") + } ctx = service.ContextWithDefaultRegistry(ctx) ctx = pause.WithDefaultManager(ctx) experimentalOptions := common.PtrValueOrDefault(options.Experimental) @@ -70,8 +90,9 @@ func New(options Options) (*Box, error) { if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" { needV2RayAPI = true } + platformInterface := service.FromContext[platform.Interface](ctx) var defaultLogWriter io.Writer - if options.PlatformInterface != nil { + if platformInterface != nil { defaultLogWriter = io.Discard } logFactory, err := log.New(log.Options{ @@ -92,64 +113,70 @@ func New(options Options) (*Box, error) { common.PtrValueOrDefault(options.DNS), common.PtrValueOrDefault(options.NTP), options.Inbounds, - options.PlatformInterface, ) if err != nil { return nil, E.Cause(err, "parse route options") } - inbounds := make([]adapter.Inbound, 0, len(options.Inbounds)) - outbounds := make([]adapter.Outbound, 0, len(options.Outbounds)) for i, inboundOptions := range options.Inbounds { - var in adapter.Inbound + var currentInbound adapter.Inbound var tag string if inboundOptions.Tag != "" { tag = inboundOptions.Tag } else { tag = F.ToString(i) } - in, err = inbound.New( + currentInbound, err = inboundRegistry.CreateInbound( ctx, router, logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")), tag, - inboundOptions, - options.PlatformInterface, + inboundOptions.Type, + inboundOptions.Options, ) if err != nil { return nil, E.Cause(err, "parse inbound[", i, "]") } - inbounds = append(inbounds, in) + inbounds = append(inbounds, currentInbound) } for i, outboundOptions := range options.Outbounds { - var out adapter.Outbound + var currentOutbound adapter.Outbound var tag string if outboundOptions.Tag != "" { tag = outboundOptions.Tag } else { tag = F.ToString(i) } - out, err = outbound.New( - ctx, + outboundCtx := ctx + if tag != "" { + // TODO: remove this + outboundCtx = adapter.WithContext(outboundCtx, &adapter.InboundContext{ + Outbound: tag, + }) + } + currentOutbound, err = outboundRegistry.CreateOutbound( + outboundCtx, router, logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")), tag, - outboundOptions) + outboundOptions.Type, + outboundOptions.Options, + ) if err != nil { return nil, E.Cause(err, "parse outbound[", i, "]") } - outbounds = append(outbounds, out) + outbounds = append(outbounds, currentOutbound) } err = router.Initialize(inbounds, outbounds, func() adapter.Outbound { - out, oErr := outbound.New(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.Outbound{Type: "direct", Tag: "default"}) - common.Must(oErr) - outbounds = append(outbounds, out) - return out + defaultOutbound, cErr := direct.NewOutbound(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.DirectOutboundOptions{}) + common.Must(cErr) + outbounds = append(outbounds, defaultOutbound) + return defaultOutbound }) if err != nil { return nil, err } - if options.PlatformInterface != nil { - err = options.PlatformInterface.Initialize(ctx, router) + if platformInterface != nil { + err = platformInterface.Initialize(ctx, router) if err != nil { return nil, E.Cause(err, "initialize platform interface") } diff --git a/cmd/sing-box/cmd.go b/cmd/sing-box/cmd.go index a2d4a00b35..dc7a830965 100644 --- a/cmd/sing-box/cmd.go +++ b/cmd/sing-box/cmd.go @@ -7,8 +7,9 @@ import ( "strconv" "time" + "github.com/sagernet/sing-box" "github.com/sagernet/sing-box/experimental/deprecated" - _ "github.com/sagernet/sing-box/include" + "github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/filemanager" @@ -68,4 +69,5 @@ func preRun(cmd *cobra.Command, args []string) { configPaths = append(configPaths, "config.json") } globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger())) + globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry()) } diff --git a/cmd/sing-box/cmd_format.go b/cmd/sing-box/cmd_format.go index fc47c5a8dd..9856c76336 100644 --- a/cmd/sing-box/cmd_format.go +++ b/cmd/sing-box/cmd_format.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "os" "path/filepath" @@ -38,7 +39,7 @@ func format() error { return err } for _, optionsEntry := range optionsList { - optionsEntry.options, err = badjson.Omitempty(optionsEntry.options) + optionsEntry.options, err = badjson.Omitempty(context.TODO(), optionsEntry.options) if err != nil { return err } diff --git a/cmd/sing-box/cmd_merge.go b/cmd/sing-box/cmd_merge.go index 10dd38a1db..fa194ed342 100644 --- a/cmd/sing-box/cmd_merge.go +++ b/cmd/sing-box/cmd_merge.go @@ -68,29 +68,19 @@ func merge(outputPath string) error { } func mergePathResources(options *option.Options) error { - for index, inbound := range options.Inbounds { - rawOptions, err := inbound.RawOptions() - if err != nil { - return err - } - if tlsOptions, containsTLSOptions := rawOptions.(option.InboundTLSOptionsWrapper); containsTLSOptions { + for _, inbound := range options.Inbounds { + if tlsOptions, containsTLSOptions := inbound.Options.(option.InboundTLSOptionsWrapper); containsTLSOptions { tlsOptions.ReplaceInboundTLSOptions(mergeTLSInboundOptions(tlsOptions.TakeInboundTLSOptions())) } - options.Inbounds[index] = inbound } - for index, outbound := range options.Outbounds { - rawOptions, err := outbound.RawOptions() - if err != nil { - return err - } + for _, outbound := range options.Outbounds { switch outbound.Type { case C.TypeSSH: - outbound.SSHOptions = mergeSSHOutboundOptions(outbound.SSHOptions) + mergeSSHOutboundOptions(outbound.Options.(*option.SSHOutboundOptions)) } - if tlsOptions, containsTLSOptions := rawOptions.(option.OutboundTLSOptionsWrapper); containsTLSOptions { + if tlsOptions, containsTLSOptions := outbound.Options.(option.OutboundTLSOptionsWrapper); containsTLSOptions { tlsOptions.ReplaceOutboundTLSOptions(mergeTLSOutboundOptions(tlsOptions.TakeOutboundTLSOptions())) } - options.Outbounds[index] = outbound } return nil } @@ -138,13 +128,12 @@ func mergeTLSOutboundOptions(options *option.OutboundTLSOptions) *option.Outboun return options } -func mergeSSHOutboundOptions(options option.SSHOutboundOptions) option.SSHOutboundOptions { +func mergeSSHOutboundOptions(options *option.SSHOutboundOptions) { if options.PrivateKeyPath != "" { if content, err := os.ReadFile(os.ExpandEnv(options.PrivateKeyPath)); err == nil { options.PrivateKey = trimStringArray(strings.Split(string(content), "\n")) } } - return options } func trimStringArray(array []string) []string { diff --git a/cmd/sing-box/cmd_run.go b/cmd/sing-box/cmd_run.go index 6850cd19e3..f31db9dc82 100644 --- a/cmd/sing-box/cmd_run.go +++ b/cmd/sing-box/cmd_run.go @@ -57,7 +57,7 @@ func readConfigAt(path string) (*OptionsEntry, error) { if err != nil { return nil, E.Cause(err, "read config at ", path) } - options, err := json.UnmarshalExtended[option.Options](configContent) + options, err := json.UnmarshalExtendedContext[option.Options](globalCtx, configContent) if err != nil { return nil, E.Cause(err, "decode config at ", path) } @@ -109,13 +109,13 @@ func readConfigAndMerge() (option.Options, error) { } var mergedMessage json.RawMessage for _, options := range optionsList { - mergedMessage, err = badjson.MergeJSON(options.options.RawMessage, mergedMessage, false) + mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false) if err != nil { return option.Options{}, E.Cause(err, "merge config at ", options.path) } } var mergedOptions option.Options - err = mergedOptions.UnmarshalJSON(mergedMessage) + err = mergedOptions.UnmarshalJSONContext(globalCtx, mergedMessage) if err != nil { return option.Options{}, E.Cause(err, "unmarshal merged config") } diff --git a/cmd/sing-box/cmd_tools.go b/cmd/sing-box/cmd_tools.go index 86b9302e09..f4c8736ec1 100644 --- a/cmd/sing-box/cmd_tools.go +++ b/cmd/sing-box/cmd_tools.go @@ -30,7 +30,7 @@ func createPreStartedClient() (*box.Box, error) { return nil, err } } - instance, err := box.New(box.Options{Options: options}) + instance, err := box.New(box.Options{Context: globalCtx, Options: options}) if err != nil { return nil, E.Cause(err, "create service") } diff --git a/common/dialer/default.go b/common/dialer/default.go index f4fd99fbca..02678961a7 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -125,7 +125,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi setMultiPathTCP(&dialer4) } if options.IsWireGuardListener { - for _, controlFn := range wgControlFns { + for _, controlFn := range WgControlFns { listener.Control = control.Append(listener.Control, controlFn) } } diff --git a/common/dialer/wireguard.go b/common/dialer/wireguard.go index 195133c650..fbd323d800 100644 --- a/common/dialer/wireguard.go +++ b/common/dialer/wireguard.go @@ -2,8 +2,12 @@ package dialer import ( "net" + + "github.com/sagernet/sing/common/control" ) type WireGuardListener interface { ListenPacketCompat(network, address string) (net.PacketConn, error) } + +var WgControlFns []control.Func diff --git a/common/dialer/wireguard_control.go b/common/dialer/wireguard_control.go deleted file mode 100644 index def86411a5..0000000000 --- a/common/dialer/wireguard_control.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build with_wireguard - -package dialer - -import ( - "github.com/sagernet/wireguard-go/conn" -) - -var _ WireGuardListener = (conn.Listener)(nil) - -var wgControlFns = conn.ControlFns diff --git a/common/dialer/wiregurad_stub.go b/common/dialer/wiregurad_stub.go deleted file mode 100644 index d30c223a95..0000000000 --- a/common/dialer/wiregurad_stub.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !with_wireguard - -package dialer - -import ( - "github.com/sagernet/sing/common/control" -) - -var wgControlFns []control.Func diff --git a/common/listener/listener.go b/common/listener/listener.go new file mode 100644 index 0000000000..b42b0434b2 --- /dev/null +++ b/common/listener/listener.go @@ -0,0 +1,136 @@ +package listener + +import ( + "context" + "net" + "sync/atomic" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/settings" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type Listener struct { + ctx context.Context + logger logger.ContextLogger + network []string + listenOptions option.ListenOptions + connHandler adapter.ConnectionHandlerEx + packetHandler adapter.PacketHandlerEx + oobPacketHandler adapter.OOBPacketHandlerEx + threadUnsafePacketWriter bool + disablePacketOutput bool + setSystemProxy bool + systemProxySOCKS bool + + tcpListener net.Listener + systemProxy settings.SystemProxy + udpConn *net.UDPConn + udpAddr M.Socksaddr + packetOutbound chan *N.PacketBuffer + packetOutboundClosed chan struct{} + shutdown atomic.Bool +} + +type Options struct { + Context context.Context + Logger logger.ContextLogger + Network []string + Listen option.ListenOptions + ConnectionHandler adapter.ConnectionHandlerEx + PacketHandler adapter.PacketHandlerEx + OOBPacketHandler adapter.OOBPacketHandlerEx + ThreadUnsafePacketWriter bool + DisablePacketOutput bool + SetSystemProxy bool + SystemProxySOCKS bool +} + +func New( + options Options, +) *Listener { + return &Listener{ + ctx: options.Context, + logger: options.Logger, + network: options.Network, + listenOptions: options.Listen, + connHandler: options.ConnectionHandler, + packetHandler: options.PacketHandler, + oobPacketHandler: options.OOBPacketHandler, + threadUnsafePacketWriter: options.ThreadUnsafePacketWriter, + disablePacketOutput: options.DisablePacketOutput, + setSystemProxy: options.SetSystemProxy, + systemProxySOCKS: options.SystemProxySOCKS, + } +} + +func (l *Listener) Start() error { + if common.Contains(l.network, N.NetworkTCP) { + _, err := l.ListenTCP() + if err != nil { + return err + } + go l.loopTCPIn() + } + if common.Contains(l.network, N.NetworkUDP) { + _, err := l.ListenUDP() + if err != nil { + return err + } + l.packetOutboundClosed = make(chan struct{}) + l.packetOutbound = make(chan *N.PacketBuffer, 64) + go l.loopUDPIn() + if !l.disablePacketOutput { + go l.loopUDPOut() + } + } + if l.setSystemProxy { + listenPort := M.SocksaddrFromNet(l.tcpListener.Addr()).Port + var listenAddrString string + listenAddr := l.listenOptions.Listen.Build() + if listenAddr.IsUnspecified() { + listenAddrString = "127.0.0.1" + } else { + listenAddrString = listenAddr.String() + } + systemProxy, err := settings.NewSystemProxy(l.ctx, M.ParseSocksaddrHostPort(listenAddrString, listenPort), l.systemProxySOCKS) + if err != nil { + return E.Cause(err, "initialize system proxy") + } + err = systemProxy.Enable() + if err != nil { + return E.Cause(err, "set system proxy") + } + l.systemProxy = systemProxy + } + return nil +} + +func (l *Listener) Close() error { + l.shutdown.Store(true) + var err error + if l.systemProxy != nil && l.systemProxy.IsEnabled() { + err = l.systemProxy.Disable() + } + return E.Errors(err, common.Close( + l.tcpListener, + common.PtrOrNil(l.udpConn), + )) +} + +func (l *Listener) TCPListener() net.Listener { + return l.tcpListener +} + +func (l *Listener) UDPConn() *net.UDPConn { + return l.udpConn +} + +func (l *Listener) ListenOptions() option.ListenOptions { + return l.listenOptions +} diff --git a/inbound/default_tcp_go1.21.go b/common/listener/listener_go121.go similarity index 90% rename from inbound/default_tcp_go1.21.go rename to common/listener/listener_go121.go index 906818cbe2..5af1b05a30 100644 --- a/inbound/default_tcp_go1.21.go +++ b/common/listener/listener_go121.go @@ -1,6 +1,6 @@ //go:build go1.21 -package inbound +package listener import "net" diff --git a/common/listener/listener_go123.go b/common/listener/listener_go123.go new file mode 100644 index 0000000000..2e1f4cf43a --- /dev/null +++ b/common/listener/listener_go123.go @@ -0,0 +1,16 @@ +//go:build go1.23 + +package listener + +import ( + "net" + "time" +) + +func setKeepAliveConfig(listener *net.ListenConfig, idle time.Duration, interval time.Duration) { + listener.KeepAliveConfig = net.KeepAliveConfig{ + Enable: true, + Idle: idle, + Interval: interval, + } +} diff --git a/inbound/default_tcp_nongo1.21.go b/common/listener/listener_nongo121.go similarity index 87% rename from inbound/default_tcp_nongo1.21.go rename to common/listener/listener_nongo121.go index d19adb1957..36073afe33 100644 --- a/inbound/default_tcp_nongo1.21.go +++ b/common/listener/listener_nongo121.go @@ -1,6 +1,6 @@ //go:build !go1.21 -package inbound +package listener import "net" diff --git a/common/listener/listener_nongo123.go b/common/listener/listener_nongo123.go new file mode 100644 index 0000000000..e1582981c2 --- /dev/null +++ b/common/listener/listener_nongo123.go @@ -0,0 +1,15 @@ +//go:build !go1.23 + +package listener + +import ( + "net" + "time" + + "github.com/sagernet/sing/common/control" +) + +func setKeepAliveConfig(listener *net.ListenConfig, idle time.Duration, interval time.Duration) { + listener.KeepAlive = idle + listener.Control = control.Append(listener.Control, control.SetKeepAlivePeriod(idle, interval)) +} diff --git a/common/listener/listener_tcp.go b/common/listener/listener_tcp.go new file mode 100644 index 0000000000..02cef3f04d --- /dev/null +++ b/common/listener/listener_tcp.go @@ -0,0 +1,85 @@ +package listener + +import ( + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + + "github.com/metacubex/tfo-go" +) + +func (l *Listener) ListenTCP() (net.Listener, error) { + var err error + bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(), l.listenOptions.ListenPort) + var tcpListener net.Listener + var listenConfig net.ListenConfig + if l.listenOptions.TCPKeepAlive >= 0 { + keepIdle := time.Duration(l.listenOptions.TCPKeepAlive) + if keepIdle == 0 { + keepIdle = C.TCPKeepAliveInitial + } + keepInterval := time.Duration(l.listenOptions.TCPKeepAliveInterval) + if keepInterval == 0 { + keepInterval = C.TCPKeepAliveInterval + } + setKeepAliveConfig(&listenConfig, keepIdle, keepInterval) + } + if l.listenOptions.TCPMultiPath { + if !go121Available { + return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.") + } + setMultiPathTCP(&listenConfig) + } + if l.listenOptions.TCPFastOpen { + var tfoConfig tfo.ListenConfig + tfoConfig.ListenConfig = listenConfig + tcpListener, err = tfoConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String()) + } else { + tcpListener, err = listenConfig.Listen(l.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String()) + } + if err == nil { + l.logger.Info("tcp server started at ", tcpListener.Addr()) + } + //nolint:staticcheck + if l.listenOptions.ProxyProtocol || l.listenOptions.ProxyProtocolAcceptNoHeader { + return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0") + } + l.tcpListener = tcpListener + return tcpListener, err +} + +func (l *Listener) loopTCPIn() { + tcpListener := l.tcpListener + var metadata adapter.InboundContext + for { + conn, err := tcpListener.Accept() + if err != nil { + //nolint:staticcheck + if netError, isNetError := err.(net.Error); isNetError && netError.Temporary() { + l.logger.Error(err) + continue + } + if l.shutdown.Load() && E.IsClosed(err) { + return + } + l.tcpListener.Close() + l.logger.Error("tcp listener closed: ", err) + continue + } + //nolint:staticcheck + metadata.InboundDetour = l.listenOptions.Detour + //nolint:staticcheck + metadata.InboundOptions = l.listenOptions.InboundOptions + metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap() + metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() + ctx := log.ContextWithNewID(l.ctx) + l.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + go l.connHandler.NewConnectionEx(ctx, conn, metadata, nil) + } +} diff --git a/common/listener/listener_udp.go b/common/listener/listener_udp.go new file mode 100644 index 0000000000..0c3220a7f1 --- /dev/null +++ b/common/listener/listener_udp.go @@ -0,0 +1,153 @@ +package listener + +import ( + "net" + "os" + + "github.com/sagernet/sing/common/buf" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func (l *Listener) ListenUDP() (net.PacketConn, error) { + bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(), l.listenOptions.ListenPort) + var lc net.ListenConfig + var udpFragment bool + if l.listenOptions.UDPFragment != nil { + udpFragment = *l.listenOptions.UDPFragment + } else { + udpFragment = l.listenOptions.UDPFragmentDefault + } + if !udpFragment { + lc.Control = control.Append(lc.Control, control.DisableUDPFragment()) + } + udpConn, err := lc.ListenPacket(l.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String()) + if err != nil { + return nil, err + } + l.udpConn = udpConn.(*net.UDPConn) + l.udpAddr = bindAddr + l.logger.Info("udp server started at ", udpConn.LocalAddr()) + return udpConn, err +} + +func (l *Listener) UDPAddr() M.Socksaddr { + return l.udpAddr +} + +func (l *Listener) PacketWriter() N.PacketWriter { + return (*packetWriter)(l) +} + +func (l *Listener) loopUDPIn() { + defer close(l.packetOutboundClosed) + var buffer *buf.Buffer + if !l.threadUnsafePacketWriter { + buffer = buf.NewPacket() + defer buffer.Release() + buffer.IncRef() + defer buffer.DecRef() + } + if l.oobPacketHandler != nil { + oob := make([]byte, 1024) + for { + if l.threadUnsafePacketWriter { + buffer = buf.NewPacket() + } else { + buffer.Reset() + } + n, oobN, _, addr, err := l.udpConn.ReadMsgUDPAddrPort(buffer.FreeBytes(), oob) + if err != nil { + if l.threadUnsafePacketWriter { + buffer.Release() + } + if l.shutdown.Load() && E.IsClosed(err) { + return + } + l.udpConn.Close() + l.logger.Error("udp listener closed: ", err) + return + } + buffer.Truncate(n) + l.oobPacketHandler.NewPacketEx(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap()) + } + } else { + for { + if l.threadUnsafePacketWriter { + buffer = buf.NewPacket() + } else { + buffer.Reset() + } + n, addr, err := l.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes()) + if err != nil { + if l.threadUnsafePacketWriter { + buffer.Release() + } + if l.shutdown.Load() && E.IsClosed(err) { + return + } + l.udpConn.Close() + l.logger.Error("udp listener closed: ", err) + return + } + buffer.Truncate(n) + l.packetHandler.NewPacketEx(buffer, M.SocksaddrFromNetIP(addr).Unwrap()) + } + } +} + +func (l *Listener) loopUDPOut() { + for { + select { + case packet := <-l.packetOutbound: + destination := packet.Destination.AddrPort() + _, err := l.udpConn.WriteToUDPAddrPort(packet.Buffer.Bytes(), destination) + packet.Buffer.Release() + N.PutPacketBuffer(packet) + if err != nil { + if l.shutdown.Load() && E.IsClosed(err) { + return + } + l.udpConn.Close() + l.logger.Error("udp listener write back: ", destination, ": ", err) + return + } + continue + case <-l.packetOutboundClosed: + } + for { + select { + case packet := <-l.packetOutbound: + packet.Buffer.Release() + N.PutPacketBuffer(packet) + default: + return + } + } + } +} + +type packetWriter Listener + +func (w *packetWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + packet := N.NewPacketBuffer() + packet.Buffer = buffer + packet.Destination = destination + select { + case w.packetOutbound <- packet: + return nil + default: + buffer.Release() + N.PutPacketBuffer(packet) + if w.shutdown.Load() { + return os.ErrClosed + } + w.logger.Trace("dropped packet to ", destination) + return nil + } +} + +func (w *packetWriter) WriteIsThreadUnsafe() { +} diff --git a/experimental/clashapi/api_meta_group.go b/experimental/clashapi/api_meta_group.go index 396dee7f60..531311f4fe 100644 --- a/experimental/clashapi/api_meta_group.go +++ b/experimental/clashapi/api_meta_group.go @@ -10,7 +10,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/urltest" - "github.com/sagernet/sing-box/outbound" + "github.com/sagernet/sing-box/protocol/group" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/batch" "github.com/sagernet/sing/common/json/badjson" @@ -59,7 +59,7 @@ func getGroup(server *Server) func(w http.ResponseWriter, r *http.Request) { func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) - group, ok := proxy.(adapter.OutboundGroup) + outboundGroup, ok := proxy.(adapter.OutboundGroup) if !ok { render.Status(r, http.StatusNotFound) render.JSON(w, r, ErrNotFound) @@ -82,10 +82,10 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) defer cancel() var result map[string]uint16 - if urlTestGroup, isURLTestGroup := group.(adapter.URLTestGroup); isURLTestGroup { + if urlTestGroup, isURLTestGroup := outboundGroup.(adapter.URLTestGroup); isURLTestGroup { result, err = urlTestGroup.URLTest(ctx) } else { - outbounds := common.FilterNotNil(common.Map(group.All(), func(it string) adapter.Outbound { + outbounds := common.FilterNotNil(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { itOutbound, _ := server.router.Outbound(it) return itOutbound })) @@ -95,7 +95,7 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) var resultAccess sync.Mutex for _, detour := range outbounds { tag := detour.Tag() - realTag := outbound.RealTag(detour) + realTag := group.RealTag(detour) if checked[realTag] { continue } diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go index 7a807c1fae..4a9564ee86 100644 --- a/experimental/clashapi/proxies.go +++ b/experimental/clashapi/proxies.go @@ -11,7 +11,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/outbound" + "github.com/sagernet/sing-box/protocol/group" "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json/badjson" @@ -168,7 +168,7 @@ func updateProxy(w http.ResponseWriter, r *http.Request) { } proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound) - selector, ok := proxy.(*outbound.Selector) + selector, ok := proxy.(*group.Selector) if !ok { render.Status(r, http.StatusBadRequest) render.JSON(w, r, newError("Must be a Selector")) @@ -204,7 +204,7 @@ func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request) delay, err := urltest.URLTest(ctx, url, proxy) defer func() { - realTag := outbound.RealTag(proxy) + realTag := group.RealTag(proxy) if err != nil { server.urlTestHistory.DeleteURLTestHistory(realTag) } else { diff --git a/experimental/libbox/command_group.go b/experimental/libbox/command_group.go index 3a8d2a0749..0915f56b83 100644 --- a/experimental/libbox/command_group.go +++ b/experimental/libbox/command_group.go @@ -9,7 +9,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/urltest" - "github.com/sagernet/sing-box/outbound" + "github.com/sagernet/sing-box/protocol/group" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/varbin" "github.com/sagernet/sing/service" @@ -118,14 +118,14 @@ func writeGroups(writer io.Writer, boxService *BoxService) error { } var groups []OutboundGroup for _, iGroup := range iGroups { - var group OutboundGroup - group.Tag = iGroup.Tag() - group.Type = iGroup.Type() - _, group.Selectable = iGroup.(*outbound.Selector) - group.Selected = iGroup.Now() + var outboundGroup OutboundGroup + outboundGroup.Tag = iGroup.Tag() + outboundGroup.Type = iGroup.Type() + _, outboundGroup.Selectable = iGroup.(*group.Selector) + outboundGroup.Selected = iGroup.Now() if cacheFile != nil { - if isExpand, loaded := cacheFile.LoadGroupExpand(group.Tag); loaded { - group.IsExpand = isExpand + if isExpand, loaded := cacheFile.LoadGroupExpand(outboundGroup.Tag); loaded { + outboundGroup.IsExpand = isExpand } } @@ -142,12 +142,12 @@ func writeGroups(writer io.Writer, boxService *BoxService) error { item.URLTestTime = history.Time.Unix() item.URLTestDelay = int32(history.Delay) } - group.ItemList = append(group.ItemList, &item) + outboundGroup.ItemList = append(outboundGroup.ItemList, &item) } - if len(group.ItemList) < 2 { + if len(outboundGroup.ItemList) < 2 { continue } - groups = append(groups, group) + groups = append(groups, outboundGroup) } return varbin.Write(writer, binary.BigEndian, groups) } diff --git a/experimental/libbox/command_select.go b/experimental/libbox/command_select.go index e1e67e6068..f352005d11 100644 --- a/experimental/libbox/command_select.go +++ b/experimental/libbox/command_select.go @@ -4,7 +4,7 @@ import ( "encoding/binary" "net" - "github.com/sagernet/sing-box/outbound" + "github.com/sagernet/sing-box/protocol/group" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/varbin" ) @@ -47,7 +47,7 @@ func (s *CommandServer) handleSelectOutbound(conn net.Conn) error { if !isLoaded { return writeError(conn, E.New("selector not found: ", groupTag)) } - selector, isSelector := outboundGroup.(*outbound.Selector) + selector, isSelector := outboundGroup.(*group.Selector) if !isSelector { return writeError(conn, E.New("outbound is not a selector: ", groupTag)) } diff --git a/experimental/libbox/command_urltest.go b/experimental/libbox/command_urltest.go index 6feda3f8ef..c72ded8a3c 100644 --- a/experimental/libbox/command_urltest.go +++ b/experimental/libbox/command_urltest.go @@ -7,7 +7,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/urltest" - "github.com/sagernet/sing-box/outbound" + "github.com/sagernet/sing-box/protocol/group" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/batch" E "github.com/sagernet/sing/common/exceptions" @@ -49,7 +49,7 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if !isOutboundGroup { return writeError(conn, E.New("outbound is not a group: ", groupTag)) } - urlTest, isURLTest := abstractOutboundGroup.(*outbound.URLTest) + urlTest, isURLTest := abstractOutboundGroup.(*group.URLTest) if isURLTest { go urlTest.CheckOutbounds() } else { diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index c8d0693acb..cc8057f49d 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -10,6 +10,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/process" "github.com/sagernet/sing-box/experimental/libbox/platform" + "github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" @@ -17,10 +18,11 @@ import ( "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/logger" "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" ) -func parseConfig(configContent string) (option.Options, error) { - options, err := json.UnmarshalExtended[option.Options]([]byte(configContent)) +func parseConfig(ctx context.Context, configContent string) (option.Options, error) { + options, err := json.UnmarshalExtendedContext[option.Options](ctx, []byte(configContent)) if err != nil { return option.Options{}, E.Cause(err, "decode config") } @@ -28,16 +30,17 @@ func parseConfig(configContent string) (option.Options, error) { } func CheckConfig(configContent string) error { - options, err := parseConfig(configContent) + ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry()) + options, err := parseConfig(ctx, configContent) if err != nil { return err } - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(ctx) defer cancel() + ctx = service.ContextWith[platform.Interface](ctx, (*platformInterfaceStub)(nil)) instance, err := box.New(box.Options{ - Context: ctx, - Options: options, - PlatformInterface: (*platformInterfaceStub)(nil), + Context: ctx, + Options: options, }) if err == nil { instance.Close() @@ -140,7 +143,7 @@ func (s *platformInterfaceStub) SendNotification(notification *platform.Notifica } func FormatConfig(configContent string) (*StringBox, error) { - options, err := parseConfig(configContent) + options, err := parseConfig(box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry()), configContent) if err != nil { return nil, err } diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 9bfa8a70d7..f6f56b6f6f 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -17,6 +17,7 @@ import ( "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/experimental/libbox/internal/procfs" "github.com/sagernet/sing-box/experimental/libbox/platform" + "github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" @@ -41,21 +42,22 @@ type BoxService struct { } func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) { - options, err := parseConfig(configContent) + ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry()) + options, err := parseConfig(ctx, configContent) if err != nil { return nil, err } runtimeDebug.FreeOSMemory() - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(ctx) ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID) urlTestHistoryStorage := urltest.NewHistoryStorage() ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage) ctx = service.ContextWith[deprecated.Manager](ctx, new(deprecatedManager)) platformWrapper := &platformInterfaceWrapper{iif: platformInterface, useProcFS: platformInterface.UseProcFS()} + ctx = service.ContextWith[platform.Interface](ctx, platformWrapper) instance, err := box.New(box.Options{ Context: ctx, Options: options, - PlatformInterface: platformWrapper, PlatformLogWriter: platformWrapper, }) if err != nil { diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go index 950cbe4edc..184d525012 100644 --- a/experimental/libbox/setup.go +++ b/experimental/libbox/setup.go @@ -10,7 +10,6 @@ import ( "github.com/sagernet/sing-box/common/humanize" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/locale" - _ "github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/log" ) diff --git a/go.mod b/go.mod index da517b17ba..3123b18d6c 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/sagernet/sing-box go 1.20 require ( - berty.tech/go-libtor v1.0.385 github.com/caddyserver/certmagic v0.20.0 github.com/cloudflare/circl v1.3.7 github.com/cretz/bine v0.2.0 @@ -17,7 +16,6 @@ require ( github.com/metacubex/tfo-go v0.0.0-20241006021335-daedaf0ca7aa github.com/mholt/acmez v1.2.0 github.com/miekg/dns v1.1.62 - github.com/ooni/go-libtor v1.1.8 github.com/oschwald/maxminddb-golang v1.12.0 github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1 github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a diff --git a/go.sum b/go.sum index 2671007554..0c5c79aa71 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -berty.tech/go-libtor v1.0.385 h1:RWK94C3hZj6Z2GdvePpHJLnWYobFr3bY/OdUJ5aoEXw= -berty.tech/go-libtor v1.0.385/go.mod h1:9swOOQVb+kmvuAlsgWUK/4c52pm69AdbJsxLzk+fJEw= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= @@ -11,7 +9,6 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -88,8 +85,6 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss= github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= -github.com/ooni/go-libtor v1.1.8 h1:Wo3V3DVTxl5vZdxtQakqYP+DAHx7pPtAFSl1bnAa08w= -github.com/ooni/go-libtor v1.1.8/go.mod h1:q1YyLwRD9GeMyeerVvwc0vJ2YgwDLTp2bdVcrh/JXyI= github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs= github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY= github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= @@ -155,7 +150,6 @@ github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3k github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= @@ -177,7 +171,6 @@ go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= -golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= @@ -191,7 +184,6 @@ golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/inbound/builder.go b/inbound/builder.go deleted file mode 100644 index ddfd361dbd..0000000000 --- a/inbound/builder.go +++ /dev/null @@ -1,54 +0,0 @@ -package inbound - -import ( - "context" - - "github.com/sagernet/sing-box/adapter" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/experimental/libbox/platform" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" - E "github.com/sagernet/sing/common/exceptions" -) - -func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Inbound, platformInterface platform.Interface) (adapter.Inbound, error) { - if options.Type == "" { - return nil, E.New("missing inbound type") - } - switch options.Type { - case C.TypeTun: - return NewTun(ctx, router, logger, tag, options.TunOptions, platformInterface) - case C.TypeRedirect: - return NewRedirect(ctx, router, logger, tag, options.RedirectOptions), nil - case C.TypeTProxy: - return NewTProxy(ctx, router, logger, tag, options.TProxyOptions), nil - case C.TypeDirect: - return NewDirect(ctx, router, logger, tag, options.DirectOptions), nil - case C.TypeSOCKS: - return NewSocks(ctx, router, logger, tag, options.SocksOptions), nil - case C.TypeHTTP: - return NewHTTP(ctx, router, logger, tag, options.HTTPOptions) - case C.TypeMixed: - return NewMixed(ctx, router, logger, tag, options.MixedOptions), nil - case C.TypeShadowsocks: - return NewShadowsocks(ctx, router, logger, tag, options.ShadowsocksOptions) - case C.TypeVMess: - return NewVMess(ctx, router, logger, tag, options.VMessOptions) - case C.TypeTrojan: - return NewTrojan(ctx, router, logger, tag, options.TrojanOptions) - case C.TypeNaive: - return NewNaive(ctx, router, logger, tag, options.NaiveOptions) - case C.TypeHysteria: - return NewHysteria(ctx, router, logger, tag, options.HysteriaOptions) - case C.TypeShadowTLS: - return NewShadowTLS(ctx, router, logger, tag, options.ShadowTLSOptions) - case C.TypeVLESS: - return NewVLESS(ctx, router, logger, tag, options.VLESSOptions) - case C.TypeTUIC: - return NewTUIC(ctx, router, logger, tag, options.TUICOptions) - case C.TypeHysteria2: - return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options) - default: - return nil, E.New("unknown inbound type: ", options.Type) - } -} diff --git a/inbound/default.go b/inbound/default.go deleted file mode 100644 index 880dd26f61..0000000000 --- a/inbound/default.go +++ /dev/null @@ -1,209 +0,0 @@ -package inbound - -import ( - "context" - "net" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/settings" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/atomic" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -var _ adapter.Inbound = (*myInboundAdapter)(nil) - -type myInboundAdapter struct { - protocol string - network []string - ctx context.Context - router adapter.ConnectionRouterEx - logger log.ContextLogger - tag string - listenOptions option.ListenOptions - connHandler adapter.ConnectionHandlerEx - packetHandler adapter.PacketHandlerEx - oobPacketHandler adapter.OOBPacketHandlerEx - packetUpstream any - - // http mixed - - setSystemProxy bool - systemProxy settings.SystemProxy - - // internal - - tcpListener net.Listener - udpConn *net.UDPConn - udpAddr M.Socksaddr - packetOutboundClosed chan struct{} - packetOutbound chan *myInboundPacket - - inShutdown atomic.Bool -} - -func (a *myInboundAdapter) Type() string { - return a.protocol -} - -func (a *myInboundAdapter) Tag() string { - return a.tag -} - -func (a *myInboundAdapter) Start() error { - var err error - if common.Contains(a.network, N.NetworkTCP) { - _, err = a.ListenTCP() - if err != nil { - return err - } - go a.loopTCPIn() - } - if common.Contains(a.network, N.NetworkUDP) { - _, err = a.ListenUDP() - if err != nil { - return err - } - a.packetOutboundClosed = make(chan struct{}) - a.packetOutbound = make(chan *myInboundPacket) - if a.oobPacketHandler != nil { - if _, threadUnsafeHandler := common.Cast[N.ThreadUnsafeWriter](a.packetUpstream); !threadUnsafeHandler { - go a.loopUDPOOBIn() - } else { - go a.loopUDPOOBInThreadSafe() - } - } else { - if _, threadUnsafeHandler := common.Cast[N.ThreadUnsafeWriter](a.packetUpstream); !threadUnsafeHandler { - go a.loopUDPIn() - } else { - go a.loopUDPInThreadSafe() - } - go a.loopUDPOut() - } - } - if a.setSystemProxy { - listenPort := M.SocksaddrFromNet(a.tcpListener.Addr()).Port - var listenAddrString string - listenAddr := a.listenOptions.Listen.Build() - if listenAddr.IsUnspecified() { - listenAddrString = "127.0.0.1" - } else { - listenAddrString = listenAddr.String() - } - var systemProxy settings.SystemProxy - systemProxy, err = settings.NewSystemProxy(a.ctx, M.ParseSocksaddrHostPort(listenAddrString, listenPort), a.protocol == C.TypeMixed) - if err != nil { - return E.Cause(err, "initialize system proxy") - } - err = systemProxy.Enable() - if err != nil { - return E.Cause(err, "set system proxy") - } - a.systemProxy = systemProxy - } - return nil -} - -func (a *myInboundAdapter) Close() error { - a.inShutdown.Store(true) - var err error - if a.systemProxy != nil && a.systemProxy.IsEnabled() { - err = a.systemProxy.Disable() - } - return E.Errors(err, common.Close( - a.tcpListener, - common.PtrOrNil(a.udpConn), - )) -} - -func (a *myInboundAdapter) upstreamHandler(metadata adapter.InboundContext) adapter.UpstreamHandlerAdapter { - return adapter.NewUpstreamHandler(metadata, a.newConnection, a.streamPacketConnection, a) -} - -func (a *myInboundAdapter) upstreamContextHandler() adapter.UpstreamHandlerAdapter { - return adapter.NewUpstreamContextHandler(a.newConnection, a.newPacketConnection, a) -} - -func (a *myInboundAdapter) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - a.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) - return a.router.RouteConnection(ctx, conn, metadata) -} - -func (a *myInboundAdapter) streamPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - a.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) - return a.router.RoutePacketConnection(ctx, conn, metadata) -} - -func (a *myInboundAdapter) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - ctx = log.ContextWithNewID(ctx) - a.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) - a.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) - return a.router.RoutePacketConnection(ctx, conn, metadata) -} - -func (a *myInboundAdapter) upstreamHandlerEx(metadata adapter.InboundContext) adapter.UpstreamHandlerAdapterEx { - return adapter.NewUpstreamHandlerEx(metadata, a.newConnectionEx, a.streamPacketConnectionEx) -} - -func (a *myInboundAdapter) upstreamContextHandlerEx() adapter.UpstreamHandlerAdapterEx { - return adapter.NewUpstreamContextHandlerEx(a.newConnectionEx, a.newPacketConnectionEx) -} - -func (a *myInboundAdapter) newConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - a.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) - a.router.RouteConnectionEx(ctx, conn, metadata, onClose) -} - -func (a *myInboundAdapter) newPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - ctx = log.ContextWithNewID(ctx) - a.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) - a.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) - a.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) -} - -func (a *myInboundAdapter) streamPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - a.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) - a.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) -} - -func (a *myInboundAdapter) createMetadata(conn net.Conn, metadata adapter.InboundContext) adapter.InboundContext { - metadata.Inbound = a.tag - metadata.InboundType = a.protocol - metadata.InboundDetour = a.listenOptions.Detour - metadata.InboundOptions = a.listenOptions.InboundOptions - if !metadata.Source.IsValid() { - metadata.Source = M.SocksaddrFromNet(conn.RemoteAddr()).Unwrap() - } - if !metadata.Destination.IsValid() { - metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() - } - if tcpConn, isTCP := common.Cast[*net.TCPConn](conn); isTCP { - metadata.OriginDestination = M.SocksaddrFromNet(tcpConn.LocalAddr()).Unwrap() - } - return metadata -} - -// Deprecated: don't use -func (a *myInboundAdapter) newError(err error) { - a.logger.Error(err) -} - -// Deprecated: don't use -func (a *myInboundAdapter) NewError(ctx context.Context, err error) { - NewError(a.logger, ctx, err) -} - -// Deprecated: don't use -func NewError(logger log.ContextLogger, ctx context.Context, err error) { - common.Close(err) - if E.IsClosedOrCanceled(err) { - logger.DebugContext(ctx, "connection closed: ", err) - return - } - logger.ErrorContext(ctx, err) -} diff --git a/inbound/default_tcp.go b/inbound/default_tcp.go deleted file mode 100644 index d38f96fe6e..0000000000 --- a/inbound/default_tcp.go +++ /dev/null @@ -1,84 +0,0 @@ -package inbound - -import ( - "context" - "net" - - "github.com/sagernet/sing-box/adapter" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing/common/control" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -func (a *myInboundAdapter) ListenTCP() (net.Listener, error) { - var err error - bindAddr := M.SocksaddrFrom(a.listenOptions.Listen.Build(), a.listenOptions.ListenPort) - var tcpListener net.Listener - var listenConfig net.ListenConfig - // TODO: Add an option to customize the keep alive period - listenConfig.KeepAlive = C.TCPKeepAliveInitial - listenConfig.Control = control.Append(listenConfig.Control, control.SetKeepAlivePeriod(C.TCPKeepAliveInitial, C.TCPKeepAliveInterval)) - if a.listenOptions.TCPMultiPath { - if !go121Available { - return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.") - } - setMultiPathTCP(&listenConfig) - } - if a.listenOptions.TCPFastOpen { - if !go120Available { - return nil, E.New("TCP Fast Open requires go1.20, please recompile your binary.") - } - tcpListener, err = listenTFO(listenConfig, a.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String()) - } else { - tcpListener, err = listenConfig.Listen(a.ctx, M.NetworkFromNetAddr(N.NetworkTCP, bindAddr.Addr), bindAddr.String()) - } - if err == nil { - a.logger.Info("tcp server started at ", tcpListener.Addr()) - } - if a.listenOptions.ProxyProtocol || a.listenOptions.ProxyProtocolAcceptNoHeader { - return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0") - } - a.tcpListener = tcpListener - return tcpListener, err -} - -func (a *myInboundAdapter) loopTCPIn() { - tcpListener := a.tcpListener - for { - conn, err := tcpListener.Accept() - if err != nil { - //goland:noinspection GoDeprecation - //nolint:staticcheck - if netError, isNetError := err.(net.Error); isNetError && netError.Temporary() { - a.logger.Error(err) - continue - } - if a.inShutdown.Load() && E.IsClosed(err) { - return - } - a.tcpListener.Close() - a.logger.Error("serve error: ", err) - continue - } - go a.injectTCP(conn, adapter.InboundContext{}) - } -} - -func (a *myInboundAdapter) injectTCP(conn net.Conn, metadata adapter.InboundContext) { - ctx := log.ContextWithNewID(a.ctx) - metadata = a.createMetadata(conn, metadata) - a.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) - a.connHandler.NewConnectionEx(ctx, conn, metadata, nil) -} - -func (a *myInboundAdapter) routeTCP(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { - metadata := a.createMetadata(conn, adapter.InboundContext{ - Source: source, - Destination: destination, - }) - a.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) - a.connHandler.NewConnectionEx(ctx, conn, metadata, onClose) -} diff --git a/inbound/default_tcp_go1.20.go b/inbound/default_tcp_go1.20.go deleted file mode 100644 index 23949b067e..0000000000 --- a/inbound/default_tcp_go1.20.go +++ /dev/null @@ -1,18 +0,0 @@ -//go:build go1.20 - -package inbound - -import ( - "context" - "net" - - "github.com/metacubex/tfo-go" -) - -const go120Available = true - -func listenTFO(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.Listener, error) { - var tfoConfig tfo.ListenConfig - tfoConfig.ListenConfig = listenConfig - return tfoConfig.Listen(ctx, network, address) -} diff --git a/inbound/default_tcp_nongo1.20.go b/inbound/default_tcp_nongo1.20.go deleted file mode 100644 index e7a026bc85..0000000000 --- a/inbound/default_tcp_nongo1.20.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build !go1.20 - -package inbound - -import ( - "context" - "net" - "os" -) - -const go120Available = false - -func listenTFO(listenConfig net.ListenConfig, ctx context.Context, network string, address string) (net.Listener, error) { - return nil, os.ErrInvalid -} diff --git a/inbound/default_udp.go b/inbound/default_udp.go deleted file mode 100644 index 6bcde79dae..0000000000 --- a/inbound/default_udp.go +++ /dev/null @@ -1,208 +0,0 @@ -package inbound - -import ( - "net" - "os" - "time" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/control" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -func (a *myInboundAdapter) ListenUDP() (net.PacketConn, error) { - bindAddr := M.SocksaddrFrom(a.listenOptions.Listen.Build(), a.listenOptions.ListenPort) - var lc net.ListenConfig - var udpFragment bool - if a.listenOptions.UDPFragment != nil { - udpFragment = *a.listenOptions.UDPFragment - } else { - udpFragment = a.listenOptions.UDPFragmentDefault - } - if !udpFragment { - lc.Control = control.Append(lc.Control, control.DisableUDPFragment()) - } - udpConn, err := lc.ListenPacket(a.ctx, M.NetworkFromNetAddr(N.NetworkUDP, bindAddr.Addr), bindAddr.String()) - if err != nil { - return nil, err - } - a.udpConn = udpConn.(*net.UDPConn) - a.udpAddr = bindAddr - a.logger.Info("udp server started at ", udpConn.LocalAddr()) - return udpConn, err -} - -func (a *myInboundAdapter) loopUDPIn() { - defer close(a.packetOutboundClosed) - buffer := buf.NewPacket() - defer buffer.Release() - buffer.IncRef() - defer buffer.DecRef() - for { - buffer.Reset() - n, addr, err := a.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes()) - if err != nil { - return - } - buffer.Truncate(n) - a.packetHandler.NewPacketEx(buffer, M.SocksaddrFromNetIP(addr).Unwrap()) - } -} - -func (a *myInboundAdapter) loopUDPOOBIn() { - defer close(a.packetOutboundClosed) - buffer := buf.NewPacket() - defer buffer.Release() - buffer.IncRef() - defer buffer.DecRef() - oob := make([]byte, 1024) - for { - buffer.Reset() - n, oobN, _, addr, err := a.udpConn.ReadMsgUDPAddrPort(buffer.FreeBytes(), oob) - if err != nil { - return - } - buffer.Truncate(n) - a.oobPacketHandler.NewPacketEx(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap()) - } -} - -func (a *myInboundAdapter) loopUDPInThreadSafe() { - defer close(a.packetOutboundClosed) - for { - buffer := buf.NewPacket() - n, addr, err := a.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes()) - if err != nil { - buffer.Release() - return - } - buffer.Truncate(n) - a.packetHandler.NewPacketEx(buffer, M.SocksaddrFromNetIP(addr).Unwrap()) - } -} - -func (a *myInboundAdapter) loopUDPOOBInThreadSafe() { - defer close(a.packetOutboundClosed) - oob := make([]byte, 1024) - for { - buffer := buf.NewPacket() - n, oobN, _, addr, err := a.udpConn.ReadMsgUDPAddrPort(buffer.FreeBytes(), oob) - if err != nil { - buffer.Release() - return - } - buffer.Truncate(n) - a.oobPacketHandler.NewPacketEx(buffer, oob[:oobN], M.SocksaddrFromNetIP(addr).Unwrap()) - } -} - -func (a *myInboundAdapter) loopUDPOut() { - for { - select { - case packet := <-a.packetOutbound: - err := a.writePacket(packet.buffer, packet.destination) - if err != nil && !E.IsClosed(err) { - a.logger.Error(E.New("write back udp: ", err)) - } - continue - case <-a.packetOutboundClosed: - } - for { - select { - case packet := <-a.packetOutbound: - packet.buffer.Release() - default: - return - } - } - } -} - -func (a *myInboundAdapter) packetConn() N.PacketConn { - return (*myInboundPacketAdapter)(a) -} - -func (a *myInboundAdapter) createPacketMetadata(conn N.PacketConn, metadata adapter.InboundContext) adapter.InboundContext { - metadata.Inbound = a.tag - metadata.InboundType = a.protocol - metadata.InboundDetour = a.listenOptions.Detour - metadata.InboundOptions = a.listenOptions.InboundOptions - if !metadata.Destination.IsValid() { - metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() - } - metadata.OriginDestination = a.udpAddr - return metadata -} - -func (a *myInboundAdapter) createPacketMetadataEx(source M.Socksaddr, destination M.Socksaddr) adapter.InboundContext { - var metadata adapter.InboundContext - metadata.Inbound = a.tag - metadata.InboundType = a.protocol - metadata.InboundDetour = a.listenOptions.Detour - metadata.InboundOptions = a.listenOptions.InboundOptions - metadata.Source = source - metadata.Destination = destination - metadata.OriginDestination = a.udpAddr - return metadata -} - -func (a *myInboundAdapter) writePacket(buffer *buf.Buffer, destination M.Socksaddr) error { - defer buffer.Release() - return common.Error(a.udpConn.WriteToUDPAddrPort(buffer.Bytes(), destination.AddrPort())) -} - -type myInboundPacketAdapter myInboundAdapter - -func (s *myInboundPacketAdapter) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) { - n, addr, err := s.udpConn.ReadFromUDPAddrPort(buffer.FreeBytes()) - if err != nil { - return M.Socksaddr{}, err - } - buffer.Truncate(n) - return M.SocksaddrFromNetIP(addr), nil -} - -func (s *myInboundPacketAdapter) WriteIsThreadUnsafe() { -} - -type myInboundPacket struct { - buffer *buf.Buffer - destination M.Socksaddr -} - -func (s *myInboundPacketAdapter) Upstream() any { - return s.udpConn -} - -func (s *myInboundPacketAdapter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { - select { - case s.packetOutbound <- &myInboundPacket{buffer, destination}: - return nil - case <-s.packetOutboundClosed: - return os.ErrClosed - } -} - -func (s *myInboundPacketAdapter) Close() error { - return s.udpConn.Close() -} - -func (s *myInboundPacketAdapter) LocalAddr() net.Addr { - return s.udpConn.LocalAddr() -} - -func (s *myInboundPacketAdapter) SetDeadline(t time.Time) error { - return s.udpConn.SetDeadline(t) -} - -func (s *myInboundPacketAdapter) SetReadDeadline(t time.Time) error { - return s.udpConn.SetReadDeadline(t) -} - -func (s *myInboundPacketAdapter) SetWriteDeadline(t time.Time) error { - return s.udpConn.SetWriteDeadline(t) -} diff --git a/inbound/direct.go b/inbound/direct.go deleted file mode 100644 index b9a99f12cc..0000000000 --- a/inbound/direct.go +++ /dev/null @@ -1,111 +0,0 @@ -package inbound - -import ( - "context" - "net" - "time" - - "github.com/sagernet/sing-box/adapter" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common/buf" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/udpnat2" -) - -var _ adapter.Inbound = (*Direct)(nil) - -type Direct struct { - myInboundAdapter - udpNat *udpnat.Service - overrideOption int - overrideDestination M.Socksaddr -} - -func NewDirect(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DirectInboundOptions) *Direct { - options.UDPFragmentDefault = true - inbound := &Direct{ - myInboundAdapter: myInboundAdapter{ - protocol: C.TypeDirect, - network: options.Network.Build(), - ctx: ctx, - router: router, - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - }, - } - if options.OverrideAddress != "" && options.OverridePort != 0 { - inbound.overrideOption = 1 - inbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort) - } else if options.OverrideAddress != "" { - inbound.overrideOption = 2 - inbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort) - } else if options.OverridePort != 0 { - inbound.overrideOption = 3 - inbound.overrideDestination = M.Socksaddr{Port: options.OverridePort} - } - var udpTimeout time.Duration - if options.UDPTimeout != 0 { - udpTimeout = time.Duration(options.UDPTimeout) - } else { - udpTimeout = C.UDPTimeout - } - inbound.udpNat = udpnat.New(inbound, inbound.preparePacketConnection, udpTimeout, false) - inbound.connHandler = inbound - inbound.packetHandler = inbound - return inbound -} - -func (d *Direct) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - switch d.overrideOption { - case 1: - metadata.Destination = d.overrideDestination - case 2: - destination := d.overrideDestination - destination.Port = metadata.Destination.Port - metadata.Destination = destination - case 3: - metadata.Destination.Port = d.overrideDestination.Port - } - d.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) - return d.router.RouteConnection(ctx, conn, metadata) -} - -func (d *Direct) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { - var destination M.Socksaddr - switch d.overrideOption { - case 1: - destination = d.overrideDestination - case 2: - destination = d.overrideDestination - destination.Port = source.Port - case 3: - destination = source - destination.Port = d.overrideDestination.Port - } - d.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, destination, nil) -} - -func (d *Direct) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - d.newConnectionEx(ctx, conn, metadata, onClose) -} - -func (d *Direct) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { - d.newPacketConnectionEx(ctx, conn, d.createPacketMetadataEx(source, destination), onClose) -} - -func (d *Direct) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) { - return true, d.ctx, &directPacketWriter{d.packetConn(), source}, nil -} - -type directPacketWriter struct { - writer N.PacketWriter - source M.Socksaddr -} - -func (w *directPacketWriter) WritePacket(buffer *buf.Buffer, addr M.Socksaddr) error { - return w.writer.WritePacket(buffer, w.source) -} diff --git a/inbound/http.go b/inbound/http.go deleted file mode 100644 index 20c8f6903d..0000000000 --- a/inbound/http.go +++ /dev/null @@ -1,119 +0,0 @@ -package inbound - -import ( - std_bufio "bufio" - "context" - "net" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/tls" - "github.com/sagernet/sing-box/common/uot" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/auth" - E "github.com/sagernet/sing/common/exceptions" - N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/protocol/http" -) - -var ( - _ adapter.Inbound = (*HTTP)(nil) - _ adapter.TCPInjectableInbound = (*HTTP)(nil) -) - -type HTTP struct { - myInboundAdapter - authenticator *auth.Authenticator - tlsConfig tls.ServerConfig -} - -func NewHTTP(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (*HTTP, error) { - inbound := &HTTP{ - myInboundAdapter: myInboundAdapter{ - protocol: C.TypeHTTP, - network: []string{N.NetworkTCP}, - ctx: ctx, - router: uot.NewRouter(router, logger), - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - setSystemProxy: options.SetSystemProxy, - }, - authenticator: auth.NewAuthenticator(options.Users), - } - if options.TLS != nil { - tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) - if err != nil { - return nil, err - } - inbound.tlsConfig = tlsConfig - } - inbound.connHandler = inbound - return inbound, nil -} - -func (h *HTTP) Start() error { - if h.tlsConfig != nil { - err := h.tlsConfig.Start() - if err != nil { - return E.Cause(err, "create TLS config") - } - } - return h.myInboundAdapter.Start() -} - -func (h *HTTP) Close() error { - return common.Close( - &h.myInboundAdapter, - h.tlsConfig, - ) -} - -func (h *HTTP) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - err := h.newConnection(ctx, conn, metadata, onClose) - N.CloseOnHandshakeFailure(conn, onClose, err) - if err != nil { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) - } -} - -func (h *HTTP) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { - var err error - if h.tlsConfig != nil { - conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig) - if err != nil { - return err - } - } - return http.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, h.upstreamUserHandlerEx(metadata), metadata.Source, onClose) -} - -func (a *myInboundAdapter) upstreamUserHandlerEx(metadata adapter.InboundContext) adapter.UpstreamHandlerAdapterEx { - return adapter.NewUpstreamHandlerEx(metadata, a.newUserConnection, a.streamUserPacketConnection) -} - -func (a *myInboundAdapter) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - user, loaded := auth.UserFromContext[string](ctx) - if !loaded { - a.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) - a.router.RouteConnectionEx(ctx, conn, metadata, onClose) - return - } - metadata.User = user - a.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) - a.router.RouteConnectionEx(ctx, conn, metadata, onClose) -} - -func (a *myInboundAdapter) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - user, loaded := auth.UserFromContext[string](ctx) - if !loaded { - a.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) - a.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) - return - } - metadata.User = user - a.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) - a.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) -} diff --git a/inbound/hysteria_stub.go b/inbound/hysteria_stub.go deleted file mode 100644 index fab86bb56f..0000000000 --- a/inbound/hysteria_stub.go +++ /dev/null @@ -1,20 +0,0 @@ -//go:build !with_quic - -package inbound - -import ( - "context" - - "github.com/sagernet/sing-box/adapter" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" -) - -func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (adapter.Inbound, error) { - return nil, C.ErrQUICNotIncluded -} - -func NewHysteria2(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (adapter.Inbound, error) { - return nil, C.ErrQUICNotIncluded -} diff --git a/inbound/mixed.go b/inbound/mixed.go deleted file mode 100644 index 81f6a43a09..0000000000 --- a/inbound/mixed.go +++ /dev/null @@ -1,70 +0,0 @@ -package inbound - -import ( - std_bufio "bufio" - "context" - "net" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/uot" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common/auth" - E "github.com/sagernet/sing/common/exceptions" - N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/protocol/http" - "github.com/sagernet/sing/protocol/socks" - "github.com/sagernet/sing/protocol/socks/socks4" - "github.com/sagernet/sing/protocol/socks/socks5" -) - -var ( - _ adapter.Inbound = (*Mixed)(nil) - _ adapter.TCPInjectableInbound = (*Mixed)(nil) -) - -type Mixed struct { - myInboundAdapter - authenticator *auth.Authenticator -} - -func NewMixed(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) *Mixed { - inbound := &Mixed{ - myInboundAdapter{ - protocol: C.TypeMixed, - network: []string{N.NetworkTCP}, - ctx: ctx, - router: uot.NewRouter(router, logger), - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - setSystemProxy: options.SetSystemProxy, - }, - auth.NewAuthenticator(options.Users), - } - inbound.connHandler = inbound - return inbound -} - -func (h *Mixed) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - err := h.newConnection(ctx, conn, metadata, onClose) - N.CloseOnHandshakeFailure(conn, onClose, err) - if err != nil { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) - } -} - -func (h *Mixed) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { - reader := std_bufio.NewReader(conn) - headerBytes, err := reader.Peek(1) - if err != nil { - return E.Cause(err, "peek first byte") - } - switch headerBytes[0] { - case socks4.Version, socks5.Version: - return socks.HandleConnectionEx(ctx, conn, reader, h.authenticator, nil, h.upstreamUserHandlerEx(metadata), metadata.Source, metadata.Destination, onClose) - default: - return http.HandleConnectionEx(ctx, conn, reader, h.authenticator, nil, h.upstreamUserHandlerEx(metadata), metadata.Source, onClose) - } -} diff --git a/inbound/naive_quic.go b/inbound/naive_quic.go deleted file mode 100644 index 9f99bf278b..0000000000 --- a/inbound/naive_quic.go +++ /dev/null @@ -1,47 +0,0 @@ -//go:build with_quic - -package inbound - -import ( - "github.com/sagernet/quic-go" - "github.com/sagernet/quic-go/http3" - "github.com/sagernet/sing-quic" - E "github.com/sagernet/sing/common/exceptions" -) - -func (n *Naive) configureHTTP3Listener() error { - err := qtls.ConfigureHTTP3(n.tlsConfig) - if err != nil { - return err - } - - udpConn, err := n.ListenUDP() - if err != nil { - return err - } - - quicListener, err := qtls.ListenEarly(udpConn, n.tlsConfig, &quic.Config{ - MaxIncomingStreams: 1 << 60, - Allow0RTT: true, - }) - if err != nil { - udpConn.Close() - return err - } - - h3Server := &http3.Server{ - Port: int(n.listenOptions.ListenPort), - Handler: n, - } - - go func() { - sErr := h3Server.ServeListener(quicListener) - udpConn.Close() - if sErr != nil && !E.IsClosedOrCanceled(sErr) { - n.logger.Error("http3 server serve error: ", sErr) - } - }() - - n.h3Server = h3Server - return nil -} diff --git a/inbound/naive_quic_stub.go b/inbound/naive_quic_stub.go deleted file mode 100644 index 90f697e46d..0000000000 --- a/inbound/naive_quic_stub.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build !with_quic - -package inbound - -import ( - C "github.com/sagernet/sing-box/constant" -) - -func (n *Naive) configureHTTP3Listener() error { - return C.ErrQUICNotIncluded -} diff --git a/inbound/redirect.go b/inbound/redirect.go deleted file mode 100644 index c4c6faf341..0000000000 --- a/inbound/redirect.go +++ /dev/null @@ -1,45 +0,0 @@ -package inbound - -import ( - "context" - "net" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/redir" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -type Redirect struct { - myInboundAdapter -} - -func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.RedirectInboundOptions) *Redirect { - redirect := &Redirect{ - myInboundAdapter{ - protocol: C.TypeRedirect, - network: []string{N.NetworkTCP}, - ctx: ctx, - router: router, - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - }, - } - redirect.connHandler = redirect - return redirect -} - -func (r *Redirect) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - destination, err := redir.GetOriginalDestination(conn) - if err != nil { - conn.Close() - r.logger.ErrorContext(ctx, "process connection from ", conn.RemoteAddr(), ": get redirect destination: ", err) - return - } - metadata.Destination = M.SocksaddrFromNetIP(destination) - r.newConnectionEx(ctx, conn, metadata, onClose) -} diff --git a/inbound/shadowsocks.go b/inbound/shadowsocks.go deleted file mode 100644 index 3fff231d8f..0000000000 --- a/inbound/shadowsocks.go +++ /dev/null @@ -1,114 +0,0 @@ -package inbound - -import ( - "context" - "net" - "time" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/mux" - "github.com/sagernet/sing-box/common/uot" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing-shadowsocks" - "github.com/sagernet/sing-shadowsocks/shadowaead" - "github.com/sagernet/sing-shadowsocks/shadowaead_2022" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/ntp" -) - -func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) { - if len(options.Users) > 0 && len(options.Destinations) > 0 { - return nil, E.New("users and destinations options must not be combined") - } - if len(options.Users) > 0 { - return newShadowsocksMulti(ctx, router, logger, tag, options) - } else if len(options.Destinations) > 0 { - return newShadowsocksRelay(ctx, router, logger, tag, options) - } else { - return newShadowsocks(ctx, router, logger, tag, options) - } -} - -var ( - _ adapter.Inbound = (*Shadowsocks)(nil) - _ adapter.TCPInjectableInbound = (*Shadowsocks)(nil) -) - -type Shadowsocks struct { - myInboundAdapter - service shadowsocks.Service -} - -func newShadowsocks(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*Shadowsocks, error) { - inbound := &Shadowsocks{ - myInboundAdapter: myInboundAdapter{ - protocol: C.TypeShadowsocks, - network: options.Network.Build(), - ctx: ctx, - router: uot.NewRouter(router, logger), - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - }, - } - - inbound.connHandler = inbound - inbound.packetHandler = inbound - var err error - inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) - if err != nil { - return nil, err - } - - var udpTimeout time.Duration - if options.UDPTimeout != 0 { - udpTimeout = time.Duration(options.UDPTimeout) - } else { - udpTimeout = C.UDPTimeout - } - switch { - case options.Method == shadowsocks.MethodNone: - inbound.service = shadowsocks.NewNoneService(int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound)) - case common.Contains(shadowaead.List, options.Method): - inbound.service, err = shadowaead.NewService(options.Method, nil, options.Password, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound)) - case common.Contains(shadowaead_2022.List, options.Method): - inbound.service, err = shadowaead_2022.NewServiceWithPassword(options.Method, options.Password, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ntp.TimeFuncFromContext(ctx)) - default: - err = E.New("unsupported method: ", options.Method) - } - inbound.packetUpstream = inbound.service - return inbound, err -} - -func (h *Shadowsocks) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) - N.CloseOnHandshakeFailure(conn, onClose, err) - if err != nil { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) - } -} - -func (h *Shadowsocks) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { - err := h.service.NewPacket(h.ctx, h.packetConn(), buffer, M.Metadata{Source: source}) - if err != nil { - h.logger.Error(E.Cause(err, "process packet from ", source)) - } -} - -func (h *Shadowsocks) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) - return h.router.RouteConnection(ctx, conn, h.createMetadata(conn, metadata)) -} - -func (h *Shadowsocks) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - ctx = log.ContextWithNewID(ctx) - h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) - h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) - return h.router.RoutePacketConnection(ctx, conn, h.createPacketMetadata(conn, metadata)) -} diff --git a/inbound/socks.go b/inbound/socks.go deleted file mode 100644 index 04b0a77dc7..0000000000 --- a/inbound/socks.go +++ /dev/null @@ -1,52 +0,0 @@ -package inbound - -import ( - std_bufio "bufio" - "context" - "net" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/uot" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing/common/auth" - E "github.com/sagernet/sing/common/exceptions" - N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/protocol/socks" -) - -var ( - _ adapter.Inbound = (*Socks)(nil) - _ adapter.TCPInjectableInbound = (*Socks)(nil) -) - -type Socks struct { - myInboundAdapter - authenticator *auth.Authenticator -} - -func NewSocks(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SocksInboundOptions) *Socks { - inbound := &Socks{ - myInboundAdapter{ - protocol: C.TypeSOCKS, - network: []string{N.NetworkTCP}, - ctx: ctx, - router: uot.NewRouter(router, logger), - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - }, - auth.NewAuthenticator(options.Users), - } - inbound.connHandler = inbound - return inbound -} - -func (h *Socks) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - err := socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, h.upstreamUserHandlerEx(metadata), metadata.Source, metadata.Destination, onClose) - N.CloseOnHandshakeFailure(conn, onClose, err) - if err != nil { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) - } -} diff --git a/inbound/tuic_stub.go b/inbound/tuic_stub.go deleted file mode 100644 index bfd402ab45..0000000000 --- a/inbound/tuic_stub.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build !with_quic - -package inbound - -import ( - "context" - - "github.com/sagernet/sing-box/adapter" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" -) - -func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (adapter.Inbound, error) { - return nil, C.ErrQUICNotIncluded -} diff --git a/include/quic.go b/include/quic.go index 1bcc0fbc90..980b458113 100644 --- a/include/quic.go +++ b/include/quic.go @@ -3,6 +3,24 @@ package include import ( + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/protocol/hysteria" + "github.com/sagernet/sing-box/protocol/hysteria2" + _ "github.com/sagernet/sing-box/protocol/naive/quic" + "github.com/sagernet/sing-box/protocol/tuic" _ "github.com/sagernet/sing-box/transport/v2rayquic" _ "github.com/sagernet/sing-dns/quic" ) + +func registerQUICInbounds(registry *inbound.Registry) { + hysteria.RegisterInbound(registry) + tuic.RegisterInbound(registry) + hysteria2.RegisterInbound(registry) +} + +func registerQUICOutbounds(registry *outbound.Registry) { + hysteria.RegisterOutbound(registry) + tuic.RegisterOutbound(registry) + hysteria2.RegisterOutbound(registry) +} diff --git a/include/quic_stub.go b/include/quic_stub.go index 43aa58d996..66c0859038 100644 --- a/include/quic_stub.go +++ b/include/quic_stub.go @@ -4,11 +4,18 @@ package include import ( "context" + "io" + "net/http" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/protocol/naive" "github.com/sagernet/sing-box/transport/v2ray" "github.com/sagernet/sing-dns" "github.com/sagernet/sing/common/logger" @@ -29,3 +36,30 @@ func init() { }, ) } + +func registerQUICInbounds(registry *inbound.Registry) { + inbound.Register[option.HysteriaInboundOptions](registry, C.TypeHysteria, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (adapter.Inbound, error) { + return nil, C.ErrQUICNotIncluded + }) + inbound.Register[option.TUICInboundOptions](registry, C.TypeTUIC, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (adapter.Inbound, error) { + return nil, C.ErrQUICNotIncluded + }) + inbound.Register[option.Hysteria2InboundOptions](registry, C.TypeHysteria2, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (adapter.Inbound, error) { + return nil, C.ErrQUICNotIncluded + }) + naive.ConfigureHTTP3ListenerFunc = func(listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, logger logger.Logger) (io.Closer, error) { + return nil, C.ErrQUICNotIncluded + } +} + +func registerQUICOutbounds(registry *outbound.Registry) { + outbound.Register[option.HysteriaOutboundOptions](registry, C.TypeHysteria, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (adapter.Outbound, error) { + return nil, C.ErrQUICNotIncluded + }) + outbound.Register[option.TUICOutboundOptions](registry, C.TypeTUIC, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICOutboundOptions) (adapter.Outbound, error) { + return nil, C.ErrQUICNotIncluded + }) + outbound.Register[option.Hysteria2OutboundOptions](registry, C.TypeHysteria2, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2OutboundOptions) (adapter.Outbound, error) { + return nil, C.ErrQUICNotIncluded + }) +} diff --git a/include/registry.go b/include/registry.go new file mode 100644 index 0000000000..03fb33f245 --- /dev/null +++ b/include/registry.go @@ -0,0 +1,95 @@ +package include + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/adapter/outbound" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/protocol/block" + "github.com/sagernet/sing-box/protocol/direct" + "github.com/sagernet/sing-box/protocol/dns" + "github.com/sagernet/sing-box/protocol/group" + "github.com/sagernet/sing-box/protocol/http" + "github.com/sagernet/sing-box/protocol/mixed" + "github.com/sagernet/sing-box/protocol/naive" + "github.com/sagernet/sing-box/protocol/redirect" + "github.com/sagernet/sing-box/protocol/shadowsocks" + "github.com/sagernet/sing-box/protocol/shadowtls" + "github.com/sagernet/sing-box/protocol/socks" + "github.com/sagernet/sing-box/protocol/ssh" + "github.com/sagernet/sing-box/protocol/tor" + "github.com/sagernet/sing-box/protocol/trojan" + "github.com/sagernet/sing-box/protocol/tun" + "github.com/sagernet/sing-box/protocol/vless" + "github.com/sagernet/sing-box/protocol/vmess" + E "github.com/sagernet/sing/common/exceptions" +) + +func InboundRegistry() *inbound.Registry { + registry := inbound.NewRegistry() + + tun.RegisterInbound(registry) + redirect.RegisterRedirect(registry) + redirect.RegisterTProxy(registry) + direct.RegisterInbound(registry) + + socks.RegisterInbound(registry) + http.RegisterInbound(registry) + mixed.RegisterInbound(registry) + + shadowsocks.RegisterInbound(registry) + vmess.RegisterInbound(registry) + trojan.RegisterInbound(registry) + naive.RegisterInbound(registry) + shadowtls.RegisterInbound(registry) + vless.RegisterInbound(registry) + + registerQUICInbounds(registry) + registerStubForRemovedInbounds(registry) + + return registry +} + +func OutboundRegistry() *outbound.Registry { + registry := outbound.NewRegistry() + + direct.RegisterOutbound(registry) + + block.RegisterOutbound(registry) + dns.RegisterOutbound(registry) + + group.RegisterSelector(registry) + group.RegisterURLTest(registry) + + socks.RegisterOutbound(registry) + http.RegisterOutbound(registry) + shadowsocks.RegisterOutbound(registry) + vmess.RegisterOutbound(registry) + trojan.RegisterOutbound(registry) + tor.RegisterOutbound(registry) + ssh.RegisterOutbound(registry) + shadowtls.RegisterOutbound(registry) + vless.RegisterOutbound(registry) + + registerQUICOutbounds(registry) + registerWireGuardOutbound(registry) + registerStubForRemovedOutbounds(registry) + + return registry +} + +func registerStubForRemovedInbounds(registry *inbound.Registry) { + inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) { + return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0") + }) +} + +func registerStubForRemovedOutbounds(registry *outbound.Registry) { + outbound.Register[option.ShadowsocksROutboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksROutboundOptions) (adapter.Outbound, error) { + return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0") + }) +} diff --git a/include/wireguard.go b/include/wireguard.go new file mode 100644 index 0000000000..dfc3a242a5 --- /dev/null +++ b/include/wireguard.go @@ -0,0 +1,12 @@ +//go:build with_wireguard + +package include + +import ( + "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/protocol/wireguard" +) + +func registerWireGuardOutbound(registry *outbound.Registry) { + wireguard.RegisterOutbound(registry) +} diff --git a/include/wireguard_stub.go b/include/wireguard_stub.go new file mode 100644 index 0000000000..a9e84522bb --- /dev/null +++ b/include/wireguard_stub.go @@ -0,0 +1,20 @@ +//go:build !with_wireguard + +package include + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func registerWireGuardOutbound(registry *outbound.Registry) { + outbound.Register[option.WireGuardOutboundOptions](registry, C.TypeWireGuard, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardOutboundOptions) (adapter.Outbound, error) { + return nil, E.New(`WireGuard is not included in this build, rebuild with -tags with_wireguard`) + }) +} diff --git a/option/inbound.go b/option/inbound.go index d38799048f..651d02846c 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -1,100 +1,49 @@ package option import ( + "context" "time" - C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/service" ) +type InboundOptionsRegistry interface { + CreateOptions(outboundType string) (any, bool) +} + type _Inbound struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - TunOptions TunInboundOptions `json:"-"` - RedirectOptions RedirectInboundOptions `json:"-"` - TProxyOptions TProxyInboundOptions `json:"-"` - DirectOptions DirectInboundOptions `json:"-"` - SocksOptions SocksInboundOptions `json:"-"` - HTTPOptions HTTPMixedInboundOptions `json:"-"` - MixedOptions HTTPMixedInboundOptions `json:"-"` - ShadowsocksOptions ShadowsocksInboundOptions `json:"-"` - VMessOptions VMessInboundOptions `json:"-"` - TrojanOptions TrojanInboundOptions `json:"-"` - NaiveOptions NaiveInboundOptions `json:"-"` - HysteriaOptions HysteriaInboundOptions `json:"-"` - ShadowTLSOptions ShadowTLSInboundOptions `json:"-"` - VLESSOptions VLESSInboundOptions `json:"-"` - TUICOptions TUICInboundOptions `json:"-"` - Hysteria2Options Hysteria2InboundOptions `json:"-"` + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Options any `json:"-"` } type Inbound _Inbound -func (h *Inbound) RawOptions() (any, error) { - var rawOptionsPtr any - switch h.Type { - case C.TypeTun: - rawOptionsPtr = &h.TunOptions - case C.TypeRedirect: - rawOptionsPtr = &h.RedirectOptions - case C.TypeTProxy: - rawOptionsPtr = &h.TProxyOptions - case C.TypeDirect: - rawOptionsPtr = &h.DirectOptions - case C.TypeSOCKS: - rawOptionsPtr = &h.SocksOptions - case C.TypeHTTP: - rawOptionsPtr = &h.HTTPOptions - case C.TypeMixed: - rawOptionsPtr = &h.MixedOptions - case C.TypeShadowsocks: - rawOptionsPtr = &h.ShadowsocksOptions - case C.TypeVMess: - rawOptionsPtr = &h.VMessOptions - case C.TypeTrojan: - rawOptionsPtr = &h.TrojanOptions - case C.TypeNaive: - rawOptionsPtr = &h.NaiveOptions - case C.TypeHysteria: - rawOptionsPtr = &h.HysteriaOptions - case C.TypeShadowTLS: - rawOptionsPtr = &h.ShadowTLSOptions - case C.TypeVLESS: - rawOptionsPtr = &h.VLESSOptions - case C.TypeTUIC: - rawOptionsPtr = &h.TUICOptions - case C.TypeHysteria2: - rawOptionsPtr = &h.Hysteria2Options - case "": - return nil, E.New("missing inbound type") - default: - return nil, E.New("unknown inbound type: ", h.Type) - } - return rawOptionsPtr, nil +func (h *Inbound) MarshalJSONContext(ctx context.Context) ([]byte, error) { + return badjson.MarshallObjectsContext(ctx, (*_Inbound)(h), h.Options) } -func (h Inbound) MarshalJSON() ([]byte, error) { - rawOptions, err := h.RawOptions() - if err != nil { - return nil, err - } - return MarshallObjects((_Inbound)(h), rawOptions) -} - -func (h *Inbound) UnmarshalJSON(bytes []byte) error { - err := json.Unmarshal(bytes, (*_Inbound)(h)) +func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) error { + err := json.Unmarshal(content, (*_Inbound)(h)) if err != nil { return err } - rawOptions, err := h.RawOptions() - if err != nil { - return err + registry := service.FromContext[InboundOptionsRegistry](ctx) + if registry == nil { + return E.New("missing inbound options registry in context") } - err = UnmarshallExcluded(bytes, (*_Inbound)(h), rawOptions) + options, loaded := registry.CreateOptions(h.Type) + if !loaded { + return E.New("unknown inbound type: ", h.Type) + } + err = badjson.UnmarshallExcludedContext(ctx, content, (*_Inbound)(h), options) if err != nil { return err } + h.Options = options return nil } @@ -105,19 +54,24 @@ type InboundOptions struct { SniffTimeout Duration `json:"sniff_timeout,omitempty"` DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` + Detour string `json:"detour,omitempty"` } type ListenOptions struct { - Listen *ListenAddress `json:"listen,omitempty"` - ListenPort uint16 `json:"listen_port,omitempty"` - TCPFastOpen bool `json:"tcp_fast_open,omitempty"` - TCPMultiPath bool `json:"tcp_multi_path,omitempty"` - UDPFragment *bool `json:"udp_fragment,omitempty"` - UDPFragmentDefault bool `json:"-"` - UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` - ProxyProtocol bool `json:"proxy_protocol,omitempty"` - ProxyProtocolAcceptNoHeader bool `json:"proxy_protocol_accept_no_header,omitempty"` - Detour string `json:"detour,omitempty"` + Listen *ListenAddress `json:"listen,omitempty"` + ListenPort uint16 `json:"listen_port,omitempty"` + TCPKeepAlive Duration `json:"tcp_keep_alive,omitempty"` + TCPKeepAliveInterval Duration `json:"tcp_keep_alive_interval,omitempty"` + TCPFastOpen bool `json:"tcp_fast_open,omitempty"` + TCPMultiPath bool `json:"tcp_multi_path,omitempty"` + UDPFragment *bool `json:"udp_fragment,omitempty"` + UDPFragmentDefault bool `json:"-"` + UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` + + // Deprecated: removed + ProxyProtocol bool `json:"proxy_protocol,omitempty"` + // Deprecated: removed + ProxyProtocolAcceptNoHeader bool `json:"proxy_protocol_accept_no_header,omitempty"` InboundOptions } diff --git a/option/json.go b/option/json.go deleted file mode 100644 index 775141d5aa..0000000000 --- a/option/json.go +++ /dev/null @@ -1,71 +0,0 @@ -package option - -import ( - "github.com/sagernet/sing/common" - E "github.com/sagernet/sing/common/exceptions" - "github.com/sagernet/sing/common/json" - "github.com/sagernet/sing/common/json/badjson" -) - -func ToMap(v any) (*badjson.JSONObject, error) { - inputContent, err := json.Marshal(v) - if err != nil { - return nil, err - } - var content badjson.JSONObject - err = content.UnmarshalJSON(inputContent) - if err != nil { - return nil, err - } - return &content, nil -} - -func MergeObjects(objects ...any) (*badjson.JSONObject, error) { - var content badjson.JSONObject - for _, object := range objects { - objectMap, err := ToMap(object) - if err != nil { - return nil, err - } - content.PutAll(objectMap) - } - return &content, nil -} - -func MarshallObjects(objects ...any) ([]byte, error) { - objects = common.FilterNotNil(objects) - if len(objects) == 1 { - return json.Marshal(objects[0]) - } - content, err := MergeObjects(objects...) - if err != nil { - return nil, err - } - return content.MarshalJSON() -} - -func UnmarshallExcluded(inputContent []byte, parentObject any, object any) error { - parentContent, err := ToMap(parentObject) - if err != nil { - return err - } - var content badjson.JSONObject - err = content.UnmarshalJSON(inputContent) - if err != nil { - return err - } - for _, key := range parentContent.Keys() { - content.Remove(key) - } - if object == nil { - if content.IsEmpty() { - return nil - } - return E.New("unexpected key: ", content.Keys()[0]) - } - inputContent, err = content.MarshalJSON() - if err != nil { - return err - } - return json.UnmarshalDisallowUnknownFields(inputContent, object) -} diff --git a/option/config.go b/option/options.go similarity index 84% rename from option/config.go rename to option/options.go index 3f5d7602c0..13a16c08b5 100644 --- a/option/config.go +++ b/option/options.go @@ -2,6 +2,7 @@ package option import ( "bytes" + "context" "github.com/sagernet/sing/common/json" ) @@ -20,8 +21,8 @@ type _Options struct { type Options _Options -func (o *Options) UnmarshalJSON(content []byte) error { - decoder := json.NewDecoder(bytes.NewReader(content)) +func (o *Options) UnmarshalJSONContext(ctx context.Context, content []byte) error { + decoder := json.NewDecoderContext(ctx, bytes.NewReader(content)) decoder.DisallowUnknownFields() err := decoder.Decode((*_Options)(o)) if err != nil { @@ -38,3 +39,5 @@ type LogOptions struct { Timestamp bool `json:"timestamp,omitempty"` DisableColor bool `json:"-"` } + +type StubOptions struct{} diff --git a/option/outbound.go b/option/outbound.go index 6c943cd9af..00a20aa577 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -1,104 +1,49 @@ package option import ( - C "github.com/sagernet/sing-box/constant" + "context" + E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/service" ) +type OutboundOptionsRegistry interface { + CreateOptions(outboundType string) (any, bool) +} + type _Outbound struct { - Type string `json:"type"` - Tag string `json:"tag,omitempty"` - DirectOptions DirectOutboundOptions `json:"-"` - SocksOptions SocksOutboundOptions `json:"-"` - HTTPOptions HTTPOutboundOptions `json:"-"` - ShadowsocksOptions ShadowsocksOutboundOptions `json:"-"` - VMessOptions VMessOutboundOptions `json:"-"` - TrojanOptions TrojanOutboundOptions `json:"-"` - WireGuardOptions WireGuardOutboundOptions `json:"-"` - HysteriaOptions HysteriaOutboundOptions `json:"-"` - TorOptions TorOutboundOptions `json:"-"` - SSHOptions SSHOutboundOptions `json:"-"` - ShadowTLSOptions ShadowTLSOutboundOptions `json:"-"` - ShadowsocksROptions ShadowsocksROutboundOptions `json:"-"` - VLESSOptions VLESSOutboundOptions `json:"-"` - TUICOptions TUICOutboundOptions `json:"-"` - Hysteria2Options Hysteria2OutboundOptions `json:"-"` - SelectorOptions SelectorOutboundOptions `json:"-"` - URLTestOptions URLTestOutboundOptions `json:"-"` + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Options any `json:"-"` } type Outbound _Outbound -func (h *Outbound) RawOptions() (any, error) { - var rawOptionsPtr any - switch h.Type { - case C.TypeDirect: - rawOptionsPtr = &h.DirectOptions - case C.TypeBlock, C.TypeDNS: - rawOptionsPtr = nil - case C.TypeSOCKS: - rawOptionsPtr = &h.SocksOptions - case C.TypeHTTP: - rawOptionsPtr = &h.HTTPOptions - case C.TypeShadowsocks: - rawOptionsPtr = &h.ShadowsocksOptions - case C.TypeVMess: - rawOptionsPtr = &h.VMessOptions - case C.TypeTrojan: - rawOptionsPtr = &h.TrojanOptions - case C.TypeWireGuard: - rawOptionsPtr = &h.WireGuardOptions - case C.TypeHysteria: - rawOptionsPtr = &h.HysteriaOptions - case C.TypeTor: - rawOptionsPtr = &h.TorOptions - case C.TypeSSH: - rawOptionsPtr = &h.SSHOptions - case C.TypeShadowTLS: - rawOptionsPtr = &h.ShadowTLSOptions - case C.TypeShadowsocksR: - rawOptionsPtr = &h.ShadowsocksROptions - case C.TypeVLESS: - rawOptionsPtr = &h.VLESSOptions - case C.TypeTUIC: - rawOptionsPtr = &h.TUICOptions - case C.TypeHysteria2: - rawOptionsPtr = &h.Hysteria2Options - case C.TypeSelector: - rawOptionsPtr = &h.SelectorOptions - case C.TypeURLTest: - rawOptionsPtr = &h.URLTestOptions - case "": - return nil, E.New("missing outbound type") - default: - return nil, E.New("unknown outbound type: ", h.Type) - } - return rawOptionsPtr, nil +func (h *Outbound) MarshalJSONContext(ctx context.Context) ([]byte, error) { + return badjson.MarshallObjectsContext(ctx, (*_Outbound)(h), h.Options) } -func (h *Outbound) MarshalJSON() ([]byte, error) { - rawOptions, err := h.RawOptions() - if err != nil { - return nil, err - } - return MarshallObjects((*_Outbound)(h), rawOptions) -} - -func (h *Outbound) UnmarshalJSON(bytes []byte) error { - err := json.Unmarshal(bytes, (*_Outbound)(h)) +func (h *Outbound) UnmarshalJSONContext(ctx context.Context, content []byte) error { + err := json.Unmarshal(content, (*_Outbound)(h)) if err != nil { return err } - rawOptions, err := h.RawOptions() - if err != nil { - return err + registry := service.FromContext[OutboundOptionsRegistry](ctx) + if registry == nil { + return E.New("missing outbound options registry in context") + } + options, loaded := registry.CreateOptions(h.Type) + if !loaded { + return E.New("unknown outbound type: ", h.Type) } - err = UnmarshallExcluded(bytes, (*_Outbound)(h), rawOptions) + err = badjson.UnmarshallExcludedContext(ctx, content, (*_Outbound)(h), options) if err != nil { return err } + h.Options = options return nil } diff --git a/option/rule.go b/option/rule.go index 0b11cbdda1..07e6ddbec4 100644 --- a/option/rule.go +++ b/option/rule.go @@ -7,6 +7,7 @@ import ( "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" ) type _Rule struct { @@ -28,7 +29,7 @@ func (r Rule) MarshalJSON() ([]byte, error) { default: return nil, E.New("unknown rule type: " + r.Type) } - return MarshallObjects((_Rule)(r), v) + return badjson.MarshallObjects((_Rule)(r), v) } func (r *Rule) UnmarshalJSON(bytes []byte) error { @@ -46,7 +47,7 @@ func (r *Rule) UnmarshalJSON(bytes []byte) error { default: return E.New("unknown rule type: " + r.Type) } - err = UnmarshallExcluded(bytes, (*_Rule)(r), v) + err = badjson.UnmarshallExcluded(bytes, (*_Rule)(r), v) if err != nil { return err } @@ -109,7 +110,7 @@ type DefaultRule struct { } func (r *DefaultRule) MarshalJSON() ([]byte, error) { - return MarshallObjects(r.RawDefaultRule, r.RuleAction) + return badjson.MarshallObjects(r.RawDefaultRule, r.RuleAction) } func (r *DefaultRule) UnmarshalJSON(data []byte) error { @@ -117,7 +118,7 @@ func (r *DefaultRule) UnmarshalJSON(data []byte) error { if err != nil { return err } - return UnmarshallExcluded(data, &r.RawDefaultRule, &r.RuleAction) + return badjson.UnmarshallExcluded(data, &r.RawDefaultRule, &r.RuleAction) } func (r *DefaultRule) IsValid() bool { @@ -139,7 +140,7 @@ type LogicalRule struct { } func (r *LogicalRule) MarshalJSON() ([]byte, error) { - return MarshallObjects(r._LogicalRule, r.RuleAction) + return badjson.MarshallObjects(r._LogicalRule, r.RuleAction) } func (r *LogicalRule) UnmarshalJSON(data []byte) error { @@ -147,7 +148,7 @@ func (r *LogicalRule) UnmarshalJSON(data []byte) error { if err != nil { return err } - return UnmarshallExcluded(data, &r._LogicalRule, &r.RuleAction) + return badjson.UnmarshallExcluded(data, &r._LogicalRule, &r.RuleAction) } func (r *LogicalRule) IsValid() bool { diff --git a/option/rule_action.go b/option/rule_action.go index f446d81d5f..e752a2beaf 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -4,6 +4,7 @@ import ( C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" ) type _RuleAction struct { @@ -36,9 +37,9 @@ func (r RuleAction) MarshalJSON() ([]byte, error) { return nil, E.New("unknown rule action: " + r.Action) } if v == nil { - return MarshallObjects((_RuleAction)(r)) + return badjson.MarshallObjects((_RuleAction)(r)) } - return MarshallObjects((_RuleAction)(r), v) + return badjson.MarshallObjects((_RuleAction)(r), v) } func (r *RuleAction) UnmarshalJSON(data []byte) error { @@ -68,7 +69,7 @@ func (r *RuleAction) UnmarshalJSON(data []byte) error { // check unknown fields return json.UnmarshalDisallowUnknownFields(data, &_RuleAction{}) } - return UnmarshallExcluded(data, (*_RuleAction)(r), v) + return badjson.UnmarshallExcluded(data, (*_RuleAction)(r), v) } type _DNSRuleAction struct { @@ -95,9 +96,9 @@ func (r DNSRuleAction) MarshalJSON() ([]byte, error) { return nil, E.New("unknown DNS rule action: " + r.Action) } if v == nil { - return MarshallObjects((_DNSRuleAction)(r)) + return badjson.MarshallObjects((_DNSRuleAction)(r)) } - return MarshallObjects((_DNSRuleAction)(r), v) + return badjson.MarshallObjects((_DNSRuleAction)(r), v) } func (r *DNSRuleAction) UnmarshalJSON(data []byte) error { @@ -121,7 +122,7 @@ func (r *DNSRuleAction) UnmarshalJSON(data []byte) error { // check unknown fields return json.UnmarshalDisallowUnknownFields(data, &_DNSRuleAction{}) } - return UnmarshallExcluded(data, (*_DNSRuleAction)(r), v) + return badjson.UnmarshallExcluded(data, (*_DNSRuleAction)(r), v) } type RouteActionOptions struct { diff --git a/option/rule_dns.go b/option/rule_dns.go index b328c45c01..8c4b6ab83b 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -7,6 +7,7 @@ import ( "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" ) type _DNSRule struct { @@ -28,7 +29,7 @@ func (r DNSRule) MarshalJSON() ([]byte, error) { default: return nil, E.New("unknown rule type: " + r.Type) } - return MarshallObjects((_DNSRule)(r), v) + return badjson.MarshallObjects((_DNSRule)(r), v) } func (r *DNSRule) UnmarshalJSON(bytes []byte) error { @@ -46,7 +47,7 @@ func (r *DNSRule) UnmarshalJSON(bytes []byte) error { default: return E.New("unknown rule type: " + r.Type) } - err = UnmarshallExcluded(bytes, (*_DNSRule)(r), v) + err = badjson.UnmarshallExcluded(bytes, (*_DNSRule)(r), v) if err != nil { return err } @@ -111,7 +112,7 @@ type DefaultDNSRule struct { } func (r *DefaultDNSRule) MarshalJSON() ([]byte, error) { - return MarshallObjects(r.RawDefaultDNSRule, r.DNSRuleAction) + return badjson.MarshallObjects(r.RawDefaultDNSRule, r.DNSRuleAction) } func (r *DefaultDNSRule) UnmarshalJSON(data []byte) error { @@ -119,7 +120,7 @@ func (r *DefaultDNSRule) UnmarshalJSON(data []byte) error { if err != nil { return err } - return UnmarshallExcluded(data, &r.RawDefaultDNSRule, &r.DNSRuleAction) + return badjson.UnmarshallExcluded(data, &r.RawDefaultDNSRule, &r.DNSRuleAction) } func (r *DefaultDNSRule) IsValid() bool { @@ -141,7 +142,7 @@ type LogicalDNSRule struct { } func (r *LogicalDNSRule) MarshalJSON() ([]byte, error) { - return MarshallObjects(r._LogicalDNSRule, r.DNSRuleAction) + return badjson.MarshallObjects(r._LogicalDNSRule, r.DNSRuleAction) } func (r *LogicalDNSRule) UnmarshalJSON(data []byte) error { @@ -149,7 +150,7 @@ func (r *LogicalDNSRule) UnmarshalJSON(data []byte) error { if err != nil { return err } - return UnmarshallExcluded(data, &r._LogicalDNSRule, &r.DNSRuleAction) + return badjson.UnmarshallExcluded(data, &r._LogicalDNSRule, &r.DNSRuleAction) } func (r *LogicalDNSRule) IsValid() bool { diff --git a/option/rule_set.go b/option/rule_set.go index e0f10bf180..3bf9aa5c48 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -9,6 +9,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" "go4.org/netipx" ) @@ -37,7 +38,7 @@ func (r RuleSet) MarshalJSON() ([]byte, error) { default: return nil, E.New("unknown rule-set type: " + r.Type) } - return MarshallObjects((_RuleSet)(r), v) + return badjson.MarshallObjects((_RuleSet)(r), v) } func (r *RuleSet) UnmarshalJSON(bytes []byte) error { @@ -71,7 +72,7 @@ func (r *RuleSet) UnmarshalJSON(bytes []byte) error { } else { r.Format = "" } - err = UnmarshallExcluded(bytes, (*_RuleSet)(r), v) + err = badjson.UnmarshallExcluded(bytes, (*_RuleSet)(r), v) if err != nil { return err } @@ -107,7 +108,7 @@ func (r HeadlessRule) MarshalJSON() ([]byte, error) { default: return nil, E.New("unknown rule type: " + r.Type) } - return MarshallObjects((_HeadlessRule)(r), v) + return badjson.MarshallObjects((_HeadlessRule)(r), v) } func (r *HeadlessRule) UnmarshalJSON(bytes []byte) error { @@ -125,7 +126,7 @@ func (r *HeadlessRule) UnmarshalJSON(bytes []byte) error { default: return E.New("unknown rule type: " + r.Type) } - err = UnmarshallExcluded(bytes, (*_HeadlessRule)(r), v) + err = badjson.UnmarshallExcluded(bytes, (*_HeadlessRule)(r), v) if err != nil { return err } @@ -203,7 +204,7 @@ func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { default: return nil, E.New("unknown rule-set version: ", r.Version) } - return MarshallObjects((_PlainRuleSetCompat)(r), v) + return badjson.MarshallObjects((_PlainRuleSetCompat)(r), v) } func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { @@ -220,7 +221,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { default: return E.New("unknown rule-set version: ", r.Version) } - err = UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v) + err = badjson.UnmarshallExcluded(bytes, (*_PlainRuleSetCompat)(r), v) if err != nil { return err } diff --git a/option/simple.go b/option/simple.go index ba9d6bf190..78171ce4c2 100644 --- a/option/simple.go +++ b/option/simple.go @@ -14,7 +14,7 @@ type HTTPMixedInboundOptions struct { InboundTLSOptionsContainer } -type SocksOutboundOptions struct { +type SOCKSOutboundOptions struct { DialerOptions ServerOptions Version string `json:"version,omitempty"` diff --git a/option/tls_acme.go b/option/tls_acme.go index 17d515e20b..9c2e081fa8 100644 --- a/option/tls_acme.go +++ b/option/tls_acme.go @@ -4,6 +4,7 @@ import ( C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" ) type InboundACMEOptions struct { @@ -45,7 +46,7 @@ func (o ACMEDNS01ChallengeOptions) MarshalJSON() ([]byte, error) { default: return nil, E.New("unknown provider type: " + o.Provider) } - return MarshallObjects((_ACMEDNS01ChallengeOptions)(o), v) + return badjson.MarshallObjects((_ACMEDNS01ChallengeOptions)(o), v) } func (o *ACMEDNS01ChallengeOptions) UnmarshalJSON(bytes []byte) error { @@ -62,7 +63,7 @@ func (o *ACMEDNS01ChallengeOptions) UnmarshalJSON(bytes []byte) error { default: return E.New("unknown provider type: " + o.Provider) } - err = UnmarshallExcluded(bytes, (*_ACMEDNS01ChallengeOptions)(o), v) + err = badjson.UnmarshallExcluded(bytes, (*_ACMEDNS01ChallengeOptions)(o), v) if err != nil { return err } diff --git a/option/v2ray_transport.go b/option/v2ray_transport.go index fcd81f9462..f87b175d39 100644 --- a/option/v2ray_transport.go +++ b/option/v2ray_transport.go @@ -4,6 +4,7 @@ import ( C "github.com/sagernet/sing-box/constant" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" ) type _V2RayTransportOptions struct { @@ -35,7 +36,7 @@ func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) { default: return nil, E.New("unknown transport type: " + o.Type) } - return MarshallObjects((_V2RayTransportOptions)(o), v) + return badjson.MarshallObjects((_V2RayTransportOptions)(o), v) } func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error { @@ -58,7 +59,7 @@ func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error { default: return E.New("unknown transport type: " + o.Type) } - err = UnmarshallExcluded(bytes, (*_V2RayTransportOptions)(o), v) + err = badjson.UnmarshallExcluded(bytes, (*_V2RayTransportOptions)(o), v) if err != nil { return err } diff --git a/outbound/block.go b/outbound/block.go deleted file mode 100644 index b6ccefe2be..0000000000 --- a/outbound/block.go +++ /dev/null @@ -1,54 +0,0 @@ -package outbound - -import ( - "context" - "io" - "net" - - "github.com/sagernet/sing-box/adapter" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -var _ adapter.Outbound = (*Block)(nil) - -type Block struct { - myOutboundAdapter -} - -func NewBlock(logger log.ContextLogger, tag string) *Block { - return &Block{ - myOutboundAdapter{ - protocol: C.TypeBlock, - network: []string{N.NetworkTCP, N.NetworkUDP}, - logger: logger, - tag: tag, - }, - } -} - -func (h *Block) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - h.logger.InfoContext(ctx, "blocked connection to ", destination) - return nil, io.EOF -} - -func (h *Block) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - h.logger.InfoContext(ctx, "blocked packet connection to ", destination) - return nil, io.EOF -} - -// Deprecated -func (h *Block) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - conn.Close() - h.logger.InfoContext(ctx, "blocked connection to ", metadata.Destination) - return nil -} - -// Deprecated -func (h *Block) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - conn.Close() - h.logger.InfoContext(ctx, "blocked packet connection to ", metadata.Destination) - return nil -} diff --git a/outbound/builder.go b/outbound/builder.go deleted file mode 100644 index d895b56dda..0000000000 --- a/outbound/builder.go +++ /dev/null @@ -1,65 +0,0 @@ -package outbound - -import ( - "context" - - "github.com/sagernet/sing-box/adapter" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" - E "github.com/sagernet/sing/common/exceptions" -) - -func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Outbound) (adapter.Outbound, error) { - if tag != "" { - ctx = adapter.WithContext(ctx, &adapter.InboundContext{ - Outbound: tag, - }) - } - if options.Type == "" { - return nil, E.New("missing outbound type") - } - ctx = ContextWithTag(ctx, tag) - switch options.Type { - case C.TypeDirect: - return NewDirect(router, logger, tag, options.DirectOptions) - case C.TypeBlock: - return NewBlock(logger, tag), nil - case C.TypeDNS: - return NewDNS(router, tag), nil - case C.TypeSOCKS: - return NewSocks(router, logger, tag, options.SocksOptions) - case C.TypeHTTP: - return NewHTTP(ctx, router, logger, tag, options.HTTPOptions) - case C.TypeShadowsocks: - return NewShadowsocks(ctx, router, logger, tag, options.ShadowsocksOptions) - case C.TypeVMess: - return NewVMess(ctx, router, logger, tag, options.VMessOptions) - case C.TypeTrojan: - return NewTrojan(ctx, router, logger, tag, options.TrojanOptions) - case C.TypeWireGuard: - return NewWireGuard(ctx, router, logger, tag, options.WireGuardOptions) - case C.TypeHysteria: - return NewHysteria(ctx, router, logger, tag, options.HysteriaOptions) - case C.TypeTor: - return NewTor(ctx, router, logger, tag, options.TorOptions) - case C.TypeSSH: - return NewSSH(ctx, router, logger, tag, options.SSHOptions) - case C.TypeShadowTLS: - return NewShadowTLS(ctx, router, logger, tag, options.ShadowTLSOptions) - case C.TypeShadowsocksR: - return NewShadowsocksR(ctx, router, logger, tag, options.ShadowsocksROptions) - case C.TypeVLESS: - return NewVLESS(ctx, router, logger, tag, options.VLESSOptions) - case C.TypeTUIC: - return NewTUIC(ctx, router, logger, tag, options.TUICOptions) - case C.TypeHysteria2: - return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options) - case C.TypeSelector: - return NewSelector(ctx, router, logger, tag, options.SelectorOptions) - case C.TypeURLTest: - return NewURLTest(ctx, router, logger, tag, options.URLTestOptions) - default: - return nil, E.New("unknown outbound type: ", options.Type) - } -} diff --git a/outbound/hysteria_stub.go b/outbound/hysteria_stub.go deleted file mode 100644 index 84db5305bc..0000000000 --- a/outbound/hysteria_stub.go +++ /dev/null @@ -1,20 +0,0 @@ -//go:build !with_quic - -package outbound - -import ( - "context" - - "github.com/sagernet/sing-box/adapter" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" -) - -func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (adapter.Outbound, error) { - return nil, C.ErrQUICNotIncluded -} - -func NewHysteria2(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2OutboundOptions) (adapter.Outbound, error) { - return nil, C.ErrQUICNotIncluded -} diff --git a/outbound/lookback.go b/outbound/lookback.go deleted file mode 100644 index aeb7451dba..0000000000 --- a/outbound/lookback.go +++ /dev/null @@ -1,14 +0,0 @@ -package outbound - -import "context" - -type outboundTagKey struct{} - -func ContextWithTag(ctx context.Context, outboundTag string) context.Context { - return context.WithValue(ctx, outboundTagKey{}, outboundTag) -} - -func TagFromContext(ctx context.Context) (string, bool) { - value, loaded := ctx.Value(outboundTagKey{}).(string) - return value, loaded -} diff --git a/outbound/shadowsocksr.go b/outbound/shadowsocksr.go deleted file mode 100644 index 615a71e4e4..0000000000 --- a/outbound/shadowsocksr.go +++ /dev/null @@ -1,18 +0,0 @@ -//go:build with_shadowsocksr - -package outbound - -import ( - "context" - "os" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" -) - -var _ int = "ShadowsocksR is deprecated and removed in sing-box 1.6.0" - -func NewShadowsocksR(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksROutboundOptions) (adapter.Outbound, error) { - return nil, os.ErrInvalid -} diff --git a/outbound/shadowsocksr_stub.go b/outbound/shadowsocksr_stub.go deleted file mode 100644 index 94971da0bc..0000000000 --- a/outbound/shadowsocksr_stub.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build !with_shadowsocksr - -package outbound - -import ( - "context" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" - E "github.com/sagernet/sing/common/exceptions" -) - -func NewShadowsocksR(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksROutboundOptions) (adapter.Outbound, error) { - return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0") -} diff --git a/outbound/tor_embed.go b/outbound/tor_embed.go deleted file mode 100644 index d80b49ae66..0000000000 --- a/outbound/tor_embed.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build with_embedded_tor && !(android || ios) - -package outbound - -import ( - "berty.tech/go-libtor" - "github.com/cretz/bine/tor" -) - -func newConfig() tor.StartConf { - return tor.StartConf{ - ProcessCreator: libtor.Creator, - UseEmbeddedControlConn: true, - } -} diff --git a/outbound/tor_embed_mobile.go b/outbound/tor_embed_mobile.go deleted file mode 100644 index 0900d8c938..0000000000 --- a/outbound/tor_embed_mobile.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build with_embedded_tor && (android || ios) - -package outbound - -import ( - "github.com/cretz/bine/tor" - "github.com/ooni/go-libtor" -) - -func newConfig() tor.StartConf { - return tor.StartConf{ - ProcessCreator: libtor.Creator, - UseEmbeddedControlConn: true, - } -} diff --git a/outbound/tor_external.go b/outbound/tor_external.go deleted file mode 100644 index 6bce95d1d9..0000000000 --- a/outbound/tor_external.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !with_embedded_tor - -package outbound - -import "github.com/cretz/bine/tor" - -func newConfig() tor.StartConf { - return tor.StartConf{} -} diff --git a/outbound/tuic_stub.go b/outbound/tuic_stub.go deleted file mode 100644 index a6372c9ee3..0000000000 --- a/outbound/tuic_stub.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build !with_quic - -package outbound - -import ( - "context" - - "github.com/sagernet/sing-box/adapter" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" -) - -func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICOutboundOptions) (adapter.Outbound, error) { - return nil, C.ErrQUICNotIncluded -} diff --git a/outbound/wireguard_stub.go b/outbound/wireguard_stub.go deleted file mode 100644 index 3a8b0e87a5..0000000000 --- a/outbound/wireguard_stub.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build !with_wireguard - -package outbound - -import ( - "context" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" - E "github.com/sagernet/sing/common/exceptions" -) - -func NewWireGuard(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardOutboundOptions) (adapter.Outbound, error) { - return nil, E.New(`WireGuard is not included in this build, rebuild with -tags with_wireguard`) -} diff --git a/protocol/block/outbound.go b/protocol/block/outbound.go new file mode 100644 index 0000000000..75bc7797e0 --- /dev/null +++ b/protocol/block/outbound.go @@ -0,0 +1,42 @@ +package block + +import ( + "context" + "net" + "syscall" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.StubOptions](registry, C.TypeBlock, New) +} + +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger +} + +func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, _ option.StubOptions) (adapter.Outbound, error) { + return &Outbound{ + Adapter: outbound.NewAdapter(C.TypeBlock, []string{N.NetworkTCP, N.NetworkUDP}, tag, nil), + logger: logger, + }, nil +} + +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + h.logger.InfoContext(ctx, "blocked connection to ", destination) + return nil, syscall.EPERM +} + +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + h.logger.InfoContext(ctx, "blocked packet connection to ", destination) + return nil, syscall.EPERM +} diff --git a/protocol/direct/inbound.go b/protocol/direct/inbound.go new file mode 100644 index 0000000000..568a72cbd0 --- /dev/null +++ b/protocol/direct/inbound.go @@ -0,0 +1,139 @@ +package direct + +import ( + "context" + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/buf" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/udpnat2" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.DirectInboundOptions](registry, C.TypeDirect, NewInbound) +} + +type Inbound struct { + inbound.Adapter + ctx context.Context + router adapter.ConnectionRouterEx + logger log.ContextLogger + listener *listener.Listener + udpNat *udpnat.Service + overrideOption int + overrideDestination M.Socksaddr +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DirectInboundOptions) (adapter.Inbound, error) { + options.UDPFragmentDefault = true + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeDirect, tag), + ctx: ctx, + router: router, + logger: logger, + } + if options.OverrideAddress != "" && options.OverridePort != 0 { + inbound.overrideOption = 1 + inbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort) + } else if options.OverrideAddress != "" { + inbound.overrideOption = 2 + inbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort) + } else if options.OverridePort != 0 { + inbound.overrideOption = 3 + inbound.overrideDestination = M.Socksaddr{Port: options.OverridePort} + } + var udpTimeout time.Duration + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } else { + udpTimeout = C.UDPTimeout + } + inbound.udpNat = udpnat.New(inbound, inbound.preparePacketConnection, udpTimeout, false) + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: options.Network.Build(), + Listen: options.ListenOptions, + ConnectionHandler: inbound, + PacketHandler: inbound, + }) + return inbound, nil +} + +func (i *Inbound) Start() error { + return i.listener.Start() +} + +func (i *Inbound) Close() error { + return i.listener.Close() +} + +func (i *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + switch i.overrideOption { + case 1: + metadata.Destination = i.overrideDestination + case 2: + destination := i.overrideDestination + destination.Port = metadata.Destination.Port + metadata.Destination = destination + case 3: + metadata.Destination.Port = i.overrideDestination.Port + } + i.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + return i.router.RouteConnection(ctx, conn, metadata) +} + +func (i *Inbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { + var destination M.Socksaddr + switch i.overrideOption { + case 1: + destination = i.overrideDestination + case 2: + destination = i.overrideDestination + destination.Port = source.Port + case 3: + destination = source + destination.Port = i.overrideDestination.Port + } + i.udpNat.NewPacket([][]byte{buffer.Bytes()}, source, destination, nil) +} + +func (i *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + i.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + metadata.Inbound = i.Tag() + metadata.InboundType = i.Type() + i.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (i *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + i.logger.InfoContext(ctx, "inbound packet connection from ", source) + i.logger.InfoContext(ctx, "inbound packet connection to ", destination) + var metadata adapter.InboundContext + metadata.Inbound = i.Tag() + metadata.InboundType = i.Type() + metadata.Source = source + metadata.Destination = destination + metadata.OriginDestination = i.listener.UDPAddr() + i.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + +func (i *Inbound) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) { + return true, log.ContextWithNewID(i.ctx), &directPacketWriter{i.listener.PacketWriter(), source}, nil +} + +type directPacketWriter struct { + writer N.PacketWriter + source M.Socksaddr +} + +func (w *directPacketWriter) WritePacket(buffer *buf.Buffer, addr M.Socksaddr) error { + return w.writer.WritePacket(buffer, w.source) +} diff --git a/outbound/direct_loopback_detect.go b/protocol/direct/loopback_detect.go similarity index 99% rename from outbound/direct_loopback_detect.go rename to protocol/direct/loopback_detect.go index 1469b9d017..5a184e692a 100644 --- a/outbound/direct_loopback_detect.go +++ b/protocol/direct/loopback_detect.go @@ -1,4 +1,4 @@ -package outbound +package direct import ( "net" diff --git a/outbound/direct.go b/protocol/direct/outbound.go similarity index 76% rename from outbound/direct.go rename to protocol/direct/outbound.go index 415a72f370..32c1ed8fda 100644 --- a/outbound/direct.go +++ b/protocol/direct/outbound.go @@ -1,4 +1,4 @@ -package outbound +package direct import ( "context" @@ -7,6 +7,7 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" @@ -14,17 +15,20 @@ import ( "github.com/sagernet/sing-dns" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) -var ( - _ adapter.Outbound = (*Direct)(nil) - _ N.ParallelDialer = (*Direct)(nil) -) +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.DirectOutboundOptions](registry, C.TypeDirect, NewOutbound) +} + +var _ N.ParallelDialer = (*Outbound)(nil) -type Direct struct { - myOutboundAdapter +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger dialer N.Dialer domainStrategy dns.DomainStrategy fallbackDelay time.Duration @@ -33,21 +37,15 @@ type Direct struct { // loopBack *loopBackDetector } -func NewDirect(router adapter.Router, logger log.ContextLogger, tag string, options option.DirectOutboundOptions) (*Direct, error) { +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DirectOutboundOptions) (adapter.Outbound, error) { options.UDPFragmentDefault = true outboundDialer, err := dialer.New(router, options.DialerOptions) if err != nil { return nil, err } - outbound := &Direct{ - myOutboundAdapter: myOutboundAdapter{ - protocol: C.TypeDirect, - network: []string{N.NetworkTCP, N.NetworkUDP}, - router: router, - logger: logger, - tag: tag, - dependencies: withDialerDependency(options.DialerOptions), - }, + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeDirect, []string{N.NetworkTCP, N.NetworkUDP}, tag, options.DialerOptions), + logger: logger, domainStrategy: dns.DomainStrategy(options.DomainStrategy), fallbackDelay: time.Duration(options.FallbackDelay), dialer: outboundDialer, @@ -69,9 +67,9 @@ func NewDirect(router adapter.Router, logger log.ContextLogger, tag string, opti return outbound, nil } -func (h *Direct) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) - metadata.Outbound = h.tag + metadata.Outbound = h.Tag() metadata.Destination = destination switch h.overrideOption { case 1: @@ -98,9 +96,9 @@ func (h *Direct) DialContext(ctx context.Context, network string, destination M. return h.dialer.DialContext(ctx, network, destination) } -func (h *Direct) DialParallel(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr) (net.Conn, error) { +func (h *Outbound) DialParallel(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) - metadata.Outbound = h.tag + metadata.Outbound = h.Tag() metadata.Destination = destination switch h.overrideOption { case 1, 2: @@ -125,9 +123,9 @@ func (h *Direct) DialParallel(ctx context.Context, network string, destination M return N.DialParallel(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == dns.DomainStrategyPreferIPv6, h.fallbackDelay) } -func (h *Direct) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { ctx, metadata := adapter.ExtendContext(ctx) - metadata.Outbound = h.tag + metadata.Outbound = h.Tag() metadata.Destination = destination originDestination := destination switch h.overrideOption { @@ -156,14 +154,14 @@ func (h *Direct) ListenPacket(ctx context.Context, destination M.Socksaddr) (net return conn, nil } -/*func (h *Direct) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +/*func (h *Outbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if h.loopBack.CheckConn(metadata.Source.AddrPort(), M.AddrPortFromNet(conn.LocalAddr())) { return E.New("reject loopback connection to ", metadata.Destination) } return NewConnection(ctx, h, conn, metadata) } -func (h *Direct) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (h *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { if h.loopBack.CheckPacketConn(metadata.Source.AddrPort(), M.AddrPortFromNet(conn.LocalAddr())) { return E.New("reject loopback packet connection to ", metadata.Destination) } diff --git a/outbound/dns.go b/protocol/dns/handle.go similarity index 83% rename from outbound/dns.go rename to protocol/dns/handle.go index d9c92f19ec..23ed1c0c79 100644 --- a/outbound/dns.go +++ b/protocol/dns/handle.go @@ -1,11 +1,9 @@ -package outbound +package dns import ( "context" "encoding/binary" "net" - "os" - "time" "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" @@ -21,44 +19,6 @@ import ( mDNS "github.com/miekg/dns" ) -var _ adapter.Outbound = (*DNS)(nil) - -type DNS struct { - myOutboundAdapter -} - -func NewDNS(router adapter.Router, tag string) *DNS { - return &DNS{ - myOutboundAdapter{ - protocol: C.TypeDNS, - network: []string{N.NetworkTCP, N.NetworkUDP}, - router: router, - tag: tag, - }, - } -} - -func (d *DNS) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - return nil, os.ErrInvalid -} - -func (d *DNS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - return nil, os.ErrInvalid -} - -// Deprecated -func (d *DNS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - metadata.Destination = M.Socksaddr{} - defer conn.Close() - for { - conn.SetReadDeadline(time.Now().Add(C.DNSTimeout)) - err := HandleStreamDNSRequest(ctx, d.router, conn, metadata) - if err != nil { - return err - } - } -} - func HandleStreamDNSRequest(ctx context.Context, router adapter.Router, conn net.Conn, metadata adapter.InboundContext) error { var queryLength uint16 err := binary.Read(conn, binary.BigEndian, &queryLength) @@ -100,11 +60,6 @@ func HandleStreamDNSRequest(ctx context.Context, router adapter.Router, conn net return nil } -// Deprecated -func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return NewDNSPacketConnection(ctx, d.router, conn, nil, metadata) -} - func NewDNSPacketConnection(ctx context.Context, router adapter.Router, conn N.PacketConn, cachedPackets []*N.PacketBuffer, metadata adapter.InboundContext) error { metadata.Destination = M.Socksaddr{} var reader N.PacketReader = conn diff --git a/protocol/dns/outbound.go b/protocol/dns/outbound.go new file mode 100644 index 0000000000..7ce9fde2f2 --- /dev/null +++ b/protocol/dns/outbound.go @@ -0,0 +1,61 @@ +package dns + +import ( + "context" + "net" + "os" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.StubOptions](registry, C.TypeDNS, NewOutbound) +} + +type Outbound struct { + outbound.Adapter + router adapter.Router + logger logger.ContextLogger +} + +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.StubOptions) (adapter.Outbound, error) { + return &Outbound{ + Adapter: outbound.NewAdapter(C.TypeDNS, []string{N.NetworkTCP, N.NetworkUDP}, tag, nil), + router: router, + logger: logger, + }, nil +} + +func (d *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + return nil, os.ErrInvalid +} + +func (d *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return nil, os.ErrInvalid +} + +// Deprecated +func (d *Outbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + metadata.Destination = M.Socksaddr{} + defer conn.Close() + for { + conn.SetReadDeadline(time.Now().Add(C.DNSTimeout)) + err := HandleStreamDNSRequest(ctx, d.router, conn, metadata) + if err != nil { + return err + } + } +} + +// Deprecated +func (d *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + return NewDNSPacketConnection(ctx, d.router, conn, nil, metadata) +} diff --git a/outbound/selector.go b/protocol/group/selector.go similarity index 82% rename from outbound/selector.go rename to protocol/group/selector.go index 64e6a2f936..8ade27a943 100644 --- a/outbound/selector.go +++ b/protocol/group/selector.go @@ -1,28 +1,33 @@ -package outbound +package group import ( "context" "net" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/interrupt" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" ) -var ( - _ adapter.Outbound = (*Selector)(nil) - _ adapter.OutboundGroup = (*Selector)(nil) -) +func RegisterSelector(registry *outbound.Registry) { + outbound.Register[option.SelectorOutboundOptions](registry, C.TypeSelector, NewSelector) +} + +var _ adapter.OutboundGroup = (*Selector)(nil) type Selector struct { - myOutboundAdapter + outbound.Adapter ctx context.Context + router adapter.Router + logger logger.ContextLogger tags []string defaultTag string outbounds map[string]adapter.Outbound @@ -31,16 +36,12 @@ type Selector struct { interruptExternalConnections bool } -func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (*Selector, error) { +func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (adapter.Outbound, error) { outbound := &Selector{ - myOutboundAdapter: myOutboundAdapter{ - protocol: C.TypeSelector, - router: router, - logger: logger, - tag: tag, - dependencies: options.Outbounds, - }, + Adapter: outbound.NewAdapter(C.TypeSelector, nil, tag, options.Outbounds), ctx: ctx, + router: router, + logger: logger, tags: options.Outbounds, defaultTag: options.Default, outbounds: make(map[string]adapter.Outbound), @@ -69,10 +70,10 @@ func (s *Selector) Start() error { s.outbounds[tag] = detour } - if s.tag != "" { + if s.Tag() != "" { cacheFile := service.FromContext[adapter.CacheFile](s.ctx) if cacheFile != nil { - selected := cacheFile.LoadSelected(s.tag) + selected := cacheFile.LoadSelected(s.Tag()) if selected != "" { detour, loaded := s.outbounds[selected] if loaded { @@ -117,10 +118,10 @@ func (s *Selector) SelectOutbound(tag string) bool { return true } s.selected = detour - if s.tag != "" { + if s.Tag() != "" { cacheFile := service.FromContext[adapter.CacheFile](s.ctx) if cacheFile != nil { - err := cacheFile.StoreSelected(s.tag, tag) + err := cacheFile.StoreSelected(s.Tag(), tag) if err != nil { s.logger.Error("store selected: ", err) } @@ -153,7 +154,7 @@ func (s *Selector) NewConnection(ctx context.Context, conn net.Conn, metadata ad if legacyHandler, ok := s.selected.(adapter.ConnectionHandler); ok { return legacyHandler.NewConnection(ctx, conn, metadata) } else { - return NewConnection(ctx, s.selected, conn, metadata) + return outbound.NewConnection(ctx, s.selected, conn, metadata) } } @@ -164,7 +165,7 @@ func (s *Selector) NewPacketConnection(ctx context.Context, conn N.PacketConn, m if legacyHandler, ok := s.selected.(adapter.PacketConnectionHandler); ok { return legacyHandler.NewPacketConnection(ctx, conn, metadata) } else { - return NewPacketConnection(ctx, s.selected, conn, metadata) + return outbound.NewPacketConnection(ctx, s.selected, conn, metadata) } } diff --git a/outbound/urltest.go b/protocol/group/urltest.go similarity index 94% rename from outbound/urltest.go rename to protocol/group/urltest.go index 564a0ddc3a..ccdf809d9b 100644 --- a/outbound/urltest.go +++ b/protocol/group/urltest.go @@ -1,4 +1,4 @@ -package outbound +package group import ( "context" @@ -7,6 +7,7 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/interrupt" "github.com/sagernet/sing-box/common/urltest" C "github.com/sagernet/sing-box/constant" @@ -22,15 +23,20 @@ import ( "github.com/sagernet/sing/service/pause" ) +func RegisterURLTest(registry *outbound.Registry) { + outbound.Register[option.URLTestOutboundOptions](registry, C.TypeURLTest, NewURLTest) +} + var ( - _ adapter.Outbound = (*URLTest)(nil) _ adapter.OutboundGroup = (*URLTest)(nil) _ adapter.InterfaceUpdateListener = (*URLTest)(nil) ) type URLTest struct { - myOutboundAdapter + outbound.Adapter ctx context.Context + router adapter.Router + logger log.ContextLogger tags []string link string interval time.Duration @@ -40,17 +46,12 @@ type URLTest struct { interruptExternalConnections bool } -func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (*URLTest, error) { +func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (adapter.Outbound, error) { outbound := &URLTest{ - myOutboundAdapter: myOutboundAdapter{ - protocol: C.TypeURLTest, - network: []string{N.NetworkTCP, N.NetworkUDP}, - router: router, - logger: logger, - tag: tag, - dependencies: options.Outbounds, - }, + Adapter: outbound.NewAdapter(C.TypeURLTest, []string{N.NetworkTCP, N.NetworkUDP}, tag, options.Outbounds), ctx: ctx, + router: router, + logger: logger, tags: options.Outbounds, link: options.URL, interval: time.Duration(options.Interval), @@ -171,14 +172,14 @@ func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (ne // Deprecated func (s *URLTest) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { ctx = interrupt.ContextWithIsExternalConnection(ctx) - return NewConnection(ctx, s, conn, metadata) + return outbound.NewConnection(ctx, s, conn, metadata) } // TODO // Deprecated func (s *URLTest) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { ctx = interrupt.ContextWithIsExternalConnection(ctx) - return NewPacketConnection(ctx, s, conn, metadata) + return outbound.NewPacketConnection(ctx, s, conn, metadata) } func (s *URLTest) InterfaceUpdated() { diff --git a/protocol/http/inbound.go b/protocol/http/inbound.go new file mode 100644 index 0000000000..87ed9a1089 --- /dev/null +++ b/protocol/http/inbound.go @@ -0,0 +1,122 @@ +package http + +import ( + std_bufio "bufio" + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/common/uot" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/protocol/http" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.HTTPMixedInboundOptions](registry, C.TypeHTTP, NewInbound) +} + +var _ adapter.TCPInjectableInbound = (*Inbound)(nil) + +type Inbound struct { + inbound.Adapter + router adapter.ConnectionRouterEx + logger log.ContextLogger + listener *listener.Listener + authenticator *auth.Authenticator + tlsConfig tls.ServerConfig +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeHTTP, tag), + router: uot.NewRouter(router, logger), + logger: logger, + authenticator: auth.NewAuthenticator(options.Users), + } + if options.TLS != nil { + tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + inbound.tlsConfig = tlsConfig + } + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: inbound, + SetSystemProxy: options.SetSystemProxy, + SystemProxySOCKS: false, + }) + return inbound, nil +} + +func (h *Inbound) Start() error { + if h.tlsConfig != nil { + err := h.tlsConfig.Start() + if err != nil { + return E.Cause(err, "create TLS config") + } + } + return h.listener.Start() +} + +func (h *Inbound) Close() error { + return common.Close( + &h.listener, + h.tlsConfig, + ) +} + +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.newConnection(ctx, conn, metadata, onClose) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } +} + +func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { + var err error + if h.tlsConfig != nil { + conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig) + if err != nil { + return err + } + } + return http.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) +} + +func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + user, loaded := auth.UserFromContext[string](ctx) + if !loaded { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) + return + } + metadata.User = user + h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + user, loaded := auth.UserFromContext[string](ctx) + if !loaded { + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) + return + } + metadata.User = user + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/outbound/http.go b/protocol/http/outbound.go similarity index 56% rename from outbound/http.go rename to protocol/http/outbound.go index 6f15afb5dc..4c930591b1 100644 --- a/outbound/http.go +++ b/protocol/http/outbound.go @@ -1,4 +1,4 @@ -package outbound +package http import ( "context" @@ -6,25 +6,30 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" sHTTP "github.com/sagernet/sing/protocol/http" ) -var _ adapter.Outbound = (*HTTP)(nil) +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.HTTPOutboundOptions](registry, C.TypeHTTP, NewOutbound) +} -type HTTP struct { - myOutboundAdapter +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger client *sHTTP.Client } -func NewHTTP(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPOutboundOptions) (*HTTP, error) { +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPOutboundOptions) (adapter.Outbound, error) { outboundDialer, err := dialer.New(router, options.DialerOptions) if err != nil { return nil, err @@ -33,16 +38,10 @@ func NewHTTP(ctx context.Context, router adapter.Router, logger log.ContextLogge if err != nil { return nil, err } - return &HTTP{ - myOutboundAdapter{ - protocol: C.TypeHTTP, - network: []string{N.NetworkTCP}, - router: router, - logger: logger, - tag: tag, - dependencies: withDialerDependency(options.DialerOptions), - }, - sHTTP.NewClient(sHTTP.Options{ + return &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHTTP, []string{N.NetworkTCP}, tag, options.DialerOptions), + logger: logger, + client: sHTTP.NewClient(sHTTP.Options{ Dialer: detour, Server: options.ServerOptions.Build(), Username: options.Username, @@ -53,14 +52,14 @@ func NewHTTP(ctx context.Context, router adapter.Router, logger log.ContextLogge }, nil } -func (h *HTTP) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) - metadata.Outbound = h.tag + metadata.Outbound = h.Tag() metadata.Destination = destination h.logger.InfoContext(ctx, "outbound connection to ", destination) return h.client.DialContext(ctx, network, destination) } -func (h *HTTP) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return nil, os.ErrInvalid } diff --git a/inbound/hysteria.go b/protocol/hysteria/inbound.go similarity index 71% rename from inbound/hysteria.go rename to protocol/hysteria/inbound.go index 427d7df430..f1f6da3ef6 100644 --- a/inbound/hysteria.go +++ b/protocol/hysteria/inbound.go @@ -1,6 +1,4 @@ -//go:build with_quic - -package inbound +package hysteria import ( "context" @@ -8,7 +6,9 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/humanize" + "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" @@ -20,16 +20,21 @@ import ( N "github.com/sagernet/sing/common/network" ) -var _ adapter.Inbound = (*Hysteria)(nil) +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.HysteriaInboundOptions](registry, C.TypeHysteria, NewInbound) +} -type Hysteria struct { - myInboundAdapter +type Inbound struct { + inbound.Adapter + router adapter.Router + logger log.ContextLogger + listener *listener.Listener tlsConfig tls.ServerConfig service *hysteria.Service[int] userNameList []string } -func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (*Hysteria, error) { +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaInboundOptions) (adapter.Inbound, error) { options.UDPFragmentDefault = true if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired @@ -38,16 +43,15 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL if err != nil { return nil, err } - inbound := &Hysteria{ - myInboundAdapter: myInboundAdapter{ - protocol: C.TypeHysteria, - network: []string{N.NetworkUDP}, - ctx: ctx, - router: router, - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - }, + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeHysteria, tag), + router: router, + logger: logger, + listener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Listen: options.ListenOptions, + }), tlsConfig: tlsConfig, } var sendBps, receiveBps uint64 @@ -113,9 +117,12 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL return inbound, nil } -func (h *Hysteria) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { ctx = log.ContextWithNewID(ctx) - metadata = h.createMetadata(conn, metadata) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { @@ -127,9 +134,13 @@ func (h *Hysteria) newConnection(ctx context.Context, conn net.Conn, metadata ad return h.router.RouteConnection(ctx, conn, metadata) } -func (h *Hysteria) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { ctx = log.ContextWithNewID(ctx) - metadata = h.createPacketMetadata(conn, metadata) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions + metadata.OriginDestination = h.listener.UDPAddr() h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { @@ -141,23 +152,23 @@ func (h *Hysteria) newPacketConnection(ctx context.Context, conn N.PacketConn, m return h.router.RoutePacketConnection(ctx, conn, metadata) } -func (h *Hysteria) Start() error { +func (h *Inbound) Start() error { if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { return err } } - packetConn, err := h.myInboundAdapter.ListenUDP() + packetConn, err := h.listener.ListenUDP() if err != nil { return err } return h.service.Start(packetConn) } -func (h *Hysteria) Close() error { +func (h *Inbound) Close() error { return common.Close( - &h.myInboundAdapter, + &h.listener, h.tlsConfig, common.PtrOrNil(h.service), ) diff --git a/outbound/hysteria.go b/protocol/hysteria/outbound.go similarity index 76% rename from outbound/hysteria.go rename to protocol/hysteria/outbound.go index f3c30739cf..6df32eaf71 100644 --- a/outbound/hysteria.go +++ b/protocol/hysteria/outbound.go @@ -1,6 +1,4 @@ -//go:build with_quic - -package outbound +package hysteria import ( "context" @@ -8,31 +6,39 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/humanize" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/protocol/tuic" "github.com/sagernet/sing-quic/hysteria" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.HysteriaOutboundOptions](registry, C.TypeHysteria, NewOutbound) +} + var ( - _ adapter.Outbound = (*TUIC)(nil) - _ adapter.InterfaceUpdateListener = (*TUIC)(nil) + _ adapter.Outbound = (*tuic.Outbound)(nil) + _ adapter.InterfaceUpdateListener = (*tuic.Outbound)(nil) ) -type Hysteria struct { - myOutboundAdapter +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger client *hysteria.Client } -func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (*Hysteria, error) { +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HysteriaOutboundOptions) (adapter.Outbound, error) { options.UDPFragmentDefault = true if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired @@ -88,20 +94,14 @@ func NewHysteria(ctx context.Context, router adapter.Router, logger log.ContextL if err != nil { return nil, err } - return &Hysteria{ - myOutboundAdapter: myOutboundAdapter{ - protocol: C.TypeHysteria, - network: networkList, - router: router, - logger: logger, - tag: tag, - dependencies: withDialerDependency(options.DialerOptions), - }, - client: client, + return &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHysteria, networkList, tag, options.DialerOptions), + logger: logger, + client: client, }, nil } -func (h *Hysteria) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) @@ -117,15 +117,15 @@ func (h *Hysteria) DialContext(ctx context.Context, network string, destination } } -func (h *Hysteria) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) return h.client.ListenPacket(ctx, destination) } -func (h *Hysteria) InterfaceUpdated() { +func (h *Outbound) InterfaceUpdated() { h.client.CloseWithError(E.New("network changed")) } -func (h *Hysteria) Close() error { +func (h *Outbound) Close() error { return h.client.CloseWithError(os.ErrClosed) } diff --git a/inbound/hysteria2.go b/protocol/hysteria2/inbound.go similarity index 74% rename from inbound/hysteria2.go rename to protocol/hysteria2/inbound.go index c13e9531f5..cbf811097f 100644 --- a/inbound/hysteria2.go +++ b/protocol/hysteria2/inbound.go @@ -1,6 +1,4 @@ -//go:build with_quic - -package inbound +package hysteria2 import ( "context" @@ -11,6 +9,8 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" @@ -23,16 +23,21 @@ import ( N "github.com/sagernet/sing/common/network" ) -var _ adapter.Inbound = (*Hysteria2)(nil) +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.Hysteria2InboundOptions](registry, C.TypeHysteria2, NewInbound) +} -type Hysteria2 struct { - myInboundAdapter +type Inbound struct { + inbound.Adapter + router adapter.Router + logger log.ContextLogger + listener *listener.Listener tlsConfig tls.ServerConfig service *hysteria2.Service[int] userNameList []string } -func NewHysteria2(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (*Hysteria2, error) { +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2InboundOptions) (adapter.Inbound, error) { options.UDPFragmentDefault = true if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired @@ -76,16 +81,15 @@ func NewHysteria2(ctx context.Context, router adapter.Router, logger log.Context return nil, E.New("unknown masquerade URL scheme: ", masqueradeURL.Scheme) } } - inbound := &Hysteria2{ - myInboundAdapter: myInboundAdapter{ - protocol: C.TypeHysteria2, - network: []string{N.NetworkUDP}, - ctx: ctx, - router: router, - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - }, + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeHysteria2, tag), + router: router, + logger: logger, + listener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Listen: options.ListenOptions, + }), tlsConfig: tlsConfig, } var udpTimeout time.Duration @@ -124,9 +128,12 @@ func NewHysteria2(ctx context.Context, router adapter.Router, logger log.Context return inbound, nil } -func (h *Hysteria2) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { ctx = log.ContextWithNewID(ctx) - metadata = h.createMetadata(conn, metadata) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { @@ -138,9 +145,13 @@ func (h *Hysteria2) newConnection(ctx context.Context, conn net.Conn, metadata a return h.router.RouteConnection(ctx, conn, metadata) } -func (h *Hysteria2) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { ctx = log.ContextWithNewID(ctx) - metadata = h.createPacketMetadata(conn, metadata) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions + metadata.OriginDestination = h.listener.UDPAddr() h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { @@ -152,23 +163,23 @@ func (h *Hysteria2) newPacketConnection(ctx context.Context, conn N.PacketConn, return h.router.RoutePacketConnection(ctx, conn, metadata) } -func (h *Hysteria2) Start() error { +func (h *Inbound) Start() error { if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { return err } } - packetConn, err := h.myInboundAdapter.ListenUDP() + packetConn, err := h.listener.ListenUDP() if err != nil { return err } return h.service.Start(packetConn) } -func (h *Hysteria2) Close() error { +func (h *Inbound) Close() error { return common.Close( - &h.myInboundAdapter, + &h.listener, h.tlsConfig, common.PtrOrNil(h.service), ) diff --git a/outbound/hysteria2.go b/protocol/hysteria2/outbound.go similarity index 69% rename from outbound/hysteria2.go rename to protocol/hysteria2/outbound.go index 5e46f6a866..5ebc6c91f3 100644 --- a/outbound/hysteria2.go +++ b/protocol/hysteria2/outbound.go @@ -1,6 +1,4 @@ -//go:build with_quic - -package outbound +package hysteria2 import ( "context" @@ -8,31 +6,39 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/protocol/tuic" "github.com/sagernet/sing-quic/hysteria" "github.com/sagernet/sing-quic/hysteria2" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.Hysteria2OutboundOptions](registry, C.TypeHysteria2, NewOutbound) +} + var ( - _ adapter.Outbound = (*TUIC)(nil) - _ adapter.InterfaceUpdateListener = (*TUIC)(nil) + _ adapter.Outbound = (*tuic.Outbound)(nil) + _ adapter.InterfaceUpdateListener = (*tuic.Outbound)(nil) ) -type Hysteria2 struct { - myOutboundAdapter +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger client *hysteria2.Client } -func NewHysteria2(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2OutboundOptions) (*Hysteria2, error) { +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Hysteria2OutboundOptions) (adapter.Outbound, error) { options.UDPFragmentDefault = true if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired @@ -74,20 +80,14 @@ func NewHysteria2(ctx context.Context, router adapter.Router, logger log.Context if err != nil { return nil, err } - return &Hysteria2{ - myOutboundAdapter: myOutboundAdapter{ - protocol: C.TypeHysteria2, - network: networkList, - router: router, - logger: logger, - tag: tag, - dependencies: withDialerDependency(options.DialerOptions), - }, - client: client, + return &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHysteria2, networkList, tag, options.DialerOptions), + logger: logger, + client: client, }, nil } -func (h *Hysteria2) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) @@ -103,15 +103,15 @@ func (h *Hysteria2) DialContext(ctx context.Context, network string, destination } } -func (h *Hysteria2) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) return h.client.ListenPacket(ctx) } -func (h *Hysteria2) InterfaceUpdated() { +func (h *Outbound) InterfaceUpdated() { h.client.CloseWithError(E.New("network changed")) } -func (h *Hysteria2) Close() error { +func (h *Outbound) Close() error { return h.client.CloseWithError(os.ErrClosed) } diff --git a/protocol/mixed/inbound.go b/protocol/mixed/inbound.go new file mode 100644 index 0000000000..e57b791fec --- /dev/null +++ b/protocol/mixed/inbound.go @@ -0,0 +1,109 @@ +package mixed + +import ( + std_bufio "bufio" + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/uot" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/protocol/http" + "github.com/sagernet/sing/protocol/socks" + "github.com/sagernet/sing/protocol/socks/socks4" + "github.com/sagernet/sing/protocol/socks/socks5" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.HTTPMixedInboundOptions](registry, C.TypeMixed, NewInbound) +} + +var _ adapter.TCPInjectableInbound = (*Inbound)(nil) + +type Inbound struct { + inbound.Adapter + router adapter.ConnectionRouterEx + logger log.ContextLogger + listener *listener.Listener + authenticator *auth.Authenticator +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPMixedInboundOptions) (adapter.Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeMixed, tag), + router: uot.NewRouter(router, logger), + logger: logger, + authenticator: auth.NewAuthenticator(options.Users), + } + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: inbound, + SetSystemProxy: options.SetSystemProxy, + SystemProxySOCKS: true, + }) + return inbound, nil +} + +func (h *Inbound) Start() error { + return h.listener.Start() +} + +func (h *Inbound) Close() error { + return h.listener.Close() +} + +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.newConnection(ctx, conn, metadata, onClose) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } +} + +func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { + reader := std_bufio.NewReader(conn) + headerBytes, err := reader.Peek(1) + if err != nil { + return E.Cause(err, "peek first byte") + } + switch headerBytes[0] { + case socks4.Version, socks5.Version: + return socks.HandleConnectionEx(ctx, conn, reader, h.authenticator, nil, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, metadata.Destination, onClose) + default: + return http.HandleConnectionEx(ctx, conn, reader, h.authenticator, nil, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) + } +} + +func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + user, loaded := auth.UserFromContext[string](ctx) + if !loaded { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) + return + } + metadata.User = user + h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + user, loaded := auth.UserFromContext[string](ctx) + if !loaded { + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) + return + } + metadata.User = user + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/protocol/naive/inbound.go b/protocol/naive/inbound.go new file mode 100644 index 0000000000..1a561aeaf2 --- /dev/null +++ b/protocol/naive/inbound.go @@ -0,0 +1,248 @@ +package naive + +import ( + "context" + "io" + "math/rand" + "net" + "net/http" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/common/uot" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/v2rayhttp" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + sHttp "github.com/sagernet/sing/protocol/http" +) + +var ConfigureHTTP3ListenerFunc func(listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, logger logger.Logger) (io.Closer, error) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.NaiveInboundOptions](registry, C.TypeNaive, NewInbound) +} + +type Inbound struct { + inbound.Adapter + ctx context.Context + router adapter.ConnectionRouterEx + logger logger.ContextLogger + listener *listener.Listener + network []string + networkIsDefault bool + authenticator *auth.Authenticator + tlsConfig tls.ServerConfig + httpServer *http.Server + h3Server io.Closer +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.NaiveInboundOptions) (adapter.Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeNaive, tag), + ctx: ctx, + router: uot.NewRouter(router, logger), + logger: logger, + listener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Listen: options.ListenOptions, + }), + networkIsDefault: options.Network == "", + network: options.Network.Build(), + authenticator: auth.NewAuthenticator(options.Users), + } + if common.Contains(inbound.network, N.NetworkUDP) { + if options.TLS == nil || !options.TLS.Enabled { + return nil, E.New("TLS is required for QUIC server") + } + } + if len(options.Users) == 0 { + return nil, E.New("missing users") + } + if options.TLS != nil { + tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) + if err != nil { + return nil, err + } + inbound.tlsConfig = tlsConfig + } + return inbound, nil +} + +func (n *Inbound) Start() error { + var tlsConfig *tls.STDConfig + if n.tlsConfig != nil { + err := n.tlsConfig.Start() + if err != nil { + return E.Cause(err, "create TLS config") + } + tlsConfig, err = n.tlsConfig.Config() + if err != nil { + return err + } + } + if common.Contains(n.network, N.NetworkTCP) { + tcpListener, err := n.listener.ListenTCP() + if err != nil { + return err + } + n.httpServer = &http.Server{ + Handler: n, + TLSConfig: tlsConfig, + BaseContext: func(listener net.Listener) context.Context { + return n.ctx + }, + } + go func() { + var sErr error + if tlsConfig != nil { + sErr = n.httpServer.ServeTLS(tcpListener, "", "") + } else { + sErr = n.httpServer.Serve(tcpListener) + } + if sErr != nil && !E.IsClosedOrCanceled(sErr) { + n.logger.Error("http server serve error: ", sErr) + } + }() + } + + if common.Contains(n.network, N.NetworkUDP) { + http3Server, err := ConfigureHTTP3ListenerFunc(n.listener, n, n.tlsConfig, n.logger) + if err == nil { + n.h3Server = http3Server + } else if len(n.network) > 1 { + n.logger.Warn(E.Cause(err, "naive http3 disabled")) + } else { + return err + } + } + + return nil +} + +func (n *Inbound) Close() error { + return common.Close( + &n.listener, + common.PtrOrNil(n.httpServer), + n.h3Server, + n.tlsConfig, + ) +} + +func (n *Inbound) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + ctx := log.ContextWithNewID(request.Context()) + if request.Method != "CONNECT" { + rejectHTTP(writer, http.StatusBadRequest) + n.badRequest(ctx, request, E.New("not CONNECT request")) + return + } else if request.Header.Get("Padding") == "" { + rejectHTTP(writer, http.StatusBadRequest) + n.badRequest(ctx, request, E.New("missing naive padding")) + return + } + userName, password, authOk := sHttp.ParseBasicAuth(request.Header.Get("Proxy-Authorization")) + if authOk { + authOk = n.authenticator.Verify(userName, password) + } + if !authOk { + rejectHTTP(writer, http.StatusProxyAuthRequired) + n.badRequest(ctx, request, E.New("authorization failed")) + return + } + writer.Header().Set("Padding", generateNaivePaddingHeader()) + writer.WriteHeader(http.StatusOK) + writer.(http.Flusher).Flush() + + hostPort := request.URL.Host + if hostPort == "" { + hostPort = request.Host + } + source := sHttp.SourceAddress(request) + destination := M.ParseSocksaddr(hostPort) + + if hijacker, isHijacker := writer.(http.Hijacker); isHijacker { + conn, _, err := hijacker.Hijack() + if err != nil { + n.badRequest(ctx, request, E.New("hijack failed")) + return + } + n.newConnection(ctx, false, &naiveH1Conn{Conn: conn}, userName, source, destination) + } else { + n.newConnection(ctx, true, &naiveH2Conn{reader: request.Body, writer: writer, flusher: writer.(http.Flusher)}, userName, source, destination) + } +} + +func (n *Inbound) newConnection(ctx context.Context, waitForClose bool, conn net.Conn, userName string, source M.Socksaddr, destination M.Socksaddr) { + if userName != "" { + n.logger.InfoContext(ctx, "[", userName, "] inbound connection from ", source) + n.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", destination) + } else { + n.logger.InfoContext(ctx, "inbound connection from ", source) + n.logger.InfoContext(ctx, "inbound connection to ", destination) + } + var metadata adapter.InboundContext + metadata.Inbound = n.Tag() + metadata.InboundType = n.Type() + metadata.InboundDetour = n.listener.ListenOptions().Detour + metadata.InboundOptions = n.listener.ListenOptions().InboundOptions + metadata.Source = source + metadata.Destination = destination + metadata.OriginDestination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() + metadata.User = userName + if !waitForClose { + n.router.RouteConnectionEx(ctx, conn, metadata, nil) + } else { + done := make(chan struct{}) + wrapper := v2rayhttp.NewHTTP2Wrapper(conn) + n.router.RouteConnectionEx(ctx, conn, metadata, N.OnceClose(func(it error) { + close(done) + })) + <-done + wrapper.CloseWrapper() + } +} + +func (n *Inbound) badRequest(ctx context.Context, request *http.Request, err error) { + n.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", request.RemoteAddr)) +} + +func rejectHTTP(writer http.ResponseWriter, statusCode int) { + hijacker, ok := writer.(http.Hijacker) + if !ok { + writer.WriteHeader(statusCode) + return + } + conn, _, err := hijacker.Hijack() + if err != nil { + writer.WriteHeader(statusCode) + return + } + if tcpConn, isTCP := common.Cast[*net.TCPConn](conn); isTCP { + tcpConn.SetLinger(0) + } + conn.Close() +} + +func generateNaivePaddingHeader() string { + paddingLen := rand.Intn(32) + 30 + padding := make([]byte, paddingLen) + bits := rand.Uint64() + for i := 0; i < 16; i++ { + // Codes that won't be Huffman coded. + padding[i] = "!#$()+<>?@[]^`{}"[bits&15] + bits >>= 4 + } + for i := 16; i < paddingLen; i++ { + padding[i] = '~' + } + return string(padding) +} diff --git a/inbound/naive.go b/protocol/naive/inbound_conn.go similarity index 58% rename from inbound/naive.go rename to protocol/naive/inbound_conn.go index 498e823cec..16944cbaff 100644 --- a/inbound/naive.go +++ b/protocol/naive/inbound_conn.go @@ -1,7 +1,6 @@ -package inbound +package naive import ( - "context" "encoding/binary" "io" "math/rand" @@ -11,228 +10,12 @@ import ( "strings" "time" - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/tls" - "github.com/sagernet/sing-box/common/uot" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/log" - "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing-box/transport/v2rayhttp" "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/auth" "github.com/sagernet/sing/common/buf" - E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/rw" - sHttp "github.com/sagernet/sing/protocol/http" ) -var _ adapter.Inbound = (*Naive)(nil) - -type Naive struct { - myInboundAdapter - authenticator *auth.Authenticator - tlsConfig tls.ServerConfig - httpServer *http.Server - h3Server any -} - -func NewNaive(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.NaiveInboundOptions) (*Naive, error) { - inbound := &Naive{ - myInboundAdapter: myInboundAdapter{ - protocol: C.TypeNaive, - network: options.Network.Build(), - ctx: ctx, - router: uot.NewRouter(router, logger), - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - }, - authenticator: auth.NewAuthenticator(options.Users), - } - if common.Contains(inbound.network, N.NetworkUDP) { - if options.TLS == nil || !options.TLS.Enabled { - return nil, E.New("TLS is required for QUIC server") - } - } - if len(options.Users) == 0 { - return nil, E.New("missing users") - } - if options.TLS != nil { - tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) - if err != nil { - return nil, err - } - inbound.tlsConfig = tlsConfig - } - return inbound, nil -} - -func (n *Naive) Start() error { - var tlsConfig *tls.STDConfig - if n.tlsConfig != nil { - err := n.tlsConfig.Start() - if err != nil { - return E.Cause(err, "create TLS config") - } - tlsConfig, err = n.tlsConfig.Config() - if err != nil { - return err - } - } - - if common.Contains(n.network, N.NetworkTCP) { - tcpListener, err := n.ListenTCP() - if err != nil { - return err - } - n.httpServer = &http.Server{ - Handler: n, - TLSConfig: tlsConfig, - BaseContext: func(listener net.Listener) context.Context { - return n.ctx - }, - } - go func() { - var sErr error - if tlsConfig != nil { - sErr = n.httpServer.ServeTLS(tcpListener, "", "") - } else { - sErr = n.httpServer.Serve(tcpListener) - } - if sErr != nil && !E.IsClosedOrCanceled(sErr) { - n.logger.Error("http server serve error: ", sErr) - } - }() - } - - if common.Contains(n.network, N.NetworkUDP) { - err := n.configureHTTP3Listener() - if !C.WithQUIC && len(n.network) > 1 { - n.logger.Warn(E.Cause(err, "naive http3 disabled")) - } else if err != nil { - return err - } - } - - return nil -} - -func (n *Naive) Close() error { - return common.Close( - &n.myInboundAdapter, - common.PtrOrNil(n.httpServer), - n.h3Server, - n.tlsConfig, - ) -} - -func (n *Naive) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - ctx := log.ContextWithNewID(request.Context()) - if request.Method != "CONNECT" { - rejectHTTP(writer, http.StatusBadRequest) - n.badRequest(ctx, request, E.New("not CONNECT request")) - return - } else if request.Header.Get("Padding") == "" { - rejectHTTP(writer, http.StatusBadRequest) - n.badRequest(ctx, request, E.New("missing naive padding")) - return - } - userName, password, authOk := sHttp.ParseBasicAuth(request.Header.Get("Proxy-Authorization")) - if authOk { - authOk = n.authenticator.Verify(userName, password) - } - if !authOk { - rejectHTTP(writer, http.StatusProxyAuthRequired) - n.badRequest(ctx, request, E.New("authorization failed")) - return - } - writer.Header().Set("Padding", generateNaivePaddingHeader()) - writer.WriteHeader(http.StatusOK) - writer.(http.Flusher).Flush() - - hostPort := request.URL.Host - if hostPort == "" { - hostPort = request.Host - } - source := sHttp.SourceAddress(request) - destination := M.ParseSocksaddr(hostPort) - - if hijacker, isHijacker := writer.(http.Hijacker); isHijacker { - conn, _, err := hijacker.Hijack() - if err != nil { - n.badRequest(ctx, request, E.New("hijack failed")) - return - } - n.newConnection(ctx, false, &naiveH1Conn{Conn: conn}, userName, source, destination) - } else { - n.newConnection(ctx, true, &naiveH2Conn{reader: request.Body, writer: writer, flusher: writer.(http.Flusher)}, userName, source, destination) - } -} - -func (n *Naive) newConnection(ctx context.Context, waitForClose bool, conn net.Conn, userName string, source M.Socksaddr, destination M.Socksaddr) { - if userName != "" { - n.logger.InfoContext(ctx, "[", userName, "] inbound connection from ", source) - n.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", destination) - } else { - n.logger.InfoContext(ctx, "inbound connection from ", source) - n.logger.InfoContext(ctx, "inbound connection to ", destination) - } - metadata := n.createMetadata(conn, adapter.InboundContext{ - Source: source, - Destination: destination, - User: userName, - }) - if !waitForClose { - n.router.RouteConnectionEx(ctx, conn, metadata, nil) - } else { - done := make(chan struct{}) - wrapper := v2rayhttp.NewHTTP2Wrapper(conn) - n.router.RouteConnectionEx(ctx, conn, metadata, N.OnceClose(func(it error) { - close(done) - })) - <-done - wrapper.CloseWrapper() - } -} - -func (n *Naive) badRequest(ctx context.Context, request *http.Request, err error) { - n.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", request.RemoteAddr)) -} - -func rejectHTTP(writer http.ResponseWriter, statusCode int) { - hijacker, ok := writer.(http.Hijacker) - if !ok { - writer.WriteHeader(statusCode) - return - } - conn, _, err := hijacker.Hijack() - if err != nil { - writer.WriteHeader(statusCode) - return - } - if tcpConn, isTCP := common.Cast[*net.TCPConn](conn); isTCP { - tcpConn.SetLinger(0) - } - conn.Close() -} - -func generateNaivePaddingHeader() string { - paddingLen := rand.Intn(32) + 30 - padding := make([]byte, paddingLen) - bits := rand.Uint64() - for i := 0; i < 16; i++ { - // Codes that won't be Huffman coded. - padding[i] = "!#$()+<>?@[]^`{}"[bits&15] - bits >>= 4 - } - for i := 16; i < paddingLen; i++ { - padding[i] = '~' - } - return string(padding) -} - const kFirstPaddings = 8 type naiveH1Conn struct { diff --git a/protocol/naive/quic/inbound_init.go b/protocol/naive/quic/inbound_init.go new file mode 100644 index 0000000000..f495c860f8 --- /dev/null +++ b/protocol/naive/quic/inbound_init.go @@ -0,0 +1,52 @@ +package quic + +import ( + "io" + "net/http" + + "github.com/sagernet/quic-go" + "github.com/sagernet/quic-go/http3" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/tls" + "github.com/sagernet/sing-box/protocol/naive" + "github.com/sagernet/sing-quic" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" +) + +func init() { + naive.ConfigureHTTP3ListenerFunc = func(listener *listener.Listener, handler http.Handler, tlsConfig tls.ServerConfig, logger logger.Logger) (io.Closer, error) { + err := qtls.ConfigureHTTP3(tlsConfig) + if err != nil { + return nil, err + } + + udpConn, err := listener.ListenUDP() + if err != nil { + return nil, err + } + + quicListener, err := qtls.ListenEarly(udpConn, tlsConfig, &quic.Config{ + MaxIncomingStreams: 1 << 60, + Allow0RTT: true, + }) + if err != nil { + udpConn.Close() + return nil, err + } + + h3Server := &http3.Server{ + Handler: handler, + } + + go func() { + sErr := h3Server.ServeListener(quicListener) + udpConn.Close() + if sErr != nil && !E.IsClosedOrCanceled(sErr) { + logger.Error("http3 server closed: ", sErr) + } + }() + + return quicListener, nil + } +} diff --git a/protocol/redirect/redirect.go b/protocol/redirect/redirect.go new file mode 100644 index 0000000000..71e1fced1a --- /dev/null +++ b/protocol/redirect/redirect.go @@ -0,0 +1,65 @@ +package redirect + +import ( + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/redir" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func RegisterRedirect(registry *inbound.Registry) { + inbound.Register[option.RedirectInboundOptions](registry, C.TypeRedirect, NewRedirect) +} + +type Redirect struct { + inbound.Adapter + router adapter.Router + logger log.ContextLogger + listener *listener.Listener +} + +func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.RedirectInboundOptions) (adapter.Inbound, error) { + redirect := &Redirect{ + Adapter: inbound.NewAdapter(C.TypeRedirect, tag), + router: router, + logger: logger, + } + redirect.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: redirect, + }) + return redirect, nil +} + +func (h *Redirect) Start() error { + return h.listener.Start() +} + +func (h *Redirect) Close() error { + return h.listener.Close() +} + +func (h *Redirect) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + destination, err := redir.GetOriginalDestination(conn) + if err != nil { + conn.Close() + h.logger.ErrorContext(ctx, "process connection from ", conn.RemoteAddr(), ": get redirect destination: ", err) + return + } + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.Destination = M.SocksaddrFromNetIP(destination) + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/inbound/tproxy.go b/protocol/redirect/tproxy.go similarity index 60% rename from inbound/tproxy.go rename to protocol/redirect/tproxy.go index 40653c797b..dee40ec5a8 100644 --- a/inbound/tproxy.go +++ b/protocol/redirect/tproxy.go @@ -1,4 +1,4 @@ -package inbound +package redirect import ( "context" @@ -8,6 +8,8 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/redir" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" @@ -21,22 +23,25 @@ import ( "github.com/sagernet/sing/common/udpnat2" ) +func RegisterTProxy(registry *inbound.Registry) { + inbound.Register[option.TProxyInboundOptions](registry, C.TypeTProxy, NewTProxy) +} + type TProxy struct { - myInboundAdapter - udpNat *udpnat.Service + inbound.Adapter + ctx context.Context + router adapter.Router + logger log.ContextLogger + listener *listener.Listener + udpNat *udpnat.Service } -func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TProxyInboundOptions) *TProxy { +func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TProxyInboundOptions) (adapter.Inbound, error) { tproxy := &TProxy{ - myInboundAdapter: myInboundAdapter{ - protocol: C.TypeTProxy, - network: options.Network.Build(), - ctx: ctx, - router: router, - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - }, + Adapter: inbound.NewAdapter(C.TypeTProxy, tag), + ctx: ctx, + router: router, + logger: logger, } var udpTimeout time.Duration if options.UDPTimeout != 0 { @@ -44,28 +49,34 @@ func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLog } else { udpTimeout = C.UDPTimeout } - tproxy.connHandler = tproxy - tproxy.oobPacketHandler = tproxy tproxy.udpNat = udpnat.New(tproxy, tproxy.preparePacketConnection, udpTimeout, false) - return tproxy + tproxy.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: options.Network.Build(), + Listen: options.ListenOptions, + ConnectionHandler: tproxy, + OOBPacketHandler: tproxy, + }) + return tproxy, nil } func (t *TProxy) Start() error { - err := t.myInboundAdapter.Start() + err := t.listener.Start() if err != nil { return err } - if t.tcpListener != nil { - err = control.Conn(common.MustCast[syscall.Conn](t.tcpListener), func(fd uintptr) error { - return redir.TProxy(fd, M.SocksaddrFromNet(t.tcpListener.Addr()).Addr.Is6()) + if listener := t.listener.TCPListener(); listener != nil { + err = control.Conn(common.MustCast[syscall.Conn](listener), func(fd uintptr) error { + return redir.TProxy(fd, M.SocksaddrFromNet(listener.Addr()).Addr.Is6()) }) if err != nil { return E.Cause(err, "configure tproxy TCP listener") } } - if t.udpConn != nil { - err = control.Conn(t.udpConn, func(fd uintptr) error { - return redir.TProxy(fd, M.SocksaddrFromNet(t.udpConn.LocalAddr()).Addr.Is6()) + if conn := t.listener.UDPConn(); conn != nil { + err = control.Conn(conn, func(fd uintptr) error { + return redir.TProxy(fd, M.SocksaddrFromNet(conn.LocalAddr()).Addr.Is6()) }) if err != nil { return E.Cause(err, "configure tproxy UDP listener") @@ -74,13 +85,26 @@ func (t *TProxy) Start() error { return nil } +func (t *TProxy) Close() error { + return t.listener.Close() +} + func (t *TProxy) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() - t.newConnectionEx(ctx, conn, metadata, onClose) + t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + t.router.RouteConnectionEx(ctx, conn, metadata, onClose) } func (t *TProxy) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { - t.newPacketConnectionEx(ctx, conn, t.createPacketMetadataEx(source, destination), onClose) + t.logger.InfoContext(ctx, "inbound packet connection from ", source) + t.logger.InfoContext(ctx, "inbound packet connection to ", destination) + var metadata adapter.InboundContext + metadata.Inbound = t.Tag() + metadata.InboundType = t.Type() + metadata.Source = source + metadata.Destination = destination + metadata.OriginDestination = t.listener.UDPAddr() + t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } func (t *TProxy) NewPacketEx(buffer *buf.Buffer, oob []byte, source M.Socksaddr) { @@ -100,8 +124,9 @@ type tproxyPacketWriter struct { } func (t *TProxy) preparePacketConnection(source M.Socksaddr, destination M.Socksaddr, userData any) (bool, context.Context, N.PacketWriter, N.CloseHandlerFunc) { - writer := &tproxyPacketWriter{ctx: t.ctx, source: source.AddrPort(), destination: destination} - return true, t.ctx, writer, func(it error) { + ctx := log.ContextWithNewID(t.ctx) + writer := &tproxyPacketWriter{ctx: ctx, source: source.AddrPort(), destination: destination} + return true, ctx, writer, func(it error) { common.Close(common.PtrOrNil(writer.conn)) } } diff --git a/protocol/shadowsocks/inbound.go b/protocol/shadowsocks/inbound.go new file mode 100644 index 0000000000..b23516d995 --- /dev/null +++ b/protocol/shadowsocks/inbound.go @@ -0,0 +1,179 @@ +package shadowsocks + +import ( + "context" + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/mux" + "github.com/sagernet/sing-box/common/uot" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-shadowsocks" + "github.com/sagernet/sing-shadowsocks/shadowaead" + "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/ntp" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocks, NewInbound) +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) { + if len(options.Users) > 0 && len(options.Destinations) > 0 { + return nil, E.New("users and destinations options must not be combined") + } + if len(options.Users) > 0 { + return newMultiInbound(ctx, router, logger, tag, options) + } else if len(options.Destinations) > 0 { + return newRelayInbound(ctx, router, logger, tag, options) + } else { + return newInbound(ctx, router, logger, tag, options) + } +} + +var _ adapter.TCPInjectableInbound = (*Inbound)(nil) + +type Inbound struct { + inbound.Adapter + ctx context.Context + router adapter.ConnectionRouterEx + logger logger.ContextLogger + listener *listener.Listener + service shadowsocks.Service +} + +func newInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeShadowsocks, tag), + ctx: ctx, + router: uot.NewRouter(router, logger), + logger: logger, + } + var err error + inbound.router, err = mux.NewRouterWithOptions(router, logger, common.PtrValueOrDefault(options.Multiplex)) + if err != nil { + return nil, err + } + var udpTimeout time.Duration + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } else { + udpTimeout = C.UDPTimeout + } + switch { + case options.Method == shadowsocks.MethodNone: + inbound.service = shadowsocks.NewNoneService(int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound)) + case common.Contains(shadowaead.List, options.Method): + inbound.service, err = shadowaead.NewService(options.Method, nil, options.Password, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound)) + case common.Contains(shadowaead_2022.List, options.Method): + inbound.service, err = shadowaead_2022.NewServiceWithPassword(options.Method, options.Password, int64(udpTimeout.Seconds()), adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, inbound), ntp.TimeFuncFromContext(ctx)) + default: + err = E.New("unsupported method: ", options.Method) + } + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: options.Network.Build(), + Listen: options.ListenOptions, + ConnectionHandler: inbound, + PacketHandler: inbound, + ThreadUnsafePacketWriter: true, + }) + return inbound, err +} + +func (h *Inbound) Start() error { + return h.listener.Start() +} + +func (h *Inbound) Close() error { + return h.listener.Close() +} + +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } +} + +func (h *Inbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { + err := h.service.NewPacket(h.ctx, &stubPacketConn{h.listener.PacketWriter()}, buffer, M.Metadata{Source: source}) + if err != nil { + h.logger.Error(E.Cause(err, "process packet from ", source)) + } +} + +func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + return h.router.RouteConnection(ctx, conn, metadata) +} + +func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + ctx = log.ContextWithNewID(ctx) + h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions + return h.router.RoutePacketConnection(ctx, conn, metadata) +} + +var _ N.PacketConn = (*stubPacketConn)(nil) + +type stubPacketConn struct { + N.PacketWriter +} + +func (c *stubPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) { + panic("stub!") +} + +func (c *stubPacketConn) Close() error { + return nil +} + +func (c *stubPacketConn) LocalAddr() net.Addr { + panic("stub!") +} + +func (c *stubPacketConn) SetDeadline(t time.Time) error { + panic("stub!") +} + +func (c *stubPacketConn) SetReadDeadline(t time.Time) error { + panic("stub!") +} + +func (c *stubPacketConn) SetWriteDeadline(t time.Time) error { + panic("stub!") +} + +func (h *Inbound) NewError(ctx context.Context, err error) { + NewError(h.logger, ctx, err) +} + +// Deprecated: remove +func NewError(logger logger.ContextLogger, ctx context.Context, err error) { + common.Close(err) + if E.IsClosedOrCanceled(err) { + logger.DebugContext(ctx, "connection closed: ", err) + return + } + logger.ErrorContext(ctx, err) +} diff --git a/inbound/shadowsocks_multi.go b/protocol/shadowsocks/inbound_multi.go similarity index 59% rename from inbound/shadowsocks_multi.go rename to protocol/shadowsocks/inbound_multi.go index 2953419490..0e1efedfc2 100644 --- a/inbound/shadowsocks_multi.go +++ b/protocol/shadowsocks/inbound_multi.go @@ -1,4 +1,4 @@ -package inbound +package shadowsocks import ( "context" @@ -7,6 +7,8 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" @@ -20,36 +22,31 @@ import ( "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ntp" ) -var ( - _ adapter.Inbound = (*ShadowsocksMulti)(nil) - _ adapter.TCPInjectableInbound = (*ShadowsocksMulti)(nil) -) +var _ adapter.TCPInjectableInbound = (*MultiInbound)(nil) -type ShadowsocksMulti struct { - myInboundAdapter - service shadowsocks.MultiService[int] - users []option.ShadowsocksUser +type MultiInbound struct { + inbound.Adapter + ctx context.Context + router adapter.ConnectionRouterEx + logger logger.ContextLogger + listener *listener.Listener + service shadowsocks.MultiService[int] + users []option.ShadowsocksUser } -func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*ShadowsocksMulti, error) { - inbound := &ShadowsocksMulti{ - myInboundAdapter: myInboundAdapter{ - protocol: C.TypeShadowsocks, - network: options.Network.Build(), - ctx: ctx, - router: uot.NewRouter(router, logger), - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - }, +func newMultiInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*MultiInbound, error) { + inbound := &MultiInbound{ + Adapter: inbound.NewAdapter(C.TypeShadowsocks, tag), + ctx: ctx, + router: uot.NewRouter(router, logger), + logger: logger, } - inbound.connHandler = inbound - inbound.packetHandler = inbound var err error inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { @@ -91,12 +88,28 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log. return nil, err } inbound.service = service - inbound.packetUpstream = service inbound.users = options.Users + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: options.Network.Build(), + Listen: options.ListenOptions, + ConnectionHandler: inbound, + PacketHandler: inbound, + ThreadUnsafePacketWriter: true, + }) return inbound, err } -func (h *ShadowsocksMulti) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *MultiInbound) Start() error { + return h.listener.Start() +} + +func (h *MultiInbound) Close() error { + return h.listener.Close() +} + +func (h *MultiInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { @@ -104,14 +117,14 @@ func (h *ShadowsocksMulti) NewConnectionEx(ctx context.Context, conn net.Conn, m } } -func (h *ShadowsocksMulti) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { - err := h.service.NewPacket(h.ctx, h.packetConn(), buffer, M.Metadata{Source: source}) +func (h *MultiInbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { + err := h.service.NewPacket(h.ctx, &stubPacketConn{h.listener.PacketWriter()}, buffer, M.Metadata{Source: source}) if err != nil { h.logger.Error(E.Cause(err, "process packet from ", source)) } } -func (h *ShadowsocksMulti) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *MultiInbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid @@ -123,10 +136,12 @@ func (h *ShadowsocksMulti) newConnection(ctx context.Context, conn net.Conn, met metadata.User = user } h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) - return h.router.RouteConnection(ctx, conn, h.createMetadata(conn, metadata)) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + return h.router.RouteConnection(ctx, conn, metadata) } -func (h *ShadowsocksMulti) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (h *MultiInbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid @@ -140,5 +155,13 @@ func (h *ShadowsocksMulti) newPacketConnection(ctx context.Context, conn N.Packe ctx = log.ContextWithNewID(ctx) h.logger.InfoContext(ctx, "[", user, "] inbound packet connection from ", metadata.Source) h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) - return h.router.RoutePacketConnection(ctx, conn, h.createPacketMetadata(conn, metadata)) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions + return h.router.RoutePacketConnection(ctx, conn, metadata) +} + +func (h *MultiInbound) NewError(ctx context.Context, err error) { + NewError(h.logger, ctx, err) } diff --git a/inbound/shadowsocks_relay.go b/protocol/shadowsocks/inbound_relay.go similarity index 57% rename from inbound/shadowsocks_relay.go rename to protocol/shadowsocks/inbound_relay.go index 02246a3f3c..5818ca29f1 100644 --- a/inbound/shadowsocks_relay.go +++ b/protocol/shadowsocks/inbound_relay.go @@ -1,4 +1,4 @@ -package inbound +package shadowsocks import ( "context" @@ -7,6 +7,8 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" @@ -18,36 +20,31 @@ import ( "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) -var ( - _ adapter.Inbound = (*ShadowsocksRelay)(nil) - _ adapter.TCPInjectableInbound = (*ShadowsocksRelay)(nil) -) +var _ adapter.TCPInjectableInbound = (*RelayInbound)(nil) -type ShadowsocksRelay struct { - myInboundAdapter +type RelayInbound struct { + inbound.Adapter + ctx context.Context + router adapter.ConnectionRouterEx + logger logger.ContextLogger + listener *listener.Listener service *shadowaead_2022.RelayService[int] destinations []option.ShadowsocksDestination } -func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*ShadowsocksRelay, error) { - inbound := &ShadowsocksRelay{ - myInboundAdapter: myInboundAdapter{ - protocol: C.TypeShadowsocks, - network: options.Network.Build(), - ctx: ctx, - router: uot.NewRouter(router, logger), - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - }, +func newRelayInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (*RelayInbound, error) { + inbound := &RelayInbound{ + Adapter: inbound.NewAdapter(C.TypeShadowsocks, tag), + ctx: ctx, + router: uot.NewRouter(router, logger), + logger: logger, destinations: options.Destinations, } - inbound.connHandler = inbound - inbound.packetHandler = inbound var err error inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) if err != nil { @@ -77,11 +74,27 @@ func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log. return nil, err } inbound.service = service - inbound.packetUpstream = service + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: options.Network.Build(), + Listen: options.ListenOptions, + ConnectionHandler: inbound, + PacketHandler: inbound, + ThreadUnsafePacketWriter: true, + }) return inbound, err } -func (h *ShadowsocksRelay) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *RelayInbound) Start() error { + return h.listener.Start() +} + +func (h *RelayInbound) Close() error { + return h.listener.Close() +} + +func (h *RelayInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { @@ -89,14 +102,14 @@ func (h *ShadowsocksRelay) NewConnectionEx(ctx context.Context, conn net.Conn, m } } -func (h *ShadowsocksRelay) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { - err := h.service.NewPacket(h.ctx, h.packetConn(), buffer, M.Metadata{Source: source}) +func (h *RelayInbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { + err := h.service.NewPacket(h.ctx, &stubPacketConn{h.listener.PacketWriter()}, buffer, M.Metadata{Source: source}) if err != nil { h.logger.Error(E.Cause(err, "process packet from ", source)) } } -func (h *ShadowsocksRelay) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *RelayInbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { destinationIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid @@ -108,10 +121,12 @@ func (h *ShadowsocksRelay) newConnection(ctx context.Context, conn net.Conn, met metadata.User = destination } h.logger.InfoContext(ctx, "[", destination, "] inbound connection to ", metadata.Destination) - return h.router.RouteConnection(ctx, conn, h.createMetadata(conn, metadata)) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + return h.router.RouteConnection(ctx, conn, metadata) } -func (h *ShadowsocksRelay) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (h *RelayInbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { destinationIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid @@ -125,5 +140,13 @@ func (h *ShadowsocksRelay) newPacketConnection(ctx context.Context, conn N.Packe ctx = log.ContextWithNewID(ctx) h.logger.InfoContext(ctx, "[", destination, "] inbound packet connection from ", metadata.Source) h.logger.InfoContext(ctx, "[", destination, "] inbound packet connection to ", metadata.Destination) - return h.router.RoutePacketConnection(ctx, conn, h.createPacketMetadata(conn, metadata)) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions + return h.router.RoutePacketConnection(ctx, conn, metadata) +} + +func (h *RelayInbound) NewError(ctx context.Context, err error) { + NewError(h.logger, ctx, err) } diff --git a/outbound/shadowsocks.go b/protocol/shadowsocks/outbound.go similarity index 80% rename from outbound/shadowsocks.go rename to protocol/shadowsocks/outbound.go index 153542742a..73b3838503 100644 --- a/outbound/shadowsocks.go +++ b/protocol/shadowsocks/outbound.go @@ -1,10 +1,11 @@ -package outbound +package shadowsocks import ( "context" "net" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/mux" C "github.com/sagernet/sing-box/constant" @@ -15,15 +16,19 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/uot" ) -var _ adapter.Outbound = (*Shadowsocks)(nil) +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.ShadowsocksOutboundOptions](registry, C.TypeShadowsocks, NewOutbound) +} -type Shadowsocks struct { - myOutboundAdapter +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger dialer N.Dialer method shadowsocks.Method serverAddr M.Socksaddr @@ -32,7 +37,7 @@ type Shadowsocks struct { multiplexDialer *mux.Client } -func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksOutboundOptions) (*Shadowsocks, error) { +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksOutboundOptions) (adapter.Outbound, error) { method, err := shadowsocks.CreateMethod(ctx, options.Method, shadowsocks.MethodOptions{ Password: options.Password, }) @@ -43,15 +48,9 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte if err != nil { return nil, err } - outbound := &Shadowsocks{ - myOutboundAdapter: myOutboundAdapter{ - protocol: C.TypeShadowsocks, - network: options.Network.Build(), - router: router, - logger: logger, - tag: tag, - dependencies: withDialerDependency(options.DialerOptions), - }, + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeShadowsocks, options.Network.Build(), tag, options.DialerOptions), + logger: logger, dialer: outboundDialer, method: method, serverAddr: options.ServerOptions.Build(), @@ -78,9 +77,9 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte return outbound, nil } -func (h *Shadowsocks) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) - metadata.Outbound = h.tag + metadata.Outbound = h.Tag() metadata.Destination = destination if h.multiplexDialer == nil { switch N.NetworkName(network) { @@ -106,9 +105,9 @@ func (h *Shadowsocks) DialContext(ctx context.Context, network string, destinati } } -func (h *Shadowsocks) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { ctx, metadata := adapter.ExtendContext(ctx) - metadata.Outbound = h.tag + metadata.Outbound = h.Tag() metadata.Destination = destination if h.multiplexDialer == nil { if h.uotClient != nil { @@ -125,24 +124,24 @@ func (h *Shadowsocks) ListenPacket(ctx context.Context, destination M.Socksaddr) } } -func (h *Shadowsocks) InterfaceUpdated() { +func (h *Outbound) InterfaceUpdated() { if h.multiplexDialer != nil { h.multiplexDialer.Reset() } return } -func (h *Shadowsocks) Close() error { +func (h *Outbound) Close() error { return common.Close(common.PtrOrNil(h.multiplexDialer)) } var _ N.Dialer = (*shadowsocksDialer)(nil) -type shadowsocksDialer Shadowsocks +type shadowsocksDialer Outbound func (h *shadowsocksDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) - metadata.Outbound = h.tag + metadata.Outbound = h.Tag() metadata.Destination = destination switch N.NetworkName(network) { case N.NetworkTCP: @@ -170,7 +169,7 @@ func (h *shadowsocksDialer) DialContext(ctx context.Context, network string, des func (h *shadowsocksDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { ctx, metadata := adapter.ExtendContext(ctx) - metadata.Outbound = h.tag + metadata.Outbound = h.Tag() metadata.Destination = destination outConn, err := h.dialer.DialContext(ctx, N.NetworkUDP, h.serverAddr) if err != nil { diff --git a/inbound/shadowtls.go b/protocol/shadowtls/inbound.go similarity index 62% rename from inbound/shadowtls.go rename to protocol/shadowtls/inbound.go index ca14228635..6887e838a5 100644 --- a/inbound/shadowtls.go +++ b/protocol/shadowtls/inbound.go @@ -1,11 +1,13 @@ -package inbound +package shadowtls import ( "context" "net" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing-box/common/listener" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -13,25 +15,27 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" N "github.com/sagernet/sing/common/network" ) -type ShadowTLS struct { - myInboundAdapter - service *shadowtls.Service +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.ShadowTLSInboundOptions](registry, C.TypeShadowTLS, NewInbound) } -func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSInboundOptions) (*ShadowTLS, error) { - inbound := &ShadowTLS{ - myInboundAdapter: myInboundAdapter{ - protocol: C.TypeShadowTLS, - network: []string{N.NetworkTCP}, - ctx: ctx, - router: router, - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - }, +type Inbound struct { + inbound.Adapter + router adapter.Router + logger logger.ContextLogger + listener *listener.Listener + service *shadowtls.Service +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSInboundOptions) (adapter.Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeShadowTLS, tag), + router: router, + logger: logger, } if options.Version == 0 { @@ -68,22 +72,36 @@ func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.Context }, HandshakeForServerName: handshakeForServerName, StrictMode: options.StrictMode, - Handler: adapter.NewUpstreamContextHandler(inbound.newConnection, nil, inbound), + Handler: adapter.NewUpstreamContextHandler(inbound.newConnection, nil, nil), Logger: logger, }) if err != nil { return nil, err } inbound.service = service - inbound.connHandler = inbound + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: inbound, + }) return inbound, nil } -func (h *ShadowTLS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) Start() error { + return h.listener.Start() +} + +func (h *Inbound) Close() error { + return h.listener.Close() +} + +func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata)) } -func (h *ShadowTLS) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if userName, _ := auth.UserFromContext[string](ctx); userName != "" { metadata.User = userName h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination) @@ -93,7 +111,7 @@ func (h *ShadowTLS) newConnection(ctx context.Context, conn net.Conn, metadata a return h.router.RouteConnection(ctx, conn, metadata) } -func (h *ShadowTLS) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := h.NewConnection(ctx, conn, metadata) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { diff --git a/outbound/shadowtls.go b/protocol/shadowtls/outbound.go similarity index 74% rename from outbound/shadowtls.go rename to protocol/shadowtls/outbound.go index ff1b9d6c45..7d46a8f682 100644 --- a/outbound/shadowtls.go +++ b/protocol/shadowtls/outbound.go @@ -1,4 +1,4 @@ -package outbound +package shadowtls import ( "context" @@ -6,6 +6,7 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" @@ -17,23 +18,18 @@ import ( N "github.com/sagernet/sing/common/network" ) -var _ adapter.Outbound = (*ShadowTLS)(nil) +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.ShadowTLSOutboundOptions](registry, C.TypeShadowTLS, NewOutbound) +} -type ShadowTLS struct { - myOutboundAdapter +type Outbound struct { + outbound.Adapter client *shadowtls.Client } -func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSOutboundOptions) (*ShadowTLS, error) { - outbound := &ShadowTLS{ - myOutboundAdapter: myOutboundAdapter{ - protocol: C.TypeShadowTLS, - network: []string{N.NetworkTCP}, - router: router, - logger: logger, - tag: tag, - dependencies: withDialerDependency(options.DialerOptions), - }, +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSOutboundOptions) (adapter.Outbound, error) { + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeShadowTLS, []string{N.NetworkTCP}, tag, options.DialerOptions), } if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired @@ -91,9 +87,9 @@ func NewShadowTLS(ctx context.Context, router adapter.Router, logger log.Context return outbound, nil } -func (h *ShadowTLS) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) - metadata.Outbound = h.tag + metadata.Outbound = h.Tag() metadata.Destination = destination switch N.NetworkName(network) { case N.NetworkTCP: @@ -103,6 +99,6 @@ func (h *ShadowTLS) DialContext(ctx context.Context, network string, destination } } -func (h *ShadowTLS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return nil, os.ErrInvalid } diff --git a/protocol/socks/inbound.go b/protocol/socks/inbound.go new file mode 100644 index 0000000000..29649a88df --- /dev/null +++ b/protocol/socks/inbound.go @@ -0,0 +1,91 @@ +package socks + +import ( + std_bufio "bufio" + "context" + "net" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" + "github.com/sagernet/sing-box/common/uot" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/auth" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/protocol/socks" +) + +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.SocksInboundOptions](registry, C.TypeSOCKS, NewInbound) +} + +var _ adapter.TCPInjectableInbound = (*Inbound)(nil) + +type Inbound struct { + inbound.Adapter + router adapter.ConnectionRouterEx + logger logger.ContextLogger + listener *listener.Listener + authenticator *auth.Authenticator +} + +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SocksInboundOptions) (adapter.Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeSOCKS, tag), + router: uot.NewRouter(router, logger), + logger: logger, + authenticator: auth.NewAuthenticator(options.Users), + } + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: inbound, + }) + return inbound, nil +} + +func (h *Inbound) Start() error { + return h.listener.Start() +} + +func (h *Inbound) Close() error { + return h.listener.Close() +} + +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, metadata.Destination, onClose) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } +} + +func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + user, loaded := auth.UserFromContext[string](ctx) + if !loaded { + h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) + return + } + metadata.User = user + h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + user, loaded := auth.UserFromContext[string](ctx) + if !loaded { + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) + return + } + metadata.User = user + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} diff --git a/outbound/socks.go b/protocol/socks/outbound.go similarity index 65% rename from outbound/socks.go rename to protocol/socks/outbound.go index 575d6eb3dd..0194800a7c 100644 --- a/outbound/socks.go +++ b/protocol/socks/outbound.go @@ -1,10 +1,11 @@ -package outbound +package socks import ( "context" "net" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" @@ -12,22 +13,29 @@ import ( "github.com/sagernet/sing-dns" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/uot" "github.com/sagernet/sing/protocol/socks" ) -var _ adapter.Outbound = (*Socks)(nil) +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.SOCKSOutboundOptions](registry, C.TypeSOCKS, NewOutbound) +} + +var _ adapter.Outbound = (*Outbound)(nil) -type Socks struct { - myOutboundAdapter +type Outbound struct { + outbound.Adapter + router adapter.Router + logger logger.ContextLogger client *socks.Client resolve bool uotClient *uot.Client } -func NewSocks(router adapter.Router, logger log.ContextLogger, tag string, options option.SocksOutboundOptions) (*Socks, error) { +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SOCKSOutboundOptions) (adapter.Outbound, error) { var version socks.Version var err error if options.Version != "" { @@ -42,15 +50,10 @@ func NewSocks(router adapter.Router, logger log.ContextLogger, tag string, optio if err != nil { return nil, err } - outbound := &Socks{ - myOutboundAdapter: myOutboundAdapter{ - protocol: C.TypeSOCKS, - network: options.Network.Build(), - router: router, - logger: logger, - tag: tag, - dependencies: withDialerDependency(options.DialerOptions), - }, + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSOCKS, options.Network.Build(), tag, options.DialerOptions), + router: router, + logger: logger, client: socks.NewClient(outboundDialer, options.ServerOptions.Build(), version, options.Username, options.Password), resolve: version == socks.Version4, } @@ -64,9 +67,9 @@ func NewSocks(router adapter.Router, logger log.ContextLogger, tag string, optio return outbound, nil } -func (h *Socks) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) - metadata.Outbound = h.tag + metadata.Outbound = h.Tag() metadata.Destination = destination switch N.NetworkName(network) { case N.NetworkTCP: @@ -90,9 +93,9 @@ func (h *Socks) DialContext(ctx context.Context, network string, destination M.S return h.client.DialContext(ctx, network, destination) } -func (h *Socks) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { ctx, metadata := adapter.ExtendContext(ctx) - metadata.Outbound = h.tag + metadata.Outbound = h.Tag() metadata.Destination = destination if h.uotClient != nil { h.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination) @@ -115,20 +118,20 @@ func (h *Socks) ListenPacket(ctx context.Context, destination M.Socksaddr) (net. // TODO // Deprecated -func (h *Socks) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Outbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if h.resolve { - return NewDirectConnection(ctx, h.router, h, conn, metadata, dns.DomainStrategyUseIPv4) + return outbound.NewDirectConnection(ctx, h.router, h, conn, metadata, dns.DomainStrategyUseIPv4) } else { - return NewConnection(ctx, h, conn, metadata) + return outbound.NewConnection(ctx, h, conn, metadata) } } // TODO // Deprecated -func (h *Socks) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (h *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { if h.resolve { - return NewDirectPacketConnection(ctx, h.router, h, conn, metadata, dns.DomainStrategyUseIPv4) + return outbound.NewDirectPacketConnection(ctx, h.router, h, conn, metadata, dns.DomainStrategyUseIPv4) } else { - return NewPacketConnection(ctx, h, conn, metadata) + return outbound.NewPacketConnection(ctx, h, conn, metadata) } } diff --git a/outbound/ssh.go b/protocol/ssh/outbound.go similarity index 80% rename from outbound/ssh.go rename to protocol/ssh/outbound.go index 28abe9a5e3..62a2a8d9b5 100644 --- a/outbound/ssh.go +++ b/protocol/ssh/outbound.go @@ -1,4 +1,4 @@ -package outbound +package ssh import ( "bytes" @@ -12,26 +12,30 @@ import ( "sync" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "golang.org/x/crypto/ssh" ) -var ( - _ adapter.Outbound = (*SSH)(nil) - _ adapter.InterfaceUpdateListener = (*SSH)(nil) -) +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.SSHOutboundOptions](registry, C.TypeSSH, NewOutbound) +} -type SSH struct { - myOutboundAdapter +var _ adapter.InterfaceUpdateListener = (*Outbound)(nil) + +type Outbound struct { + outbound.Adapter ctx context.Context + logger logger.ContextLogger dialer N.Dialer serverAddr M.Socksaddr user string @@ -44,21 +48,15 @@ type SSH struct { client *ssh.Client } -func NewSSH(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SSHOutboundOptions) (*SSH, error) { +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SSHOutboundOptions) (adapter.Outbound, error) { outboundDialer, err := dialer.New(router, options.DialerOptions) if err != nil { return nil, err } - outbound := &SSH{ - myOutboundAdapter: myOutboundAdapter{ - protocol: C.TypeSSH, - network: []string{N.NetworkTCP}, - router: router, - logger: logger, - tag: tag, - dependencies: withDialerDependency(options.DialerOptions), - }, + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSSH, []string{N.NetworkTCP}, tag, options.DialerOptions), ctx: ctx, + logger: logger, dialer: outboundDialer, serverAddr: options.ServerOptions.Build(), user: options.User, @@ -122,7 +120,7 @@ func randomVersion() string { return version } -func (s *SSH) connect() (*ssh.Client, error) { +func (s *Outbound) connect() (*ssh.Client, error) { if s.client != nil { return s.client, nil } @@ -179,16 +177,16 @@ func (s *SSH) connect() (*ssh.Client, error) { return client, nil } -func (s *SSH) InterfaceUpdated() { +func (s *Outbound) InterfaceUpdated() { common.Close(s.clientConn) return } -func (s *SSH) Close() error { +func (s *Outbound) Close() error { return common.Close(s.clientConn) } -func (s *SSH) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (s *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { client, err := s.connect() if err != nil { return nil, err @@ -196,6 +194,6 @@ func (s *SSH) DialContext(ctx context.Context, network string, destination M.Soc return client.Dial(network, destination.String()) } -func (s *SSH) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (s *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return nil, os.ErrInvalid } diff --git a/outbound/tor.go b/protocol/tor/outbound.go similarity index 83% rename from outbound/tor.go rename to protocol/tor/outbound.go index ccc0c0cfe5..89a295b822 100644 --- a/outbound/tor.go +++ b/protocol/tor/outbound.go @@ -1,4 +1,4 @@ -package outbound +package tor import ( "context" @@ -8,6 +8,7 @@ import ( "strings" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" @@ -15,6 +16,7 @@ import ( "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/rw" @@ -24,11 +26,14 @@ import ( "github.com/cretz/bine/tor" ) -var _ adapter.Outbound = (*Tor)(nil) +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.TorOutboundOptions](registry, C.TypeTor, NewOutbound) +} -type Tor struct { - myOutboundAdapter +type Outbound struct { + outbound.Adapter ctx context.Context + logger logger.ContextLogger proxy *ProxyListener startConf *tor.StartConf options map[string]string @@ -37,8 +42,8 @@ type Tor struct { socksClient *socks.Client } -func NewTor(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TorOutboundOptions) (*Tor, error) { - startConf := newConfig() +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TorOutboundOptions) (adapter.Outbound, error) { + var startConf tor.StartConf startConf.DataDir = os.ExpandEnv(options.DataDirectory) startConf.TempDataDirBase = os.TempDir() startConf.ExtraArgs = options.ExtraArgs @@ -74,23 +79,17 @@ func NewTor(ctx context.Context, router adapter.Router, logger log.ContextLogger if err != nil { return nil, err } - return &Tor{ - myOutboundAdapter: myOutboundAdapter{ - protocol: C.TypeTor, - network: []string{N.NetworkTCP}, - router: router, - logger: logger, - tag: tag, - dependencies: withDialerDependency(options.DialerOptions), - }, + return &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTor, []string{N.NetworkTCP}, tag, options.DialerOptions), ctx: ctx, + logger: logger, proxy: NewProxyListener(ctx, logger, outboundDialer), startConf: &startConf, options: options.Options, }, nil } -func (t *Tor) Start() error { +func (t *Outbound) Start() error { err := t.start() if err != nil { t.Close() @@ -106,7 +105,7 @@ var torLogEvents = []control.EventCode{ control.EventCodeLogWarn, } -func (t *Tor) start() error { +func (t *Outbound) start() error { torInstance, err := tor.Start(t.ctx, t.startConf) if err != nil { return E.New(strings.ToLower(err.Error())) @@ -168,7 +167,7 @@ func (t *Tor) start() error { return nil } -func (t *Tor) recvLoop() { +func (t *Outbound) recvLoop() { for rawEvent := range t.events { switch event := rawEvent.(type) { case *control.LogEvent: @@ -191,7 +190,7 @@ func (t *Tor) recvLoop() { } } -func (t *Tor) Close() error { +func (t *Outbound) Close() error { err := common.Close( common.PtrOrNil(t.proxy), common.PtrOrNil(t.instance), @@ -203,11 +202,11 @@ func (t *Tor) Close() error { return err } -func (t *Tor) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (t *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { t.logger.InfoContext(ctx, "outbound connection to ", destination) return t.socksClient.DialContext(ctx, network, destination) } -func (t *Tor) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (t *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return nil, os.ErrInvalid } diff --git a/outbound/proxy.go b/protocol/tor/proxy.go similarity index 94% rename from outbound/proxy.go rename to protocol/tor/proxy.go index 38c1845392..ef60bd1f7e 100644 --- a/outbound/proxy.go +++ b/protocol/tor/proxy.go @@ -1,4 +1,4 @@ -package outbound +package tor import ( "context" @@ -7,6 +7,7 @@ import ( "net" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" @@ -106,7 +107,7 @@ func (l *ProxyListener) NewConnection(ctx context.Context, conn net.Conn, upstre metadata.Network = N.NetworkTCP metadata.Destination = upstreamMetadata.Destination l.logger.InfoContext(ctx, "proxy connection to ", metadata.Destination) - return NewConnection(ctx, l.dialer, conn, metadata) + return outbound.NewConnection(ctx, l.dialer, conn, metadata) } func (l *ProxyListener) NewPacketConnection(ctx context.Context, conn N.PacketConn, upstreamMetadata M.Metadata) error { @@ -114,5 +115,5 @@ func (l *ProxyListener) NewPacketConnection(ctx context.Context, conn N.PacketCo metadata.Network = N.NetworkUDP metadata.Destination = upstreamMetadata.Destination l.logger.InfoContext(ctx, "proxy packet connection to ", metadata.Destination) - return NewPacketConnection(ctx, l.dialer, conn, metadata) + return outbound.NewPacketConnection(ctx, l.dialer, conn, metadata) } diff --git a/inbound/trojan.go b/protocol/trojan/inbound.go similarity index 68% rename from inbound/trojan.go rename to protocol/trojan/inbound.go index ce003dda70..010ae8baf2 100644 --- a/inbound/trojan.go +++ b/protocol/trojan/inbound.go @@ -1,4 +1,4 @@ -package inbound +package trojan import ( "context" @@ -6,6 +6,8 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" @@ -21,13 +23,17 @@ import ( N "github.com/sagernet/sing/common/network" ) -var ( - _ adapter.Inbound = (*Trojan)(nil) - _ adapter.TCPInjectableInbound = (*Trojan)(nil) -) +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.TrojanInboundOptions](registry, C.TypeTrojan, NewInbound) +} + +var _ adapter.TCPInjectableInbound = (*Inbound)(nil) -type Trojan struct { - myInboundAdapter +type Inbound struct { + inbound.Adapter + router adapter.ConnectionRouterEx + logger log.ContextLogger + listener *listener.Listener service *trojan.Service[int] users []option.TrojanUser tlsConfig tls.ServerConfig @@ -36,18 +42,12 @@ type Trojan struct { transport adapter.V2RayServerTransport } -func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanInboundOptions) (*Trojan, error) { - inbound := &Trojan{ - myInboundAdapter: myInboundAdapter{ - protocol: C.TypeTrojan, - network: []string{N.NetworkTCP}, - ctx: ctx, - router: router, - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - }, - users: options.Users, +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanInboundOptions) (adapter.Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeTrojan, tag), + router: router, + logger: logger, + users: options.Users, } if options.TLS != nil { tlsConfig, err := tls.NewServer(ctx, logger, common.PtrValueOrDefault(options.TLS)) @@ -80,7 +80,7 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog } fallbackHandler = adapter.NewUpstreamContextHandler(inbound.fallbackConnection, nil, nil) } - service := trojan.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound), fallbackHandler) + service := trojan.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, nil), fallbackHandler, logger) err := service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.TrojanUser) int { return index }), common.Map(options.Users, func(it option.TrojanUser) string { @@ -90,7 +90,7 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog return nil, err } if options.Transport != nil { - inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*trojanTransportHandler)(inbound)) + inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*inboundTransportHandler)(inbound)) if err != nil { return nil, E.Cause(err, "create server transport: ", options.Transport.Type) } @@ -100,11 +100,17 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog return nil, err } inbound.service = service - inbound.connHandler = inbound + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: inbound, + }) return inbound, nil } -func (h *Trojan) Start() error { +func (h *Inbound) Start() error { if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { @@ -112,10 +118,10 @@ func (h *Trojan) Start() error { } } if h.transport == nil { - return h.myInboundAdapter.Start() + return h.listener.Start() } if common.Contains(h.transport.Network(), N.NetworkTCP) { - tcpListener, err := h.myInboundAdapter.ListenTCP() + tcpListener, err := h.listener.ListenTCP() if err != nil { return err } @@ -127,7 +133,7 @@ func (h *Trojan) Start() error { }() } if common.Contains(h.transport.Network(), N.NetworkUDP) { - udpConn, err := h.myInboundAdapter.ListenUDP() + udpConn, err := h.listener.ListenUDP() if err != nil { return err } @@ -141,15 +147,15 @@ func (h *Trojan) Start() error { return nil } -func (h *Trojan) Close() error { +func (h *Inbound) Close() error { return common.Close( - &h.myInboundAdapter, + &h.listener, h.tlsConfig, h.transport, ) } -func (h *Trojan) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { var err error if h.tlsConfig != nil && h.transport == nil { conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig) @@ -160,7 +166,7 @@ func (h *Trojan) NewConnection(ctx context.Context, conn net.Conn, metadata adap return h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata)) } -func (h *Trojan) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := h.NewConnection(ctx, conn, metadata) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { @@ -168,7 +174,7 @@ func (h *Trojan) NewConnectionEx(ctx context.Context, conn net.Conn, metadata ad } } -func (h *Trojan) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid @@ -183,7 +189,7 @@ func (h *Trojan) newConnection(ctx context.Context, conn net.Conn, metadata adap return h.router.RouteConnection(ctx, conn, metadata) } -func (h *Trojan) fallbackConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) fallbackConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { var fallbackAddr M.Socksaddr if len(h.fallbackAddrTLSNextProto) > 0 { if tlsConn, loaded := common.Cast[tls.Conn](conn); loaded { @@ -206,7 +212,7 @@ func (h *Trojan) fallbackConnection(ctx context.Context, conn net.Conn, metadata return h.router.RouteConnection(ctx, conn, metadata) } -func (h *Trojan) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid @@ -221,10 +227,18 @@ func (h *Trojan) newPacketConnection(ctx context.Context, conn N.PacketConn, met return h.router.RoutePacketConnection(ctx, conn, metadata) } -var _ adapter.V2RayServerTransportHandler = (*trojanTransportHandler)(nil) +var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil) -type trojanTransportHandler Trojan +type inboundTransportHandler Inbound -func (t *trojanTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { - (*Trojan)(t).routeTCP(ctx, conn, source, destination, onClose) +func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions + metadata.Source = source + metadata.Destination = destination + h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + (*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose) } diff --git a/outbound/trojan.go b/protocol/trojan/outbound.go similarity index 79% rename from outbound/trojan.go rename to protocol/trojan/outbound.go index ee0b2a4b9e..f64c48c3c5 100644 --- a/outbound/trojan.go +++ b/protocol/trojan/outbound.go @@ -1,10 +1,11 @@ -package outbound +package trojan import ( "context" "net" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/tls" @@ -16,14 +17,18 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) -var _ adapter.Outbound = (*Trojan)(nil) +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.TrojanOutboundOptions](registry, C.TypeTrojan, NewOutbound) +} -type Trojan struct { - myOutboundAdapter +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger dialer N.Dialer serverAddr M.Socksaddr key [56]byte @@ -32,20 +37,14 @@ type Trojan struct { transport adapter.V2RayClientTransport } -func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanOutboundOptions) (*Trojan, error) { +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanOutboundOptions) (adapter.Outbound, error) { outboundDialer, err := dialer.New(router, options.DialerOptions) if err != nil { return nil, err } - outbound := &Trojan{ - myOutboundAdapter: myOutboundAdapter{ - protocol: C.TypeTrojan, - network: options.Network.Build(), - router: router, - logger: logger, - tag: tag, - dependencies: withDialerDependency(options.DialerOptions), - }, + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTrojan, options.Network.Build(), tag, options.DialerOptions), + logger: logger, dialer: outboundDialer, serverAddr: options.ServerOptions.Build(), key: trojan.Key(options.Password), @@ -69,7 +68,7 @@ func NewTrojan(ctx context.Context, router adapter.Router, logger log.ContextLog return outbound, nil } -func (h *Trojan) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { if h.multiplexDialer == nil { switch N.NetworkName(network) { case N.NetworkTCP: @@ -89,7 +88,7 @@ func (h *Trojan) DialContext(ctx context.Context, network string, destination M. } } -func (h *Trojan) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if h.multiplexDialer == nil { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) return (*trojanDialer)(h).ListenPacket(ctx, destination) @@ -99,7 +98,7 @@ func (h *Trojan) ListenPacket(ctx context.Context, destination M.Socksaddr) (net } } -func (h *Trojan) InterfaceUpdated() { +func (h *Outbound) InterfaceUpdated() { if h.transport != nil { h.transport.Close() } @@ -109,15 +108,15 @@ func (h *Trojan) InterfaceUpdated() { return } -func (h *Trojan) Close() error { +func (h *Outbound) Close() error { return common.Close(common.PtrOrNil(h.multiplexDialer), h.transport) } -type trojanDialer Trojan +type trojanDialer Outbound func (h *trojanDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) - metadata.Outbound = h.tag + metadata.Outbound = h.Tag() metadata.Destination = destination var conn net.Conn var err error diff --git a/inbound/tuic.go b/protocol/tuic/inbound.go similarity index 68% rename from inbound/tuic.go rename to protocol/tuic/inbound.go index b067c43cd9..33de10d5cb 100644 --- a/inbound/tuic.go +++ b/protocol/tuic/inbound.go @@ -1,6 +1,4 @@ -//go:build with_quic - -package inbound +package tuic import ( "context" @@ -8,6 +6,8 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/common/uot" C "github.com/sagernet/sing-box/constant" @@ -22,16 +22,21 @@ import ( "github.com/gofrs/uuid/v5" ) -var _ adapter.Inbound = (*TUIC)(nil) +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.TUICInboundOptions](registry, C.TypeTUIC, NewInbound) +} -type TUIC struct { - myInboundAdapter +type Inbound struct { + inbound.Adapter + router adapter.ConnectionRouterEx + logger log.ContextLogger + listener *listener.Listener tlsConfig tls.ServerConfig server *tuic.Service[int] userNameList []string } -func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (*TUIC, error) { +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICInboundOptions) (adapter.Inbound, error) { options.UDPFragmentDefault = true if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired @@ -40,16 +45,15 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge if err != nil { return nil, err } - inbound := &TUIC{ - myInboundAdapter: myInboundAdapter{ - protocol: C.TypeTUIC, - network: []string{N.NetworkUDP}, - ctx: ctx, - router: uot.NewRouter(router, logger), - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - }, + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeTUIC, tag), + router: uot.NewRouter(router, logger), + logger: logger, + listener: listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Listen: options.ListenOptions, + }), tlsConfig: tlsConfig, } var udpTimeout time.Duration @@ -95,9 +99,12 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge return inbound, nil } -func (h *TUIC) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { ctx = log.ContextWithNewID(ctx) - metadata = h.createMetadata(conn, metadata) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { @@ -109,9 +116,13 @@ func (h *TUIC) newConnection(ctx context.Context, conn net.Conn, metadata adapte return h.router.RouteConnection(ctx, conn, metadata) } -func (h *TUIC) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { ctx = log.ContextWithNewID(ctx) - metadata = h.createPacketMetadata(conn, metadata) + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions + metadata.OriginDestination = h.listener.UDPAddr() h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { @@ -123,23 +134,23 @@ func (h *TUIC) newPacketConnection(ctx context.Context, conn N.PacketConn, metad return h.router.RoutePacketConnection(ctx, conn, metadata) } -func (h *TUIC) Start() error { +func (h *Inbound) Start() error { if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { return err } } - packetConn, err := h.myInboundAdapter.ListenUDP() + packetConn, err := h.listener.ListenUDP() if err != nil { return err } return h.server.Start(packetConn) } -func (h *TUIC) Close() error { +func (h *Inbound) Close() error { return common.Close( - &h.myInboundAdapter, + &h.listener, h.tlsConfig, common.PtrOrNil(h.server), ) diff --git a/outbound/tuic.go b/protocol/tuic/outbound.go similarity index 76% rename from outbound/tuic.go rename to protocol/tuic/outbound.go index aaf998b16e..691d1658de 100644 --- a/outbound/tuic.go +++ b/protocol/tuic/outbound.go @@ -1,6 +1,4 @@ -//go:build with_quic - -package outbound +package tuic import ( "context" @@ -9,6 +7,7 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" @@ -18,6 +17,7 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/uot" @@ -25,18 +25,20 @@ import ( "github.com/gofrs/uuid/v5" ) -var ( - _ adapter.Outbound = (*TUIC)(nil) - _ adapter.InterfaceUpdateListener = (*TUIC)(nil) -) +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.TUICOutboundOptions](registry, C.TypeTUIC, NewOutbound) +} + +var _ adapter.InterfaceUpdateListener = (*Outbound)(nil) -type TUIC struct { - myOutboundAdapter +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger client *tuic.Client udpStream bool } -func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICOutboundOptions) (*TUIC, error) { +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TUICOutboundOptions) (adapter.Outbound, error) { options.UDPFragmentDefault = true if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired @@ -77,21 +79,15 @@ func NewTUIC(ctx context.Context, router adapter.Router, logger log.ContextLogge if err != nil { return nil, err } - return &TUIC{ - myOutboundAdapter: myOutboundAdapter{ - protocol: C.TypeTUIC, - network: options.Network.Build(), - router: router, - logger: logger, - tag: tag, - dependencies: withDialerDependency(options.DialerOptions), - }, + return &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTUIC, options.Network.Build(), tag, options.DialerOptions), + logger: logger, client: client, udpStream: options.UDPOverStream, }, nil } -func (h *TUIC) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { switch N.NetworkName(network) { case N.NetworkTCP: h.logger.InfoContext(ctx, "outbound connection to ", destination) @@ -119,7 +115,7 @@ func (h *TUIC) DialContext(ctx context.Context, network string, destination M.So } } -func (h *TUIC) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if h.udpStream { h.logger.InfoContext(ctx, "outbound stream packet connection to ", destination) streamConn, err := h.client.DialConn(ctx, uot.RequestDestination(uot.Version)) @@ -136,10 +132,10 @@ func (h *TUIC) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.P } } -func (h *TUIC) InterfaceUpdated() { +func (h *Outbound) InterfaceUpdated() { _ = h.client.CloseWithError(E.New("network changed")) } -func (h *TUIC) Close() error { +func (h *Outbound) Close() error { return h.client.CloseWithError(os.ErrClosed) } diff --git a/inbound/tun.go b/protocol/tun/inbound.go similarity index 92% rename from inbound/tun.go rename to protocol/tun/inbound.go index 11b16428b9..ff679c8ec8 100644 --- a/inbound/tun.go +++ b/protocol/tun/inbound.go @@ -1,4 +1,4 @@ -package inbound +package tun import ( "context" @@ -11,6 +11,7 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/common/taskmonitor" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/deprecated" @@ -24,13 +25,16 @@ import ( N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ranges" "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" "go4.org/netipx" ) -var _ adapter.Inbound = (*TUN)(nil) +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.TunInboundOptions](registry, C.TypeTun, NewInbound) +} -type TUN struct { +type Inbound struct { tag string ctx context.Context router adapter.Router @@ -55,7 +59,7 @@ type TUN struct { routeExcludeAddressSet []*netipx.IPSet } -func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*TUN, error) { +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions) (adapter.Inbound, error) { address := options.Address var deprecatedAddressUsed bool //nolint:staticcheck @@ -164,7 +168,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger outputMark = tun.DefaultAutoRedirectOutputMark } - inbound := &TUN{ + inbound := &Inbound{ tag: tag, ctx: ctx, router: router, @@ -198,7 +202,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger endpointIndependentNat: options.EndpointIndependentNat, udpTimeout: udpTimeout, stack: options.Stack, - platformInterface: platformInterface, + platformInterface: service.FromContext[platform.Interface](ctx), platformOptions: common.PtrValueOrDefault(options.Platform), } if options.AutoRedirect { @@ -285,15 +289,15 @@ func parseRange(uidRanges []ranges.Range[uint32], rangeList []string) ([]ranges. return uidRanges, nil } -func (t *TUN) Type() string { +func (t *Inbound) Type() string { return C.TypeTun } -func (t *TUN) Tag() string { +func (t *Inbound) Tag() string { return t.tag } -func (t *TUN) Start() error { +func (t *Inbound) Start() error { if C.IsAndroid && t.platformInterface == nil { t.tunOptions.BuildAndroidRules(t.router.PackageManager()) } @@ -350,7 +354,7 @@ func (t *TUN) Start() error { return nil } -func (t *TUN) PostStart() error { +func (t *Inbound) PostStart() error { monitor := taskmonitor.New(t.logger, C.StartTimeout) if t.autoRedirect != nil { t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) @@ -389,7 +393,7 @@ func (t *TUN) PostStart() error { return nil } -func (t *TUN) updateRouteAddressSet(it adapter.RuleSet) { +func (t *Inbound) updateRouteAddressSet(it adapter.RuleSet) { t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet) t.autoRedirect.UpdateRouteAddressSet() @@ -397,7 +401,7 @@ func (t *TUN) updateRouteAddressSet(it adapter.RuleSet) { t.routeExcludeAddressSet = nil } -func (t *TUN) Close() error { +func (t *Inbound) Close() error { return common.Close( t.tunStack, t.tunIf, @@ -405,7 +409,7 @@ func (t *TUN) Close() error { ) } -func (t *TUN) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr) error { +func (t *Inbound) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr) error { return t.router.PreMatch(adapter.InboundContext{ Inbound: t.tag, InboundType: C.TypeTun, @@ -416,7 +420,7 @@ func (t *TUN) PrepareConnection(network string, source M.Socksaddr, destination }) } -func (t *TUN) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { +func (t *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) var metadata adapter.InboundContext metadata.Inbound = t.tag @@ -429,7 +433,7 @@ func (t *TUN) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socks t.router.RouteConnectionEx(ctx, conn, metadata, onClose) } -func (t *TUN) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { +func (t *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) var metadata adapter.InboundContext metadata.Inbound = t.tag @@ -442,7 +446,7 @@ func (t *TUN) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, sour t.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } -type autoRedirectHandler TUN +type autoRedirectHandler Inbound func (t *autoRedirectHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) diff --git a/inbound/vless.go b/protocol/vless/inbound.go similarity index 60% rename from inbound/vless.go rename to protocol/vless/inbound.go index ec26bd8859..0641549b0a 100644 --- a/inbound/vless.go +++ b/protocol/vless/inbound.go @@ -1,4 +1,4 @@ -package inbound +package vless import ( "context" @@ -6,6 +6,8 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/common/uot" @@ -20,37 +22,36 @@ import ( "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) -var ( - _ adapter.Inbound = (*VLESS)(nil) - _ adapter.TCPInjectableInbound = (*VLESS)(nil) -) +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.VLESSInboundOptions](registry, C.TypeVLESS, NewInbound) +} -type VLESS struct { - myInboundAdapter +var _ adapter.TCPInjectableInbound = (*Inbound)(nil) + +type Inbound struct { + inbound.Adapter ctx context.Context + router adapter.ConnectionRouterEx + logger logger.ContextLogger + listener *listener.Listener users []option.VLESSUser service *vless.Service[int] tlsConfig tls.ServerConfig transport adapter.V2RayServerTransport } -func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSInboundOptions) (*VLESS, error) { - inbound := &VLESS{ - myInboundAdapter: myInboundAdapter{ - protocol: C.TypeVLESS, - network: []string{N.NetworkTCP}, - ctx: ctx, - router: uot.NewRouter(router, logger), - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - }, - ctx: ctx, - users: options.Users, +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSInboundOptions) (adapter.Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeVLESS, tag), + ctx: ctx, + router: uot.NewRouter(router, logger), + logger: logger, + users: options.Users, } var err error inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) @@ -73,16 +74,22 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg } } if options.Transport != nil { - inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*vlessTransportHandler)(inbound)) + inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*inboundTransportHandler)(inbound)) if err != nil { return nil, E.Cause(err, "create server transport: ", options.Transport.Type) } } - inbound.connHandler = inbound + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: inbound, + }) return inbound, nil } -func (h *VLESS) Start() error { +func (h *Inbound) Start() error { if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { @@ -90,10 +97,10 @@ func (h *VLESS) Start() error { } } if h.transport == nil { - return h.myInboundAdapter.Start() + return h.listener.Start() } if common.Contains(h.transport.Network(), N.NetworkTCP) { - tcpListener, err := h.myInboundAdapter.ListenTCP() + tcpListener, err := h.listener.ListenTCP() if err != nil { return err } @@ -105,7 +112,7 @@ func (h *VLESS) Start() error { }() } if common.Contains(h.transport.Network(), N.NetworkUDP) { - udpConn, err := h.myInboundAdapter.ListenUDP() + udpConn, err := h.listener.ListenUDP() if err != nil { return err } @@ -119,16 +126,16 @@ func (h *VLESS) Start() error { return nil } -func (h *VLESS) Close() error { +func (h *Inbound) Close() error { return common.Close( h.service, - &h.myInboundAdapter, + &h.listener, h.tlsConfig, h.transport, ) } -func (h *VLESS) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { var err error if h.tlsConfig != nil && h.transport == nil { conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig) @@ -139,7 +146,7 @@ func (h *VLESS) NewConnection(ctx context.Context, conn net.Conn, metadata adapt return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata)) } -func (h *VLESS) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := h.NewConnection(ctx, conn, metadata) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { @@ -147,7 +154,7 @@ func (h *VLESS) NewConnectionEx(ctx context.Context, conn net.Conn, metadata ada } } -func (h *VLESS) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid @@ -162,7 +169,7 @@ func (h *VLESS) newConnection(ctx context.Context, conn net.Conn, metadata adapt return h.router.RouteConnection(ctx, conn, metadata) } -func (h *VLESS) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid @@ -183,10 +190,32 @@ func (h *VLESS) newPacketConnection(ctx context.Context, conn N.PacketConn, meta return h.router.RoutePacketConnection(ctx, conn, metadata) } -var _ adapter.V2RayServerTransportHandler = (*vlessTransportHandler)(nil) +var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil) + +type inboundTransportHandler Inbound -type vlessTransportHandler VLESS +func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions + metadata.Source = source + metadata.Destination = destination + h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + (*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) NewError(ctx context.Context, err error) { + NewError(h.logger, ctx, err) +} -func (t *vlessTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { - t.routeTCP(ctx, conn, source, destination, onClose) +// Deprecated: remove +func NewError(logger logger.ContextLogger, ctx context.Context, err error) { + common.Close(err) + if E.IsClosedOrCanceled(err) { + logger.DebugContext(ctx, "connection closed: ", err) + return + } + logger.ErrorContext(ctx, err) } diff --git a/outbound/vless.go b/protocol/vless/outbound.go similarity index 84% rename from outbound/vless.go rename to protocol/vless/outbound.go index 536a1e8fba..1074549e8d 100644 --- a/outbound/vless.go +++ b/protocol/vless/outbound.go @@ -1,10 +1,11 @@ -package outbound +package vless import ( "context" "net" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/tls" @@ -17,14 +18,18 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) -var _ adapter.Outbound = (*VLESS)(nil) +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.VLESSOutboundOptions](registry, C.TypeVLESS, NewOutbound) +} -type VLESS struct { - myOutboundAdapter +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger dialer N.Dialer client *vless.Client serverAddr M.Socksaddr @@ -35,20 +40,14 @@ type VLESS struct { xudp bool } -func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSOutboundOptions) (*VLESS, error) { +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSOutboundOptions) (adapter.Outbound, error) { outboundDialer, err := dialer.New(router, options.DialerOptions) if err != nil { return nil, err } - outbound := &VLESS{ - myOutboundAdapter: myOutboundAdapter{ - protocol: C.TypeVLESS, - network: options.Network.Build(), - router: router, - logger: logger, - tag: tag, - dependencies: withDialerDependency(options.DialerOptions), - }, + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeVLESS, options.Network.Build(), tag, options.DialerOptions), + logger: logger, dialer: outboundDialer, serverAddr: options.ServerOptions.Build(), } @@ -88,7 +87,7 @@ func NewVLESS(ctx context.Context, router adapter.Router, logger log.ContextLogg return outbound, nil } -func (h *VLESS) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { if h.multiplexDialer == nil { switch N.NetworkName(network) { case N.NetworkTCP: @@ -108,7 +107,7 @@ func (h *VLESS) DialContext(ctx context.Context, network string, destination M.S } } -func (h *VLESS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if h.multiplexDialer == nil { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) return (*vlessDialer)(h).ListenPacket(ctx, destination) @@ -118,7 +117,7 @@ func (h *VLESS) ListenPacket(ctx context.Context, destination M.Socksaddr) (net. } } -func (h *VLESS) InterfaceUpdated() { +func (h *Outbound) InterfaceUpdated() { if h.transport != nil { h.transport.Close() } @@ -128,15 +127,15 @@ func (h *VLESS) InterfaceUpdated() { return } -func (h *VLESS) Close() error { +func (h *Outbound) Close() error { return common.Close(common.PtrOrNil(h.multiplexDialer), h.transport) } -type vlessDialer VLESS +type vlessDialer Outbound func (h *vlessDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) - metadata.Outbound = h.tag + metadata.Outbound = h.Tag() metadata.Destination = destination var conn net.Conn var err error @@ -179,7 +178,7 @@ func (h *vlessDialer) DialContext(ctx context.Context, network string, destinati func (h *vlessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) ctx, metadata := adapter.ExtendContext(ctx) - metadata.Outbound = h.tag + metadata.Outbound = h.Tag() metadata.Destination = destination var conn net.Conn var err error diff --git a/inbound/vmess.go b/protocol/vmess/inbound.go similarity index 62% rename from inbound/vmess.go rename to protocol/vmess/inbound.go index 9099bd6293..1c80f376e1 100644 --- a/inbound/vmess.go +++ b/protocol/vmess/inbound.go @@ -1,4 +1,4 @@ -package inbound +package vmess import ( "context" @@ -6,6 +6,8 @@ import ( "os" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/common/listener" "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/tls" "github.com/sagernet/sing-box/common/uot" @@ -19,38 +21,37 @@ import ( "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ntp" ) -var ( - _ adapter.Inbound = (*VMess)(nil) - _ adapter.TCPInjectableInbound = (*VMess)(nil) -) +func RegisterInbound(registry *inbound.Registry) { + inbound.Register[option.VMessInboundOptions](registry, C.TypeVMess, NewInbound) +} -type VMess struct { - myInboundAdapter +var _ adapter.TCPInjectableInbound = (*Inbound)(nil) + +type Inbound struct { + inbound.Adapter ctx context.Context + router adapter.ConnectionRouterEx + logger logger.ContextLogger + listener *listener.Listener service *vmess.Service[int] users []option.VMessUser tlsConfig tls.ServerConfig transport adapter.V2RayServerTransport } -func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessInboundOptions) (*VMess, error) { - inbound := &VMess{ - myInboundAdapter: myInboundAdapter{ - protocol: C.TypeVMess, - network: []string{N.NetworkTCP}, - ctx: ctx, - router: uot.NewRouter(router, logger), - logger: logger, - tag: tag, - listenOptions: options.ListenOptions, - }, - ctx: ctx, - users: options.Users, +func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessInboundOptions) (adapter.Inbound, error) { + inbound := &Inbound{ + Adapter: inbound.NewAdapter(C.TypeVMess, tag), + ctx: ctx, + router: uot.NewRouter(router, logger), + logger: logger, + users: options.Users, } var err error inbound.router, err = mux.NewRouterWithOptions(inbound.router, logger, common.PtrValueOrDefault(options.Multiplex)) @@ -83,16 +84,22 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg } } if options.Transport != nil { - inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*vmessTransportHandler)(inbound)) + inbound.transport, err = v2ray.NewServerTransport(ctx, logger, common.PtrValueOrDefault(options.Transport), inbound.tlsConfig, (*inboundTransportHandler)(inbound)) if err != nil { return nil, E.Cause(err, "create server transport: ", options.Transport.Type) } } - inbound.connHandler = inbound + inbound.listener = listener.New(listener.Options{ + Context: ctx, + Logger: logger, + Network: []string{N.NetworkTCP}, + Listen: options.ListenOptions, + ConnectionHandler: inbound, + }) return inbound, nil } -func (h *VMess) Start() error { +func (h *Inbound) Start() error { err := h.service.Start() if err != nil { return err @@ -104,10 +111,10 @@ func (h *VMess) Start() error { } } if h.transport == nil { - return h.myInboundAdapter.Start() + return h.listener.Start() } if common.Contains(h.transport.Network(), N.NetworkTCP) { - tcpListener, err := h.myInboundAdapter.ListenTCP() + tcpListener, err := h.listener.ListenTCP() if err != nil { return err } @@ -119,7 +126,7 @@ func (h *VMess) Start() error { }() } if common.Contains(h.transport.Network(), N.NetworkUDP) { - udpConn, err := h.myInboundAdapter.ListenUDP() + udpConn, err := h.listener.ListenUDP() if err != nil { return err } @@ -133,16 +140,16 @@ func (h *VMess) Start() error { return nil } -func (h *VMess) Close() error { +func (h *Inbound) Close() error { return common.Close( h.service, - &h.myInboundAdapter, + &h.listener, h.tlsConfig, h.transport, ) } -func (h *VMess) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { var err error if h.tlsConfig != nil && h.transport == nil { conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig) @@ -153,7 +160,7 @@ func (h *VMess) NewConnection(ctx context.Context, conn net.Conn, metadata adapt return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata)) } -func (h *VMess) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := h.NewConnection(ctx, conn, metadata) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { @@ -161,7 +168,7 @@ func (h *VMess) NewConnectionEx(ctx context.Context, conn net.Conn, metadata ada } } -func (h *VMess) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid @@ -176,7 +183,7 @@ func (h *VMess) newConnection(ctx context.Context, conn net.Conn, metadata adapt return h.router.RouteConnection(ctx, conn, metadata) } -func (h *VMess) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid @@ -197,10 +204,32 @@ func (h *VMess) newPacketConnection(ctx context.Context, conn N.PacketConn, meta return h.router.RoutePacketConnection(ctx, conn, metadata) } -var _ adapter.V2RayServerTransportHandler = (*vmessTransportHandler)(nil) +var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil) + +type inboundTransportHandler Inbound -type vmessTransportHandler VMess +func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions + metadata.Source = source + metadata.Destination = destination + h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) + (*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) NewError(ctx context.Context, err error) { + NewError(h.logger, ctx, err) +} -func (t *vmessTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { - (*VMess)(t).routeTCP(ctx, conn, source, destination, onClose) +// Deprecated: remove +func NewError(logger logger.ContextLogger, ctx context.Context, err error) { + common.Close(err) + if E.IsClosedOrCanceled(err) { + logger.DebugContext(ctx, "connection closed: ", err) + return + } + logger.ErrorContext(ctx, err) } diff --git a/outbound/vmess.go b/protocol/vmess/outbound.go similarity index 84% rename from outbound/vmess.go rename to protocol/vmess/outbound.go index 126d2fd05f..759ea8baa4 100644 --- a/outbound/vmess.go +++ b/protocol/vmess/outbound.go @@ -1,10 +1,11 @@ -package outbound +package vmess import ( "context" "net" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/mux" "github.com/sagernet/sing-box/common/tls" @@ -16,15 +17,19 @@ import ( "github.com/sagernet/sing-vmess/packetaddr" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ntp" ) -var _ adapter.Outbound = (*VMess)(nil) +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.VMessOutboundOptions](registry, C.TypeVMess, NewOutbound) +} -type VMess struct { - myOutboundAdapter +type Outbound struct { + outbound.Adapter + logger logger.ContextLogger dialer N.Dialer client *vmess.Client serverAddr M.Socksaddr @@ -35,20 +40,14 @@ type VMess struct { xudp bool } -func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessOutboundOptions) (*VMess, error) { +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessOutboundOptions) (adapter.Outbound, error) { outboundDialer, err := dialer.New(router, options.DialerOptions) if err != nil { return nil, err } - outbound := &VMess{ - myOutboundAdapter: myOutboundAdapter{ - protocol: C.TypeVMess, - network: options.Network.Build(), - router: router, - logger: logger, - tag: tag, - dependencies: withDialerDependency(options.DialerOptions), - }, + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeVMess, options.Network.Build(), tag, options.DialerOptions), + logger: logger, dialer: outboundDialer, serverAddr: options.ServerOptions.Build(), } @@ -102,7 +101,7 @@ func NewVMess(ctx context.Context, router adapter.Router, logger log.ContextLogg return outbound, nil } -func (h *VMess) InterfaceUpdated() { +func (h *Outbound) InterfaceUpdated() { if h.transport != nil { h.transport.Close() } @@ -112,11 +111,11 @@ func (h *VMess) InterfaceUpdated() { return } -func (h *VMess) Close() error { +func (h *Outbound) Close() error { return common.Close(common.PtrOrNil(h.multiplexDialer), h.transport) } -func (h *VMess) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (h *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { if h.multiplexDialer == nil { switch N.NetworkName(network) { case N.NetworkTCP: @@ -136,7 +135,7 @@ func (h *VMess) DialContext(ctx context.Context, network string, destination M.S } } -func (h *VMess) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if h.multiplexDialer == nil { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) return (*vmessDialer)(h).ListenPacket(ctx, destination) @@ -146,11 +145,11 @@ func (h *VMess) ListenPacket(ctx context.Context, destination M.Socksaddr) (net. } } -type vmessDialer VMess +type vmessDialer Outbound func (h *vmessDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) - metadata.Outbound = h.tag + metadata.Outbound = h.Tag() metadata.Destination = destination var conn net.Conn var err error @@ -178,7 +177,7 @@ func (h *vmessDialer) DialContext(ctx context.Context, network string, destinati func (h *vmessDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { ctx, metadata := adapter.ExtendContext(ctx) - metadata.Outbound = h.tag + metadata.Outbound = h.Tag() metadata.Destination = destination var conn net.Conn var err error diff --git a/protocol/wireguard/init.go b/protocol/wireguard/init.go new file mode 100644 index 0000000000..848c113be8 --- /dev/null +++ b/protocol/wireguard/init.go @@ -0,0 +1,10 @@ +package wireguard + +import ( + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/wireguard-go/conn" +) + +func init() { + dialer.WgControlFns = conn.ControlFns +} diff --git a/outbound/wireguard.go b/protocol/wireguard/outbound.go similarity index 75% rename from outbound/wireguard.go rename to protocol/wireguard/outbound.go index e6e4b8f8e4..901f8782d6 100644 --- a/outbound/wireguard.go +++ b/protocol/wireguard/outbound.go @@ -1,6 +1,4 @@ -//go:build with_wireguard - -package outbound +package wireguard import ( "context" @@ -12,6 +10,7 @@ import ( "strings" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" @@ -21,6 +20,7 @@ import ( "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/x/list" @@ -30,14 +30,17 @@ import ( "github.com/sagernet/wireguard-go/device" ) -var ( - _ adapter.Outbound = (*WireGuard)(nil) - _ adapter.InterfaceUpdateListener = (*WireGuard)(nil) -) +func RegisterOutbound(registry *outbound.Registry) { + outbound.Register[option.WireGuardOutboundOptions](registry, C.TypeWireGuard, NewOutbound) +} -type WireGuard struct { - myOutboundAdapter +var _ adapter.InterfaceUpdateListener = (*Outbound)(nil) + +type Outbound struct { + outbound.Adapter ctx context.Context + router adapter.Router + logger logger.ContextLogger workers int peers []wireguard.PeerConfig useStdNetBind bool @@ -51,17 +54,12 @@ type WireGuard struct { tunDevice wireguard.Device } -func NewWireGuard(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardOutboundOptions) (*WireGuard, error) { - outbound := &WireGuard{ - myOutboundAdapter: myOutboundAdapter{ - protocol: C.TypeWireGuard, - network: options.Network.Build(), - router: router, - logger: logger, - tag: tag, - dependencies: withDialerDependency(options.DialerOptions), - }, +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardOutboundOptions) (adapter.Outbound, error) { + outbound := &Outbound{ + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeWireGuard, options.Network.Build(), tag, options.DialerOptions), ctx: ctx, + router: router, + logger: logger, workers: options.Workers, pauseManager: service.FromContext[pause.Manager](ctx), } @@ -111,7 +109,7 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context return outbound, nil } -func (w *WireGuard) Start() error { +func (w *Outbound) Start() error { if common.Any(w.peers, func(peer wireguard.PeerConfig) bool { return !peer.Endpoint.IsValid() }) { @@ -121,7 +119,7 @@ func (w *WireGuard) Start() error { return w.start() } -func (w *WireGuard) PostStart() error { +func (w *Outbound) PostStart() error { if common.All(w.peers, func(peer wireguard.PeerConfig) bool { return peer.Endpoint.IsValid() }) { @@ -130,7 +128,7 @@ func (w *WireGuard) PostStart() error { return w.start() } -func (w *WireGuard) start() error { +func (w *Outbound) start() error { err := wireguard.ResolvePeers(w.ctx, w.router, w.peers) if err != nil { return err @@ -150,7 +148,7 @@ func (w *WireGuard) start() error { connectAddr = w.peers[0].Endpoint reserved = w.peers[0].Reserved } - bind = wireguard.NewClientBind(w.ctx, w, w.listener, isConnect, connectAddr, reserved) + bind = wireguard.NewClientBind(w.ctx, w.logger, w.listener, isConnect, connectAddr, reserved) } if w.useStdNetBind || len(w.peers) > 1 { for _, peer := range w.peers { @@ -184,7 +182,7 @@ func (w *WireGuard) start() error { return nil } -func (w *WireGuard) Close() error { +func (w *Outbound) Close() error { if w.device != nil { w.device.Close() } @@ -194,12 +192,12 @@ func (w *WireGuard) Close() error { return nil } -func (w *WireGuard) InterfaceUpdated() { +func (w *Outbound) InterfaceUpdated() { w.device.BindUpdate() return } -func (w *WireGuard) onPauseUpdated(event int) { +func (w *Outbound) onPauseUpdated(event int) { switch event { case pause.EventDevicePaused: w.device.Down() @@ -208,7 +206,7 @@ func (w *WireGuard) onPauseUpdated(event int) { } } -func (w *WireGuard) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (w *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { switch network { case N.NetworkTCP: w.logger.InfoContext(ctx, "outbound connection to ", destination) @@ -225,7 +223,7 @@ func (w *WireGuard) DialContext(ctx context.Context, network string, destination return w.tunDevice.DialContext(ctx, network, destination) } -func (w *WireGuard) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (w *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { w.logger.InfoContext(ctx, "outbound packet connection to ", destination) if destination.IsFqdn() { destinationAddresses, err := w.router.LookupDefault(ctx, destination.Fqdn) @@ -243,12 +241,12 @@ func (w *WireGuard) ListenPacket(ctx context.Context, destination M.Socksaddr) ( // TODO // Deprecated -func (w *WireGuard) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return NewDirectConnection(ctx, w.router, w, conn, metadata, dns.DomainStrategyAsIS) +func (w *Outbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + return outbound.NewDirectConnection(ctx, w.router, w, conn, metadata, dns.DomainStrategyAsIS) } // TODO // Deprecated -func (w *WireGuard) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return NewDirectPacketConnection(ctx, w.router, w, conn, metadata, dns.DomainStrategyAsIS) +func (w *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + return outbound.NewDirectPacketConnection(ctx, w.router, w, conn, metadata, dns.DomainStrategyAsIS) } diff --git a/route/dns.go b/route/dns.go index cc0760b851..98ea8d6b34 100644 --- a/route/dns.go +++ b/route/dns.go @@ -8,7 +8,7 @@ import ( "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/outbound" + dnsOutbound "github.com/sagernet/sing-box/protocol/dns" "github.com/sagernet/sing-dns" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/buf" @@ -24,7 +24,7 @@ func (r *Router) hijackDNSStream(ctx context.Context, conn net.Conn, metadata ad metadata.Destination = M.Socksaddr{} for { conn.SetReadDeadline(time.Now().Add(C.DNSTimeout)) - err := outbound.HandleStreamDNSRequest(ctx, r, conn, metadata) + err := dnsOutbound.HandleStreamDNSRequest(ctx, r, conn, metadata) if err != nil { return err } @@ -48,7 +48,7 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB }) return } - err := outbound.NewDNSPacketConnection(ctx, r, conn, packetBuffers, metadata) + err := dnsOutbound.NewDNSPacketConnection(ctx, r, conn, packetBuffers, metadata) if err != nil && !E.IsClosedOrCanceled(err) { r.dnsLogger.ErrorContext(ctx, E.Cause(err, "process packet connection")) } diff --git a/route/route.go b/route/route.go index ebffddddbf..6c68cf79cb 100644 --- a/route/route.go +++ b/route/route.go @@ -12,12 +12,12 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/conntrack" "github.com/sagernet/sing-box/common/process" "github.com/sagernet/sing-box/common/sniff" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing-box/outbound" "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing-dns" "github.com/sagernet/sing-mux" diff --git a/route/router.go b/route/router.go index 05349485d2..b6cc8051ba 100644 --- a/route/router.go +++ b/route/router.go @@ -99,7 +99,6 @@ func NewRouter( dnsOptions option.DNSOptions, ntpOptions option.NTPOptions, inbounds []option.Inbound, - platformInterface platform.Interface, ) (*Router, error) { router := &Router{ ctx: ctx, @@ -122,10 +121,13 @@ func NewRouter( defaultInterface: options.DefaultInterface, defaultMark: options.DefaultMark, pauseManager: service.FromContext[pause.Manager](ctx), - platformInterface: platformInterface, + platformInterface: service.FromContext[platform.Interface](ctx), needWIFIState: hasRule(options.Rules, isWIFIRule) || hasDNSRule(dnsOptions.Rules, isWIFIDNSRule), needPackageManager: common.Any(inbounds, func(inbound option.Inbound) bool { - return len(inbound.TunOptions.IncludePackage) > 0 || len(inbound.TunOptions.ExcludePackage) > 0 + if tunOptions, isTUN := inbound.Options.(*option.TunInboundOptions); isTUN && tunOptions.AutoRoute { + return true + } + return false }), } router.dnsClient = dns.NewClient(dns.ClientOptions{ @@ -324,9 +326,15 @@ func NewRouter( router.fakeIPStore = fakeip.NewStore(ctx, router.logger, inet4Range, inet6Range) } - usePlatformDefaultInterfaceMonitor := platformInterface != nil && platformInterface.UsePlatformDefaultInterfaceMonitor() + usePlatformDefaultInterfaceMonitor := router.platformInterface != nil && router.platformInterface.UsePlatformDefaultInterfaceMonitor() needInterfaceMonitor := options.AutoDetectInterface || common.Any(inbounds, func(inbound option.Inbound) bool { - return inbound.HTTPOptions.SetSystemProxy || inbound.MixedOptions.SetSystemProxy || inbound.TunOptions.AutoRoute + if httpMixedOptions, isHTTPMixed := inbound.Options.(*option.HTTPMixedInboundOptions); isHTTPMixed && httpMixedOptions.SetSystemProxy { + return true + } + if tunOptions, isTUN := inbound.Options.(*option.TunInboundOptions); isTUN && tunOptions.AutoRoute { + return true + } + return false }) if !usePlatformDefaultInterfaceMonitor { @@ -339,7 +347,7 @@ func NewRouter( interfaceMonitor, err := tun.NewDefaultInterfaceMonitor(router.networkMonitor, router.logger, tun.DefaultInterfaceMonitorOptions{ InterfaceFinder: router.interfaceFinder, OverrideAndroidVPN: options.OverrideAndroidVPN, - UnderNetworkExtension: platformInterface != nil && platformInterface.UnderNetworkExtension(), + UnderNetworkExtension: router.platformInterface != nil && router.platformInterface.UnderNetworkExtension(), }) if err != nil { return nil, E.New("auto_detect_interface unsupported on current platform") @@ -348,7 +356,7 @@ func NewRouter( router.interfaceMonitor = interfaceMonitor } } else { - interfaceMonitor := platformInterface.CreateDefaultInterfaceMonitor(router.logger) + interfaceMonitor := router.platformInterface.CreateDefaultInterfaceMonitor(router.logger) interfaceMonitor.RegisterCallback(router.notifyNetworkUpdate) router.interfaceMonitor = interfaceMonitor } diff --git a/test/brutal_test.go b/test/brutal_test.go index 18aae2e201..ce1d2c2a8b 100644 --- a/test/brutal_test.go +++ b/test/brutal_test.go @@ -15,7 +15,7 @@ func TestBrutalShadowsocks(t *testing.T) { method := shadowaead_2022.List[0] password := mkBase64(t, 16) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -46,7 +46,7 @@ func TestBrutalShadowsocks(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, @@ -100,7 +100,7 @@ func TestBrutalTrojan(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") password := mkBase64(t, 16) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -138,7 +138,7 @@ func TestBrutalTrojan(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, @@ -197,7 +197,7 @@ func TestBrutalTrojan(t *testing.T) { func TestBrutalVMess(t *testing.T) { user, _ := uuid.NewV4() startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -227,7 +227,7 @@ func TestBrutalVMess(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, @@ -279,7 +279,7 @@ func TestBrutalVMess(t *testing.T) { func TestBrutalVLESS(t *testing.T) { user, _ := uuid.NewV4() startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -326,7 +326,7 @@ func TestBrutalVLESS(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, diff --git a/test/direct_test.go b/test/direct_test.go index 1dbf1de1b6..c4fd8c5ea3 100644 --- a/test/direct_test.go +++ b/test/direct_test.go @@ -11,7 +11,7 @@ import ( // Since this is a feature one-off added by outsiders, I won't address these anymore. func _TestProxyProtocol(t *testing.T) { startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -33,7 +33,7 @@ func _TestProxyProtocol(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, diff --git a/test/domain_inbound_test.go b/test/domain_inbound_test.go index 1ca2121dae..c82b0d2974 100644 --- a/test/domain_inbound_test.go +++ b/test/domain_inbound_test.go @@ -6,7 +6,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" - dns "github.com/sagernet/sing-dns" + "github.com/sagernet/sing-dns" "github.com/gofrs/uuid/v5" ) @@ -14,7 +14,7 @@ import ( func TestTUICDomainUDP(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -49,7 +49,7 @@ func TestTUICDomainUDP(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, diff --git a/test/ech_test.go b/test/ech_test.go index 90eae1f48c..eeac1acb72 100644 --- a/test/ech_test.go +++ b/test/ech_test.go @@ -16,7 +16,7 @@ func TestECH(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org", false)) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -55,7 +55,7 @@ func TestECH(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, @@ -109,7 +109,7 @@ func TestECHQUIC(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org", false)) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -145,7 +145,7 @@ func TestECHQUIC(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, @@ -199,7 +199,7 @@ func TestECHHysteria2(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org", false)) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -235,7 +235,7 @@ func TestECHHysteria2(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, diff --git a/test/http_test.go b/test/http_test.go index 88385c2757..7e72400534 100644 --- a/test/http_test.go +++ b/test/http_test.go @@ -10,7 +10,7 @@ import ( func TestHTTPSelf(t *testing.T) { startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -31,7 +31,7 @@ func TestHTTPSelf(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, diff --git a/test/hysteria2_test.go b/test/hysteria2_test.go index 9ca2f5d38d..665da552c3 100644 --- a/test/hysteria2_test.go +++ b/test/hysteria2_test.go @@ -28,7 +28,7 @@ func testHysteria2Self(t *testing.T, salamanderPassword string) { } } startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -63,7 +63,7 @@ func testHysteria2Self(t *testing.T, salamanderPassword string) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, @@ -115,7 +115,7 @@ func testHysteria2Self(t *testing.T, salamanderPassword string) { func TestHysteria2Inbound(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeHysteria2, Hysteria2Options: option.Hysteria2InboundOptions{ @@ -167,7 +167,7 @@ func TestHysteria2Outbound(t *testing.T) { }, }) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, MixedOptions: option.HTTPMixedInboundOptions{ @@ -178,7 +178,7 @@ func TestHysteria2Outbound(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeHysteria2, Hysteria2Options: option.Hysteria2OutboundOptions{ diff --git a/test/hysteria_test.go b/test/hysteria_test.go index bde1b9fa73..dce00390a4 100644 --- a/test/hysteria_test.go +++ b/test/hysteria_test.go @@ -11,7 +11,7 @@ import ( func TestHysteriaSelf(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -46,7 +46,7 @@ func TestHysteriaSelf(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, @@ -98,7 +98,7 @@ func TestHysteriaSelf(t *testing.T) { func TestHysteriaInbound(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeHysteria, HysteriaOptions: option.HysteriaInboundOptions{ @@ -149,7 +149,7 @@ func TestHysteriaOutbound(t *testing.T) { }, }) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, MixedOptions: option.HTTPMixedInboundOptions{ @@ -160,7 +160,7 @@ func TestHysteriaOutbound(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeHysteria, HysteriaOptions: option.HysteriaOutboundOptions{ diff --git a/test/inbound_detour_test.go b/test/inbound_detour_test.go index 9505c217ff..c26c81a799 100644 --- a/test/inbound_detour_test.go +++ b/test/inbound_detour_test.go @@ -13,7 +13,7 @@ func TestChainedInbound(t *testing.T) { method := shadowaead_2022.List[0] password := mkBase64(t, 16) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -49,7 +49,7 @@ func TestChainedInbound(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, diff --git a/test/mux_cool_test.go b/test/mux_cool_test.go index 81130fad9a..e72f244fc5 100644 --- a/test/mux_cool_test.go +++ b/test/mux_cool_test.go @@ -37,7 +37,7 @@ func TestMuxCoolServer(t *testing.T) { }) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeVMess, VMessOptions: option.VMessInboundOptions{ @@ -81,7 +81,7 @@ func TestMuxCoolClient(t *testing.T) { }) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, MixedOptions: option.HTTPMixedInboundOptions{ @@ -92,7 +92,7 @@ func TestMuxCoolClient(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeVMess, VMessOptions: option.VMessOutboundOptions{ @@ -112,7 +112,7 @@ func TestMuxCoolClient(t *testing.T) { func TestMuxCoolSelf(t *testing.T) { user := newUUID() startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -139,7 +139,7 @@ func TestMuxCoolSelf(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, diff --git a/test/mux_test.go b/test/mux_test.go index 8d75518554..335def2e93 100644 --- a/test/mux_test.go +++ b/test/mux_test.go @@ -55,7 +55,7 @@ func testShadowsocksMux(t *testing.T, options option.OutboundMultiplexOptions) { method := shadowaead_2022.List[0] password := mkBase64(t, 16) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -81,7 +81,7 @@ func testShadowsocksMux(t *testing.T, options option.OutboundMultiplexOptions) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, @@ -125,7 +125,7 @@ func testShadowsocksMux(t *testing.T, options option.OutboundMultiplexOptions) { func testVMessMux(t *testing.T, options option.OutboundMultiplexOptions) { user, _ := uuid.NewV4() startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -154,7 +154,7 @@ func testVMessMux(t *testing.T, options option.OutboundMultiplexOptions) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, diff --git a/test/naive_test.go b/test/naive_test.go index 1a1547da17..fe3e7dce93 100644 --- a/test/naive_test.go +++ b/test/naive_test.go @@ -13,7 +13,7 @@ import ( func TestNaiveInboundWithNginx(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeNaive, NaiveOptions: option.NaiveInboundOptions{ @@ -59,7 +59,7 @@ func TestNaiveInboundWithNginx(t *testing.T) { func TestNaiveInbound(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeNaive, NaiveOptions: option.NaiveInboundOptions{ @@ -103,7 +103,7 @@ func TestNaiveInbound(t *testing.T) { func TestNaiveHTTP3Inbound(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeNaive, NaiveOptions: option.NaiveInboundOptions{ diff --git a/test/shadowsocks_legacy_test.go b/test/shadowsocks_legacy_test.go index 8075a7df23..ae6f38e400 100644 --- a/test/shadowsocks_legacy_test.go +++ b/test/shadowsocks_legacy_test.go @@ -24,7 +24,7 @@ func testShadowsocksLegacy(t *testing.T, method string) { }, }) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, MixedOptions: option.HTTPMixedInboundOptions{ @@ -35,7 +35,7 @@ func testShadowsocksLegacy(t *testing.T, method string) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeShadowsocks, ShadowsocksOptions: option.ShadowsocksOutboundOptions{ diff --git a/test/shadowsocks_test.go b/test/shadowsocks_test.go index 4ef1ee9d93..0f7af7650d 100644 --- a/test/shadowsocks_test.go +++ b/test/shadowsocks_test.go @@ -99,7 +99,7 @@ func testShadowsocksInboundWithShadowsocksRust(t *testing.T, method string, pass Cmd: []string{"-s", F.ToString("127.0.0.1:", serverPort), "-b", F.ToString("0.0.0.0:", clientPort), "-m", method, "-k", password, "-U"}, }) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeShadowsocks, ShadowsocksOptions: option.ShadowsocksInboundOptions{ @@ -124,7 +124,7 @@ func testShadowsocksOutboundWithShadowsocksRust(t *testing.T, method string, pas Cmd: []string{"-s", F.ToString("0.0.0.0:", serverPort), "-m", method, "-k", password, "-U"}, }) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, MixedOptions: option.HTTPMixedInboundOptions{ @@ -135,7 +135,7 @@ func testShadowsocksOutboundWithShadowsocksRust(t *testing.T, method string, pas }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeShadowsocks, ShadowsocksOptions: option.ShadowsocksOutboundOptions{ @@ -154,7 +154,7 @@ func testShadowsocksOutboundWithShadowsocksRust(t *testing.T, method string, pas func testShadowsocksSelf(t *testing.T, method string, password string) { startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -177,7 +177,7 @@ func testShadowsocksSelf(t *testing.T, method string, password string) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, @@ -221,7 +221,7 @@ func TestShadowsocksUoT(t *testing.T) { method := shadowaead_2022.List[0] password := mkBase64(t, 16) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -244,7 +244,7 @@ func TestShadowsocksUoT(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, @@ -289,7 +289,7 @@ func TestShadowsocksUoT(t *testing.T) { func testShadowsocks2022EIH(t *testing.T, method string, password string) { startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -317,7 +317,7 @@ func testShadowsocks2022EIH(t *testing.T, method string, password string) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, diff --git a/test/shadowtls_test.go b/test/shadowtls_test.go index 6f9ee1e566..71e8d9fa2a 100644 --- a/test/shadowtls_test.go +++ b/test/shadowtls_test.go @@ -37,7 +37,7 @@ func testShadowTLS(t *testing.T, version int, password string, utlsEanbled bool) method := shadowaead_2022.List[0] ssPassword := mkBase64(t, 16) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, MixedOptions: option.HTTPMixedInboundOptions{ @@ -80,7 +80,7 @@ func testShadowTLS(t *testing.T, version int, password string, utlsEanbled bool) }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeShadowsocks, ShadowsocksOptions: option.ShadowsocksOutboundOptions{ @@ -142,7 +142,7 @@ func testShadowTLS(t *testing.T, version int, password string, utlsEanbled bool) func TestShadowTLSFallback(t *testing.T) { startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeShadowTLS, ShadowTLSOptions: option.ShadowTLSInboundOptions{ @@ -189,7 +189,7 @@ func TestShadowTLSInbound(t *testing.T) { Cmd: []string{"--v3", "--threads", "1", "client", "--listen", "0.0.0.0:" + F.ToString(otherPort), "--server", "127.0.0.1:" + F.ToString(serverPort), "--sni", "google.com", "--password", password}, }) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "in", @@ -232,7 +232,7 @@ func TestShadowTLSInbound(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, @@ -283,7 +283,7 @@ func TestShadowTLSOutbound(t *testing.T) { Env: []string{"RUST_LOG=trace"}, }) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, MixedOptions: option.HTTPMixedInboundOptions{ @@ -306,7 +306,7 @@ func TestShadowTLSOutbound(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeShadowsocks, ShadowsocksOptions: option.ShadowsocksOutboundOptions{ diff --git a/test/ss_plugin_test.go b/test/ss_plugin_test.go index 94606b70fd..3f837b4e92 100644 --- a/test/ss_plugin_test.go +++ b/test/ss_plugin_test.go @@ -33,7 +33,7 @@ func testShadowsocksPlugin(t *testing.T, name string, opts string, args string) }, }) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, MixedOptions: option.HTTPMixedInboundOptions{ @@ -44,7 +44,7 @@ func testShadowsocksPlugin(t *testing.T, name string, opts string, args string) }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeShadowsocks, ShadowsocksOptions: option.ShadowsocksOutboundOptions{ diff --git a/test/tfo_test.go b/test/tfo_test.go index 7bd34e2db7..458a936d6b 100644 --- a/test/tfo_test.go +++ b/test/tfo_test.go @@ -13,7 +13,7 @@ func TestTCPSlowOpen(t *testing.T) { method := shadowaead.List[0] password := mkBase64(t, 16) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -37,7 +37,7 @@ func TestTCPSlowOpen(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, diff --git a/test/tls_test.go b/test/tls_test.go index cfc6c1a50b..b42d924f42 100644 --- a/test/tls_test.go +++ b/test/tls_test.go @@ -11,7 +11,7 @@ import ( func TestUTLS(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -46,7 +46,7 @@ func TestUTLS(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, diff --git a/test/trojan_test.go b/test/trojan_test.go index f88ec88502..1a206c66a4 100644 --- a/test/trojan_test.go +++ b/test/trojan_test.go @@ -20,7 +20,7 @@ func TestTrojanOutbound(t *testing.T) { }, }) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, MixedOptions: option.HTTPMixedInboundOptions{ @@ -31,7 +31,7 @@ func TestTrojanOutbound(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeTrojan, TrojanOptions: option.TrojanOutboundOptions{ @@ -57,7 +57,7 @@ func TestTrojanOutbound(t *testing.T) { func TestTrojanSelf(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -92,7 +92,7 @@ func TestTrojanSelf(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, @@ -140,7 +140,7 @@ func TestTrojanSelf(t *testing.T) { func TestTrojanPlainSelf(t *testing.T) { startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -167,7 +167,7 @@ func TestTrojanPlainSelf(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, diff --git a/test/tuic_test.go b/test/tuic_test.go index 5b838f22f7..41fb759997 100644 --- a/test/tuic_test.go +++ b/test/tuic_test.go @@ -29,7 +29,7 @@ func testTUICSelf(t *testing.T, udpStream bool, zeroRTTHandshake bool) { udpRelayMode = "quic" } startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -62,7 +62,7 @@ func testTUICSelf(t *testing.T, udpStream bool, zeroRTTHandshake bool) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, @@ -113,7 +113,7 @@ func testTUICSelf(t *testing.T, udpStream bool, zeroRTTHandshake bool) { func TestTUICInbound(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeTUIC, TUICOptions: option.TUICInboundOptions{ @@ -160,7 +160,7 @@ func TestTUICOutbound(t *testing.T) { }, }) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, MixedOptions: option.HTTPMixedInboundOptions{ @@ -171,7 +171,7 @@ func TestTUICOutbound(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeTUIC, TUICOptions: option.TUICOutboundOptions{ diff --git a/test/v2ray_api_test.go b/test/v2ray_api_test.go index 1bea41a678..cd7ae2c445 100644 --- a/test/v2ray_api_test.go +++ b/test/v2ray_api_test.go @@ -14,7 +14,7 @@ import ( func TestV2RayAPI(t *testing.T) { i := startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "in", @@ -26,7 +26,7 @@ func TestV2RayAPI(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, Tag: "out", diff --git a/test/v2ray_grpc_test.go b/test/v2ray_grpc_test.go index fa43f753a1..5cf875435d 100644 --- a/test/v2ray_grpc_test.go +++ b/test/v2ray_grpc_test.go @@ -27,7 +27,7 @@ func testV2RayGRPCInbound(t *testing.T, forceLite bool) { require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeVMess, VMessOptions: option.VMessInboundOptions{ @@ -126,7 +126,7 @@ func testV2RayGRPCOutbound(t *testing.T, forceLite bool) { }, }) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -138,7 +138,7 @@ func testV2RayGRPCOutbound(t *testing.T, forceLite bool) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeVMess, Tag: "vmess-out", diff --git a/test/v2ray_transport_test.go b/test/v2ray_transport_test.go index c7362f345f..27074e78e1 100644 --- a/test/v2ray_transport_test.go +++ b/test/v2ray_transport_test.go @@ -44,7 +44,7 @@ func testVMessTransportSelf(t *testing.T, server *option.V2RayTransportOptions, require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -80,7 +80,7 @@ func testVMessTransportSelf(t *testing.T, server *option.V2RayTransportOptions, }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, @@ -133,7 +133,7 @@ func testTrojanTransportSelf(t *testing.T, server *option.V2RayTransportOptions, require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -169,7 +169,7 @@ func testTrojanTransportSelf(t *testing.T, server *option.V2RayTransportOptions, }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, @@ -224,7 +224,7 @@ func TestVMessQUICSelf(t *testing.T) { require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -260,7 +260,7 @@ func TestVMessQUICSelf(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, @@ -312,7 +312,7 @@ func testV2RayTransportNOTLSSelf(t *testing.T, transport *option.V2RayTransportO user, err := uuid.DefaultGenerator.NewV4() require.NoError(t, err) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -340,7 +340,7 @@ func testV2RayTransportNOTLSSelf(t *testing.T, transport *option.V2RayTransportO }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, diff --git a/test/v2ray_ws_test.go b/test/v2ray_ws_test.go index 0e238c28b6..de8d4bdce6 100644 --- a/test/v2ray_ws_test.go +++ b/test/v2ray_ws_test.go @@ -61,7 +61,7 @@ func testV2RayWebsocketInbound(t *testing.T, maxEarlyData uint32, earlyDataHeade require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeVMess, VMessOptions: option.VMessInboundOptions{ @@ -158,7 +158,7 @@ func testV2RayWebsocketOutbound(t *testing.T, maxEarlyData uint32, earlyDataHead }, }) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -170,7 +170,7 @@ func testV2RayWebsocketOutbound(t *testing.T, maxEarlyData uint32, earlyDataHead }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeVMess, Tag: "vmess-out", diff --git a/test/vmess_test.go b/test/vmess_test.go index fcf7bf8f47..9f81d9a015 100644 --- a/test/vmess_test.go +++ b/test/vmess_test.go @@ -181,7 +181,7 @@ func testVMessInboundWithV2Ray(t *testing.T, security string, alterId int, authe }) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeVMess, VMessOptions: option.VMessInboundOptions{ @@ -229,7 +229,7 @@ func testVMessOutboundWithV2Ray(t *testing.T, security string, globalPadding boo }) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, MixedOptions: option.HTTPMixedInboundOptions{ @@ -240,7 +240,7 @@ func testVMessOutboundWithV2Ray(t *testing.T, security string, globalPadding boo }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeVMess, VMessOptions: option.VMessOutboundOptions{ @@ -263,7 +263,7 @@ func testVMessOutboundWithV2Ray(t *testing.T, security string, globalPadding boo func testVMessSelf(t *testing.T, security string, alterId int, globalPadding bool, authenticatedLength bool, packetAddr bool) { user := newUUID() startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, Tag: "mixed-in", @@ -291,7 +291,7 @@ func testVMessSelf(t *testing.T, security string, alterId int, globalPadding boo }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeDirect, }, diff --git a/test/wireguard_test.go b/test/wireguard_test.go index 50e87ee04d..70c0e5a558 100644 --- a/test/wireguard_test.go +++ b/test/wireguard_test.go @@ -21,7 +21,7 @@ func _TestWireGuard(t *testing.T) { }) time.Sleep(5 * time.Second) startInstance(t, option.Options{ - Inbounds: []option.Inbound{ + Inbounds: []option.LegacyInbound{ { Type: C.TypeMixed, MixedOptions: option.HTTPMixedInboundOptions{ @@ -32,7 +32,7 @@ func _TestWireGuard(t *testing.T) { }, }, }, - Outbounds: []option.Outbound{ + LegacyOutbounds: []option.LegacyOutbound{ { Type: C.TypeWireGuard, WireGuardOptions: option.WireGuardOutboundOptions{ diff --git a/test/wrapper_test.go b/test/wrapper_test.go index d2b6b9ffc1..a7c23f335e 100644 --- a/test/wrapper_test.go +++ b/test/wrapper_test.go @@ -10,7 +10,7 @@ import ( ) func TestOptionsWrapper(t *testing.T) { - inbound := option.Inbound{ + inbound := option.LegacyInbound{ Type: C.TypeHTTP, HTTPOptions: option.HTTPMixedInboundOptions{ InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ diff --git a/transport/trojan/mux.go b/transport/trojan/mux.go index b1cc9985c9..0329bd40fe 100644 --- a/transport/trojan/mux.go +++ b/transport/trojan/mux.go @@ -8,12 +8,13 @@ import ( "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/task" "github.com/sagernet/smux" ) -func HandleMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler) error { +func HandleMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler, logger logger.ContextLogger) error { session, err := smux.Server(conn, smuxConfig()) if err != nil { return err @@ -26,7 +27,7 @@ func HandleMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata if err != nil { return err } - go newMuxConnection(ctx, stream, metadata, handler) + go newMuxConnection(ctx, stream, metadata, handler, logger) } }) group.Cleanup(func() { @@ -35,10 +36,10 @@ func HandleMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata return group.Run(ctx) } -func newMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler) { +func newMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler, logger logger.ContextLogger) { err := newMuxConnection0(ctx, conn, metadata, handler) if err != nil { - handler.NewError(ctx, E.Cause(err, "process trojan-go multiplex connection")) + logger.ErrorContext(ctx, E.Cause(err, "process trojan-go multiplex connection")) } } diff --git a/transport/trojan/service.go b/transport/trojan/service.go index 97f674ab93..978d737fe5 100644 --- a/transport/trojan/service.go +++ b/transport/trojan/service.go @@ -9,6 +9,7 @@ import ( "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/rw" @@ -17,7 +18,6 @@ import ( type Handler interface { N.TCPConnectionHandler N.UDPConnectionHandler - E.Handler } type Service[K comparable] struct { @@ -25,14 +25,16 @@ type Service[K comparable] struct { keys map[[56]byte]K handler Handler fallbackHandler N.TCPConnectionHandler + logger logger.ContextLogger } -func NewService[K comparable](handler Handler, fallbackHandler N.TCPConnectionHandler) *Service[K] { +func NewService[K comparable](handler Handler, fallbackHandler N.TCPConnectionHandler, logger logger.ContextLogger) *Service[K] { return &Service[K]{ users: make(map[K][56]byte), keys: make(map[[56]byte]K), handler: handler, fallbackHandler: fallbackHandler, + logger: logger, } } @@ -110,7 +112,7 @@ func (s *Service[K]) NewConnection(ctx context.Context, conn net.Conn, metadata return s.handler.NewPacketConnection(ctx, &PacketConn{Conn: conn}, metadata) // case CommandMux: default: - return HandleMuxConnection(ctx, conn, metadata, s.handler) + return HandleMuxConnection(ctx, conn, metadata, s.handler, s.logger) } } diff --git a/transport/wireguard/client_bind.go b/transport/wireguard/client_bind.go index 6c534532a1..20e7c0790c 100644 --- a/transport/wireguard/client_bind.go +++ b/transport/wireguard/client_bind.go @@ -10,6 +10,7 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" @@ -21,10 +22,10 @@ var _ conn.Bind = (*ClientBind)(nil) type ClientBind struct { ctx context.Context + logger logger.Logger pauseManager pause.Manager bindCtx context.Context bindDone context.CancelFunc - errorHandler E.Handler dialer N.Dialer reservedForEndpoint map[netip.AddrPort][3]uint8 connAccess sync.Mutex @@ -35,11 +36,11 @@ type ClientBind struct { reserved [3]uint8 } -func NewClientBind(ctx context.Context, errorHandler E.Handler, dialer N.Dialer, isConnect bool, connectAddr netip.AddrPort, reserved [3]uint8) *ClientBind { +func NewClientBind(ctx context.Context, logger logger.Logger, dialer N.Dialer, isConnect bool, connectAddr netip.AddrPort, reserved [3]uint8) *ClientBind { return &ClientBind{ ctx: ctx, + logger: logger, pauseManager: service.FromContext[pause.Manager](ctx), - errorHandler: errorHandler, dialer: dialer, reservedForEndpoint: make(map[netip.AddrPort][3]uint8), done: make(chan struct{}), @@ -115,7 +116,7 @@ func (c *ClientBind) receive(packets [][]byte, sizes []int, eps []conn.Endpoint) return default: } - c.errorHandler.NewError(context.Background(), E.Cause(err, "connect to server")) + c.logger.Error(E.Cause(err, "connect to server")) err = nil c.pauseManager.WaitActive() time.Sleep(time.Second) @@ -127,7 +128,7 @@ func (c *ClientBind) receive(packets [][]byte, sizes []int, eps []conn.Endpoint) select { case <-c.done: default: - c.errorHandler.NewError(context.Background(), E.Cause(err, "read packet")) + c.logger.Error(context.Background(), E.Cause(err, "read packet")) err = nil } return diff --git a/transport/wireguard/resolve.go b/transport/wireguard/resolve.go index 5b4124d201..d7a1d19c03 100644 --- a/transport/wireguard/resolve.go +++ b/transport/wireguard/resolve.go @@ -8,7 +8,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/option" - dns "github.com/sagernet/sing-dns" + "github.com/sagernet/sing-dns" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" ) From 7d26bac5acf40d6b95f8df112b6ecb31690d5550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 6 Nov 2024 17:23:00 +0800 Subject: [PATCH 06/49] Remove unused reject methods --- cmd/sing-box/cmd_format.go | 3 +-- constant/rule.go | 8 ++----- option/rule_action.go | 18 +++++++------- protocol/tun/inbound.go | 18 +++++++++----- route/route.go | 7 +++--- route/rule/rule_action.go | 49 +++++++++++++++++++++++++++----------- route/rule/rule_default.go | 4 ++-- route/rule/rule_dns.go | 4 ++-- 8 files changed, 65 insertions(+), 46 deletions(-) diff --git a/cmd/sing-box/cmd_format.go b/cmd/sing-box/cmd_format.go index 9856c76336..ab59c9ae5d 100644 --- a/cmd/sing-box/cmd_format.go +++ b/cmd/sing-box/cmd_format.go @@ -2,7 +2,6 @@ package main import ( "bytes" - "context" "os" "path/filepath" @@ -39,7 +38,7 @@ func format() error { return err } for _, optionsEntry := range optionsList { - optionsEntry.options, err = badjson.Omitempty(context.TODO(), optionsEntry.options) + optionsEntry.options, err = badjson.Omitempty(globalCtx, optionsEntry.options) if err != nil { return err } diff --git a/constant/rule.go b/constant/rule.go index c7717376c1..73227175d8 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -34,10 +34,6 @@ const ( ) const ( - RuleActionRejectMethodDefault = "default" - RuleActionRejectMethodReset = "reset" - RuleActionRejectMethodNetworkUnreachable = "network-unreachable" - RuleActionRejectMethodHostUnreachable = "host-unreachable" - RuleActionRejectMethodPortUnreachable = "port-unreachable" - RuleActionRejectMethodDrop = "drop" + RuleActionRejectMethodDefault = "default" + RuleActionRejectMethodDrop = "drop" ) diff --git a/option/rule_action.go b/option/rule_action.go index e752a2beaf..3a40e1c069 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -73,11 +73,9 @@ func (r *RuleAction) UnmarshalJSON(data []byte) error { } type _DNSRuleAction struct { - Action string `json:"action,omitempty"` - RouteOptions DNSRouteActionOptions `json:"-"` - RejectOptions RejectActionOptions `json:"-"` - SniffOptions RouteActionSniff `json:"-"` - ResolveOptions RouteActionResolve `json:"-"` + Action string `json:"action,omitempty"` + RouteOptions DNSRouteActionOptions `json:"-"` + RejectOptions RejectActionOptions `json:"-"` } type DNSRuleAction _DNSRuleAction @@ -139,6 +137,7 @@ type DNSRouteActionOptions struct { type _RejectActionOptions struct { Method string `json:"method,omitempty"` + NoDrop bool `json:"no_drop,omitempty"` } type RejectActionOptions _RejectActionOptions @@ -151,14 +150,13 @@ func (r *RejectActionOptions) UnmarshalJSON(bytes []byte) error { switch r.Method { case "", C.RuleActionRejectMethodDefault: r.Method = C.RuleActionRejectMethodDefault - case C.RuleActionRejectMethodReset, - C.RuleActionRejectMethodNetworkUnreachable, - C.RuleActionRejectMethodHostUnreachable, - C.RuleActionRejectMethodPortUnreachable, - C.RuleActionRejectMethodDrop: + case C.RuleActionRejectMethodDrop: default: return E.New("unknown reject method: " + r.Method) } + if r.Method == C.RuleActionRejectMethodDrop && r.NoDrop { + return E.New("no_drop is not allowed when method is drop") + } return nil } diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index ff679c8ec8..4be30d61b7 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -343,19 +343,25 @@ func (t *Inbound) Start() error { if err != nil { return err } - monitor.Start("initiating tun stack") - err = tunStack.Start() - monitor.Finish() t.tunStack = tunStack - if err != nil { - return err - } t.logger.Info("started at ", t.tunOptions.Name) return nil } func (t *Inbound) PostStart() error { monitor := taskmonitor.New(t.logger, C.StartTimeout) + monitor.Start("starting tun stack") + err := t.tunStack.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "starting tun stack") + } + monitor.Start("starting tun interface") + err = t.tunIf.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "starting TUN interface") + } if t.autoRedirect != nil { t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) for _, routeRuleSet := range t.routeRuleSet { diff --git a/route/route.go b/route/route.go index 6c68cf79cb..2c199a2d25 100644 --- a/route/route.go +++ b/route/route.go @@ -8,7 +8,6 @@ import ( "os" "os/user" "strings" - "syscall" "time" "github.com/sagernet/sing-box/adapter" @@ -107,7 +106,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad selectReturn = true case *rule.RuleActionReject: buf.ReleaseMulti(buffers) - N.CloseOnHandshakeFailure(conn, onClose, action.Error()) + N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx)) return nil case *rule.RuleActionHijackDNS: for _, buffer := range buffers { @@ -252,7 +251,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m selectReturn = true case *rule.RuleActionReject: N.ReleaseMultiPacketBuffer(packetBuffers) - N.CloseOnHandshakeFailure(conn, onClose, syscall.ECONNREFUSED) + N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx)) return nil case *rule.RuleActionHijackDNS: r.hijackDNSPacket(ctx, conn, packetBuffers, metadata) @@ -317,7 +316,7 @@ func (r *Router) PreMatch(metadata adapter.InboundContext) error { if !isReject { return nil } - return rejectAction.Error() + return rejectAction.Error(nil) } func (r *Router) matchRule( diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index 57b7364794..031f181c7c 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -1,10 +1,10 @@ package rule import ( + "context" "net/netip" - "os" "strings" - "syscall" + "sync" "time" "github.com/sagernet/sing-box/adapter" @@ -13,11 +13,15 @@ import ( "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-dns" "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/logger" + + "golang.org/x/sys/unix" ) -func NewRuleAction(action option.RuleAction) (adapter.RuleAction, error) { +func NewRuleAction(logger logger.ContextLogger, action option.RuleAction) (adapter.RuleAction, error) { switch action.Action { case C.RuleActionTypeRoute: return &RuleActionRoute{ @@ -29,6 +33,8 @@ func NewRuleAction(action option.RuleAction) (adapter.RuleAction, error) { case C.RuleActionTypeReject: return &RuleActionReject{ Method: action.RejectOptions.Method, + NoDrop: action.RejectOptions.NoDrop, + logger: logger, }, nil case C.RuleActionTypeHijackDNS: return &RuleActionHijackDNS{}, nil @@ -48,7 +54,7 @@ func NewRuleAction(action option.RuleAction) (adapter.RuleAction, error) { } } -func NewDNSRuleAction(action option.DNSRuleAction) adapter.RuleAction { +func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) adapter.RuleAction { switch action.Action { case C.RuleActionTypeRoute: return &RuleActionDNSRoute{ @@ -62,6 +68,8 @@ func NewDNSRuleAction(action option.DNSRuleAction) adapter.RuleAction { case C.RuleActionTypeReject: return &RuleActionReject{ Method: action.RejectOptions.Method, + NoDrop: action.RejectOptions.NoDrop, + logger: logger, } default: panic(F.ToString("unknown rule action: ", action.Action)) @@ -107,7 +115,11 @@ func (r *RuleActionReturn) String() string { } type RuleActionReject struct { - Method string + Method string + NoDrop bool + logger logger.ContextLogger + dropAccess sync.Mutex + dropCounter []time.Time } func (r *RuleActionReject) Type() string { @@ -121,21 +133,30 @@ func (r *RuleActionReject) String() string { return F.ToString("reject(", r.Method, ")") } -func (r *RuleActionReject) Error() error { +func (r *RuleActionReject) Error(ctx context.Context) error { + var returnErr error switch r.Method { - case C.RuleActionRejectMethodReset: - return os.ErrClosed - case C.RuleActionRejectMethodNetworkUnreachable: - return syscall.ENETUNREACH - case C.RuleActionRejectMethodHostUnreachable: - return syscall.EHOSTUNREACH - case C.RuleActionRejectMethodDefault, C.RuleActionRejectMethodPortUnreachable: - return syscall.ECONNREFUSED + case C.RuleActionRejectMethodDefault: + returnErr = unix.ECONNREFUSED case C.RuleActionRejectMethodDrop: return tun.ErrDrop default: panic(F.ToString("unknown reject method: ", r.Method)) } + r.dropAccess.Lock() + defer r.dropAccess.Unlock() + timeNow := time.Now() + r.dropCounter = common.Filter(r.dropCounter, func(t time.Time) bool { + return timeNow.Sub(t) <= 30*time.Second + }) + r.dropCounter = append(r.dropCounter, timeNow) + if len(r.dropCounter) > 50 { + if ctx != nil { + r.logger.DebugContext(ctx, "dropped due to flooding") + } + return tun.ErrDrop + } + return returnErr } type RuleActionHijackDNS struct{} diff --git a/route/rule/rule_default.go b/route/rule/rule_default.go index 4f5d1e8a3c..a337c19f22 100644 --- a/route/rule/rule_default.go +++ b/route/rule/rule_default.go @@ -52,7 +52,7 @@ type RuleItem interface { } func NewDefaultRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.DefaultRule) (*DefaultRule, error) { - action, err := NewRuleAction(options.RuleAction) + action, err := NewRuleAction(logger, options.RuleAction) if err != nil { return nil, E.Cause(err, "action") } @@ -254,7 +254,7 @@ type LogicalRule struct { } func NewLogicalRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { - action, err := NewRuleAction(options.RuleAction) + action, err := NewRuleAction(logger, options.RuleAction) if err != nil { return nil, E.Cause(err, "action") } diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go index 6e57633d10..2218f6a30c 100644 --- a/route/rule/rule_dns.go +++ b/route/rule/rule_dns.go @@ -51,7 +51,7 @@ func NewDefaultDNSRule(ctx context.Context, router adapter.Router, logger log.Co rule := &DefaultDNSRule{ abstractDefaultRule: abstractDefaultRule{ invert: options.Invert, - action: NewDNSRuleAction(options.DNSRuleAction), + action: NewDNSRuleAction(logger, options.DNSRuleAction), }, } if len(options.Inbound) > 0 { @@ -287,7 +287,7 @@ func NewLogicalDNSRule(ctx context.Context, router adapter.Router, logger log.Co abstractLogicalRule: abstractLogicalRule{ rules: make([]adapter.HeadlessRule, len(options.Rules)), invert: options.Invert, - action: NewDNSRuleAction(options.DNSRuleAction), + action: NewDNSRuleAction(logger, options.DNSRuleAction), }, } switch options.Mode { From afd341adfdb6884ff3ec548f026c05c72bf32450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 6 Nov 2024 17:30:40 +0800 Subject: [PATCH 07/49] Improve rule actions --- adapter/inbound.go | 4 +- constant/rule.go | 13 +-- option/rule.go | 14 +-- option/rule_action.go | 182 ++++++++++++++++++++++++++++++++----- option/rule_dns.go | 16 ++-- protocol/dns/handle.go | 1 + route/route.go | 130 ++++++++++++++------------ route/route_dns.go | 107 +++++++++++++++++----- route/rule/rule_action.go | 111 ++++++++++++++++++---- route/rule/rule_default.go | 4 +- 10 files changed, 433 insertions(+), 149 deletions(-) diff --git a/adapter/inbound.go b/adapter/inbound.go index ca3e9e5900..f9ed17085a 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -57,7 +57,9 @@ type InboundContext struct { // Deprecated InboundOptions option.InboundOptions UDPDisableDomainUnmapping bool - DNSServer string + UDPConnect bool + + DNSServer string DestinationAddresses []netip.Addr SourceGeoIPCode string diff --git a/constant/rule.go b/constant/rule.go index 73227175d8..b1f91c608c 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -25,12 +25,13 @@ const ( ) const ( - RuleActionTypeRoute = "route" - RuleActionTypeReturn = "return" - RuleActionTypeReject = "reject" - RuleActionTypeHijackDNS = "hijack-dns" - RuleActionTypeSniff = "sniff" - RuleActionTypeResolve = "resolve" + RuleActionTypeRoute = "route" + RuleActionTypeRouteOptions = "route-options" + RuleActionTypeDirect = "direct" + RuleActionTypeReject = "reject" + RuleActionTypeHijackDNS = "hijack-dns" + RuleActionTypeSniff = "sniff" + RuleActionTypeResolve = "resolve" ) const ( diff --git a/option/rule.go b/option/rule.go index 07e6ddbec4..952afa6146 100644 --- a/option/rule.go +++ b/option/rule.go @@ -109,7 +109,7 @@ type DefaultRule struct { RuleAction } -func (r *DefaultRule) MarshalJSON() ([]byte, error) { +func (r DefaultRule) MarshalJSON() ([]byte, error) { return badjson.MarshallObjects(r.RawDefaultRule, r.RuleAction) } @@ -128,27 +128,27 @@ func (r *DefaultRule) IsValid() bool { return !reflect.DeepEqual(r, defaultValue) } -type _LogicalRule struct { +type RawLogicalRule struct { Mode string `json:"mode"` Rules []Rule `json:"rules,omitempty"` Invert bool `json:"invert,omitempty"` } type LogicalRule struct { - _LogicalRule + RawLogicalRule RuleAction } -func (r *LogicalRule) MarshalJSON() ([]byte, error) { - return badjson.MarshallObjects(r._LogicalRule, r.RuleAction) +func (r LogicalRule) MarshalJSON() ([]byte, error) { + return badjson.MarshallObjects(r.RawLogicalRule, r.RuleAction) } func (r *LogicalRule) UnmarshalJSON(data []byte) error { - err := json.Unmarshal(data, &r._LogicalRule) + err := json.Unmarshal(data, &r.RawLogicalRule) if err != nil { return err } - return badjson.UnmarshallExcluded(data, &r._LogicalRule, &r.RuleAction) + return badjson.UnmarshallExcluded(data, &r.RawLogicalRule, &r.RuleAction) } func (r *LogicalRule) IsValid() bool { diff --git a/option/rule_action.go b/option/rule_action.go index 3a40e1c069..edc197de0c 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -1,30 +1,41 @@ package option import ( + "fmt" + "time" + C "github.com/sagernet/sing-box/constant" + dns "github.com/sagernet/sing-dns" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" ) type _RuleAction struct { - Action string `json:"action,omitempty"` - RouteOptions RouteActionOptions `json:"-"` - RejectOptions RejectActionOptions `json:"-"` - SniffOptions RouteActionSniff `json:"-"` - ResolveOptions RouteActionResolve `json:"-"` + Action string `json:"action,omitempty"` + RouteOptions RouteActionOptions `json:"-"` + RouteOptionsOptions RouteOptionsActionOptions `json:"-"` + DirectOptions DirectActionOptions `json:"-"` + RejectOptions RejectActionOptions `json:"-"` + SniffOptions RouteActionSniff `json:"-"` + ResolveOptions RouteActionResolve `json:"-"` } type RuleAction _RuleAction func (r RuleAction) MarshalJSON() ([]byte, error) { + if r.Action == "" { + return json.Marshal(struct{}{}) + } var v any switch r.Action { case C.RuleActionTypeRoute: r.Action = "" v = r.RouteOptions - case C.RuleActionTypeReturn: - v = nil + case C.RuleActionTypeRouteOptions: + v = r.RouteOptionsOptions + case C.RuleActionTypeDirect: + v = r.DirectOptions case C.RuleActionTypeReject: v = r.RejectOptions case C.RuleActionTypeHijackDNS: @@ -52,8 +63,10 @@ func (r *RuleAction) UnmarshalJSON(data []byte) error { case "", C.RuleActionTypeRoute: r.Action = C.RuleActionTypeRoute v = &r.RouteOptions - case C.RuleActionTypeReturn: - v = nil + case C.RuleActionTypeRouteOptions: + v = &r.RouteOptionsOptions + case C.RuleActionTypeDirect: + v = &r.DirectOptions case C.RuleActionTypeReject: v = &r.RejectOptions case C.RuleActionTypeHijackDNS: @@ -73,29 +86,30 @@ func (r *RuleAction) UnmarshalJSON(data []byte) error { } type _DNSRuleAction struct { - Action string `json:"action,omitempty"` - RouteOptions DNSRouteActionOptions `json:"-"` - RejectOptions RejectActionOptions `json:"-"` + Action string `json:"action,omitempty"` + RouteOptions DNSRouteActionOptions `json:"-"` + RouteOptionsOptions DNSRouteOptionsActionOptions `json:"-"` + RejectOptions RejectActionOptions `json:"-"` } type DNSRuleAction _DNSRuleAction func (r DNSRuleAction) MarshalJSON() ([]byte, error) { + if r.Action == "" { + return json.Marshal(struct{}{}) + } var v any switch r.Action { case C.RuleActionTypeRoute: r.Action = "" v = r.RouteOptions - case C.RuleActionTypeReturn: - v = nil + case C.RuleActionTypeRouteOptions: + v = r.RouteOptionsOptions case C.RuleActionTypeReject: v = r.RejectOptions default: return nil, E.New("unknown DNS rule action: " + r.Action) } - if v == nil { - return badjson.MarshallObjects((_DNSRuleAction)(r)) - } return badjson.MarshallObjects((_DNSRuleAction)(r), v) } @@ -109,8 +123,8 @@ func (r *DNSRuleAction) UnmarshalJSON(data []byte) error { case "", C.RuleActionTypeRoute: r.Action = C.RuleActionTypeRoute v = &r.RouteOptions - case C.RuleActionTypeReturn: - v = nil + case C.RuleActionTypeRouteOptions: + v = &r.RouteOptionsOptions case C.RuleActionTypeReject: v = &r.RejectOptions default: @@ -123,18 +137,136 @@ func (r *DNSRuleAction) UnmarshalJSON(data []byte) error { return badjson.UnmarshallExcluded(data, (*_DNSRuleAction)(r), v) } -type RouteActionOptions struct { - Outbound string `json:"outbound"` - UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` +type _RouteActionOptions struct { + Outbound string `json:"outbound,omitempty"` +} + +type RouteActionOptions _RouteActionOptions + +func (r *RouteActionOptions) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, (*_RouteActionOptions)(r)) + if err != nil { + return err + } + if r.Outbound == "" { + return E.New("missing outbound") + } + return nil +} + +type _RouteOptionsActionOptions struct { + UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` + UDPConnect bool `json:"udp_connect,omitempty"` +} + +type RouteOptionsActionOptions _RouteOptionsActionOptions + +func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, (*_RouteOptionsActionOptions)(r)) + if err != nil { + return err + } + if *r == (RouteOptionsActionOptions{}) { + return E.New("empty route option action") + } + return nil +} + +type _DNSRouteActionOptions struct { + Server string `json:"server,omitempty"` + // Deprecated: Use DNSRouteOptionsActionOptions instead. + DisableCache bool `json:"disable_cache,omitempty"` + // Deprecated: Use DNSRouteOptionsActionOptions instead. + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + // Deprecated: Use DNSRouteOptionsActionOptions instead. + ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"` +} + +type DNSRouteActionOptions _DNSRouteActionOptions + +func (r *DNSRouteActionOptions) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, (*_DNSRouteActionOptions)(r)) + if err != nil { + return err + } + if r.Server == "" { + return E.New("missing server") + } + return nil } -type DNSRouteActionOptions struct { - Server string `json:"server"` +type _DNSRouteOptionsActionOptions struct { DisableCache bool `json:"disable_cache,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"` } +type DNSRouteOptionsActionOptions _DNSRouteOptionsActionOptions + +func (r *DNSRouteOptionsActionOptions) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, (*_DNSRouteOptionsActionOptions)(r)) + if err != nil { + return err + } + if *r == (DNSRouteOptionsActionOptions{}) { + return E.New("empty DNS route option action") + } + return nil +} + +type _DirectActionOptions DialerOptions + +type DirectActionOptions _DirectActionOptions + +func (d DirectActionOptions) Descriptions() []string { + var descriptions []string + if d.BindInterface != "" { + descriptions = append(descriptions, "bind_interface="+d.BindInterface) + } + if d.Inet4BindAddress != nil { + descriptions = append(descriptions, "inet4_bind_address="+d.Inet4BindAddress.Build().String()) + } + if d.Inet6BindAddress != nil { + descriptions = append(descriptions, "inet6_bind_address="+d.Inet6BindAddress.Build().String()) + } + if d.RoutingMark != 0 { + descriptions = append(descriptions, "routing_mark="+fmt.Sprintf("0x%x", d.RoutingMark)) + } + if d.ReuseAddr { + descriptions = append(descriptions, "reuse_addr") + } + if d.ConnectTimeout != 0 { + descriptions = append(descriptions, "connect_timeout="+time.Duration(d.ConnectTimeout).String()) + } + if d.TCPFastOpen { + descriptions = append(descriptions, "tcp_fast_open") + } + if d.TCPMultiPath { + descriptions = append(descriptions, "tcp_multi_path") + } + if d.UDPFragment != nil { + descriptions = append(descriptions, "udp_fragment="+fmt.Sprint(*d.UDPFragment)) + } + if d.DomainStrategy != DomainStrategy(dns.DomainStrategyAsIS) { + descriptions = append(descriptions, "domain_strategy="+d.DomainStrategy.String()) + } + if d.FallbackDelay != 0 { + descriptions = append(descriptions, "fallback_delay="+time.Duration(d.FallbackDelay).String()) + } + return descriptions +} + +func (d *DirectActionOptions) UnmarshalJSON(data []byte) error { + err := json.Unmarshal(data, (*_DirectActionOptions)(d)) + if err != nil { + return err + } + if d.Detour != "" { + return E.New("detour is not available in the current context") + } + return nil +} + type _RejectActionOptions struct { Method string `json:"method,omitempty"` NoDrop bool `json:"no_drop,omitempty"` @@ -155,7 +287,7 @@ func (r *RejectActionOptions) UnmarshalJSON(bytes []byte) error { return E.New("unknown reject method: " + r.Method) } if r.Method == C.RuleActionRejectMethodDrop && r.NoDrop { - return E.New("no_drop is not allowed when method is drop") + return E.New("no_drop is not available in current context") } return nil } diff --git a/option/rule_dns.go b/option/rule_dns.go index 8c4b6ab83b..0683e16a47 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -111,7 +111,7 @@ type DefaultDNSRule struct { DNSRuleAction } -func (r *DefaultDNSRule) MarshalJSON() ([]byte, error) { +func (r DefaultDNSRule) MarshalJSON() ([]byte, error) { return badjson.MarshallObjects(r.RawDefaultDNSRule, r.DNSRuleAction) } @@ -123,34 +123,34 @@ func (r *DefaultDNSRule) UnmarshalJSON(data []byte) error { return badjson.UnmarshallExcluded(data, &r.RawDefaultDNSRule, &r.DNSRuleAction) } -func (r *DefaultDNSRule) IsValid() bool { +func (r DefaultDNSRule) IsValid() bool { var defaultValue DefaultDNSRule defaultValue.Invert = r.Invert defaultValue.DNSRuleAction = r.DNSRuleAction return !reflect.DeepEqual(r, defaultValue) } -type _LogicalDNSRule struct { +type RawLogicalDNSRule struct { Mode string `json:"mode"` Rules []DNSRule `json:"rules,omitempty"` Invert bool `json:"invert,omitempty"` } type LogicalDNSRule struct { - _LogicalDNSRule + RawLogicalDNSRule DNSRuleAction } -func (r *LogicalDNSRule) MarshalJSON() ([]byte, error) { - return badjson.MarshallObjects(r._LogicalDNSRule, r.DNSRuleAction) +func (r LogicalDNSRule) MarshalJSON() ([]byte, error) { + return badjson.MarshallObjects(r.RawLogicalDNSRule, r.DNSRuleAction) } func (r *LogicalDNSRule) UnmarshalJSON(data []byte) error { - err := json.Unmarshal(data, &r._LogicalDNSRule) + err := json.Unmarshal(data, &r.RawLogicalDNSRule) if err != nil { return err } - return badjson.UnmarshallExcluded(data, &r._LogicalDNSRule, &r.DNSRuleAction) + return badjson.UnmarshallExcluded(data, &r.RawLogicalDNSRule, &r.DNSRuleAction) } func (r *LogicalDNSRule) IsValid() bool { diff --git a/protocol/dns/handle.go b/protocol/dns/handle.go index 23ed1c0c79..bc58d9e208 100644 --- a/protocol/dns/handle.go +++ b/protocol/dns/handle.go @@ -43,6 +43,7 @@ func HandleStreamDNSRequest(ctx context.Context, router adapter.Router, conn net go func() error { response, err := router.Exchange(adapter.WithContext(ctx, &metadataInQuery), &message) if err != nil { + conn.Close() return err } responseBuffer := buf.NewPacket() diff --git a/route/route.go b/route/route.go index 2c199a2d25..854fa4f19f 100644 --- a/route/route.go +++ b/route/route.go @@ -87,23 +87,34 @@ 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, conn, nil, -1) + selectedRule, _, buffers, _, err := r.matchRule(ctx, &metadata, false, conn, nil) if err != nil { return err } - var selectedOutbound adapter.Outbound - var selectReturn bool + var ( + // selectedOutbound adapter.Outbound + selectedDialer N.Dialer + selectedTag string + selectedDescription string + ) if selectedRule != nil { switch action := selectedRule.Action().(type) { case *rule.RuleActionRoute: - var loaded bool - selectedOutbound, loaded = r.Outbound(action.Outbound) + selectedOutbound, loaded := r.Outbound(action.Outbound) if !loaded { buf.ReleaseMulti(buffers) return E.New("outbound not found: ", action.Outbound) } - case *rule.RuleActionReturn: - selectReturn = true + if !common.Contains(selectedOutbound.Network(), N.NetworkTCP) { + buf.ReleaseMulti(buffers) + return E.New("TCP is not supported by outbound: ", selectedOutbound.Tag()) + } + selectedDialer = selectedOutbound + selectedTag = selectedOutbound.Tag() + selectedDescription = F.ToString("outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]") + case *rule.RuleActionDirect: + selectedDialer = action.Dialer + selectedDescription = action.String() case *rule.RuleActionReject: buf.ReleaseMulti(buffers) N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx)) @@ -116,17 +127,16 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad return nil } } - if selectedRule == nil || selectReturn { + if selectedRule == nil { if r.defaultOutboundForConnection == nil { buf.ReleaseMulti(buffers) return E.New("missing default outbound with TCP support") } - selectedOutbound = r.defaultOutboundForConnection - } - if !common.Contains(selectedOutbound.Network(), N.NetworkTCP) { - buf.ReleaseMulti(buffers) - return E.New("TCP is not supported by outbound: ", selectedOutbound.Tag()) + selectedDialer = r.defaultOutboundForConnection + selectedTag = r.defaultOutboundForConnection.Tag() + selectedDescription = F.ToString("outbound/", r.defaultOutboundForConnection.Type(), "[", r.defaultOutboundForConnection.Tag(), "]") } + for _, buffer := range buffers { conn = bufio.NewCachedConn(conn, buffer) } @@ -137,10 +147,10 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad } if r.v2rayServer != nil { if statsService := r.v2rayServer.StatsService(); statsService != nil { - conn = statsService.RoutedConnection(metadata.Inbound, selectedOutbound.Tag(), metadata.User, conn) + conn = statsService.RoutedConnection(metadata.Inbound, selectedTag, metadata.User, conn) } } - legacyOutbound, isLegacy := selectedOutbound.(adapter.ConnectionHandler) + legacyOutbound, isLegacy := selectedDialer.(adapter.ConnectionHandler) if isLegacy { err = legacyOutbound.NewConnection(ctx, conn, metadata) if err != nil { @@ -148,7 +158,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad if onClose != nil { onClose(err) } - return E.Cause(err, "outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]") + return E.Cause(err, selectedDescription) } else { if onClose != nil { onClose(nil) @@ -157,13 +167,13 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad return nil } // TODO - err = outbound.NewConnection(ctx, selectedOutbound, conn, metadata) + err = outbound.NewConnection(ctx, selectedDialer, conn, metadata) if err != nil { conn.Close() if onClose != nil { onClose(err) } - return E.Cause(err, "outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]") + return E.Cause(err, selectedDescription) } else { if onClose != nil { onClose(nil) @@ -231,24 +241,34 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m conn = deadline.NewPacketConn(bufio.NewNetPacketConn(conn)) }*/ - selectedRule, _, _, packetBuffers, err := r.matchRule(ctx, &metadata, false, nil, conn, -1) + selectedRule, _, _, packetBuffers, err := r.matchRule(ctx, &metadata, false, nil, conn) if err != nil { return err } - var selectedOutbound adapter.Outbound + var ( + selectedDialer N.Dialer + selectedTag string + selectedDescription string + ) var selectReturn bool if selectedRule != nil { switch action := selectedRule.Action().(type) { case *rule.RuleActionRoute: - var loaded bool - selectedOutbound, loaded = r.Outbound(action.Outbound) + selectedOutbound, loaded := r.Outbound(action.Outbound) if !loaded { N.ReleaseMultiPacketBuffer(packetBuffers) return E.New("outbound not found: ", action.Outbound) } - metadata.UDPDisableDomainUnmapping = action.UDPDisableDomainUnmapping - case *rule.RuleActionReturn: - selectReturn = true + if !common.Contains(selectedOutbound.Network(), N.NetworkUDP) { + N.ReleaseMultiPacketBuffer(packetBuffers) + return E.New("UDP is not supported by outbound: ", selectedOutbound.Tag()) + } + selectedDialer = selectedOutbound + selectedTag = selectedOutbound.Tag() + selectedDescription = F.ToString("outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]") + case *rule.RuleActionDirect: + selectedDialer = action.Dialer + selectedDescription = action.String() case *rule.RuleActionReject: N.ReleaseMultiPacketBuffer(packetBuffers) N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx)) @@ -263,11 +283,9 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m N.ReleaseMultiPacketBuffer(packetBuffers) return E.New("missing default outbound with UDP support") } - selectedOutbound = r.defaultOutboundForPacketConnection - } - if !common.Contains(selectedOutbound.Network(), N.NetworkUDP) { - N.ReleaseMultiPacketBuffer(packetBuffers) - return E.New("UDP is not supported by outbound: ", selectedOutbound.Tag()) + selectedDialer = r.defaultOutboundForPacketConnection + selectedTag = r.defaultOutboundForPacketConnection.Tag() + selectedDescription = F.ToString("outbound/", r.defaultOutboundForPacketConnection.Type(), "[", r.defaultOutboundForPacketConnection.Tag(), "]") } for _, buffer := range packetBuffers { conn = bufio.NewCachedPacketConn(conn, buffer.Buffer, buffer.Destination) @@ -280,32 +298,32 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m } if r.v2rayServer != nil { if statsService := r.v2rayServer.StatsService(); statsService != nil { - conn = statsService.RoutedPacketConnection(metadata.Inbound, selectedOutbound.Tag(), metadata.User, conn) + conn = statsService.RoutedPacketConnection(metadata.Inbound, selectedTag, metadata.User, conn) } } if metadata.FakeIP { conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination) } - legacyOutbound, isLegacy := selectedOutbound.(adapter.PacketConnectionHandler) + legacyOutbound, isLegacy := selectedDialer.(adapter.PacketConnectionHandler) if isLegacy { err = legacyOutbound.NewPacketConnection(ctx, conn, metadata) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { - return E.Cause(err, "outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]") + return E.Cause(err, selectedDescription) } return nil } // TODO - err = outbound.NewPacketConnection(ctx, selectedOutbound, conn, metadata) + err = outbound.NewPacketConnection(ctx, selectedDialer, conn, metadata) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { - return E.Cause(err, "outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]") + return E.Cause(err, selectedDescription) } return nil } func (r *Router) PreMatch(metadata adapter.InboundContext) error { - selectedRule, _, _, _, err := r.matchRule(r.ctx, &metadata, true, nil, nil, -1) + selectedRule, _, _, _, err := r.matchRule(r.ctx, &metadata, true, nil, nil) if err != nil { return err } @@ -321,7 +339,7 @@ func (r *Router) PreMatch(metadata adapter.InboundContext) error { func (r *Router) matchRule( ctx context.Context, metadata *adapter.InboundContext, preMatch bool, - inputConn net.Conn, inputPacketConn N.PacketConn, ruleIndex int, + inputConn net.Conn, inputPacketConn N.PacketConn, ) ( selectedRule adapter.Rule, selectedRuleIndex int, buffers []*buf.Buffer, packetBuffers []*N.PacketBuffer, fatalErr error, @@ -416,24 +434,10 @@ func (r *Router) matchRule( } match: - for ruleIndex < len(r.rules) { - rules := r.rules - if ruleIndex != -1 { - rules = rules[ruleIndex+1:] - } - var ( - currentRule adapter.Rule - currentRuleIndex int - matched bool - ) - for currentRuleIndex, currentRule = range rules { - if currentRule.Match(metadata) { - matched = true - break - } - } - if !matched { - break + for currentRuleIndex, currentRule := range r.rules { + metadata.ResetRuleCache() + if !currentRule.Match(metadata) { + continue } if !preMatch { ruleDescription := currentRule.String() @@ -444,7 +448,7 @@ match: } } else { switch currentRule.Action().Type() { - case C.RuleActionTypeReject, C.RuleActionTypeResolve: + case C.RuleActionTypeReject: ruleDescription := currentRule.String() if ruleDescription != "" { r.logger.DebugContext(ctx, "pre-match[", currentRuleIndex, "] ", currentRule, " => ", currentRule.Action()) @@ -454,6 +458,12 @@ match: } } switch action := currentRule.Action().(type) { + case *rule.RuleActionRoute: + metadata.UDPDisableDomainUnmapping = action.UDPDisableDomainUnmapping + metadata.UDPConnect = action.UDPConnect + case *rule.RuleActionRouteOptions: + metadata.UDPDisableDomainUnmapping = action.UDPDisableDomainUnmapping + metadata.UDPConnect = action.UDPConnect case *rule.RuleActionSniff: if !preMatch { newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn) @@ -476,12 +486,16 @@ match: if fatalErr != nil { return } - default: + } + actionType := currentRule.Action().Type() + if actionType == C.RuleActionTypeRoute || + actionType == C.RuleActionTypeReject || + actionType == C.RuleActionTypeHijackDNS || + (actionType == C.RuleActionTypeSniff && preMatch) { selectedRule = currentRule selectedRuleIndex = currentRuleIndex break match } - ruleIndex = currentRuleIndex } if !preMatch && metadata.Destination.Addr.IsUnspecified() { newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{}, inputConn, inputPacketConn) diff --git a/route/route_dns.go b/route/route_dns.go index 60aff6a9ff..c11c07fe1b 100644 --- a/route/route_dns.go +++ b/route/route_dns.go @@ -8,8 +8,10 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" R "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing-dns" + tun "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/cache" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" @@ -48,38 +50,63 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, if ruleIndex != -1 { dnsRules = dnsRules[ruleIndex+1:] } - for currentRuleIndex, rule := range dnsRules { - if rule.WithAddressLimit() && !isAddressQuery { + for currentRuleIndex, currentRule := range dnsRules { + if currentRule.WithAddressLimit() && !isAddressQuery { continue } metadata.ResetRuleCache() - if rule.Match(metadata) { + if currentRule.Match(metadata) { displayRuleIndex := currentRuleIndex if displayRuleIndex != -1 { displayRuleIndex += displayRuleIndex + 1 } - if routeAction, isRoute := rule.Action().(*R.RuleActionDNSRoute); isRoute { - transport, loaded := r.transportMap[routeAction.Server] + ruleDescription := currentRule.String() + if ruleDescription != "" { + r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] ", currentRule, " => ", currentRule.Action()) + } else { + r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action()) + } + switch action := currentRule.Action().(type) { + case *R.RuleActionDNSRoute: + transport, loaded := r.transportMap[action.Server] if !loaded { - r.dnsLogger.ErrorContext(ctx, "transport not found: ", routeAction.Server) + r.dnsLogger.ErrorContext(ctx, "transport not found: ", action.Server) continue } _, isFakeIP := transport.(adapter.FakeIPTransport) if isFakeIP && !allowFakeIP { continue } - options.DisableCache = isFakeIP || routeAction.DisableCache - options.RewriteTTL = routeAction.RewriteTTL - options.ClientSubnet = routeAction.ClientSubnet + if isFakeIP || action.DisableCache { + options.DisableCache = true + } + if action.RewriteTTL != nil { + options.RewriteTTL = action.RewriteTTL + } + if action.ClientSubnet.IsValid() { + options.ClientSubnet = action.ClientSubnet + } if domainStrategy, dsLoaded := r.transportDomainStrategy[transport]; dsLoaded { options.Strategy = domainStrategy } else { options.Strategy = r.defaultDomainStrategy } - r.dnsLogger.DebugContext(ctx, "match[", displayRuleIndex, "] ", rule.String(), " => ", rule.Action()) - return transport, options, rule, currentRuleIndex - } else { - return nil, options, rule, currentRuleIndex + r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action()) + return transport, options, currentRule, currentRuleIndex + case *R.RuleActionDNSRouteOptions: + if action.DisableCache { + options.DisableCache = true + } + if action.RewriteTTL != nil { + options.RewriteTTL = action.RewriteTTL + } + if action.ClientSubnet.IsValid() { + options.ClientSubnet = action.ClientSubnet + } + r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action()) + case *R.RuleActionReject: + r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action()) + return nil, options, currentRule, currentRuleIndex } } } @@ -93,9 +120,19 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, } func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) { - if len(message.Question) > 0 { - r.dnsLogger.DebugContext(ctx, "exchange ", formatQuestion(message.Question[0].String())) + if len(message.Question) != 1 { + r.dnsLogger.WarnContext(ctx, "bad question size: ", len(message.Question)) + responseMessage := mDNS.Msg{ + MsgHdr: mDNS.MsgHdr{ + Id: message.Id, + Response: true, + Rcode: mDNS.RcodeFormatError, + }, + Question: message.Question, + } + return &responseMessage, nil } + r.dnsLogger.DebugContext(ctx, "exchange ", formatQuestion(message.Question[0].String())) var ( response *mDNS.Msg cached bool @@ -107,16 +144,14 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er var metadata *adapter.InboundContext ctx, metadata = adapter.ExtendContext(ctx) metadata.Destination = M.Socksaddr{} - if len(message.Question) > 0 { - metadata.QueryType = message.Question[0].Qtype - switch metadata.QueryType { - case mDNS.TypeA: - metadata.IPVersion = 4 - case mDNS.TypeAAAA: - metadata.IPVersion = 6 - } - metadata.Domain = fqdnToDomain(message.Question[0].Name) + metadata.QueryType = message.Question[0].Qtype + switch metadata.QueryType { + case mDNS.TypeA: + metadata.IPVersion = 4 + case mDNS.TypeAAAA: + metadata.IPVersion = 6 } + metadata.Domain = fqdnToDomain(message.Question[0].Name) var ( options dns.QueryOptions rule adapter.DNSRule @@ -127,6 +162,17 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er dnsCtx := adapter.OverrideContext(ctx) var addressLimit bool transport, options, rule, ruleIndex = r.matchDNS(ctx, true, ruleIndex, isAddressQuery(message)) + if rule != nil { + switch action := rule.Action().(type) { + case *R.RuleActionReject: + switch action.Method { + case C.RuleActionRejectMethodDefault: + return dns.FixedResponse(message.Id, message.Question[0], nil, 0), nil + case C.RuleActionRejectMethodDrop: + return nil, tun.ErrDrop + } + } + } if rule != nil && rule.WithAddressLimit() { addressLimit = true response, err = r.dnsClient.ExchangeWithResponseCheck(dnsCtx, transport, message, options, func(response *mDNS.Msg) bool { @@ -164,7 +210,7 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, er if err != nil { return nil, err } - if r.dnsReverseMapping != nil && len(message.Question) > 0 && response != nil && len(response.Answer) > 0 { + if r.dnsReverseMapping != nil && response != nil && len(response.Answer) > 0 { if _, isFakeIP := transport.(adapter.FakeIPTransport); !isFakeIP { for _, answer := range response.Answer { switch record := answer.(type) { @@ -238,6 +284,17 @@ func (r *Router) Lookup(ctx context.Context, domain string, strategy dns.DomainS if strategy != dns.DomainStrategyAsIS { options.Strategy = strategy } + if rule != nil { + switch action := rule.Action().(type) { + case *R.RuleActionReject: + switch action.Method { + case C.RuleActionRejectMethodDefault: + return nil, nil + case C.RuleActionRejectMethodDrop: + return nil, tun.ErrDrop + } + } + } if rule != nil && rule.WithAddressLimit() { addressLimit = true responseAddrs, err = r.dnsClient.LookupWithResponseCheck(dnsCtx, transport, domain, options, func(responseAddrs []netip.Addr) bool { diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index 031f181c7c..620260d078 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -5,9 +5,11 @@ import ( "net/netip" "strings" "sync" + "syscall" "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" "github.com/sagernet/sing-box/option" @@ -17,19 +19,42 @@ import ( E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/logger" - - "golang.org/x/sys/unix" + N "github.com/sagernet/sing/common/network" ) -func NewRuleAction(logger logger.ContextLogger, action option.RuleAction) (adapter.RuleAction, error) { +func NewRuleAction(router adapter.Router, logger logger.ContextLogger, action option.RuleAction) (adapter.RuleAction, error) { switch action.Action { + case "": + return nil, nil case C.RuleActionTypeRoute: return &RuleActionRoute{ - Outbound: action.RouteOptions.Outbound, - UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping, + Outbound: action.RouteOptions.Outbound, + }, nil + case C.RuleActionTypeRouteOptions: + return &RuleActionRouteOptions{ + UDPDisableDomainUnmapping: action.RouteOptionsOptions.UDPDisableDomainUnmapping, + UDPConnect: action.RouteOptionsOptions.UDPConnect, + }, nil + case C.RuleActionTypeDirect: + directDialer, err := dialer.New(router, option.DialerOptions(action.DirectOptions)) + if err != nil { + return nil, err + } + var description string + descriptions := action.DirectOptions.Descriptions() + switch len(descriptions) { + case 0: + case 1: + description = F.ToString("(", descriptions[0], ")") + case 2: + description = F.ToString("(", descriptions[0], ",", descriptions[1], ")") + default: + description = F.ToString("(", descriptions[0], ",", descriptions[1], ",...)") + } + return &RuleActionDirect{ + Dialer: directDialer, + description: description, }, nil - case C.RuleActionTypeReturn: - return &RuleActionReturn{}, nil case C.RuleActionTypeReject: return &RuleActionReject{ Method: action.RejectOptions.Method, @@ -56,6 +81,8 @@ func NewRuleAction(logger logger.ContextLogger, action option.RuleAction) (adapt func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) adapter.RuleAction { switch action.Action { + case "": + return nil case C.RuleActionTypeRoute: return &RuleActionDNSRoute{ Server: action.RouteOptions.Server, @@ -63,8 +90,12 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) RewriteTTL: action.RouteOptions.RewriteTTL, ClientSubnet: action.RouteOptions.ClientSubnet.Build(), } - case C.RuleActionTypeReturn: - return &RuleActionReturn{} + case C.RuleActionTypeRouteOptions: + return &RuleActionDNSRouteOptions{ + DisableCache: action.RouteOptionsOptions.DisableCache, + RewriteTTL: action.RouteOptionsOptions.RewriteTTL, + ClientSubnet: action.RouteOptionsOptions.ClientSubnet.Build(), + } case C.RuleActionTypeReject: return &RuleActionReject{ Method: action.RejectOptions.Method, @@ -77,8 +108,7 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) } type RuleActionRoute struct { - Outbound string - UDPDisableDomainUnmapping bool + Outbound string } func (r *RuleActionRoute) Type() string { @@ -89,6 +119,26 @@ func (r *RuleActionRoute) String() string { return F.ToString("route(", r.Outbound, ")") } +type RuleActionRouteOptions struct { + UDPDisableDomainUnmapping bool + UDPConnect bool +} + +func (r *RuleActionRouteOptions) Type() string { + return C.RuleActionTypeRouteOptions +} + +func (r *RuleActionRouteOptions) String() string { + var descriptions []string + if r.UDPDisableDomainUnmapping { + descriptions = append(descriptions, "udp-disable-domain-unmapping") + } + if r.UDPConnect { + descriptions = append(descriptions, "udp-connect") + } + return F.ToString("route-options(", strings.Join(descriptions, ","), ")") +} + type RuleActionDNSRoute struct { Server string DisableCache bool @@ -104,14 +154,41 @@ func (r *RuleActionDNSRoute) String() string { return F.ToString("route(", r.Server, ")") } -type RuleActionReturn struct{} +type RuleActionDNSRouteOptions struct { + DisableCache bool + RewriteTTL *uint32 + ClientSubnet netip.Prefix +} + +func (r *RuleActionDNSRouteOptions) Type() string { + return C.RuleActionTypeRouteOptions +} + +func (r *RuleActionDNSRouteOptions) String() string { + var descriptions []string + if r.DisableCache { + descriptions = append(descriptions, "disable-cache") + } + if r.RewriteTTL != nil { + descriptions = append(descriptions, F.ToString("rewrite-ttl(", *r.RewriteTTL, ")")) + } + if r.ClientSubnet.IsValid() { + descriptions = append(descriptions, F.ToString("client-subnet(", r.ClientSubnet, ")")) + } + return F.ToString("route-options(", strings.Join(descriptions, ","), ")") +} + +type RuleActionDirect struct { + Dialer N.Dialer + description string +} -func (r *RuleActionReturn) Type() string { - return C.RuleActionTypeReturn +func (r *RuleActionDirect) Type() string { + return C.RuleActionTypeDirect } -func (r *RuleActionReturn) String() string { - return "return" +func (r *RuleActionDirect) String() string { + return "direct" + r.description } type RuleActionReject struct { @@ -137,7 +214,7 @@ func (r *RuleActionReject) Error(ctx context.Context) error { var returnErr error switch r.Method { case C.RuleActionRejectMethodDefault: - returnErr = unix.ECONNREFUSED + returnErr = syscall.ECONNREFUSED case C.RuleActionRejectMethodDrop: return tun.ErrDrop default: diff --git a/route/rule/rule_default.go b/route/rule/rule_default.go index a337c19f22..566c816e11 100644 --- a/route/rule/rule_default.go +++ b/route/rule/rule_default.go @@ -52,7 +52,7 @@ type RuleItem interface { } func NewDefaultRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.DefaultRule) (*DefaultRule, error) { - action, err := NewRuleAction(logger, options.RuleAction) + action, err := NewRuleAction(router, logger, options.RuleAction) if err != nil { return nil, E.Cause(err, "action") } @@ -254,7 +254,7 @@ type LogicalRule struct { } func NewLogicalRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { - action, err := NewRuleAction(logger, options.RuleAction) + action, err := NewRuleAction(router, logger, options.RuleAction) if err != nil { return nil, E.Cause(err, "action") } From 7ed10b35d0642bbac4c39e6f3a9ec685b16f3ed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 7 Nov 2024 12:02:36 +0800 Subject: [PATCH 08/49] Implement new deprecated warnings --- experimental/deprecated/constants.go | 30 ++++++++++++++++++++++++++++ experimental/deprecated/manager.go | 1 - experimental/libbox/service.go | 4 ++-- option/outbound.go | 11 ++++++++++ option/rule_action.go | 15 +++++++------- option/rule_dns.go | 15 +++++++------- 6 files changed, 59 insertions(+), 17 deletions(-) diff --git a/experimental/deprecated/constants.go b/experimental/deprecated/constants.go index f5b67119f2..8f7eee11ff 100644 --- a/experimental/deprecated/constants.go +++ b/experimental/deprecated/constants.go @@ -86,9 +86,39 @@ var OptionTUNAddressX = Note{ MigrationLink: "https://sing-box.sagernet.org/migration/#tun-address-fields-are-merged", } +var OptionSpecialOutbounds = Note{ + Name: "special-outbounds", + Description: "legacy special outbounds", + DeprecatedVersion: "1.11.0", + ScheduledVersion: "1.13.0", + EnvName: "SPECIAL_OUTBOUNDS", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions", +} + +var OptionInboundOptions = Note{ + Name: "inbound-options", + Description: "legacy inbound fields", + DeprecatedVersion: "1.11.0", + ScheduledVersion: "1.13.0", + EnvName: "INBOUND_OPTIONS", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions", +} + +var OptionLegacyDNSRouteOptions = Note{ + Name: "legacy-dns-route-options", + Description: "legacy dns route options", + DeprecatedVersion: "1.11.0", + ScheduledVersion: "1.12.0", + EnvName: "LEGACY_DNS_ROUTE_OPTIONS", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-legacy-dns-route-options-to-rule-actions", +} + var Options = []Note{ OptionBadMatchSource, OptionGEOIP, OptionGEOSITE, OptionTUNAddressX, + OptionSpecialOutbounds, + OptionInboundOptions, + OptionLegacyDNSRouteOptions, } diff --git a/experimental/deprecated/manager.go b/experimental/deprecated/manager.go index d12acf48fa..d7cae1797a 100644 --- a/experimental/deprecated/manager.go +++ b/experimental/deprecated/manager.go @@ -2,7 +2,6 @@ package deprecated import ( "context" - "github.com/sagernet/sing/service" ) diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index f6f56b6f6f..24fbdb4c94 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -43,16 +43,16 @@ type BoxService struct { func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) { ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry()) + ctx = service.ContextWith[deprecated.Manager](ctx, new(deprecatedManager)) + ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID) options, err := parseConfig(ctx, configContent) if err != nil { return nil, err } runtimeDebug.FreeOSMemory() ctx, cancel := context.WithCancel(ctx) - ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID) urlTestHistoryStorage := urltest.NewHistoryStorage() ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage) - ctx = service.ContextWith[deprecated.Manager](ctx, new(deprecatedManager)) platformWrapper := &platformInterfaceWrapper{iif: platformInterface, useProcFS: platformInterface.UseProcFS()} ctx = service.ContextWith[platform.Interface](ctx, platformWrapper) instance, err := box.New(box.Options{ diff --git a/option/outbound.go b/option/outbound.go index 00a20aa577..1dddb35487 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -3,6 +3,8 @@ package option import ( "context" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/deprecated" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" @@ -35,6 +37,10 @@ func (h *Outbound) UnmarshalJSONContext(ctx context.Context, content []byte) err if registry == nil { return E.New("missing outbound options registry in context") } + switch h.Type { + case C.TypeBlock, C.TypeDNS: + deprecated.Report(ctx, deprecated.OptionSpecialOutbounds) + } options, loaded := registry.CreateOptions(h.Type) if !loaded { return E.New("unknown outbound type: ", h.Type) @@ -43,6 +49,11 @@ func (h *Outbound) UnmarshalJSONContext(ctx context.Context, content []byte) err if err != nil { return err } + if listenWrapper, isListen := options.(ListenOptionsWrapper); isListen { + if listenWrapper.TakeListenOptions().InboundOptions != (InboundOptions{}) { + deprecated.Report(ctx, deprecated.OptionInboundOptions) + } + } h.Options = options return nil } diff --git a/option/rule_action.go b/option/rule_action.go index edc197de0c..a7244e179b 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -1,10 +1,12 @@ package option import ( + "context" "fmt" "time" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/deprecated" dns "github.com/sagernet/sing-dns" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" @@ -113,7 +115,7 @@ func (r DNSRuleAction) MarshalJSON() ([]byte, error) { return badjson.MarshallObjects((_DNSRuleAction)(r), v) } -func (r *DNSRuleAction) UnmarshalJSON(data []byte) error { +func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) error { err := json.Unmarshal(data, (*_DNSRuleAction)(r)) if err != nil { return err @@ -130,11 +132,7 @@ func (r *DNSRuleAction) UnmarshalJSON(data []byte) error { default: return E.New("unknown DNS rule action: " + r.Action) } - if v == nil { - // check unknown fields - return json.UnmarshalDisallowUnknownFields(data, &_DNSRuleAction{}) - } - return badjson.UnmarshallExcluded(data, (*_DNSRuleAction)(r), v) + return badjson.UnmarshallExcludedContext(ctx, data, (*_DNSRuleAction)(r), v) } type _RouteActionOptions struct { @@ -184,7 +182,7 @@ type _DNSRouteActionOptions struct { type DNSRouteActionOptions _DNSRouteActionOptions -func (r *DNSRouteActionOptions) UnmarshalJSON(data []byte) error { +func (r *DNSRouteActionOptions) UnmarshalJSONContext(ctx context.Context, data []byte) error { err := json.Unmarshal(data, (*_DNSRouteActionOptions)(r)) if err != nil { return err @@ -192,6 +190,9 @@ func (r *DNSRouteActionOptions) UnmarshalJSON(data []byte) error { if r.Server == "" { return E.New("missing server") } + if r.DisableCache || r.RewriteTTL != nil || r.ClientSubnet != nil { + deprecated.Report(ctx, deprecated.OptionLegacyDNSRouteOptions) + } return nil } diff --git a/option/rule_dns.go b/option/rule_dns.go index 0683e16a47..e7e454bb0e 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -1,6 +1,7 @@ package option import ( + "context" "reflect" C "github.com/sagernet/sing-box/constant" @@ -32,7 +33,7 @@ func (r DNSRule) MarshalJSON() ([]byte, error) { return badjson.MarshallObjects((_DNSRule)(r), v) } -func (r *DNSRule) UnmarshalJSON(bytes []byte) error { +func (r *DNSRule) UnmarshalJSONContext(ctx context.Context, bytes []byte) error { err := json.Unmarshal(bytes, (*_DNSRule)(r)) if err != nil { return err @@ -47,7 +48,7 @@ func (r *DNSRule) UnmarshalJSON(bytes []byte) error { default: return E.New("unknown rule type: " + r.Type) } - err = badjson.UnmarshallExcluded(bytes, (*_DNSRule)(r), v) + err = badjson.UnmarshallExcludedContext(ctx, bytes, (*_DNSRule)(r), v) if err != nil { return err } @@ -115,12 +116,12 @@ func (r DefaultDNSRule) MarshalJSON() ([]byte, error) { return badjson.MarshallObjects(r.RawDefaultDNSRule, r.DNSRuleAction) } -func (r *DefaultDNSRule) UnmarshalJSON(data []byte) error { - err := json.Unmarshal(data, &r.RawDefaultDNSRule) +func (r *DefaultDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error { + err := json.UnmarshalContext(ctx, data, &r.RawDefaultDNSRule) if err != nil { return err } - return badjson.UnmarshallExcluded(data, &r.RawDefaultDNSRule, &r.DNSRuleAction) + return badjson.UnmarshallExcludedContext(ctx, data, &r.RawDefaultDNSRule, &r.DNSRuleAction) } func (r DefaultDNSRule) IsValid() bool { @@ -145,12 +146,12 @@ func (r LogicalDNSRule) MarshalJSON() ([]byte, error) { return badjson.MarshallObjects(r.RawLogicalDNSRule, r.DNSRuleAction) } -func (r *LogicalDNSRule) UnmarshalJSON(data []byte) error { +func (r *LogicalDNSRule) UnmarshalJSONContext(ctx context.Context, data []byte) error { err := json.Unmarshal(data, &r.RawLogicalDNSRule) if err != nil { return err } - return badjson.UnmarshallExcluded(data, &r.RawLogicalDNSRule, &r.DNSRuleAction) + return badjson.UnmarshallExcludedContext(ctx, data, &r.RawLogicalDNSRule, &r.DNSRuleAction) } func (r *LogicalDNSRule) IsValid() bool { From 040a188c669a016f45f60fb513e53fd2b33e229d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 7 Nov 2024 13:44:00 +0800 Subject: [PATCH 09/49] Implement udp connect --- adapter/outbound/default.go | 110 ++++++++++++++++++++++++++---------- 1 file changed, 79 insertions(+), 31 deletions(-) diff --git a/adapter/outbound/default.go b/adapter/outbound/default.go index 78b9bfd8d6..bb58ff54d9 100644 --- a/adapter/outbound/default.go +++ b/adapter/outbound/default.go @@ -20,6 +20,7 @@ import ( ) func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext) error { + defer conn.Close() ctx = adapter.WithContext(ctx, &metadata) var outConn net.Conn var err error @@ -40,6 +41,7 @@ func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata a } func NewDirectConnection(ctx context.Context, router adapter.Router, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, domainStrategy dns.DomainStrategy) error { + defer conn.Close() ctx = adapter.WithContext(ctx, &metadata) var outConn net.Conn var err error @@ -67,29 +69,49 @@ func NewDirectConnection(ctx context.Context, router adapter.Router, this N.Dial } func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata adapter.InboundContext) error { + defer conn.Close() ctx = adapter.WithContext(ctx, &metadata) - var outConn net.PacketConn - var destinationAddress netip.Addr - var err error - if len(metadata.DestinationAddresses) > 0 { - outConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses) + var ( + outPacketConn net.PacketConn + outConn net.Conn + destinationAddress netip.Addr + err error + ) + if metadata.UDPConnect { + if len(metadata.DestinationAddresses) > 0 { + outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses) + } else { + outConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination) + } + if err != nil { + return N.ReportHandshakeFailure(conn, err) + } + outPacketConn = bufio.NewUnbindPacketConn(outConn) + connRemoteAddr := M.AddrFromNet(outConn.RemoteAddr()) + if connRemoteAddr != metadata.Destination.Addr { + destinationAddress = connRemoteAddr + } } else { - outConn, err = this.ListenPacket(ctx, metadata.Destination) - } - if err != nil { - return N.ReportHandshakeFailure(conn, err) + if len(metadata.DestinationAddresses) > 0 { + outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses) + } else { + outPacketConn, err = this.ListenPacket(ctx, metadata.Destination) + } + if err != nil { + return N.ReportHandshakeFailure(conn, err) + } } - err = N.ReportPacketConnHandshakeSuccess(conn, outConn) + err = N.ReportPacketConnHandshakeSuccess(conn, outPacketConn) if err != nil { - outConn.Close() + outPacketConn.Close() return err } if destinationAddress.IsValid() { if metadata.Destination.IsFqdn() { if metadata.UDPDisableDomainUnmapping { - outConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) + outPacketConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) } else { - outConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) + outPacketConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) } } if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded { @@ -104,37 +126,63 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, case C.ProtocolDNS: ctx, conn = canceler.NewPacketConn(ctx, conn, C.DNSTimeout) } - return bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(outConn)) + return bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(outPacketConn)) } func NewDirectPacketConnection(ctx context.Context, router adapter.Router, this N.Dialer, conn N.PacketConn, metadata adapter.InboundContext, domainStrategy dns.DomainStrategy) error { + defer conn.Close() ctx = adapter.WithContext(ctx, &metadata) - var outConn net.PacketConn - var destinationAddress netip.Addr - var err error - if len(metadata.DestinationAddresses) > 0 { - outConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses) - } else if metadata.Destination.IsFqdn() { - var destinationAddresses []netip.Addr - destinationAddresses, err = router.Lookup(ctx, metadata.Destination.Fqdn, domainStrategy) + var ( + outPacketConn net.PacketConn + outConn net.Conn + destinationAddress netip.Addr + err error + ) + if metadata.UDPConnect { + if len(metadata.DestinationAddresses) > 0 { + outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses) + } else if metadata.Destination.IsFqdn() { + var destinationAddresses []netip.Addr + destinationAddresses, err = router.Lookup(ctx, metadata.Destination.Fqdn, domainStrategy) + if err != nil { + return N.ReportHandshakeFailure(conn, err) + } + outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, destinationAddresses) + } else { + outConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination) + } if err != nil { return N.ReportHandshakeFailure(conn, err) } - outConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, destinationAddresses) + connRemoteAddr := M.AddrFromNet(outConn.RemoteAddr()) + if connRemoteAddr != metadata.Destination.Addr { + destinationAddress = connRemoteAddr + } } else { - outConn, err = this.ListenPacket(ctx, metadata.Destination) - } - if err != nil { - return N.ReportHandshakeFailure(conn, err) + if len(metadata.DestinationAddresses) > 0 { + outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses) + } else if metadata.Destination.IsFqdn() { + var destinationAddresses []netip.Addr + destinationAddresses, err = router.Lookup(ctx, metadata.Destination.Fqdn, domainStrategy) + if err != nil { + return N.ReportHandshakeFailure(conn, err) + } + outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, destinationAddresses) + } else { + outPacketConn, err = this.ListenPacket(ctx, metadata.Destination) + } + if err != nil { + return N.ReportHandshakeFailure(conn, err) + } } - err = N.ReportPacketConnHandshakeSuccess(conn, outConn) + err = N.ReportPacketConnHandshakeSuccess(conn, outPacketConn) if err != nil { - outConn.Close() + outPacketConn.Close() return err } if destinationAddress.IsValid() { if metadata.Destination.IsFqdn() { - outConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) + outPacketConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) } if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded { natConn.UpdateDestination(destinationAddress) @@ -148,7 +196,7 @@ func NewDirectPacketConnection(ctx context.Context, router adapter.Router, this case C.ProtocolDNS: ctx, conn = canceler.NewPacketConn(ctx, conn, C.DNSTimeout) } - return bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(outConn)) + return bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(outPacketConn)) } func CopyEarlyConn(ctx context.Context, conn net.Conn, serverConn net.Conn) error { From f12a294fb770c0c61ce183ad9dc4c0ee022daacb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 18 Nov 2024 18:55:34 +0800 Subject: [PATCH 10/49] Migrate bad options to library --- common/dialer/default.go | 5 +- common/listener/listener.go | 3 +- common/listener/listener_tcp.go | 3 +- common/listener/listener_udp.go | 3 +- experimental/deprecated/manager.go | 1 + option/dns.go | 32 ++-- option/experimental.go | 32 ++-- option/group.go | 14 +- option/inbound.go | 35 ++--- option/ntp.go | 8 +- option/outbound.go | 31 ++-- option/platform.go | 7 +- option/rule.go | 67 ++++----- option/rule_action.go | 18 ++- option/rule_dns.go | 71 ++++----- option/rule_set.go | 49 ++++--- option/simple.go | 9 +- option/ssh.go | 18 ++- option/time_unit.go | 226 ----------------------------- option/tls.go | 80 +++++----- option/tls_acme.go | 3 +- option/tuic.go | 28 ++-- option/tun.go | 69 ++++----- option/tun_platform.go | 6 +- option/types.go | 132 ----------------- option/v2ray_transport.go | 37 ++--- option/wireguard.go | 26 ++-- protocol/tun/inbound.go | 3 +- route/router.go | 4 +- route/rule/rule_action.go | 4 +- transport/sip003/v2ray.go | 3 +- 31 files changed, 356 insertions(+), 671 deletions(-) delete mode 100644 option/time_unit.go diff --git a/common/dialer/default.go b/common/dialer/default.go index 02678961a7..0c8cee18fd 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -3,6 +3,7 @@ package dialer import ( "context" "net" + "net/netip" "time" "github.com/sagernet/sing-box/adapter" @@ -102,7 +103,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi udpAddr4 string ) if options.Inet4BindAddress != nil { - bindAddr := options.Inet4BindAddress.Build() + bindAddr := options.Inet4BindAddress.Build(netip.IPv4Unspecified()) dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()} udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()} udpAddr4 = M.SocksaddrFrom(bindAddr, 0).String() @@ -113,7 +114,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi udpAddr6 string ) if options.Inet6BindAddress != nil { - bindAddr := options.Inet6BindAddress.Build() + bindAddr := options.Inet6BindAddress.Build(netip.IPv6Unspecified()) dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()} udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()} udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String() diff --git a/common/listener/listener.go b/common/listener/listener.go index b42b0434b2..289f15c51b 100644 --- a/common/listener/listener.go +++ b/common/listener/listener.go @@ -3,6 +3,7 @@ package listener import ( "context" "net" + "net/netip" "sync/atomic" "github.com/sagernet/sing-box/adapter" @@ -92,7 +93,7 @@ func (l *Listener) Start() error { if l.setSystemProxy { listenPort := M.SocksaddrFromNet(l.tcpListener.Addr()).Port var listenAddrString string - listenAddr := l.listenOptions.Listen.Build() + listenAddr := l.listenOptions.Listen.Build(netip.IPv4Unspecified()) if listenAddr.IsUnspecified() { listenAddrString = "127.0.0.1" } else { diff --git a/common/listener/listener_tcp.go b/common/listener/listener_tcp.go index 02cef3f04d..646d40179a 100644 --- a/common/listener/listener_tcp.go +++ b/common/listener/listener_tcp.go @@ -2,6 +2,7 @@ package listener import ( "net" + "net/netip" "time" "github.com/sagernet/sing-box/adapter" @@ -16,7 +17,7 @@ import ( func (l *Listener) ListenTCP() (net.Listener, error) { var err error - bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(), l.listenOptions.ListenPort) + bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort) var tcpListener net.Listener var listenConfig net.ListenConfig if l.listenOptions.TCPKeepAlive >= 0 { diff --git a/common/listener/listener_udp.go b/common/listener/listener_udp.go index 0c3220a7f1..10d6dc3868 100644 --- a/common/listener/listener_udp.go +++ b/common/listener/listener_udp.go @@ -2,6 +2,7 @@ package listener import ( "net" + "net/netip" "os" "github.com/sagernet/sing/common/buf" @@ -12,7 +13,7 @@ import ( ) func (l *Listener) ListenUDP() (net.PacketConn, error) { - bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(), l.listenOptions.ListenPort) + bindAddr := M.SocksaddrFrom(l.listenOptions.Listen.Build(netip.AddrFrom4([4]byte{127, 0, 0, 1})), l.listenOptions.ListenPort) var lc net.ListenConfig var udpFragment bool if l.listenOptions.UDPFragment != nil { diff --git a/experimental/deprecated/manager.go b/experimental/deprecated/manager.go index d7cae1797a..d12acf48fa 100644 --- a/experimental/deprecated/manager.go +++ b/experimental/deprecated/manager.go @@ -2,6 +2,7 @@ package deprecated import ( "context" + "github.com/sagernet/sing/service" ) diff --git a/option/dns.go b/option/dns.go index be947583a0..32c1ac2e1b 100644 --- a/option/dns.go +++ b/option/dns.go @@ -1,6 +1,10 @@ package option -import "net/netip" +import ( + "net/netip" + + "github.com/sagernet/sing/common/json/badoption" +) type DNSOptions struct { Servers []DNSServerOptions `json:"servers,omitempty"` @@ -12,22 +16,22 @@ type DNSOptions struct { } type DNSServerOptions struct { - Tag string `json:"tag,omitempty"` - Address string `json:"address"` - AddressResolver string `json:"address_resolver,omitempty"` - AddressStrategy DomainStrategy `json:"address_strategy,omitempty"` - AddressFallbackDelay Duration `json:"address_fallback_delay,omitempty"` - Strategy DomainStrategy `json:"strategy,omitempty"` - Detour string `json:"detour,omitempty"` - ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"` + Tag string `json:"tag,omitempty"` + Address string `json:"address"` + AddressResolver string `json:"address_resolver,omitempty"` + AddressStrategy DomainStrategy `json:"address_strategy,omitempty"` + AddressFallbackDelay badoption.Duration `json:"address_fallback_delay,omitempty"` + Strategy DomainStrategy `json:"strategy,omitempty"` + Detour string `json:"detour,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type DNSClientOptions struct { - Strategy DomainStrategy `json:"strategy,omitempty"` - DisableCache bool `json:"disable_cache,omitempty"` - DisableExpire bool `json:"disable_expire,omitempty"` - IndependentCache bool `json:"independent_cache,omitempty"` - ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"` + Strategy DomainStrategy `json:"strategy,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + DisableExpire bool `json:"disable_expire,omitempty"` + IndependentCache bool `json:"independent_cache,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type DNSFakeIPOptions struct { diff --git a/option/experimental.go b/option/experimental.go index 6ab6638550..bf0df9e78c 100644 --- a/option/experimental.go +++ b/option/experimental.go @@ -1,5 +1,7 @@ package option +import "github.com/sagernet/sing/common/json/badoption" + type ExperimentalOptions struct { CacheFile *CacheFileOptions `json:"cache_file,omitempty"` ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"` @@ -8,24 +10,24 @@ type ExperimentalOptions struct { } type CacheFileOptions struct { - Enabled bool `json:"enabled,omitempty"` - Path string `json:"path,omitempty"` - CacheID string `json:"cache_id,omitempty"` - StoreFakeIP bool `json:"store_fakeip,omitempty"` - StoreRDRC bool `json:"store_rdrc,omitempty"` - RDRCTimeout Duration `json:"rdrc_timeout,omitempty"` + Enabled bool `json:"enabled,omitempty"` + Path string `json:"path,omitempty"` + CacheID string `json:"cache_id,omitempty"` + StoreFakeIP bool `json:"store_fakeip,omitempty"` + StoreRDRC bool `json:"store_rdrc,omitempty"` + RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"` } type ClashAPIOptions struct { - ExternalController string `json:"external_controller,omitempty"` - ExternalUI string `json:"external_ui,omitempty"` - ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` - ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` - Secret string `json:"secret,omitempty"` - DefaultMode string `json:"default_mode,omitempty"` - ModeList []string `json:"-"` - AccessControlAllowOrigin Listable[string] `json:"access_control_allow_origin,omitempty"` - AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"` + ExternalController string `json:"external_controller,omitempty"` + ExternalUI string `json:"external_ui,omitempty"` + ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"` + ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"` + Secret string `json:"secret,omitempty"` + DefaultMode string `json:"default_mode,omitempty"` + ModeList []string `json:"-"` + AccessControlAllowOrigin badoption.Listable[string] `json:"access_control_allow_origin,omitempty"` + AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"` // Deprecated: migrated to global cache file CacheFile string `json:"cache_file,omitempty"` diff --git a/option/group.go b/option/group.go index 72a0f63702..02b3a5ecb9 100644 --- a/option/group.go +++ b/option/group.go @@ -1,5 +1,7 @@ package option +import "github.com/sagernet/sing/common/json/badoption" + type SelectorOutboundOptions struct { Outbounds []string `json:"outbounds"` Default string `json:"default,omitempty"` @@ -7,10 +9,10 @@ type SelectorOutboundOptions struct { } type URLTestOutboundOptions struct { - Outbounds []string `json:"outbounds"` - URL string `json:"url,omitempty"` - Interval Duration `json:"interval,omitempty"` - Tolerance uint16 `json:"tolerance,omitempty"` - IdleTimeout Duration `json:"idle_timeout,omitempty"` - InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` + Outbounds []string `json:"outbounds"` + URL string `json:"url,omitempty"` + Interval badoption.Duration `json:"interval,omitempty"` + Tolerance uint16 `json:"tolerance,omitempty"` + IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"` + InterruptExistConnections bool `json:"interrupt_exist_connections,omitempty"` } diff --git a/option/inbound.go b/option/inbound.go index 651d02846c..a67719fa04 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -7,6 +7,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" "github.com/sagernet/sing/service" ) @@ -49,24 +50,24 @@ func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) erro // Deprecated: Use rule action instead type InboundOptions struct { - SniffEnabled bool `json:"sniff,omitempty"` - SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"` - SniffTimeout Duration `json:"sniff_timeout,omitempty"` - DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` - UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` - Detour string `json:"detour,omitempty"` + SniffEnabled bool `json:"sniff,omitempty"` + SniffOverrideDestination bool `json:"sniff_override_destination,omitempty"` + SniffTimeout badoption.Duration `json:"sniff_timeout,omitempty"` + DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` + UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` + Detour string `json:"detour,omitempty"` } type ListenOptions struct { - Listen *ListenAddress `json:"listen,omitempty"` - ListenPort uint16 `json:"listen_port,omitempty"` - TCPKeepAlive Duration `json:"tcp_keep_alive,omitempty"` - TCPKeepAliveInterval Duration `json:"tcp_keep_alive_interval,omitempty"` - TCPFastOpen bool `json:"tcp_fast_open,omitempty"` - TCPMultiPath bool `json:"tcp_multi_path,omitempty"` - UDPFragment *bool `json:"udp_fragment,omitempty"` - UDPFragmentDefault bool `json:"-"` - UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` + Listen *badoption.Addr `json:"listen,omitempty"` + ListenPort uint16 `json:"listen_port,omitempty"` + TCPKeepAlive badoption.Duration `json:"tcp_keep_alive,omitempty"` + TCPKeepAliveInterval badoption.Duration `json:"tcp_keep_alive_interval,omitempty"` + TCPFastOpen bool `json:"tcp_fast_open,omitempty"` + TCPMultiPath bool `json:"tcp_multi_path,omitempty"` + UDPFragment *bool `json:"udp_fragment,omitempty"` + UDPFragmentDefault bool `json:"-"` + UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` // Deprecated: removed ProxyProtocol bool `json:"proxy_protocol,omitempty"` @@ -75,7 +76,7 @@ type ListenOptions struct { InboundOptions } -type UDPTimeoutCompat Duration +type UDPTimeoutCompat badoption.Duration func (c UDPTimeoutCompat) MarshalJSON() ([]byte, error) { return json.Marshal((time.Duration)(c).String()) @@ -88,7 +89,7 @@ func (c *UDPTimeoutCompat) UnmarshalJSON(data []byte) error { *c = UDPTimeoutCompat(time.Second * time.Duration(valueNumber)) return nil } - return json.Unmarshal(data, (*Duration)(c)) + return json.Unmarshal(data, (*badoption.Duration)(c)) } type ListenOptionsWrapper interface { diff --git a/option/ntp.go b/option/ntp.go index 0bd2489ac2..d441d95e86 100644 --- a/option/ntp.go +++ b/option/ntp.go @@ -1,9 +1,11 @@ package option +import "github.com/sagernet/sing/common/json/badoption" + type NTPOptions struct { - Enabled bool `json:"enabled,omitempty"` - Interval Duration `json:"interval,omitempty"` - WriteToSystem bool `json:"write_to_system,omitempty"` + Enabled bool `json:"enabled,omitempty"` + Interval badoption.Duration `json:"interval,omitempty"` + WriteToSystem bool `json:"write_to_system,omitempty"` ServerOptions DialerOptions } diff --git a/option/outbound.go b/option/outbound.go index 1dddb35487..0e2d5874da 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -8,6 +8,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/service" ) @@ -64,21 +65,21 @@ type DialerOptionsWrapper interface { } type DialerOptions struct { - Detour string `json:"detour,omitempty"` - BindInterface string `json:"bind_interface,omitempty"` - Inet4BindAddress *ListenAddress `json:"inet4_bind_address,omitempty"` - Inet6BindAddress *ListenAddress `json:"inet6_bind_address,omitempty"` - ProtectPath string `json:"protect_path,omitempty"` - RoutingMark uint32 `json:"routing_mark,omitempty"` - ReuseAddr bool `json:"reuse_addr,omitempty"` - ConnectTimeout Duration `json:"connect_timeout,omitempty"` - TCPFastOpen bool `json:"tcp_fast_open,omitempty"` - TCPMultiPath bool `json:"tcp_multi_path,omitempty"` - UDPFragment *bool `json:"udp_fragment,omitempty"` - UDPFragmentDefault bool `json:"-"` - DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` - FallbackDelay Duration `json:"fallback_delay,omitempty"` - IsWireGuardListener bool `json:"-"` + Detour string `json:"detour,omitempty"` + BindInterface string `json:"bind_interface,omitempty"` + Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"` + Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"` + ProtectPath string `json:"protect_path,omitempty"` + RoutingMark uint32 `json:"routing_mark,omitempty"` + ReuseAddr bool `json:"reuse_addr,omitempty"` + ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"` + TCPFastOpen bool `json:"tcp_fast_open,omitempty"` + TCPMultiPath bool `json:"tcp_multi_path,omitempty"` + UDPFragment *bool `json:"udp_fragment,omitempty"` + UDPFragmentDefault bool `json:"-"` + DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` + FallbackDelay badoption.Duration `json:"fallback_delay,omitempty"` + IsWireGuardListener bool `json:"-"` } func (o *DialerOptions) TakeDialerOptions() DialerOptions { diff --git a/option/platform.go b/option/platform.go index a43cbf230f..e4ecd6fa1e 100644 --- a/option/platform.go +++ b/option/platform.go @@ -3,6 +3,7 @@ package option import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badoption" ) type OnDemandOptions struct { @@ -12,10 +13,10 @@ type OnDemandOptions struct { type OnDemandRule struct { Action *OnDemandRuleAction `json:"action,omitempty"` - DNSSearchDomainMatch Listable[string] `json:"dns_search_domain_match,omitempty"` - DNSServerAddressMatch Listable[string] `json:"dns_server_address_match,omitempty"` + DNSSearchDomainMatch badoption.Listable[string] `json:"dns_search_domain_match,omitempty"` + DNSServerAddressMatch badoption.Listable[string] `json:"dns_server_address_match,omitempty"` InterfaceTypeMatch *OnDemandRuleInterfaceType `json:"interface_type_match,omitempty"` - SSIDMatch Listable[string] `json:"ssid_match,omitempty"` + SSIDMatch badoption.Listable[string] `json:"ssid_match,omitempty"` ProbeURL string `json:"probe_url,omitempty"` } diff --git a/option/rule.go b/option/rule.go index 952afa6146..d5ff9925a6 100644 --- a/option/rule.go +++ b/option/rule.go @@ -8,6 +8,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" ) type _Rule struct { @@ -66,39 +67,39 @@ func (r Rule) IsValid() bool { } type RawDefaultRule struct { - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol Listable[string] `json:"protocol,omitempty"` - Client Listable[string] `json:"client,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - GeoIP Listable[string] `json:"geoip,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` - IPCIDR Listable[string] `json:"ip_cidr,omitempty"` - IPIsPrivate bool `json:"ip_is_private,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - ProcessPathRegex Listable[string] `json:"process_path_regex,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - RuleSet Listable[string] `json:"rule_set,omitempty"` - RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` - Invert bool `json:"invert,omitempty"` + Inbound badoption.Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + Network badoption.Listable[string] `json:"network,omitempty"` + AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` + Protocol badoption.Listable[string] `json:"protocol,omitempty"` + Client badoption.Listable[string] `json:"client,omitempty"` + Domain badoption.Listable[string] `json:"domain,omitempty"` + DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` + Geosite badoption.Listable[string] `json:"geosite,omitempty"` + SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` + GeoIP badoption.Listable[string] `json:"geoip,omitempty"` + SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` + IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,omitempty"` + SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` + Port badoption.Listable[uint16] `json:"port,omitempty"` + PortRange badoption.Listable[string] `json:"port_range,omitempty"` + ProcessName badoption.Listable[string] `json:"process_name,omitempty"` + ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` + ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` + PackageName badoption.Listable[string] `json:"package_name,omitempty"` + User badoption.Listable[string] `json:"user,omitempty"` + UserID badoption.Listable[int32] `json:"user_id,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` + Invert bool `json:"invert,omitempty"` // Deprecated: renamed to rule_set_ip_cidr_match_source Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` diff --git a/option/rule_action.go b/option/rule_action.go index a7244e179b..7a76391ca6 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -3,6 +3,7 @@ package option import ( "context" "fmt" + "net/netip" "time" C "github.com/sagernet/sing-box/constant" @@ -11,6 +12,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" ) type _RuleAction struct { @@ -177,7 +179,7 @@ type _DNSRouteActionOptions struct { // Deprecated: Use DNSRouteOptionsActionOptions instead. RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` // Deprecated: Use DNSRouteOptionsActionOptions instead. - ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type DNSRouteActionOptions _DNSRouteActionOptions @@ -197,9 +199,9 @@ func (r *DNSRouteActionOptions) UnmarshalJSONContext(ctx context.Context, data [ } type _DNSRouteOptionsActionOptions struct { - DisableCache bool `json:"disable_cache,omitempty"` - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` - ClientSubnet *AddrPrefix `json:"client_subnet,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` + ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } type DNSRouteOptionsActionOptions _DNSRouteOptionsActionOptions @@ -225,10 +227,10 @@ func (d DirectActionOptions) Descriptions() []string { descriptions = append(descriptions, "bind_interface="+d.BindInterface) } if d.Inet4BindAddress != nil { - descriptions = append(descriptions, "inet4_bind_address="+d.Inet4BindAddress.Build().String()) + descriptions = append(descriptions, "inet4_bind_address="+d.Inet4BindAddress.Build(netip.IPv4Unspecified()).String()) } if d.Inet6BindAddress != nil { - descriptions = append(descriptions, "inet6_bind_address="+d.Inet6BindAddress.Build().String()) + descriptions = append(descriptions, "inet6_bind_address="+d.Inet6BindAddress.Build(netip.IPv6Unspecified()).String()) } if d.RoutingMark != 0 { descriptions = append(descriptions, "routing_mark="+fmt.Sprintf("0x%x", d.RoutingMark)) @@ -294,8 +296,8 @@ func (r *RejectActionOptions) UnmarshalJSON(bytes []byte) error { } type RouteActionSniff struct { - Sniffer Listable[string] `json:"sniffer,omitempty"` - Timeout Duration `json:"timeout,omitempty"` + Sniffer badoption.Listable[string] `json:"sniffer,omitempty"` + Timeout badoption.Duration `json:"timeout,omitempty"` } type RouteActionResolve struct { diff --git a/option/rule_dns.go b/option/rule_dns.go index e7e454bb0e..6c9755fda8 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -9,6 +9,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" ) type _DNSRule struct { @@ -67,41 +68,41 @@ func (r DNSRule) IsValid() bool { } type RawDefaultDNSRule struct { - Inbound Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` - Network Listable[string] `json:"network,omitempty"` - AuthUser Listable[string] `json:"auth_user,omitempty"` - Protocol Listable[string] `json:"protocol,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - Geosite Listable[string] `json:"geosite,omitempty"` - SourceGeoIP Listable[string] `json:"source_geoip,omitempty"` - GeoIP Listable[string] `json:"geoip,omitempty"` - IPCIDR Listable[string] `json:"ip_cidr,omitempty"` - IPIsPrivate bool `json:"ip_is_private,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - ProcessPathRegex Listable[string] `json:"process_path_regex,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - User Listable[string] `json:"user,omitempty"` - UserID Listable[int32] `json:"user_id,omitempty"` - Outbound Listable[string] `json:"outbound,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - RuleSet Listable[string] `json:"rule_set,omitempty"` - RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` - RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` - Invert bool `json:"invert,omitempty"` + Inbound badoption.Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` + Network badoption.Listable[string] `json:"network,omitempty"` + AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` + Protocol badoption.Listable[string] `json:"protocol,omitempty"` + Domain badoption.Listable[string] `json:"domain,omitempty"` + DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` + Geosite badoption.Listable[string] `json:"geosite,omitempty"` + SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` + GeoIP badoption.Listable[string] `json:"geoip,omitempty"` + IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,omitempty"` + SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` + SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` + Port badoption.Listable[uint16] `json:"port,omitempty"` + PortRange badoption.Listable[string] `json:"port_range,omitempty"` + ProcessName badoption.Listable[string] `json:"process_name,omitempty"` + ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` + ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` + PackageName badoption.Listable[string] `json:"package_name,omitempty"` + User badoption.Listable[string] `json:"user,omitempty"` + UserID badoption.Listable[int32] `json:"user_id,omitempty"` + Outbound badoption.Listable[string] `json:"outbound,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` + RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` + Invert bool `json:"invert,omitempty"` // Deprecated: renamed to rule_set_ip_cidr_match_source Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` diff --git a/option/rule_set.go b/option/rule_set.go index 3bf9aa5c48..fbb527ca55 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -10,6 +10,7 @@ import ( F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" "go4.org/netipx" ) @@ -84,9 +85,9 @@ type LocalRuleSet struct { } type RemoteRuleSet struct { - URL string `json:"url"` - DownloadDetour string `json:"download_detour,omitempty"` - UpdateInterval Duration `json:"update_interval,omitempty"` + URL string `json:"url"` + DownloadDetour string `json:"download_detour,omitempty"` + UpdateInterval badoption.Duration `json:"update_interval,omitempty"` } type _HeadlessRule struct { @@ -145,32 +146,32 @@ func (r HeadlessRule) IsValid() bool { } type DefaultHeadlessRule struct { - QueryType Listable[DNSQueryType] `json:"query_type,omitempty"` - Network Listable[string] `json:"network,omitempty"` - Domain Listable[string] `json:"domain,omitempty"` - DomainSuffix Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex Listable[string] `json:"domain_regex,omitempty"` - SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"` - IPCIDR Listable[string] `json:"ip_cidr,omitempty"` - SourcePort Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange Listable[string] `json:"source_port_range,omitempty"` - Port Listable[uint16] `json:"port,omitempty"` - PortRange Listable[string] `json:"port_range,omitempty"` - ProcessName Listable[string] `json:"process_name,omitempty"` - ProcessPath Listable[string] `json:"process_path,omitempty"` - ProcessPathRegex Listable[string] `json:"process_path_regex,omitempty"` - PackageName Listable[string] `json:"package_name,omitempty"` - WIFISSID Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID Listable[string] `json:"wifi_bssid,omitempty"` - Invert bool `json:"invert,omitempty"` + QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` + Network badoption.Listable[string] `json:"network,omitempty"` + Domain badoption.Listable[string] `json:"domain,omitempty"` + DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` + SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` + SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` + Port badoption.Listable[uint16] `json:"port,omitempty"` + PortRange badoption.Listable[string] `json:"port_range,omitempty"` + ProcessName badoption.Listable[string] `json:"process_name,omitempty"` + ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` + ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` + PackageName badoption.Listable[string] `json:"package_name,omitempty"` + WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` + Invert bool `json:"invert,omitempty"` DomainMatcher *domain.Matcher `json:"-"` SourceIPSet *netipx.IPSet `json:"-"` IPSet *netipx.IPSet `json:"-"` - AdGuardDomain Listable[string] `json:"-"` - AdGuardDomainMatcher *domain.AdGuardMatcher `json:"-"` + AdGuardDomain badoption.Listable[string] `json:"-"` + AdGuardDomainMatcher *domain.AdGuardMatcher `json:"-"` } func (r DefaultHeadlessRule) IsValid() bool { diff --git a/option/simple.go b/option/simple.go index 78171ce4c2..5fda30efe4 100644 --- a/option/simple.go +++ b/option/simple.go @@ -1,6 +1,9 @@ package option -import "github.com/sagernet/sing/common/auth" +import ( + "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/json/badoption" +) type SocksInboundOptions struct { ListenOptions @@ -30,6 +33,6 @@ type HTTPOutboundOptions struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` OutboundTLSOptionsContainer - Path string `json:"path,omitempty"` - Headers HTTPHeader `json:"headers,omitempty"` + Path string `json:"path,omitempty"` + Headers badoption.HTTPHeader `json:"headers,omitempty"` } diff --git a/option/ssh.go b/option/ssh.go index d0bfbf7425..1c6ca6bb96 100644 --- a/option/ssh.go +++ b/option/ssh.go @@ -1,14 +1,16 @@ package option +import "github.com/sagernet/sing/common/json/badoption" + type SSHOutboundOptions struct { DialerOptions ServerOptions - User string `json:"user,omitempty"` - Password string `json:"password,omitempty"` - PrivateKey Listable[string] `json:"private_key,omitempty"` - PrivateKeyPath string `json:"private_key_path,omitempty"` - PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"` - HostKey Listable[string] `json:"host_key,omitempty"` - HostKeyAlgorithms Listable[string] `json:"host_key_algorithms,omitempty"` - ClientVersion string `json:"client_version,omitempty"` + User string `json:"user,omitempty"` + Password string `json:"password,omitempty"` + PrivateKey badoption.Listable[string] `json:"private_key,omitempty"` + PrivateKeyPath string `json:"private_key_path,omitempty"` + PrivateKeyPassphrase string `json:"private_key_passphrase,omitempty"` + HostKey badoption.Listable[string] `json:"host_key,omitempty"` + HostKeyAlgorithms badoption.Listable[string] `json:"host_key_algorithms,omitempty"` + ClientVersion string `json:"client_version,omitempty"` } diff --git a/option/time_unit.go b/option/time_unit.go deleted file mode 100644 index 5e531dadf1..0000000000 --- a/option/time_unit.go +++ /dev/null @@ -1,226 +0,0 @@ -package option - -import ( - "errors" - "time" -) - -// Copyright 2010 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -const durationDay = 24 * time.Hour - -var unitMap = map[string]uint64{ - "ns": uint64(time.Nanosecond), - "us": uint64(time.Microsecond), - "µs": uint64(time.Microsecond), // U+00B5 = micro symbol - "μs": uint64(time.Microsecond), // U+03BC = Greek letter mu - "ms": uint64(time.Millisecond), - "s": uint64(time.Second), - "m": uint64(time.Minute), - "h": uint64(time.Hour), - "d": uint64(durationDay), -} - -// ParseDuration parses a duration string. -// A duration string is a possibly signed sequence of -// decimal numbers, each with optional fraction and a unit suffix, -// such as "300ms", "-1.5h" or "2h45m". -// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". -func ParseDuration(s string) (Duration, error) { - // [-+]?([0-9]*(\.[0-9]*)?[a-z]+)+ - orig := s - var d uint64 - neg := false - - // Consume [-+]? - if s != "" { - c := s[0] - if c == '-' || c == '+' { - neg = c == '-' - s = s[1:] - } - } - // Special case: if all that is left is "0", this is zero. - if s == "0" { - return 0, nil - } - if s == "" { - return 0, errors.New("time: invalid duration " + quote(orig)) - } - for s != "" { - var ( - v, f uint64 // integers before, after decimal point - scale float64 = 1 // value = v + f/scale - ) - - var err error - - // The next character must be [0-9.] - if !(s[0] == '.' || '0' <= s[0] && s[0] <= '9') { - return 0, errors.New("time: invalid duration " + quote(orig)) - } - // Consume [0-9]* - pl := len(s) - v, s, err = leadingInt(s) - if err != nil { - return 0, errors.New("time: invalid duration " + quote(orig)) - } - pre := pl != len(s) // whether we consumed anything before a period - - // Consume (\.[0-9]*)? - post := false - if s != "" && s[0] == '.' { - s = s[1:] - pl := len(s) - f, scale, s = leadingFraction(s) - post = pl != len(s) - } - if !pre && !post { - // no digits (e.g. ".s" or "-.s") - return 0, errors.New("time: invalid duration " + quote(orig)) - } - - // Consume unit. - i := 0 - for ; i < len(s); i++ { - c := s[i] - if c == '.' || '0' <= c && c <= '9' { - break - } - } - if i == 0 { - return 0, errors.New("time: missing unit in duration " + quote(orig)) - } - u := s[:i] - s = s[i:] - unit, ok := unitMap[u] - if !ok { - return 0, errors.New("time: unknown unit " + quote(u) + " in duration " + quote(orig)) - } - if v > 1<<63/unit { - // overflow - return 0, errors.New("time: invalid duration " + quote(orig)) - } - v *= unit - if f > 0 { - // float64 is needed to be nanosecond accurate for fractions of hours. - // v >= 0 && (f*unit/scale) <= 3.6e+12 (ns/h, h is the largest unit) - v += uint64(float64(f) * (float64(unit) / scale)) - if v > 1<<63 { - // overflow - return 0, errors.New("time: invalid duration " + quote(orig)) - } - } - d += v - if d > 1<<63 { - return 0, errors.New("time: invalid duration " + quote(orig)) - } - } - if neg { - return -Duration(d), nil - } - if d > 1<<63-1 { - return 0, errors.New("time: invalid duration " + quote(orig)) - } - return Duration(d), nil -} - -var errLeadingInt = errors.New("time: bad [0-9]*") // never printed - -// leadingInt consumes the leading [0-9]* from s. -func leadingInt[bytes []byte | string](s bytes) (x uint64, rem bytes, err error) { - i := 0 - for ; i < len(s); i++ { - c := s[i] - if c < '0' || c > '9' { - break - } - if x > 1<<63/10 { - // overflow - return 0, rem, errLeadingInt - } - x = x*10 + uint64(c) - '0' - if x > 1<<63 { - // overflow - return 0, rem, errLeadingInt - } - } - return x, s[i:], nil -} - -// leadingFraction consumes the leading [0-9]* from s. -// It is used only for fractions, so does not return an error on overflow, -// it just stops accumulating precision. -func leadingFraction(s string) (x uint64, scale float64, rem string) { - i := 0 - scale = 1 - overflow := false - for ; i < len(s); i++ { - c := s[i] - if c < '0' || c > '9' { - break - } - if overflow { - continue - } - if x > (1<<63-1)/10 { - // It's possible for overflow to give a positive number, so take care. - overflow = true - continue - } - y := x*10 + uint64(c) - '0' - if y > 1<<63 { - overflow = true - continue - } - x = y - scale *= 10 - } - return x, scale, s[i:] -} - -// These are borrowed from unicode/utf8 and strconv and replicate behavior in -// that package, since we can't take a dependency on either. -const ( - lowerhex = "0123456789abcdef" - runeSelf = 0x80 - runeError = '\uFFFD' -) - -func quote(s string) string { - buf := make([]byte, 1, len(s)+2) // slice will be at least len(s) + quotes - buf[0] = '"' - for i, c := range s { - if c >= runeSelf || c < ' ' { - // This means you are asking us to parse a time.Duration or - // time.Location with unprintable or non-ASCII characters in it. - // We don't expect to hit this case very often. We could try to - // reproduce strconv.Quote's behavior with full fidelity but - // given how rarely we expect to hit these edge cases, speed and - // conciseness are better. - var width int - if c == runeError { - width = 1 - if i+2 < len(s) && s[i:i+3] == string(runeError) { - width = 3 - } - } else { - width = len(string(c)) - } - for j := 0; j < width; j++ { - buf = append(buf, `\x`...) - buf = append(buf, lowerhex[s[i+j]>>4]) - buf = append(buf, lowerhex[s[i+j]&0xF]) - } - } else { - if c == '"' || c == '\\' { - buf = append(buf, '\\') - } - buf = append(buf, string(c)...) - } - } - buf = append(buf, '"') - return string(buf) -} diff --git a/option/tls.go b/option/tls.go index 3680afe052..72aaaef1ac 100644 --- a/option/tls.go +++ b/option/tls.go @@ -1,20 +1,22 @@ package option +import "github.com/sagernet/sing/common/json/badoption" + type InboundTLSOptions struct { - Enabled bool `json:"enabled,omitempty"` - ServerName string `json:"server_name,omitempty"` - Insecure bool `json:"insecure,omitempty"` - ALPN Listable[string] `json:"alpn,omitempty"` - MinVersion string `json:"min_version,omitempty"` - MaxVersion string `json:"max_version,omitempty"` - CipherSuites Listable[string] `json:"cipher_suites,omitempty"` - Certificate Listable[string] `json:"certificate,omitempty"` - CertificatePath string `json:"certificate_path,omitempty"` - Key Listable[string] `json:"key,omitempty"` - KeyPath string `json:"key_path,omitempty"` - ACME *InboundACMEOptions `json:"acme,omitempty"` - ECH *InboundECHOptions `json:"ech,omitempty"` - Reality *InboundRealityOptions `json:"reality,omitempty"` + Enabled bool `json:"enabled,omitempty"` + ServerName string `json:"server_name,omitempty"` + Insecure bool `json:"insecure,omitempty"` + ALPN badoption.Listable[string] `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"` + Certificate badoption.Listable[string] `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + Key badoption.Listable[string] `json:"key,omitempty"` + KeyPath string `json:"key_path,omitempty"` + ACME *InboundACMEOptions `json:"acme,omitempty"` + ECH *InboundECHOptions `json:"ech,omitempty"` + Reality *InboundRealityOptions `json:"reality,omitempty"` } type InboundTLSOptionsContainer struct { @@ -35,19 +37,19 @@ func (o *InboundTLSOptionsContainer) ReplaceInboundTLSOptions(options *InboundTL } type OutboundTLSOptions struct { - Enabled bool `json:"enabled,omitempty"` - DisableSNI bool `json:"disable_sni,omitempty"` - ServerName string `json:"server_name,omitempty"` - Insecure bool `json:"insecure,omitempty"` - ALPN Listable[string] `json:"alpn,omitempty"` - MinVersion string `json:"min_version,omitempty"` - MaxVersion string `json:"max_version,omitempty"` - CipherSuites Listable[string] `json:"cipher_suites,omitempty"` - Certificate Listable[string] `json:"certificate,omitempty"` - CertificatePath string `json:"certificate_path,omitempty"` - ECH *OutboundECHOptions `json:"ech,omitempty"` - UTLS *OutboundUTLSOptions `json:"utls,omitempty"` - Reality *OutboundRealityOptions `json:"reality,omitempty"` + Enabled bool `json:"enabled,omitempty"` + DisableSNI bool `json:"disable_sni,omitempty"` + ServerName string `json:"server_name,omitempty"` + Insecure bool `json:"insecure,omitempty"` + ALPN badoption.Listable[string] `json:"alpn,omitempty"` + MinVersion string `json:"min_version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + CipherSuites badoption.Listable[string] `json:"cipher_suites,omitempty"` + Certificate badoption.Listable[string] `json:"certificate,omitempty"` + CertificatePath string `json:"certificate_path,omitempty"` + ECH *OutboundECHOptions `json:"ech,omitempty"` + UTLS *OutboundUTLSOptions `json:"utls,omitempty"` + Reality *OutboundRealityOptions `json:"reality,omitempty"` } type OutboundTLSOptionsContainer struct { @@ -71,8 +73,8 @@ type InboundRealityOptions struct { Enabled bool `json:"enabled,omitempty"` Handshake InboundRealityHandshakeOptions `json:"handshake,omitempty"` PrivateKey string `json:"private_key,omitempty"` - ShortID Listable[string] `json:"short_id,omitempty"` - MaxTimeDifference Duration `json:"max_time_difference,omitempty"` + ShortID badoption.Listable[string] `json:"short_id,omitempty"` + MaxTimeDifference badoption.Duration `json:"max_time_difference,omitempty"` } type InboundRealityHandshakeOptions struct { @@ -81,19 +83,19 @@ type InboundRealityHandshakeOptions struct { } type InboundECHOptions struct { - Enabled bool `json:"enabled,omitempty"` - PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"` - DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"` - Key Listable[string] `json:"key,omitempty"` - KeyPath string `json:"key_path,omitempty"` + Enabled bool `json:"enabled,omitempty"` + PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"` + DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"` + Key badoption.Listable[string] `json:"key,omitempty"` + KeyPath string `json:"key_path,omitempty"` } type OutboundECHOptions struct { - Enabled bool `json:"enabled,omitempty"` - PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"` - DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"` - Config Listable[string] `json:"config,omitempty"` - ConfigPath string `json:"config_path,omitempty"` + Enabled bool `json:"enabled,omitempty"` + PQSignatureSchemesEnabled bool `json:"pq_signature_schemes_enabled,omitempty"` + DynamicRecordSizingDisabled bool `json:"dynamic_record_sizing_disabled,omitempty"` + Config badoption.Listable[string] `json:"config,omitempty"` + ConfigPath string `json:"config_path,omitempty"` } type OutboundUTLSOptions struct { diff --git a/option/tls_acme.go b/option/tls_acme.go index 9c2e081fa8..502706073d 100644 --- a/option/tls_acme.go +++ b/option/tls_acme.go @@ -5,10 +5,11 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" ) type InboundACMEOptions struct { - Domain Listable[string] `json:"domain,omitempty"` + Domain badoption.Listable[string] `json:"domain,omitempty"` DataDirectory string `json:"data_directory,omitempty"` DefaultServerName string `json:"default_server_name,omitempty"` Email string `json:"email,omitempty"` diff --git a/option/tuic.go b/option/tuic.go index 736d5a6606..a9b739ec69 100644 --- a/option/tuic.go +++ b/option/tuic.go @@ -1,12 +1,14 @@ package option +import "github.com/sagernet/sing/common/json/badoption" + type TUICInboundOptions struct { ListenOptions - Users []TUICUser `json:"users,omitempty"` - CongestionControl string `json:"congestion_control,omitempty"` - AuthTimeout Duration `json:"auth_timeout,omitempty"` - ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` - Heartbeat Duration `json:"heartbeat,omitempty"` + Users []TUICUser `json:"users,omitempty"` + CongestionControl string `json:"congestion_control,omitempty"` + AuthTimeout badoption.Duration `json:"auth_timeout,omitempty"` + ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` + Heartbeat badoption.Duration `json:"heartbeat,omitempty"` InboundTLSOptionsContainer } @@ -19,13 +21,13 @@ type TUICUser struct { type TUICOutboundOptions struct { DialerOptions ServerOptions - UUID string `json:"uuid,omitempty"` - Password string `json:"password,omitempty"` - CongestionControl string `json:"congestion_control,omitempty"` - UDPRelayMode string `json:"udp_relay_mode,omitempty"` - UDPOverStream bool `json:"udp_over_stream,omitempty"` - ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` - Heartbeat Duration `json:"heartbeat,omitempty"` - Network NetworkList `json:"network,omitempty"` + UUID string `json:"uuid,omitempty"` + Password string `json:"password,omitempty"` + CongestionControl string `json:"congestion_control,omitempty"` + UDPRelayMode string `json:"udp_relay_mode,omitempty"` + UDPOverStream bool `json:"udp_over_stream,omitempty"` + ZeroRTTHandshake bool `json:"zero_rtt_handshake,omitempty"` + Heartbeat badoption.Duration `json:"heartbeat,omitempty"` + Network NetworkList `json:"network,omitempty"` OutboundTLSOptionsContainer } diff --git a/option/tun.go b/option/tun.go index dbb1bfeae0..a7a0f6bc07 100644 --- a/option/tun.go +++ b/option/tun.go @@ -7,51 +7,52 @@ import ( E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badoption" ) type TunInboundOptions struct { - InterfaceName string `json:"interface_name,omitempty"` - MTU uint32 `json:"mtu,omitempty"` - GSO bool `json:"gso,omitempty"` - Address Listable[netip.Prefix] `json:"address,omitempty"` - AutoRoute bool `json:"auto_route,omitempty"` - IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"` - IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"` - AutoRedirect bool `json:"auto_redirect,omitempty"` - AutoRedirectInputMark FwMark `json:"auto_redirect_input_mark,omitempty"` - AutoRedirectOutputMark FwMark `json:"auto_redirect_output_mark,omitempty"` - StrictRoute bool `json:"strict_route,omitempty"` - RouteAddress Listable[netip.Prefix] `json:"route_address,omitempty"` - RouteAddressSet Listable[string] `json:"route_address_set,omitempty"` - RouteExcludeAddress Listable[netip.Prefix] `json:"route_exclude_address,omitempty"` - RouteExcludeAddressSet Listable[string] `json:"route_exclude_address_set,omitempty"` - IncludeInterface Listable[string] `json:"include_interface,omitempty"` - ExcludeInterface Listable[string] `json:"exclude_interface,omitempty"` - IncludeUID Listable[uint32] `json:"include_uid,omitempty"` - IncludeUIDRange Listable[string] `json:"include_uid_range,omitempty"` - ExcludeUID Listable[uint32] `json:"exclude_uid,omitempty"` - ExcludeUIDRange Listable[string] `json:"exclude_uid_range,omitempty"` - IncludeAndroidUser Listable[int] `json:"include_android_user,omitempty"` - IncludePackage Listable[string] `json:"include_package,omitempty"` - ExcludePackage Listable[string] `json:"exclude_package,omitempty"` - EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` - UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` - Stack string `json:"stack,omitempty"` - Platform *TunPlatformOptions `json:"platform,omitempty"` + InterfaceName string `json:"interface_name,omitempty"` + MTU uint32 `json:"mtu,omitempty"` + GSO bool `json:"gso,omitempty"` + Address badoption.Listable[netip.Prefix] `json:"address,omitempty"` + AutoRoute bool `json:"auto_route,omitempty"` + IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"` + IPRoute2RuleIndex int `json:"iproute2_rule_index,omitempty"` + AutoRedirect bool `json:"auto_redirect,omitempty"` + AutoRedirectInputMark FwMark `json:"auto_redirect_input_mark,omitempty"` + AutoRedirectOutputMark FwMark `json:"auto_redirect_output_mark,omitempty"` + StrictRoute bool `json:"strict_route,omitempty"` + RouteAddress badoption.Listable[netip.Prefix] `json:"route_address,omitempty"` + RouteAddressSet badoption.Listable[string] `json:"route_address_set,omitempty"` + RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"route_exclude_address,omitempty"` + RouteExcludeAddressSet badoption.Listable[string] `json:"route_exclude_address_set,omitempty"` + IncludeInterface badoption.Listable[string] `json:"include_interface,omitempty"` + ExcludeInterface badoption.Listable[string] `json:"exclude_interface,omitempty"` + IncludeUID badoption.Listable[uint32] `json:"include_uid,omitempty"` + IncludeUIDRange badoption.Listable[string] `json:"include_uid_range,omitempty"` + ExcludeUID badoption.Listable[uint32] `json:"exclude_uid,omitempty"` + ExcludeUIDRange badoption.Listable[string] `json:"exclude_uid_range,omitempty"` + IncludeAndroidUser badoption.Listable[int] `json:"include_android_user,omitempty"` + IncludePackage badoption.Listable[string] `json:"include_package,omitempty"` + ExcludePackage badoption.Listable[string] `json:"exclude_package,omitempty"` + EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` + UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` + Stack string `json:"stack,omitempty"` + Platform *TunPlatformOptions `json:"platform,omitempty"` InboundOptions // Deprecated: merged to Address - Inet4Address Listable[netip.Prefix] `json:"inet4_address,omitempty"` + Inet4Address badoption.Listable[netip.Prefix] `json:"inet4_address,omitempty"` // Deprecated: merged to Address - Inet6Address Listable[netip.Prefix] `json:"inet6_address,omitempty"` + Inet6Address badoption.Listable[netip.Prefix] `json:"inet6_address,omitempty"` // Deprecated: merged to RouteAddress - Inet4RouteAddress Listable[netip.Prefix] `json:"inet4_route_address,omitempty"` + Inet4RouteAddress badoption.Listable[netip.Prefix] `json:"inet4_route_address,omitempty"` // Deprecated: merged to RouteAddress - Inet6RouteAddress Listable[netip.Prefix] `json:"inet6_route_address,omitempty"` + Inet6RouteAddress badoption.Listable[netip.Prefix] `json:"inet6_route_address,omitempty"` // Deprecated: merged to RouteExcludeAddress - Inet4RouteExcludeAddress Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty"` + Inet4RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty"` // Deprecated: merged to RouteExcludeAddress - Inet6RouteExcludeAddress Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"` + Inet6RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"` } type FwMark uint32 diff --git a/option/tun_platform.go b/option/tun_platform.go index a0a54eed05..b42f6894f4 100644 --- a/option/tun_platform.go +++ b/option/tun_platform.go @@ -1,5 +1,7 @@ package option +import "github.com/sagernet/sing/common/json/badoption" + type TunPlatformOptions struct { HTTPProxy *HTTPProxyOptions `json:"http_proxy,omitempty"` } @@ -7,6 +9,6 @@ type TunPlatformOptions struct { type HTTPProxyOptions struct { Enabled bool `json:"enabled,omitempty"` ServerOptions - BypassDomain Listable[string] `json:"bypass_domain,omitempty"` - MatchDomain Listable[string] `json:"match_domain,omitempty"` + BypassDomain badoption.Listable[string] `json:"bypass_domain,omitempty"` + MatchDomain badoption.Listable[string] `json:"match_domain,omitempty"` } diff --git a/option/types.go b/option/types.go index bb6485491e..04e3f10e23 100644 --- a/option/types.go +++ b/option/types.go @@ -1,10 +1,7 @@ package option import ( - "net/http" - "net/netip" "strings" - "time" "github.com/sagernet/sing-dns" E "github.com/sagernet/sing/common/exceptions" @@ -15,79 +12,6 @@ import ( mDNS "github.com/miekg/dns" ) -type ListenAddress netip.Addr - -func NewListenAddress(addr netip.Addr) *ListenAddress { - address := ListenAddress(addr) - return &address -} - -func (a ListenAddress) MarshalJSON() ([]byte, error) { - addr := netip.Addr(a) - if !addr.IsValid() { - return nil, nil - } - return json.Marshal(addr.String()) -} - -func (a *ListenAddress) UnmarshalJSON(content []byte) error { - var value string - err := json.Unmarshal(content, &value) - if err != nil { - return err - } - addr, err := netip.ParseAddr(value) - if err != nil { - return err - } - *a = ListenAddress(addr) - return nil -} - -func (a *ListenAddress) Build() netip.Addr { - if a == nil { - return netip.AddrFrom4([4]byte{127, 0, 0, 1}) - } - return (netip.Addr)(*a) -} - -type AddrPrefix netip.Prefix - -func (a AddrPrefix) MarshalJSON() ([]byte, error) { - prefix := netip.Prefix(a) - if prefix.Bits() == prefix.Addr().BitLen() { - return json.Marshal(prefix.Addr().String()) - } else { - return json.Marshal(prefix.String()) - } -} - -func (a *AddrPrefix) UnmarshalJSON(content []byte) error { - var value string - err := json.Unmarshal(content, &value) - if err != nil { - return err - } - prefix, prefixErr := netip.ParsePrefix(value) - if prefixErr == nil { - *a = AddrPrefix(prefix) - return nil - } - addr, addrErr := netip.ParseAddr(value) - if addrErr == nil { - *a = AddrPrefix(netip.PrefixFrom(addr, addr.BitLen())) - return nil - } - return prefixErr -} - -func (a *AddrPrefix) Build() netip.Prefix { - if a == nil { - return netip.Prefix{} - } - return netip.Prefix(*a) -} - type NetworkList string func (v *NetworkList) UnmarshalJSON(content []byte) error { @@ -120,30 +44,6 @@ func (v NetworkList) Build() []string { return strings.Split(string(v), "\n") } -type Listable[T any] []T - -func (l Listable[T]) MarshalJSON() ([]byte, error) { - arrayList := []T(l) - if len(arrayList) == 1 { - return json.Marshal(arrayList[0]) - } - return json.Marshal(arrayList) -} - -func (l *Listable[T]) UnmarshalJSON(content []byte) error { - err := json.UnmarshalDisallowUnknownFields(content, (*[]T)(l)) - if err == nil { - return nil - } - var singleItem T - newError := json.UnmarshalDisallowUnknownFields(content, &singleItem) - if newError != nil { - return E.Errors(err, newError) - } - *l = []T{singleItem} - return nil -} - type DomainStrategy dns.DomainStrategy func (s DomainStrategy) String() string { @@ -206,26 +106,6 @@ func (s *DomainStrategy) UnmarshalJSON(bytes []byte) error { return nil } -type Duration time.Duration - -func (d Duration) MarshalJSON() ([]byte, error) { - return json.Marshal((time.Duration)(d).String()) -} - -func (d *Duration) UnmarshalJSON(bytes []byte) error { - var value string - err := json.Unmarshal(bytes, &value) - if err != nil { - return err - } - duration, err := ParseDuration(value) - if err != nil { - return err - } - *d = Duration(duration) - return nil -} - type DNSQueryType uint16 func (t DNSQueryType) String() string { @@ -270,15 +150,3 @@ func DNSQueryTypeToString(queryType uint16) string { } return F.ToString(queryType) } - -type HTTPHeader map[string]Listable[string] - -func (h HTTPHeader) Build() http.Header { - header := make(http.Header) - for name, values := range h { - for _, value := range values { - header.Add(name, value) - } - } - return header -} diff --git a/option/v2ray_transport.go b/option/v2ray_transport.go index f87b175d39..68c2385811 100644 --- a/option/v2ray_transport.go +++ b/option/v2ray_transport.go @@ -5,6 +5,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" ) type _V2RayTransportOptions struct { @@ -67,33 +68,33 @@ func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error { } type V2RayHTTPOptions struct { - Host Listable[string] `json:"host,omitempty"` - Path string `json:"path,omitempty"` - Method string `json:"method,omitempty"` - Headers HTTPHeader `json:"headers,omitempty"` - IdleTimeout Duration `json:"idle_timeout,omitempty"` - PingTimeout Duration `json:"ping_timeout,omitempty"` + Host badoption.Listable[string] `json:"host,omitempty"` + Path string `json:"path,omitempty"` + Method string `json:"method,omitempty"` + Headers badoption.HTTPHeader `json:"headers,omitempty"` + IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"` + PingTimeout badoption.Duration `json:"ping_timeout,omitempty"` } type V2RayWebsocketOptions struct { - Path string `json:"path,omitempty"` - Headers HTTPHeader `json:"headers,omitempty"` - MaxEarlyData uint32 `json:"max_early_data,omitempty"` - EarlyDataHeaderName string `json:"early_data_header_name,omitempty"` + Path string `json:"path,omitempty"` + Headers badoption.HTTPHeader `json:"headers,omitempty"` + MaxEarlyData uint32 `json:"max_early_data,omitempty"` + EarlyDataHeaderName string `json:"early_data_header_name,omitempty"` } type V2RayQUICOptions struct{} type V2RayGRPCOptions struct { - ServiceName string `json:"service_name,omitempty"` - IdleTimeout Duration `json:"idle_timeout,omitempty"` - PingTimeout Duration `json:"ping_timeout,omitempty"` - PermitWithoutStream bool `json:"permit_without_stream,omitempty"` - ForceLite bool `json:"-"` // for test + ServiceName string `json:"service_name,omitempty"` + IdleTimeout badoption.Duration `json:"idle_timeout,omitempty"` + PingTimeout badoption.Duration `json:"ping_timeout,omitempty"` + PermitWithoutStream bool `json:"permit_without_stream,omitempty"` + ForceLite bool `json:"-"` // for test } type V2RayHTTPUpgradeOptions struct { - Host string `json:"host,omitempty"` - Path string `json:"path,omitempty"` - Headers HTTPHeader `json:"headers,omitempty"` + Host string `json:"host,omitempty"` + Path string `json:"path,omitempty"` + Headers badoption.HTTPHeader `json:"headers,omitempty"` } diff --git a/option/wireguard.go b/option/wireguard.go index 65bfad2061..ebdf159fde 100644 --- a/option/wireguard.go +++ b/option/wireguard.go @@ -1,15 +1,19 @@ package option -import "net/netip" +import ( + "net/netip" + + "github.com/sagernet/sing/common/json/badoption" +) type WireGuardOutboundOptions struct { DialerOptions - SystemInterface bool `json:"system_interface,omitempty"` - GSO bool `json:"gso,omitempty"` - InterfaceName string `json:"interface_name,omitempty"` - LocalAddress Listable[netip.Prefix] `json:"local_address"` - PrivateKey string `json:"private_key"` - Peers []WireGuardPeer `json:"peers,omitempty"` + SystemInterface bool `json:"system_interface,omitempty"` + GSO bool `json:"gso,omitempty"` + InterfaceName string `json:"interface_name,omitempty"` + LocalAddress badoption.Listable[netip.Prefix] `json:"local_address"` + PrivateKey string `json:"private_key"` + Peers []WireGuardPeer `json:"peers,omitempty"` ServerOptions PeerPublicKey string `json:"peer_public_key"` PreSharedKey string `json:"pre_shared_key,omitempty"` @@ -21,8 +25,8 @@ type WireGuardOutboundOptions struct { type WireGuardPeer struct { ServerOptions - PublicKey string `json:"public_key,omitempty"` - PreSharedKey string `json:"pre_shared_key,omitempty"` - AllowedIPs Listable[string] `json:"allowed_ips,omitempty"` - Reserved []uint8 `json:"reserved,omitempty"` + PublicKey string `json:"public_key,omitempty"` + PreSharedKey string `json:"pre_shared_key,omitempty"` + AllowedIPs badoption.Listable[string] `json:"allowed_ips,omitempty"` + Reserved []uint8 `json:"reserved,omitempty"` } diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index 4be30d61b7..f2476223ce 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -21,6 +21,7 @@ import ( "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badoption" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ranges" @@ -257,7 +258,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return inbound, nil } -func uidToRange(uidList option.Listable[uint32]) []ranges.Range[uint32] { +func uidToRange(uidList badoption.Listable[uint32]) []ranges.Range[uint32] { return common.Map(uidList, func(uid uint32) ranges.Range[uint32] { return ranges.NewSingle(uid) }) diff --git a/route/router.go b/route/router.go index b6cc8051ba..82b1f4b50d 100644 --- a/route/router.go +++ b/route/router.go @@ -239,9 +239,9 @@ func NewRouter( } var clientSubnet netip.Prefix if server.ClientSubnet != nil { - clientSubnet = server.ClientSubnet.Build() + clientSubnet = netip.Prefix(common.PtrValueOrDefault(server.ClientSubnet)) } else if dnsOptions.ClientSubnet != nil { - clientSubnet = dnsOptions.ClientSubnet.Build() + clientSubnet = netip.Prefix(common.PtrValueOrDefault(dnsOptions.ClientSubnet)) } if serverProtocol == "" { serverProtocol = "transport" diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index 620260d078..0bf45ba25f 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -88,13 +88,13 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) Server: action.RouteOptions.Server, DisableCache: action.RouteOptions.DisableCache, RewriteTTL: action.RouteOptions.RewriteTTL, - ClientSubnet: action.RouteOptions.ClientSubnet.Build(), + ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), } case C.RuleActionTypeRouteOptions: return &RuleActionDNSRouteOptions{ DisableCache: action.RouteOptionsOptions.DisableCache, RewriteTTL: action.RouteOptionsOptions.RewriteTTL, - ClientSubnet: action.RouteOptionsOptions.ClientSubnet.Build(), + ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptionsOptions.ClientSubnet)), } case C.RuleActionTypeReject: return &RuleActionReject{ diff --git a/transport/sip003/v2ray.go b/transport/sip003/v2ray.go index c142180bbf..d7b752f6c3 100644 --- a/transport/sip003/v2ray.go +++ b/transport/sip003/v2ray.go @@ -12,6 +12,7 @@ import ( "github.com/sagernet/sing-box/transport/v2ray" "github.com/sagernet/sing-vmess" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json/badoption" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) @@ -67,7 +68,7 @@ func newV2RayPlugin(ctx context.Context, pluginOpts Args, router adapter.Router, transportOptions = option.V2RayTransportOptions{ Type: C.V2RayTransportTypeWebsocket, WebsocketOptions: option.V2RayWebsocketOptions{ - Headers: map[string]option.Listable[string]{ + Headers: map[string]badoption.Listable[string]{ "Host": []string{host}, }, Path: path, From 4a611eddf499ccf57cfa497b929f6719a9d4d8f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 6 Nov 2024 19:05:34 +0800 Subject: [PATCH 11/49] documentation: Remove outdated icons --- docs/configuration/dns/index.md | 4 ---- docs/configuration/dns/index.zh.md | 4 ---- docs/configuration/dns/server.md | 4 ---- docs/configuration/dns/server.zh.md | 4 ---- docs/configuration/experimental/cache-file.md | 4 ---- docs/configuration/experimental/cache-file.zh.md | 4 ---- 6 files changed, 24 deletions(-) diff --git a/docs/configuration/dns/index.md b/docs/configuration/dns/index.md index c0eafccc3b..f37c93a4bb 100644 --- a/docs/configuration/dns/index.md +++ b/docs/configuration/dns/index.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - !!! quote "Changes in sing-box 1.9.0" :material-plus: [client_subnet](#client_subnet) diff --git a/docs/configuration/dns/index.zh.md b/docs/configuration/dns/index.zh.md index ba390cef1c..c845ae0aa7 100644 --- a/docs/configuration/dns/index.zh.md +++ b/docs/configuration/dns/index.zh.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - !!! quote "sing-box 1.9.0 中的更改" :material-plus: [client_subnet](#client_subnet) diff --git a/docs/configuration/dns/server.md b/docs/configuration/dns/server.md index 3c5245812d..5ec75faa51 100644 --- a/docs/configuration/dns/server.md +++ b/docs/configuration/dns/server.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - !!! quote "Changes in sing-box 1.9.0" :material-plus: [client_subnet](#client_subnet) diff --git a/docs/configuration/dns/server.zh.md b/docs/configuration/dns/server.zh.md index baa117510a..9f47054164 100644 --- a/docs/configuration/dns/server.zh.md +++ b/docs/configuration/dns/server.zh.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - !!! quote "sing-box 1.9.0 中的更改" :material-plus: [client_subnet](#client_subnet) diff --git a/docs/configuration/experimental/cache-file.md b/docs/configuration/experimental/cache-file.md index b30538e591..18c430d973 100644 --- a/docs/configuration/experimental/cache-file.md +++ b/docs/configuration/experimental/cache-file.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - !!! question "Since sing-box 1.8.0" !!! quote "Changes in sing-box 1.9.0" diff --git a/docs/configuration/experimental/cache-file.zh.md b/docs/configuration/experimental/cache-file.zh.md index 6d86dc8423..656d53c4fe 100644 --- a/docs/configuration/experimental/cache-file.zh.md +++ b/docs/configuration/experimental/cache-file.zh.md @@ -1,7 +1,3 @@ ---- -icon: material/new-box ---- - !!! question "自 sing-box 1.8.0 起" !!! quote "sing-box 1.9.0 中的更改" From 6823670f3d3bddb4f5ea1a87ce0ee692fac9a250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 6 Nov 2024 19:10:26 +0800 Subject: [PATCH 12/49] documentation: Update the scheduled removal time of deprecated features --- docs/configuration/dns/rule.md | 4 ++-- docs/configuration/route/geoip.md | 2 +- docs/configuration/route/geoip.zh.md | 2 +- docs/configuration/route/geosite.md | 2 +- docs/configuration/route/geosite.zh.md | 2 +- docs/configuration/route/rule.md | 6 +++--- docs/deprecated.md | 6 +++--- docs/deprecated.zh.md | 4 ++-- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 03ca66bcc3..715c3b7fdb 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -218,7 +218,7 @@ Match domain using regular expression. !!! failure "Deprecated in sing-box 1.8.0" - Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets). + Geosite is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geosite-to-rule-sets). Match geosite. @@ -226,7 +226,7 @@ Match geosite. !!! failure "Deprecated in sing-box 1.8.0" - GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). + GeoIP is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). Match source geoip. diff --git a/docs/configuration/route/geoip.md b/docs/configuration/route/geoip.md index 8a2ed1d4a1..a045574aa0 100644 --- a/docs/configuration/route/geoip.md +++ b/docs/configuration/route/geoip.md @@ -4,7 +4,7 @@ icon: material/delete-clock !!! failure "Deprecated in sing-box 1.8.0" - GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). + GeoIP is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). ### Structure diff --git a/docs/configuration/route/geoip.zh.md b/docs/configuration/route/geoip.zh.md index fb2481e220..eb7bbe2d10 100644 --- a/docs/configuration/route/geoip.zh.md +++ b/docs/configuration/route/geoip.zh.md @@ -4,7 +4,7 @@ icon: material/delete-clock !!! failure "已在 sing-box 1.8.0 废弃" - GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。 + GeoIP 已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。 ### 结构 diff --git a/docs/configuration/route/geosite.md b/docs/configuration/route/geosite.md index 0463057153..9a1b9dce03 100644 --- a/docs/configuration/route/geosite.md +++ b/docs/configuration/route/geosite.md @@ -4,7 +4,7 @@ icon: material/delete-clock !!! failure "Deprecated in sing-box 1.8.0" - Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets). + Geosite is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geosite-to-rule-sets). ### Structure diff --git a/docs/configuration/route/geosite.zh.md b/docs/configuration/route/geosite.zh.md index eeee38ff17..7cec5b2086 100644 --- a/docs/configuration/route/geosite.zh.md +++ b/docs/configuration/route/geosite.zh.md @@ -4,7 +4,7 @@ icon: material/delete-clock !!! failure "已在 sing-box 1.8.0 废弃" - Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。 + Geosite 已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geosite)。 ### 结构 diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index b5d17f215b..5e86560c6d 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -209,7 +209,7 @@ Match domain using regular expression. !!! failure "Deprecated in sing-box 1.8.0" - Geosite is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geosite-to-rule-sets). + Geosite is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geosite-to-rule-sets). Match geosite. @@ -217,7 +217,7 @@ Match geosite. !!! failure "Deprecated in sing-box 1.8.0" - GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). + GeoIP is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). Match source geoip. @@ -225,7 +225,7 @@ Match source geoip. !!! failure "Deprecated in sing-box 1.8.0" - GeoIP is deprecated and may be removed in the future, check [Migration](/migration/#migrate-geoip-to-rule-sets). + GeoIP is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-geoip-to-rule-sets). Match geoip. diff --git a/docs/deprecated.md b/docs/deprecated.md index 9cb717cf28..604806f0f3 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -12,7 +12,7 @@ icon: material/delete-alert `inet4_route_address` and `inet6_route_address` are merged into `route_address`, `inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`. -Old fields are deprecated and will be removed in sing-box 1.11.0. +Old fields are deprecated and will be removed in sing-box 1.12.0. #### Match source rule items are renamed @@ -32,7 +32,7 @@ check [Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-o #### GeoIP -GeoIP is deprecated and may be removed in the future. +GeoIP is deprecated and may be removed in sing-box 1.12.0. The maxmind GeoIP National Database, as an IP classification database, is not entirely suitable for traffic bypassing, @@ -43,7 +43,7 @@ check [Migration](/migration/#migrate-geoip-to-rule-sets). #### Geosite -Geosite is deprecated and may be removed in the future. +Geosite is deprecated and will be removed in sing-box 1.12.0. Geosite, the `domain-list-community` project maintained by V2Ray as an early traffic bypassing solution, suffers from a number of problems, including lack of maintenance, inaccurate rules, and difficult management. diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index 5bd2ea4585..64e155d19f 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -32,7 +32,7 @@ Clash API 中的 `cache_file` 及相关功能已废弃且已迁移到独立的 ` #### GeoIP -GeoIP 已废弃且可能在不久的将来移除。 +GeoIP 已废弃且将在 sing-box 1.12.0 中被移除。 maxmind GeoIP 国家数据库作为 IP 分类数据库,不完全适合流量绕过, 且现有的实现均存在内存使用大与管理困难的问题。 @@ -42,7 +42,7 @@ sing-box 1.8.0 引入了[规则集](/configuration/rule-set/), #### Geosite -Geosite 已废弃且可能在不久的将来移除。 +Geosite 已废弃且将在 sing-box 1.12.0 中被移除。 Geosite,即由 V2Ray 维护的 domain-list-community 项目,作为早期流量绕过解决方案, 存在着包括缺少维护、规则不准确和管理困难内的大量问题。 From d297ad4c56fe2b90b59413ec04bb56336bf4e320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 6 Nov 2024 19:02:55 +0800 Subject: [PATCH 13/49] documentation: Add rule action --- docs/configuration/dns/rule.md | 48 +++-- docs/configuration/dns/rule.zh.md | 2 +- docs/configuration/dns/rule_action.md | 85 +++++++++ docs/configuration/dns/rule_action.zh.md | 86 +++++++++ docs/configuration/outbound/block.md | 10 +- docs/configuration/outbound/block.zh.md | 10 +- docs/configuration/outbound/dns.md | 8 + docs/configuration/outbound/dns.zh.md | 8 + docs/configuration/route/rule.md | 19 +- docs/configuration/route/rule.zh.md | 19 +- docs/configuration/route/rule_action.md | 139 ++++++++++++++ docs/configuration/route/rule_action.zh.md | 136 ++++++++++++++ docs/configuration/shared/listen.md | 34 +++- docs/configuration/shared/listen.zh.md | 32 ++++ docs/deprecated.md | 30 ++- docs/deprecated.zh.md | 25 ++- docs/migration.md | 208 ++++++++++++++++++++- docs/migration.zh.md | 206 ++++++++++++++++++++ mkdocs.yml | 4 + option/inbound.go | 2 +- option/rule_action.go | 6 - route/route.go | 2 +- 22 files changed, 1076 insertions(+), 43 deletions(-) create mode 100644 docs/configuration/dns/rule_action.md create mode 100644 docs/configuration/dns/rule_action.zh.md create mode 100644 docs/configuration/route/rule_action.md create mode 100644 docs/configuration/route/rule_action.zh.md diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 715c3b7fdb..5cb24e8197 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -2,6 +2,14 @@ icon: material/new-box --- +!!! quote "Changes in sing-box 1.11.0" + + :material-plus: [action](#action) + :material-alert: [server](#server) + :material-alert: [disable_cache](#disable_cache) + :material-alert: [rewrite_ttl](#rewrite_ttl) + :material-alert: [client_subnet](#client_subnet) + !!! quote "Changes in sing-box 1.10.0" :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) @@ -14,7 +22,7 @@ icon: material/new-box :material-plus: [geoip](#geoip) :material-plus: [ip_cidr](#ip_cidr) :material-plus: [ip_is_private](#ip_is_private) - :material-plus: [client_subnet](#client_subnet) + :material-plus: [client_subnet](#client_subnet) :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) !!! quote "Changes in sing-box 1.8.0" @@ -135,19 +143,15 @@ icon: material/new-box "outbound": [ "direct" ], - "server": "local", - "disable_cache": false, - "rewrite_ttl": 100, - "client_subnet": "127.0.0.1/24" + "action": "route", + "server": "local" }, { "type": "logical", "mode": "and", "rules": [], - "server": "local", - "disable_cache": false, - "rewrite_ttl": 100, - "client_subnet": "127.0.0.1/24" + "action": "route", + "server": "local" } ] } @@ -354,29 +358,35 @@ Match outbound. `any` can be used as a value to match any outbound. -#### server +#### action ==Required== -Tag of the target dns server. +See [DNS Rule Actions](../rule_action/) for details. + +#### server + +!!! failure "Deprecated in sing-box 1.11.0" + + Moved to [DNS Rule Action](../rule_action#route). #### disable_cache -Disable cache and save cache in this query. +!!! failure "Deprecated in sing-box 1.11.0" -#### rewrite_ttl + Moved to [DNS Rule Action](../rule_action#route). -Rewrite TTL in DNS responses. +#### rewrite_ttl -#### client_subnet +!!! failure "Deprecated in sing-box 1.11.0" -!!! question "Since sing-box 1.9.0" + Moved to [DNS Rule Action](../rule_action#route). -Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default. +#### client_subnet -If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. +!!! failure "Deprecated in sing-box 1.11.0" -Will overrides `dns.client_subnet` and `servers.[].client_subnet`. + Moved to [DNS Rule Action](../rule_action#route). ### Address Filter Fields diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index b484cbed6b..205b01ae00 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -14,7 +14,7 @@ icon: material/new-box :material-plus: [geoip](#geoip) :material-plus: [ip_cidr](#ip_cidr) :material-plus: [ip_is_private](#ip_is_private) - :material-plus: [client_subnet](#client_subnet) + :material-plus: [client_subnet](#client_subnet) :material-plus: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) !!! quote "sing-box 1.8.0 中的更改" diff --git a/docs/configuration/dns/rule_action.md b/docs/configuration/dns/rule_action.md new file mode 100644 index 0000000000..8943b65351 --- /dev/null +++ b/docs/configuration/dns/rule_action.md @@ -0,0 +1,85 @@ +--- +icon: material/new-box +--- + +# DNS Rule Action + +!!! question "Since sing-box 1.11.0" + +### route + +```json +{ + "action": "route", // default + "server": "", + + // for compatibility + "disable_cache": false, + "rewrite_ttl": 0, + "client_subnet": null +} +``` + +`route` inherits the classic rule behavior of routing DNS requests to the specified server. + +#### server + +==Required== + +Tag of target server. + +#### disable_cache/rewrite_ttl/client_subnet + +!!! failure "Deprecated in sing-box 1.11.0" + + Legacy route options is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-legacy-dns-route-options-to-rule-actions). + +### route-options + +```json +{ + "action": "route-options", + "disable_cache": false, + "rewrite_ttl": null, + "client_subnet": null +} +``` + +#### disable_cache + +Disable cache and save cache in this query. + +#### rewrite_ttl + +Rewrite TTL in DNS responses. + +#### client_subnet + +Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default. + +If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically. + +Will overrides `dns.client_subnet` and `servers.[].client_subnet`. + +### reject + +```json +{ + "action": "reject", + "method": "default", // default + "no_drop": false +} +``` + +`reject` reject DNS requests. + +#### method + +- `default`: Reply with NXDOMAIN. +- `drop`: Drop the request. + +#### no_drop + +If not enabled, `method` will be temporarily overwritten to `drop` after 50 triggers in 30s. + +Not available when `method` is set to drop. diff --git a/docs/configuration/dns/rule_action.zh.md b/docs/configuration/dns/rule_action.zh.md new file mode 100644 index 0000000000..8a9dc07eea --- /dev/null +++ b/docs/configuration/dns/rule_action.zh.md @@ -0,0 +1,86 @@ +--- +icon: material/new-box +--- + +# DNS 规则动作 + +!!! question "自 sing-box 1.11.0 起" + +### route + +```json +{ + "action": "route", // 默认 + "server": "", + + // 兼容性 + "disable_cache": false, + "rewrite_ttl": 0, + "client_subnet": null +} +``` + +`route` 继承了将 DNS 请求 路由到指定服务器的经典规则动作。 + +#### server + +==必填== + +目标 DNS 服务器的标签。 + +#### disable_cache/rewrite_ttl/client_subnet + +!!! failure "自 sing-box 1.11.0 起" + + 旧的路由选项已弃用,且将在 sing-box 1.12.0 中移除,参阅 [迁移指南](/migration/#migrate-legacy-dns-route-options-to-rule-actions). + +### route-options + +```json +{ + "action": "route-options", + "disable_cache": false, + "rewrite_ttl": null, + "client_subnet": null +} +``` + + +#### disable_cache + +在此查询中禁用缓存。 + +#### rewrite_ttl + +重写 DNS 回应中的 TTL。 + +#### client_subnet + +默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 + +如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 + +将覆盖 `dns.client_subnet` 与 `servers.[].client_subnet`。 + +### reject + +```json +{ + "action": "reject", + "method": "default", // default + "no_drop": false +} +``` + +`reject` 拒绝 DNS 请求。 + +#### method + +- `default`: 返回 NXDOMAIN。 +- `drop`: 丢弃请求。 + +#### no_drop + +如果未启用,则 30 秒内触发 50 次后,`method` 将被暂时覆盖为 `drop`。 + +当 `method` 设为 `drop` 时不可用。 diff --git a/docs/configuration/outbound/block.md b/docs/configuration/outbound/block.md index e27a4b3ef3..f29120ccf2 100644 --- a/docs/configuration/outbound/block.md +++ b/docs/configuration/outbound/block.md @@ -1,8 +1,14 @@ -`block` outbound closes all incoming requests. +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.11.0" + + Legacy special outbounds are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-special-outbounds-to-rule-actions). ### Structure -```json +```json F { "type": "block", "tag": "block" diff --git a/docs/configuration/outbound/block.zh.md b/docs/configuration/outbound/block.zh.md index bc0762e3ab..822478cea8 100644 --- a/docs/configuration/outbound/block.zh.md +++ b/docs/configuration/outbound/block.zh.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "已在 sing-box 1.11.0 废弃" + + 旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions). + `block` 出站关闭所有传入请求。 ### 结构 @@ -11,4 +19,4 @@ ### 字段 -无字段。 \ No newline at end of file +无字段。 diff --git a/docs/configuration/outbound/dns.md b/docs/configuration/outbound/dns.md index 1f8c547701..d73360413e 100644 --- a/docs/configuration/outbound/dns.md +++ b/docs/configuration/outbound/dns.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.11.0" + + Legacy special outbounds are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-special-outbounds-to-rule-actions). + `dns` outbound is a internal DNS server. ### Structure diff --git a/docs/configuration/outbound/dns.zh.md b/docs/configuration/outbound/dns.zh.md index 67538f6e02..3db2fefb0a 100644 --- a/docs/configuration/outbound/dns.zh.md +++ b/docs/configuration/outbound/dns.zh.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "已在 sing-box 1.11.0 废弃" + + 旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除, 参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions). + `dns` 出站是一个内部 DNS 服务器。 ### 结构 diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index 5e86560c6d..fe40d56518 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -1,7 +1,12 @@ --- -icon: material/alert-decagram +icon: material/new-box --- +!!! quote "Changes in sing-box 1.11.0" + + :material-plus: [action](#action) + :material-alert: [outbound](#outbound) + !!! quote "Changes in sing-box 1.10.0" :material-plus: [client](#client) @@ -129,6 +134,7 @@ icon: material/alert-decagram "rule_set_ipcidr_match_source": false, "rule_set_ip_cidr_match_source": false, "invert": false, + "action": "route", "outbound": "direct" }, { @@ -136,6 +142,7 @@ icon: material/alert-decagram "mode": "and", "rules": [], "invert": false, + "action": "route", "outbound": "direct" } ] @@ -357,11 +364,17 @@ Make `ip_cidr` in rule-sets match the source IP. Invert match result. -#### outbound +#### action ==Required== -Tag of the target outbound. +See [Rule Actions](../rule_action/) for details. + +#### outbound + +!!! failure "Deprecated in sing-box 1.11.0" + + Moved to [Rule Action](../rule_action#route). ### Logical Fields diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index a93ce5e53d..316339f677 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -1,7 +1,12 @@ --- -icon: material/alert-decagram +icon: material/new-box --- +!!! quote "sing-box 1.11.0 中的更改" + + :material-plus: [action](#action) + :material-alert: [outbound](#outbound) + !!! quote "sing-box 1.10.0 中的更改" :material-plus: [client](#client) @@ -127,6 +132,7 @@ icon: material/alert-decagram "rule_set_ipcidr_match_source": false, "rule_set_ip_cidr_match_source": false, "invert": false, + "action": "route", "outbound": "direct" }, { @@ -134,6 +140,7 @@ icon: material/alert-decagram "mode": "and", "rules": [], "invert": false, + "action": "route", "outbound": "direct" } ] @@ -355,11 +362,17 @@ icon: material/alert-decagram 反选匹配结果。 -#### outbound +#### action ==必填== -目标出站的标签。 +参阅 [规则行动](../rule_action/)。 + +#### outbound + +!!! failure "已在 sing-box 1.11.0 废弃" + + 已移动到 [规则行动](../rule_action#route). ### 逻辑字段 diff --git a/docs/configuration/route/rule_action.md b/docs/configuration/route/rule_action.md new file mode 100644 index 0000000000..843d756366 --- /dev/null +++ b/docs/configuration/route/rule_action.md @@ -0,0 +1,139 @@ +--- +icon: material/new-box +--- + +# Rule Action + +!!! question "Since sing-box 1.11.0" + +## Final actions + +### route + +```json +{ + "action": "route", // default + "outbound": "" +} +``` + +`route` inherits the classic rule behavior of routing connection to the specified outbound. + +#### outbound + +==Required== + +Tag of target outbound. + +### route-options + +```json +{ + "action": "route-options", + "udp_disable_domain_unmapping": false, + "udp_connect": false +} +``` + +`route-options` set options for routing. + +#### udp_disable_domain_unmapping + +If enabled, for UDP proxy requests addressed to a domain, +the original packet address will be sent in the response instead of the mapped domain. + +This option is used for compatibility with clients that +do not support receiving UDP packets with domain addresses, such as Surge. + +#### udp_connect + +If enabled, attempts to connect UDP connection to the destination instead of listen. + +### reject + +```json +{ + "action": "reject", + "method": "default", // default + "no_drop": false +} +``` + +`reject` reject connections + +The specified method is used for reject tun connections if `sniff` action has not been performed yet. + +For non-tun connections and already established connections, will just be closed. + +#### method + +- `default`: Reply with TCP RST for TCP connections, and ICMP port unreachable for UDP packets. +- `drop`: Drop packets. + +#### no_drop + +If not enabled, `method` will be temporarily overwritten to `drop` after 50 triggers in 30s. + +Not available when `method` is set to drop. + +### hijack-dns + +```json +{ + "action": "hijack-dns" +} +``` + +`hijack-dns` hijack DNS requests to the sing-box DNS module. + +## Non-final actions + +### sniff + +```json +{ + "action": "sniff", + "sniffer": [], + "timeout": "" +} +``` + +`sniff` performs protocol sniffing on connections. + +For deprecated `inbound.sniff` options, it is considered to `sniff()` performed before routing. + +#### sniffer + +Enabled sniffers. + +All sniffers enabled by default. + +Available protocol values an be found on in [Protocol Sniff](../sniff/) + +#### timeout + +Timeout for sniffing. + +`300ms` is used by default. + +### resolve + +```json +{ + "action": "resolve", + "strategy": "", + "server": "" +} +``` + +`resolve` resolve request destination from domain to IP addresses. + +#### strategy + +DNS resolution strategy, available values are: `prefer_ipv4`, `prefer_ipv6`, `ipv4_only`, `ipv6_only`. + +`dns.strategy` will be used by default. + +#### server + +Specifies DNS server tag to use instead of selecting through DNS routing. diff --git a/docs/configuration/route/rule_action.zh.md b/docs/configuration/route/rule_action.zh.md new file mode 100644 index 0000000000..ae16d85fde --- /dev/null +++ b/docs/configuration/route/rule_action.zh.md @@ -0,0 +1,136 @@ +--- +icon: material/new-box +--- + +# 规则动作 + +!!! question "自 sing-box 1.11.0 起" + +## 最终动作 + +### route + +```json +{ + "action": "route", // 默认 + "outbound": "", + "udp_disable_domain_unmapping": false +} +``` + +`route` 继承了将连接路由到指定出站的经典规则动作。 + +#### outbound + +==必填== + +目标出站的标签。 + +### route-options + +```json +{ + "action": "route-options", + "udp_disable_domain_unmapping": false, + "udp_connect": false +} +``` + +#### udp_disable_domain_unmapping + +如果启用,对于地址为域的 UDP 代理请求,将在响应中发送原始包地址而不是映射的域。 + +此选项用于兼容不支持接收带有域地址的 UDP 包的客户端,如 Surge。 + +#### udp_connect + +如果启用,将尝试将 UDP 连接 connect 到目标而不是 listen。 + +### reject + +```json +{ + "action": "reject", + "method": "default", // 默认 + "no_drop": false +} +``` + +`reject` 拒绝连接。 + +如果尚未执行 `sniff` 操作,则将使用指定方法拒绝 tun 连接。 + +对于非 tun 连接和已建立的连接,将直接关闭。 + +#### method + +- `default`: 对于 TCP 连接回复 RST,对于 UDP 包回复 ICMP 端口不可达。 +- `drop`: 丢弃数据包。 + +#### no_drop + +如果未启用,则 30 秒内触发 50 次后,`method` 将被暂时覆盖为 `drop`。 + +当 `method` 设为 `drop` 时不可用。 + +### hijack-dns + +```json +{ + "action": "hijack-dns" +} +``` + +`hijack-dns` 劫持 DNS 请求至 sing-box DNS 模块。 + +## 非最终动作 + +### sniff + +```json +{ + "action": "sniff", + "sniffer": [], + "timeout": "" +} +``` + +`sniff` 对连接执行协议嗅探。 + +对于已弃用的 `inbound.sniff` 选项,被视为在路由之前执行的 `sniff`。 + +#### sniffer + +启用的探测器。 + +默认启用所有探测器。 + +可用的协议值可以在 [协议嗅探](../sniff/) 中找到。 + +#### timeout + +探测超时时间。 + +默认使用 300ms。 + +### resolve + +```json +{ + "action": "resolve", + "strategy": "", + "server": "" +} +``` + +`resolve` 将请求的目标从域名解析为 IP 地址。 + +#### strategy + +DNS 解析策略,可用值有:`prefer_ipv4`、`prefer_ipv6`、`ipv4_only`、`ipv6_only`。 + +默认使用 `dns.strategy`。 + +#### server + +指定要使用的 DNS 服务器的标签,而不是通过 DNS 路由进行选择。 diff --git a/docs/configuration/shared/listen.md b/docs/configuration/shared/listen.md index ae3ed6a403..fa6a05b97c 100644 --- a/docs/configuration/shared/listen.md +++ b/docs/configuration/shared/listen.md @@ -1,3 +1,15 @@ +--- +icon: material/delete-clock +--- + +!!! quote "Changes in sing-box 1.11.0" + + :material-delete-clock: [sniff](#sniff) + :material-delete-clock: [sniff_override_destination](#sniff_override_destination) + :material-delete-clock: [sniff_timeout](#sniff_timeout) + :material-delete-clock: [domain_strategy](#domain_strategy) + :material-delete-clock: [udp_disable_domain_unmapping](#udp_disable_domain_unmapping) + ### Structure ```json @@ -68,24 +80,40 @@ Requires target inbound support, see [Injectable](/configuration/inbound/#fields #### sniff +!!! failure "Deprecated in sing-box 1.11.0" + + Inbound fields are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-inbound-fields-to-rule-actions). + Enable sniffing. See [Protocol Sniff](/configuration/route/sniff/) for details. #### sniff_override_destination +!!! failure "Deprecated in sing-box 1.11.0" + + Inbound fields are deprecated and will be removed in sing-box 1.13.0. + Override the connection destination address with the sniffed domain. If the domain name is invalid (like tor), this will not work. #### sniff_timeout +!!! failure "Deprecated in sing-box 1.11.0" + + Inbound fields are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-inbound-fields-to-rule-actions). + Timeout for sniffing. -300ms is used by default. +`300ms` is used by default. #### domain_strategy +!!! failure "Deprecated in sing-box 1.11.0" + + Inbound fields are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-inbound-fields-to-rule-actions). + One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. If set, the requested domain name will be resolved to IP before routing. @@ -94,6 +122,10 @@ If `sniff_override_destination` is in effect, its value will be taken as a fallb #### udp_disable_domain_unmapping +!!! failure "Deprecated in sing-box 1.11.0" + + Inbound fields are deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-legacy-inbound-fields-to-rule-actions). + If enabled, for UDP proxy requests addressed to a domain, the original packet address will be sent in the response instead of the mapped domain. diff --git a/docs/configuration/shared/listen.zh.md b/docs/configuration/shared/listen.zh.md index 398c98c53d..3b472c4dff 100644 --- a/docs/configuration/shared/listen.zh.md +++ b/docs/configuration/shared/listen.zh.md @@ -1,3 +1,15 @@ +--- +icon: material/delete-clock +--- + +!!! quote "sing-box 1.11.0 中的更改" + + :material-delete-clock: [sniff](#sniff) + :material-delete-clock: [sniff_override_destination](#sniff_override_destination) + :material-delete-clock: [sniff_timeout](#sniff_timeout) + :material-delete-clock: [domain_strategy](#domain_strategy) + :material-delete-clock: [udp_disable_domain_unmapping](#udp_disable_domain_unmapping) + ### 结构 ```json @@ -69,24 +81,40 @@ UDP NAT 过期时间,以秒为单位。 #### sniff +!!! failure "已在 sing-box 1.11.0 废弃" + + 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions). + 启用协议探测。 参阅 [协议探测](/zh/configuration/route/sniff/) #### sniff_override_destination +!!! failure "已在 sing-box 1.11.0 废弃" + + 入站字段已废弃且将在 sing-box 1.12.0 中被移除。 + 用探测出的域名覆盖连接目标地址。 如果域名无效(如 Tor),将不生效。 #### sniff_timeout +!!! failure "已在 sing-box 1.11.0 废弃" + + 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions). + 探测超时时间。 默认使用 300ms。 #### domain_strategy +!!! failure "已在 sing-box 1.11.0 废弃" + + 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions). + 可选值: `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。 如果设置,请求的域名将在路由之前解析为 IP。 @@ -95,6 +123,10 @@ UDP NAT 过期时间,以秒为单位。 #### udp_disable_domain_unmapping +!!! failure "已在 sing-box 1.11.0 废弃" + + 入站字段已废弃且将在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions). + 如果启用,对于地址为域的 UDP 代理请求,将在响应中发送原始包地址而不是映射的域。 此选项用于兼容不支持接收带有域地址的 UDP 包的客户端,如 Surge。 diff --git a/docs/deprecated.md b/docs/deprecated.md index 604806f0f3..f057319ade 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -4,6 +4,32 @@ icon: material/delete-alert # Deprecated Feature List +## 1.11.0 + +#### Legacy special outbounds + +Legacy special outbounds (`block` / `dns`) are deprecated +and can be replaced by rule actions, +check [Migration](../migration/#migrate-legacy-special-outbounds-to-rule-actions). + +Old fields will be removed in sing-box 1.13.0. + +#### Legacy inbound fields + +Legacy inbound fields (`inbound.` are deprecated +and can be replaced by rule actions, +check [Migration](../migration/#migrate-legacy-inbound-fields-to-rule-actions). + +Old fields will be removed in sing-box 1.13.0. + +#### Legacy DNS route options + +Legacy DNS route options (`disable_cache`, `rewrite_ttl`, `client_subnet`) are deprecated +and can be replaced by rule actions, +check [Migration](../migration/#migrate-legacy-dns-route-options-to-rule-actions). + +Old fields will be removed in sing-box 1.12.0. + ## 1.10.0 #### TUN address fields are merged @@ -12,7 +38,7 @@ icon: material/delete-alert `inet4_route_address` and `inet6_route_address` are merged into `route_address`, `inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`. -Old fields are deprecated and will be removed in sing-box 1.12.0. +Old fields will be removed in sing-box 1.12.0. #### Match source rule items are renamed @@ -32,7 +58,7 @@ check [Migration](/migration/#migrate-cache-file-from-clash-api-to-independent-o #### GeoIP -GeoIP is deprecated and may be removed in sing-box 1.12.0. +GeoIP is deprecated and will be removed in sing-box 1.12.0. The maxmind GeoIP National Database, as an IP classification database, is not entirely suitable for traffic bypassing, diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index 64e155d19f..2f7b28f7bd 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -4,6 +4,29 @@ icon: material/delete-alert # 废弃功能列表 +## 1.11.0 + +#### 旧的特殊出站 + +旧的特殊出站(`block` / `dns`)已废弃且可以通过规则动作替代, +参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions)。 + +旧字段将在 sing-box 1.13.0 中被移除。 + +#### 旧的入站字段 + +旧的入站字段(`inbound.`)已废弃且可以通过规则动作替代, +参阅 [迁移指南](/migration/#migrate-legacy-inbound-fields-to-rule-actions)。 + +旧字段将在 sing-box 1.13.0 中被移除。 + +#### 旧的 DNS 路由参数 + +旧的 DNS 路由参数(`disable_cache`、`rewrite_ttl`、`client_subnet`)已废弃且可以通过规则动作替代, +参阅 [迁移指南](/migration/#migrate-legacy-dns-route-options-to-rule-actions)。 + +旧字段将在 sing-box 1.12.0 中被移除。 + ## 1.10.0 #### Match source 规则项已重命名 @@ -17,7 +40,7 @@ icon: material/delete-alert `inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`, `inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。 -旧字段已废弃,且将在 sing-box 1.11.0 中被移除。 +旧字段将在 sing-box 1.11.0 中被移除。 #### 移除对 go1.18 和 go1.19 的支持 diff --git a/docs/migration.md b/docs/migration.md index 71b6169216..9207db5bc9 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -2,6 +2,212 @@ icon: material/arrange-bring-forward --- +## 1.11.0 + +### Migrate legacy special outbounds to rule actions + +Legacy special outbounds are deprecated and can be replaced by rule actions. + +!!! info "References" + + [Rule Action](/configuration/route/rule_action/) / + [Block](/configuration/outbound/block/) / + [DNS](/configuration/outbound/dns) + +=== "Block" + + === ":material-card-remove: Deprecated" + + ```json + { + "outbounds": [ + { + "type": "block", + "tag": "block" + } + ], + "route": { + "rules": [ + { + ..., + + "outbound": "block" + } + ] + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + ..., + + "action": "reject" + } + ] + } + } + ``` + +=== "DNS" + + === ":material-card-remove: Deprecated" + + ```json + { + "inbound": [ + { + ..., + + "sniff": true + } + ], + "outbounds": [ + { + "tag": "dns", + "type": "dns" + } + ], + "route": { + "rules": [ + { + "protocol": "dns", + "outbound": "dns" + } + ] + } + } + ``` + + === ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "action": "sniff" + }, + { + "protocol": "dns", + "action": "hijack-dns" + } + ] + } + } + ``` + +### Migrate legacy inbound fields to rule actions + +Inbound fields are deprecated and can be replaced by rule actions. + +!!! info "References" + + [Listen Fields](/configuration/inbound/listen/) / + [Rule](/configuration/route/rule/) / + [Rule Action](/configuration/route/rule_action/) / + [DNS Rule](/configuration/dns/rule/) / + [DNS Rule Action](/configuration/dns/rule_action/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "inbounds": [ + { + "type": "mixed", + "sniff": true, + "sniff_timeout": "1s", + "domain_strategy": "prefer_ipv4" + } + ] + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "inbounds": [ + { + "type": "mixed", + "tag": "in" + } + ], + "route": { + "rules": [ + { + "inbound": "in", + "action": "resolve", + "strategy": "prefer_ipv4" + }, + { + "inbound": "in", + "action": "sniff", + "timeout": "1s" + } + ] + } + } + ``` + +### Migrate legacy DNS route options to rule actions + +Legacy DNS route options are deprecated and can be replaced by rule actions. + +!!! info "References" + + [DNS Rule](/configuration/dns/rule/) / + [DNS Rule Action](/configuration/dns/rule_action/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "dns": { + "rules": [ + { + ..., + + "server": "local", + "disable_cache": true, + "rewrite_ttl": 600, + "client_subnet": "1.1.1.1/24" + } + ] + } + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "dns": { + "rules": [ + { + ..., + + "action": "route-options", + "disable_cache": true, + "rewrite_ttl": 600, + "client_subnet": "1.1.1.1/24" + }, + { + ..., + + "server": "local" + } + ] + } + } + ``` + ## 1.10.0 ### TUN address fields are merged @@ -10,8 +216,6 @@ icon: material/arrange-bring-forward `inet4_route_address` and `inet6_route_address` are merged into `route_address`, `inet4_route_exclude_address` and `inet6_route_exclude_address` are merged into `route_exclude_address`. -Old fields are deprecated and will be removed in sing-box 1.11.0. - !!! info "References" [TUN](/configuration/inbound/tun/) diff --git a/docs/migration.zh.md b/docs/migration.zh.md index 62fbe9edeb..f51860f720 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -2,6 +2,212 @@ icon: material/arrange-bring-forward --- +## 1.11.0 + +### 迁移旧的特殊出站到规则动作 + +旧的特殊出站已被弃用,且可以被规则动作替代。 + +!!! info "参考" + + [规则动作](/zh/configuration/route/rule_action/) / + [Block](/zh/configuration/outbound/block/) / + [DNS](/zh/configuration/outbound/dns) + +=== "Block" + + === ":material-card-remove: 弃用的" + + ```json + { + "outbounds": [ + { + "type": "block", + "tag": "block" + } + ], + "route": { + "rules": [ + { + ..., + + "outbound": "block" + } + ] + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "route": { + "rules": [ + { + ..., + + "action": "reject" + } + ] + } + } + ``` + +=== "DNS" + + === ":material-card-remove: 弃用的" + + ```json + { + "inbound": [ + { + ..., + + "sniff": true + } + ], + "outbounds": [ + { + "tag": "dns", + "type": "dns" + } + ], + "route": { + "rules": [ + { + "protocol": "dns", + "outbound": "dns" + } + ] + } + } + ``` + + === ":material-card-multiple: 新的" + + ```json + { + "route": { + "rules": [ + { + "action": "sniff" + }, + { + "protocol": "dns", + "action": "hijack-dns" + } + ] + } + } + ``` + +### 迁移旧的入站字段到规则动作 + +入站选项已被弃用,且可以被规则动作替代。 + +!!! info "参考" + + [监听字段](/zh/configuration/shared/listen/) / + [规则](/zh/configuration/route/rule/) / + [规则动作](/zh/configuration/route/rule_action/) / + [DNS 规则](/zh/configuration/dns/rule/) / + [DNS 规则动作](/zh/configuration/dns/rule_action/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "inbounds": [ + { + "type": "mixed", + "sniff": true, + "sniff_timeout": "1s", + "domain_strategy": "prefer_ipv4" + } + ] + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "inbounds": [ + { + "type": "mixed", + "tag": "in" + } + ], + "route": { + "rules": [ + { + "inbound": "in", + "action": "resolve", + "strategy": "prefer_ipv4" + }, + { + "inbound": "in", + "action": "sniff", + "timeout": "1s" + } + ] + } + } + ``` + +### 迁移旧的 DNS 路由选项到规则动作 + +旧的 DNS 路由选项已被弃用,且可以被规则动作替代。 + +!!! info "参考" + + [DNS 规则](/zh/configuration/dns/rule/) / + [DNS 规则动作](/zh/configuration/dns/rule_action/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "dns": { + "rules": [ + { + ..., + + "server": "local", + "disable_cache": true, + "rewrite_ttl": 600, + "client_subnet": "1.1.1.1/24" + } + ] + } + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "dns": { + "rules": [ + { + ..., + + "action": "route-options", + "disable_cache": true, + "rewrite_ttl": 600, + "client_subnet": "1.1.1.1/24" + }, + { + ..., + + "server": "local" + } + ] + } + } + ``` + ## 1.10.0 ### TUN 地址字段已合并 diff --git a/mkdocs.yml b/mkdocs.yml index d5cdcee470..66e8a2e99b 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -82,6 +82,7 @@ nav: - configuration/dns/index.md - DNS Server: configuration/dns/server.md - DNS Rule: configuration/dns/rule.md + - DNS Rule Action: configuration/dns/rule_action.md - FakeIP: configuration/dns/fakeip.md - NTP: - configuration/ntp/index.md @@ -90,6 +91,7 @@ nav: - GeoIP: configuration/route/geoip.md - Geosite: configuration/route/geosite.md - Route Rule: configuration/route/rule.md + - Rule Action: configuration/route/rule_action.md - Protocol Sniff: configuration/route/sniff.md - Rule Set: - configuration/rule-set/index.md @@ -218,9 +220,11 @@ plugins: Log: 日志 DNS Server: DNS 服务器 DNS Rule: DNS 规则 + DNS Rule Action: DNS 规则动作 Route: 路由 Route Rule: 路由规则 + Rule Action: 规则动作 Protocol Sniff: 协议探测 Rule Set: 规则集 diff --git a/option/inbound.go b/option/inbound.go index a67719fa04..2cc1598946 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -34,7 +34,7 @@ func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) erro } registry := service.FromContext[InboundOptionsRegistry](ctx) if registry == nil { - return E.New("missing inbound options registry in context") + return E.New("missing Inbound fields registry in context") } options, loaded := registry.CreateOptions(h.Type) if !loaded { diff --git a/option/rule_action.go b/option/rule_action.go index 7a76391ca6..3b4e8edb5f 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -148,9 +148,6 @@ func (r *RouteActionOptions) UnmarshalJSON(data []byte) error { if err != nil { return err } - if r.Outbound == "" { - return E.New("missing outbound") - } return nil } @@ -189,9 +186,6 @@ func (r *DNSRouteActionOptions) UnmarshalJSONContext(ctx context.Context, data [ if err != nil { return err } - if r.Server == "" { - return E.New("missing server") - } if r.DisableCache || r.RewriteTTL != nil || r.ClientSubnet != nil { deprecated.Report(ctx, deprecated.OptionLegacyDNSRouteOptions) } diff --git a/route/route.go b/route/route.go index 854fa4f19f..91d7f3c9bf 100644 --- a/route/route.go +++ b/route/route.go @@ -76,7 +76,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad metadata.Network = N.NetworkTCP switch metadata.Destination.Fqdn { case mux.Destination.Fqdn: - return E.New("global multiplex is deprecated since sing-box v1.7.0, enable multiplex in inbound options instead.") + return E.New("global multiplex is deprecated since sing-box v1.7.0, enable multiplex in Inbound fields instead.") case vmess.MuxDestination.Fqdn: return E.New("global multiplex (v2ray legacy) not supported since sing-box v1.7.0.") case uot.MagicAddress: From 0437ac512be584e81e1e72116f41ad057ffbca2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 9 Nov 2024 21:16:11 +0800 Subject: [PATCH 14/49] refactor: Modular inbound/outbound manager --- adapter/experimental.go | 4 +- adapter/inbound.go | 10 +- adapter/inbound/manager.go | 143 +++++ adapter/inbound/registry.go | 44 +- adapter/lifecycle.go | 41 ++ adapter/lifecycle_legacy.go | 33 ++ adapter/outbound.go | 9 + adapter/outbound/manager.go | 265 +++++++++ adapter/outbound/registry.go | 8 +- adapter/prestart.go | 8 - adapter/router.go | 18 +- box.go | 138 +++-- box_outbound.go | 85 --- cmd/sing-box/cmd_tools.go | 6 +- cmd/sing-box/cmd_tools_connect.go | 2 +- cmd/sing-box/cmd_tools_fetch.go | 2 +- cmd/sing-box/cmd_tools_fetch_http3.go | 2 +- cmd/sing-box/cmd_tools_synctime.go | 3 +- common/dialer/detour.go | 16 +- common/dialer/dialer.go | 18 +- common/dialer/router.go | 28 +- common/settings/proxy_darwin.go | 3 +- common/tls/ech_client.go | 3 +- common/tls/reality_server.go | 3 +- experimental/clashapi.go | 6 +- experimental/clashapi/api_meta_group.go | 8 +- experimental/clashapi/proxies.go | 19 +- experimental/clashapi/server.go | 64 +-- experimental/clashapi/server_resources.go | 8 +- .../clashapi/trafficontrol/tracker.go | 16 +- protocol/direct/inbound.go | 29 +- protocol/direct/outbound.go | 4 +- protocol/group/selector.go | 6 +- protocol/group/urltest.go | 10 +- protocol/http/inbound.go | 16 +- protocol/http/outbound.go | 2 +- protocol/hysteria/inbound.go | 20 +- protocol/hysteria/outbound.go | 2 +- protocol/hysteria2/inbound.go | 20 +- protocol/hysteria2/outbound.go | 2 +- protocol/mixed/inbound.go | 14 +- protocol/redirect/redirect.go | 2 + protocol/redirect/tproxy.go | 4 + protocol/shadowsocks/inbound.go | 8 +- protocol/shadowsocks/inbound_multi.go | 8 +- protocol/shadowsocks/inbound_relay.go | 8 +- protocol/shadowsocks/outbound.go | 2 +- protocol/shadowtls/inbound.go | 40 +- protocol/shadowtls/outbound.go | 2 +- protocol/socks/inbound.go | 14 +- protocol/socks/outbound.go | 2 +- protocol/ssh/outbound.go | 2 +- protocol/tor/outbound.go | 2 +- protocol/trojan/inbound.go | 24 +- protocol/trojan/outbound.go | 2 +- protocol/tuic/inbound.go | 20 +- protocol/tuic/outbound.go | 2 +- protocol/vless/inbound.go | 20 +- protocol/vless/outbound.go | 2 +- protocol/vmess/inbound.go | 20 +- protocol/vmess/outbound.go | 2 +- protocol/wireguard/outbound.go | 2 +- route/dns.go | 1 - route/router.go | 507 ++++++++---------- route/rule/rule_action.go | 4 +- route/rule/rule_default.go | 4 +- route/rule/rule_set_remote.go | 58 +- transport/dhcp/server.go | 3 +- transport/fakeip/server.go | 3 +- 69 files changed, 1169 insertions(+), 737 deletions(-) create mode 100644 adapter/inbound/manager.go create mode 100644 adapter/lifecycle.go create mode 100644 adapter/lifecycle_legacy.go create mode 100644 adapter/outbound/manager.go delete mode 100644 box_outbound.go diff --git a/adapter/experimental.go b/adapter/experimental.go index 0cab5ed5a8..bee24c4f8a 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -15,7 +15,7 @@ import ( type ClashServer interface { Service - PreStarter + LegacyPreStarter Mode() string ModeList() []string HistoryStorage() *urltest.HistoryStorage @@ -25,7 +25,7 @@ type ClashServer interface { type CacheFile interface { Service - PreStarter + LegacyPreStarter StoreFakeIP() bool FakeIPStorage diff --git a/adapter/inbound.go b/adapter/inbound.go index f9ed17085a..d80e59f7b9 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -28,7 +28,15 @@ type UDPInjectableInbound interface { type InboundRegistry interface { option.InboundOptionsRegistry - CreateInbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Inbound, error) + Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) (Inbound, error) +} + +type InboundManager interface { + NewService + Inbounds() []Inbound + Get(tag string) (Inbound, bool) + Remove(tag string) error + Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, inboundType string, options any) error } type InboundContext struct { diff --git a/adapter/inbound/manager.go b/adapter/inbound/manager.go new file mode 100644 index 0000000000..d2be4b578c --- /dev/null +++ b/adapter/inbound/manager.go @@ -0,0 +1,143 @@ +package inbound + +import ( + "context" + "os" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/taskmonitor" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" +) + +var _ adapter.InboundManager = (*Manager)(nil) + +type Manager struct { + logger log.ContextLogger + registry adapter.InboundRegistry + access sync.Mutex + started bool + stage adapter.StartStage + inbounds []adapter.Inbound + inboundByTag map[string]adapter.Inbound +} + +func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry) *Manager { + return &Manager{ + logger: logger, + registry: registry, + inboundByTag: make(map[string]adapter.Inbound), + } +} + +func (m *Manager) Start(stage adapter.StartStage) error { + m.access.Lock() + defer m.access.Unlock() + if m.started && m.stage >= stage { + panic("already started") + } + m.started = true + m.stage = stage + for _, inbound := range m.inbounds { + err := adapter.LegacyStart(inbound, stage) + if err != nil { + return E.Cause(err, stage.Action(), " inbound/", inbound.Type(), "[", inbound.Tag(), "]") + } + } + return nil +} + +func (m *Manager) Close() error { + m.access.Lock() + if !m.started { + panic("not started") + } + m.started = false + inbounds := m.inbounds + m.inbounds = nil + m.access.Unlock() + monitor := taskmonitor.New(m.logger, C.StopTimeout) + var err error + for _, inbound := range inbounds { + monitor.Start("close inbound/", inbound.Type(), "[", inbound.Tag(), "]") + err = E.Append(err, inbound.Close(), func(err error) error { + return E.Cause(err, "close inbound/", inbound.Type(), "[", inbound.Tag(), "]") + }) + monitor.Finish() + } + return nil +} + +func (m *Manager) Inbounds() []adapter.Inbound { + m.access.Lock() + defer m.access.Unlock() + return m.inbounds +} + +func (m *Manager) Get(tag string) (adapter.Inbound, bool) { + m.access.Lock() + defer m.access.Unlock() + inbound, found := m.inboundByTag[tag] + return inbound, found +} + +func (m *Manager) Remove(tag string) error { + m.access.Lock() + inbound, found := m.inboundByTag[tag] + if !found { + m.access.Unlock() + return os.ErrInvalid + } + delete(m.inboundByTag, tag) + index := common.Index(m.inbounds, func(it adapter.Inbound) bool { + return it == inbound + }) + if index == -1 { + panic("invalid inbound index") + } + m.inbounds = append(m.inbounds[:index], m.inbounds[index+1:]...) + started := m.started + m.access.Unlock() + if started { + return inbound.Close() + } + return nil +} + +func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error { + inbound, err := m.registry.Create(ctx, router, logger, tag, outboundType, options) + if err != nil { + return err + } + m.access.Lock() + defer m.access.Unlock() + if m.started { + for _, stage := range adapter.ListStartStages { + err = adapter.LegacyStart(inbound, stage) + if err != nil { + return E.Cause(err, stage.Action(), " inbound/", inbound.Type(), "[", inbound.Tag(), "]") + } + } + } + if existsInbound, loaded := m.inboundByTag[tag]; loaded { + if m.started { + err = existsInbound.Close() + if err != nil { + return E.Cause(err, "close inbound/", existsInbound.Type(), "[", existsInbound.Tag(), "]") + } + } + existsIndex := common.Index(m.inbounds, func(it adapter.Inbound) bool { + return it == existsInbound + }) + if existsIndex == -1 { + panic("invalid inbound index") + } + m.inbounds = append(m.inbounds[:existsIndex], m.inbounds[existsIndex+1:]...) + } + m.inbounds = append(m.inbounds, inbound) + m.inboundByTag[tag] = inbound + return nil +} diff --git a/adapter/inbound/registry.go b/adapter/inbound/registry.go index 9f678c90e7..01e367d8b0 100644 --- a/adapter/inbound/registry.go +++ b/adapter/inbound/registry.go @@ -15,8 +15,12 @@ type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, log func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) { registry.register(outboundType, func() any { return new(Options) - }, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Inbound, error) { - return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options.(*Options))) + }, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Inbound, error) { + var options *Options + if rawOptions != nil { + options = rawOptions.(*Options) + } + return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options)) }) } @@ -28,41 +32,41 @@ type ( ) type Registry struct { - access sync.Mutex - optionsType map[string]optionsConstructorFunc - constructors map[string]constructorFunc + access sync.Mutex + optionsType map[string]optionsConstructorFunc + constructor map[string]constructorFunc } func NewRegistry() *Registry { return &Registry{ - optionsType: make(map[string]optionsConstructorFunc), - constructors: make(map[string]constructorFunc), + optionsType: make(map[string]optionsConstructorFunc), + constructor: make(map[string]constructorFunc), } } -func (r *Registry) CreateOptions(outboundType string) (any, bool) { - r.access.Lock() - defer r.access.Unlock() - optionsConstructor, loaded := r.optionsType[outboundType] +func (m *Registry) CreateOptions(outboundType string) (any, bool) { + m.access.Lock() + defer m.access.Unlock() + optionsConstructor, loaded := m.optionsType[outboundType] if !loaded { return nil, false } return optionsConstructor(), true } -func (r *Registry) CreateInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) { - r.access.Lock() - defer r.access.Unlock() - constructor, loaded := r.constructors[outboundType] +func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Inbound, error) { + m.access.Lock() + defer m.access.Unlock() + constructor, loaded := m.constructor[outboundType] if !loaded { return nil, E.New("outbound type not found: " + outboundType) } return constructor(ctx, router, logger, tag, options) } -func (r *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) { - r.access.Lock() - defer r.access.Unlock() - r.optionsType[outboundType] = optionsConstructor - r.constructors[outboundType] = constructor +func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) { + m.access.Lock() + defer m.access.Unlock() + m.optionsType[outboundType] = optionsConstructor + m.constructor[outboundType] = constructor } diff --git a/adapter/lifecycle.go b/adapter/lifecycle.go new file mode 100644 index 0000000000..85de425d5a --- /dev/null +++ b/adapter/lifecycle.go @@ -0,0 +1,41 @@ +package adapter + +type StartStage uint8 + +const ( + StartStateInitialize StartStage = iota + StartStateStart + StartStatePostStart + StartStateStarted +) + +var ListStartStages = []StartStage{ + StartStateInitialize, + StartStateStart, + StartStatePostStart, + StartStateStarted, +} + +func (s StartStage) Action() string { + switch s { + case StartStateInitialize: + return "initialize" + case StartStateStart: + return "start" + case StartStatePostStart: + return "post-start" + case StartStateStarted: + return "start-after-started" + default: + panic("unknown stage") + } +} + +type NewService interface { + NewStarter + Close() error +} + +type NewStarter interface { + Start(stage StartStage) error +} diff --git a/adapter/lifecycle_legacy.go b/adapter/lifecycle_legacy.go new file mode 100644 index 0000000000..5968131bb4 --- /dev/null +++ b/adapter/lifecycle_legacy.go @@ -0,0 +1,33 @@ +package adapter + +type LegacyPreStarter interface { + PreStart() error +} + +type LegacyPostStarter interface { + PostStart() error +} + +func LegacyStart(starter any, stage StartStage) error { + switch stage { + case StartStateInitialize: + if preStarter, isPreStarter := starter.(interface { + PreStart() error + }); isPreStarter { + return preStarter.PreStart() + } + case StartStateStart: + if starter, isStarter := starter.(interface { + Start() error + }); isStarter { + return starter.Start() + } + case StartStatePostStart: + if postStarter, isPostStarter := starter.(interface { + PostStart() error + }); isPostStarter { + return postStarter.PostStart() + } + } + return nil +} diff --git a/adapter/outbound.go b/adapter/outbound.go index df11ed6132..b170398a96 100644 --- a/adapter/outbound.go +++ b/adapter/outbound.go @@ -22,3 +22,12 @@ type OutboundRegistry interface { option.OutboundOptionsRegistry CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error) } + +type OutboundManager interface { + NewService + Outbounds() []Outbound + Outbound(tag string) (Outbound, bool) + Default() Outbound + Remove(tag string) error + Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) error +} diff --git a/adapter/outbound/manager.go b/adapter/outbound/manager.go new file mode 100644 index 0000000000..b3e1a170bf --- /dev/null +++ b/adapter/outbound/manager.go @@ -0,0 +1,265 @@ +package outbound + +import ( + "context" + "io" + "os" + "strings" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/taskmonitor" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" +) + +var _ adapter.OutboundManager = (*Manager)(nil) + +type Manager struct { + logger log.ContextLogger + registry adapter.OutboundRegistry + defaultTag string + access sync.Mutex + started bool + stage adapter.StartStage + outbounds []adapter.Outbound + outboundByTag map[string]adapter.Outbound + dependByTag map[string][]string + defaultOutbound adapter.Outbound + defaultOutboundFallback adapter.Outbound +} + +func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, defaultTag string) *Manager { + return &Manager{ + logger: logger, + registry: registry, + defaultTag: defaultTag, + outboundByTag: make(map[string]adapter.Outbound), + dependByTag: make(map[string][]string), + } +} + +func (m *Manager) Initialize(defaultOutboundFallback adapter.Outbound) { + m.defaultOutboundFallback = defaultOutboundFallback +} + +func (m *Manager) Start(stage adapter.StartStage) error { + m.access.Lock() + defer m.access.Unlock() + if m.started && m.stage >= stage { + panic("already started") + } + m.started = true + m.stage = stage + if stage == adapter.StartStateStart { + m.startOutbounds() + } else { + for _, outbound := range m.outbounds { + err := adapter.LegacyStart(outbound, stage) + if err != nil { + return E.Cause(err, stage.Action(), " outbound/", outbound.Type(), "[", outbound.Tag(), "]") + } + } + } + return nil +} + +func (m *Manager) startOutbounds() error { + monitor := taskmonitor.New(m.logger, C.StartTimeout) + started := make(map[string]bool) + for { + canContinue := false + startOne: + for _, outboundToStart := range m.outbounds { + outboundTag := outboundToStart.Tag() + if started[outboundTag] { + continue + } + dependencies := outboundToStart.Dependencies() + for _, dependency := range dependencies { + if !started[dependency] { + continue startOne + } + } + started[outboundTag] = true + canContinue = true + if starter, isStarter := outboundToStart.(interface { + Start() error + }); isStarter { + monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]") + err := starter.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "start outbound/", outboundToStart.Type(), "[", outboundTag, "]") + } + } + } + if len(started) == len(m.outbounds) { + break + } + if canContinue { + continue + } + currentOutbound := common.Find(m.outbounds, func(it adapter.Outbound) bool { + return !started[it.Tag()] + }) + var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error + lintOutbound = func(oTree []string, oCurrent adapter.Outbound) error { + problemOutboundTag := common.Find(oCurrent.Dependencies(), func(it string) bool { + return !started[it] + }) + if common.Contains(oTree, problemOutboundTag) { + return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag) + } + problemOutbound := m.outboundByTag[problemOutboundTag] + if problemOutbound == nil { + return E.New("dependency[", problemOutboundTag, "] not found for outbound[", oCurrent.Tag(), "]") + } + return lintOutbound(append(oTree, problemOutboundTag), problemOutbound) + } + return lintOutbound([]string{currentOutbound.Tag()}, currentOutbound) + } + return nil +} + +func (m *Manager) Close() error { + monitor := taskmonitor.New(m.logger, C.StopTimeout) + m.access.Lock() + if !m.started { + panic("not started") + } + m.started = false + outbounds := m.outbounds + m.outbounds = nil + m.access.Unlock() + var err error + for _, outbound := range outbounds { + if closer, isCloser := outbound.(io.Closer); isCloser { + monitor.Start("close outbound/", outbound.Type(), "[", outbound.Tag(), "]") + err = E.Append(err, closer.Close(), func(err error) error { + return E.Cause(err, "close outbound/", outbound.Type(), "[", outbound.Tag(), "]") + }) + monitor.Finish() + } + } + return nil +} + +func (m *Manager) Outbounds() []adapter.Outbound { + m.access.Lock() + defer m.access.Unlock() + return m.outbounds +} + +func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) { + m.access.Lock() + defer m.access.Unlock() + outbound, found := m.outboundByTag[tag] + return outbound, found +} + +func (m *Manager) Default() adapter.Outbound { + m.access.Lock() + defer m.access.Unlock() + if m.defaultOutbound != nil { + return m.defaultOutbound + } else { + return m.defaultOutboundFallback + } +} + +func (m *Manager) Remove(tag string) error { + m.access.Lock() + outbound, found := m.outboundByTag[tag] + if !found { + m.access.Unlock() + return os.ErrInvalid + } + delete(m.outboundByTag, tag) + index := common.Index(m.outbounds, func(it adapter.Outbound) bool { + return it == outbound + }) + if index == -1 { + panic("invalid inbound index") + } + m.outbounds = append(m.outbounds[:index], m.outbounds[index+1:]...) + started := m.started + if m.defaultOutbound == outbound { + if len(m.outbounds) > 0 { + m.defaultOutbound = m.outbounds[0] + m.logger.Info("updated default outbound to ", m.defaultOutbound.Tag()) + } else { + m.defaultOutbound = nil + } + } + dependBy := m.dependByTag[tag] + if len(dependBy) > 0 { + return E.New("outbound[", tag, "] is depended by ", strings.Join(dependBy, ", ")) + } + dependencies := outbound.Dependencies() + for _, dependency := range dependencies { + if len(m.dependByTag[dependency]) == 1 { + delete(m.dependByTag, dependency) + } else { + m.dependByTag[dependency] = common.Filter(m.dependByTag[dependency], func(it string) bool { + return it != tag + }) + } + } + m.access.Unlock() + if started { + return common.Close(outbound) + } + return nil +} + +func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, inboundType string, options any) error { + if tag == "" { + return os.ErrInvalid + } + outbound, err := m.registry.CreateOutbound(ctx, router, logger, tag, inboundType, options) + if err != nil { + return err + } + m.access.Lock() + defer m.access.Unlock() + if m.started { + for _, stage := range adapter.ListStartStages { + err = adapter.LegacyStart(outbound, stage) + if err != nil { + return E.Cause(err, stage.Action(), " outbound/", outbound.Type(), "[", outbound.Tag(), "]") + } + } + } + if existsOutbound, loaded := m.outboundByTag[tag]; loaded { + if m.started { + err = common.Close(existsOutbound) + if err != nil { + return E.Cause(err, "close outbound/", existsOutbound.Type(), "[", existsOutbound.Tag(), "]") + } + } + existsIndex := common.Index(m.outbounds, func(it adapter.Outbound) bool { + return it == existsOutbound + }) + if existsIndex == -1 { + panic("invalid inbound index") + } + m.outbounds = append(m.outbounds[:existsIndex], m.outbounds[existsIndex+1:]...) + } + m.outbounds = append(m.outbounds, outbound) + m.outboundByTag[tag] = outbound + dependencies := outbound.Dependencies() + for _, dependency := range dependencies { + m.dependByTag[dependency] = append(m.dependByTag[dependency], tag) + } + if tag == m.defaultTag || (m.defaultTag == "" && m.defaultOutbound == nil) { + m.defaultOutbound = outbound + if m.started { + m.logger.Info("updated default outbound to ", outbound.Tag()) + } + } + return nil +} diff --git a/adapter/outbound/registry.go b/adapter/outbound/registry.go index f25631cf14..8743ba1030 100644 --- a/adapter/outbound/registry.go +++ b/adapter/outbound/registry.go @@ -15,8 +15,12 @@ type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, log func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) { registry.register(outboundType, func() any { return new(Options) - }, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Outbound, error) { - return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options.(*Options))) + }, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Outbound, error) { + var options *Options + if rawOptions != nil { + options = rawOptions.(*Options) + } + return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options)) }) } diff --git a/adapter/prestart.go b/adapter/prestart.go index 6a39aec3ff..b8e8da3083 100644 --- a/adapter/prestart.go +++ b/adapter/prestart.go @@ -1,9 +1 @@ package adapter - -type PreStarter interface { - PreStart() error -} - -type PostStarter interface { - PostStart() error -} diff --git a/adapter/router.go b/adapter/router.go index c9cd46e91a..b8ac51f573 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -15,21 +15,13 @@ import ( M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/x/list" - "github.com/sagernet/sing/service" mdns "github.com/miekg/dns" "go4.org/netipx" ) type Router interface { - Service - PreStarter - PostStarter - Cleanup() error - - Outbounds() []Outbound - Outbound(tag string) (Outbound, bool) - DefaultOutbound(network string) (Outbound, error) + NewService FakeIPStore() FakeIPStore @@ -84,14 +76,6 @@ type ConnectionRouterEx interface { RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) } -func ContextWithRouter(ctx context.Context, router Router) context.Context { - return service.ContextWith(ctx, router) -} - -func RouterFromContext(ctx context.Context) Router { - return service.FromContext[Router](ctx) -} - type RuleSet interface { Name() string StartContext(ctx context.Context, startContext *HTTPStartContext) error diff --git a/box.go b/box.go index bc714e5a32..d44e8ed058 100644 --- a/box.go +++ b/box.go @@ -9,6 +9,8 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/inbound" + "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/taskmonitor" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental" @@ -30,8 +32,8 @@ var _ adapter.Service = (*Box)(nil) type Box struct { createdAt time.Time router adapter.Router - inbounds []adapter.Inbound - outbounds []adapter.Outbound + inbound *inbound.Manager + outbound *outbound.Manager logFactory log.Factory logger log.ContextLogger preServices1 map[string]adapter.Service @@ -66,6 +68,7 @@ func New(options Options) (*Box, error) { if ctx == nil { ctx = context.Background() } + ctx = service.ContextWithDefaultRegistry(ctx) inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx) if inboundRegistry == nil { return nil, E.New("missing inbound registry in context") @@ -74,7 +77,6 @@ func New(options Options) (*Box, error) { if outboundRegistry == nil { return nil, E.New("missing outbound registry in context") } - ctx = service.ContextWithDefaultRegistry(ctx) ctx = pause.WithDefaultManager(ctx) experimentalOptions := common.PtrValueOrDefault(options.Experimental) applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) @@ -106,10 +108,15 @@ func New(options Options) (*Box, error) { if err != nil { return nil, E.Cause(err, "create log factory") } + routeOptions := common.PtrValueOrDefault(options.Route) + inboundManager := inbound.NewManager(logFactory.NewLogger("inbound-manager"), inboundRegistry) + outboundManager := outbound.NewManager(logFactory.NewLogger("outbound-manager"), outboundRegistry, routeOptions.Final) + ctx = service.ContextWith[adapter.InboundManager](ctx, inboundManager) + ctx = service.ContextWith[adapter.OutboundManager](ctx, outboundManager) router, err := route.NewRouter( ctx, logFactory, - common.PtrValueOrDefault(options.Route), + routeOptions, common.PtrValueOrDefault(options.DNS), common.PtrValueOrDefault(options.NTP), options.Inbounds, @@ -118,15 +125,13 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "parse route options") } for i, inboundOptions := range options.Inbounds { - var currentInbound adapter.Inbound var tag string if inboundOptions.Tag != "" { tag = inboundOptions.Tag } else { tag = F.ToString(i) } - currentInbound, err = inboundRegistry.CreateInbound( - ctx, + err = inboundManager.Create(ctx, router, logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")), tag, @@ -134,12 +139,10 @@ func New(options Options) (*Box, error) { inboundOptions.Options, ) if err != nil { - return nil, E.Cause(err, "parse inbound[", i, "]") + return nil, E.Cause(err, "initialize inbound[", i, "]") } - inbounds = append(inbounds, currentInbound) } for i, outboundOptions := range options.Outbounds { - var currentOutbound adapter.Outbound var tag string if outboundOptions.Tag != "" { tag = outboundOptions.Tag @@ -153,7 +156,7 @@ func New(options Options) (*Box, error) { Outbound: tag, }) } - currentOutbound, err = outboundRegistry.CreateOutbound( + err = outboundManager.Create( outboundCtx, router, logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")), @@ -162,16 +165,18 @@ func New(options Options) (*Box, error) { outboundOptions.Options, ) if err != nil { - return nil, E.Cause(err, "parse outbound[", i, "]") + return nil, E.Cause(err, "initialize outbound[", i, "]") } - outbounds = append(outbounds, currentOutbound) } - err = router.Initialize(inbounds, outbounds, func() adapter.Outbound { - defaultOutbound, cErr := direct.NewOutbound(ctx, router, logFactory.NewLogger("outbound/direct"), "direct", option.DirectOutboundOptions{}) - common.Must(cErr) - outbounds = append(outbounds, defaultOutbound) - return defaultOutbound - }) + outboundManager.Initialize(common.Must1( + direct.NewOutbound( + ctx, + router, + logFactory.NewLogger("outbound/direct"), + "direct", + option.DirectOutboundOptions{}, + ), + )) if err != nil { return nil, err } @@ -195,7 +200,7 @@ func New(options Options) (*Box, error) { if needClashAPI { clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options) - clashServer, err := experimental.NewClashServer(ctx, router, logFactory.(log.ObservableFactory), clashAPIOptions) + clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions) if err != nil { return nil, E.Cause(err, "create clash api server") } @@ -212,8 +217,8 @@ func New(options Options) (*Box, error) { } return &Box{ router: router, - inbounds: inbounds, - outbounds: outbounds, + inbound: inboundManager, + outbound: outboundManager, createdAt: createdAt, logFactory: logFactory, logger: logFactory.Logger(), @@ -271,7 +276,7 @@ func (s *Box) preStart() error { return E.Cause(err, "start logger") } for serviceName, service := range s.preServices1 { - if preService, isPreService := service.(adapter.PreStarter); isPreService { + if preService, isPreService := service.(adapter.LegacyPreStarter); isPreService { monitor.Start("pre-start ", serviceName) err := preService.PreStart() monitor.Finish() @@ -281,7 +286,7 @@ func (s *Box) preStart() error { } } for serviceName, service := range s.preServices2 { - if preService, isPreService := service.(adapter.PreStarter); isPreService { + if preService, isPreService := service.(adapter.LegacyPreStarter); isPreService { monitor.Start("pre-start ", serviceName) err := preService.PreStart() monitor.Finish() @@ -290,15 +295,15 @@ func (s *Box) preStart() error { } } } - err = s.router.PreStart() + err = s.router.Start(adapter.StartStateInitialize) if err != nil { - return E.Cause(err, "pre-start router") + return E.Cause(err, "initialize router") } - err = s.startOutbounds() + err = s.outbound.Start(adapter.StartStateStart) if err != nil { return err } - return s.router.Start() + return s.router.Start(adapter.StartStateStart) } func (s *Box) start() error { @@ -318,52 +323,39 @@ func (s *Box) start() error { return E.Cause(err, "start ", serviceName) } } - for i, in := range s.inbounds { - var tag string - if in.Tag() == "" { - tag = F.ToString(i) - } else { - tag = in.Tag() - } - err = in.Start() - if err != nil { - return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]") - } - } - err = s.postStart() + err = s.inbound.Start(adapter.StartStateStart) if err != nil { return err } - return s.router.Cleanup() -} - -func (s *Box) postStart() error { for serviceName, service := range s.postServices { err := service.Start() if err != nil { return E.Cause(err, "start ", serviceName) } } - // TODO: reorganize ALL start order - for _, out := range s.outbounds { - if lateOutbound, isLateOutbound := out.(adapter.PostStarter); isLateOutbound { - err := lateOutbound.PostStart() - if err != nil { - return E.Cause(err, "post-start outbound/", out.Tag()) - } - } + err = s.outbound.Start(adapter.StartStatePostStart) + if err != nil { + return err } - err := s.router.PostStart() + err = s.router.Start(adapter.StartStatePostStart) if err != nil { return err } - for _, in := range s.inbounds { - if lateInbound, isLateInbound := in.(adapter.PostStarter); isLateInbound { - err = lateInbound.PostStart() - if err != nil { - return E.Cause(err, "post-start inbound/", in.Tag()) - } - } + err = s.inbound.Start(adapter.StartStatePostStart) + if err != nil { + return err + } + err = s.router.Start(adapter.StartStateStarted) + if err != nil { + return err + } + err = s.outbound.Start(adapter.StartStateStarted) + if err != nil { + return err + } + err = s.inbound.Start(adapter.StartStateStarted) + if err != nil { + return err } return nil } @@ -384,20 +376,8 @@ func (s *Box) Close() error { }) monitor.Finish() } - for i, in := range s.inbounds { - monitor.Start("close inbound/", in.Type(), "[", i, "]") - errors = E.Append(errors, in.Close(), func(err error) error { - return E.Cause(err, "close inbound/", in.Type(), "[", i, "]") - }) - monitor.Finish() - } - for i, out := range s.outbounds { - monitor.Start("close outbound/", out.Type(), "[", i, "]") - errors = E.Append(errors, common.Close(out), func(err error) error { - return E.Cause(err, "close outbound/", out.Type(), "[", i, "]") - }) - monitor.Finish() - } + errors = E.Errors(errors, s.inbound.Close()) + errors = E.Errors(errors, s.outbound.Close()) monitor.Start("close router") if err := common.Close(s.router); err != nil { errors = E.Append(errors, err, func(err error) error { @@ -427,6 +407,14 @@ func (s *Box) Close() error { return errors } +func (s *Box) Inbound() adapter.InboundManager { + return s.inbound +} + +func (s *Box) Outbound() adapter.OutboundManager { + return s.outbound +} + func (s *Box) Router() adapter.Router { return s.router } diff --git a/box_outbound.go b/box_outbound.go deleted file mode 100644 index f03f3b7d41..0000000000 --- a/box_outbound.go +++ /dev/null @@ -1,85 +0,0 @@ -package box - -import ( - "strings" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/taskmonitor" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing/common" - E "github.com/sagernet/sing/common/exceptions" - F "github.com/sagernet/sing/common/format" -) - -func (s *Box) startOutbounds() error { - monitor := taskmonitor.New(s.logger, C.StartTimeout) - outboundTags := make(map[adapter.Outbound]string) - outbounds := make(map[string]adapter.Outbound) - for i, outboundToStart := range s.outbounds { - var outboundTag string - if outboundToStart.Tag() == "" { - outboundTag = F.ToString(i) - } else { - outboundTag = outboundToStart.Tag() - } - if _, exists := outbounds[outboundTag]; exists { - return E.New("outbound tag ", outboundTag, " duplicated") - } - outboundTags[outboundToStart] = outboundTag - outbounds[outboundTag] = outboundToStart - } - started := make(map[string]bool) - for { - canContinue := false - startOne: - for _, outboundToStart := range s.outbounds { - outboundTag := outboundTags[outboundToStart] - if started[outboundTag] { - continue - } - dependencies := outboundToStart.Dependencies() - for _, dependency := range dependencies { - if !started[dependency] { - continue startOne - } - } - started[outboundTag] = true - canContinue = true - if starter, isStarter := outboundToStart.(interface { - Start() error - }); isStarter { - monitor.Start("initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]") - err := starter.Start() - monitor.Finish() - if err != nil { - return E.Cause(err, "initialize outbound/", outboundToStart.Type(), "[", outboundTag, "]") - } - } - } - if len(started) == len(s.outbounds) { - break - } - if canContinue { - continue - } - currentOutbound := common.Find(s.outbounds, func(it adapter.Outbound) bool { - return !started[outboundTags[it]] - }) - var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error - lintOutbound = func(oTree []string, oCurrent adapter.Outbound) error { - problemOutboundTag := common.Find(oCurrent.Dependencies(), func(it string) bool { - return !started[it] - }) - if common.Contains(oTree, problemOutboundTag) { - return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag) - } - problemOutbound := outbounds[problemOutboundTag] - if problemOutbound == nil { - return E.New("dependency[", problemOutboundTag, "] not found for outbound[", outboundTags[oCurrent], "]") - } - return lintOutbound(append(oTree, problemOutboundTag), problemOutbound) - } - return lintOutbound([]string{outboundTags[currentOutbound]}, currentOutbound) - } - return nil -} diff --git a/cmd/sing-box/cmd_tools.go b/cmd/sing-box/cmd_tools.go index f4c8736ec1..55e5b45873 100644 --- a/cmd/sing-box/cmd_tools.go +++ b/cmd/sing-box/cmd_tools.go @@ -41,11 +41,11 @@ func createPreStartedClient() (*box.Box, error) { return instance, nil } -func createDialer(instance *box.Box, network string, outboundTag string) (N.Dialer, error) { +func createDialer(instance *box.Box, outboundTag string) (N.Dialer, error) { if outboundTag == "" { - return instance.Router().DefaultOutbound(N.NetworkName(network)) + return instance.Outbound().Default(), nil } else { - outbound, loaded := instance.Router().Outbound(outboundTag) + outbound, loaded := instance.Outbound().Outbound(outboundTag) if !loaded { return nil, E.New("outbound not found: ", outboundTag) } diff --git a/cmd/sing-box/cmd_tools_connect.go b/cmd/sing-box/cmd_tools_connect.go index 3ea04bcd40..d352d533e7 100644 --- a/cmd/sing-box/cmd_tools_connect.go +++ b/cmd/sing-box/cmd_tools_connect.go @@ -45,7 +45,7 @@ func connect(address string) error { return err } defer instance.Close() - dialer, err := createDialer(instance, commandConnectFlagNetwork, commandToolsFlagOutbound) + dialer, err := createDialer(instance, commandToolsFlagOutbound) if err != nil { return err } diff --git a/cmd/sing-box/cmd_tools_fetch.go b/cmd/sing-box/cmd_tools_fetch.go index 3f62424a49..5ee3b87510 100644 --- a/cmd/sing-box/cmd_tools_fetch.go +++ b/cmd/sing-box/cmd_tools_fetch.go @@ -48,7 +48,7 @@ func fetch(args []string) error { httpClient = &http.Client{ Transport: &http.Transport{ DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - dialer, err := createDialer(instance, network, commandToolsFlagOutbound) + dialer, err := createDialer(instance, commandToolsFlagOutbound) if err != nil { return nil, err } diff --git a/cmd/sing-box/cmd_tools_fetch_http3.go b/cmd/sing-box/cmd_tools_fetch_http3.go index 9a7e440272..b7a31a726d 100644 --- a/cmd/sing-box/cmd_tools_fetch_http3.go +++ b/cmd/sing-box/cmd_tools_fetch_http3.go @@ -16,7 +16,7 @@ import ( ) func initializeHTTP3Client(instance *box.Box) error { - dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound) + dialer, err := createDialer(instance, commandToolsFlagOutbound) if err != nil { return err } diff --git a/cmd/sing-box/cmd_tools_synctime.go b/cmd/sing-box/cmd_tools_synctime.go index 20d73a6d67..38510eb8e1 100644 --- a/cmd/sing-box/cmd_tools_synctime.go +++ b/cmd/sing-box/cmd_tools_synctime.go @@ -9,7 +9,6 @@ import ( "github.com/sagernet/sing-box/log" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ntp" "github.com/spf13/cobra" @@ -45,7 +44,7 @@ func syncTime() error { if err != nil { return err } - dialer, err := createDialer(instance, N.NetworkUDP, commandToolsFlagOutbound) + dialer, err := createDialer(instance, commandToolsFlagOutbound) if err != nil { return err } diff --git a/common/dialer/detour.go b/common/dialer/detour.go index 81600913d4..c1d40faa1d 100644 --- a/common/dialer/detour.go +++ b/common/dialer/detour.go @@ -12,15 +12,15 @@ import ( ) type DetourDialer struct { - router adapter.Router - detour string - dialer N.Dialer - initOnce sync.Once - initErr error + outboundManager adapter.OutboundManager + detour string + dialer N.Dialer + initOnce sync.Once + initErr error } -func NewDetour(router adapter.Router, detour string) N.Dialer { - return &DetourDialer{router: router, detour: detour} +func NewDetour(outboundManager adapter.OutboundManager, detour string) N.Dialer { + return &DetourDialer{outboundManager: outboundManager, detour: detour} } func (d *DetourDialer) Start() error { @@ -31,7 +31,7 @@ func (d *DetourDialer) Start() error { func (d *DetourDialer) Dialer() (N.Dialer, error) { d.initOnce.Do(func() { var loaded bool - d.dialer, loaded = d.router.Outbound(d.detour) + d.dialer, loaded = d.outboundManager.Outbound(d.detour) if !loaded { d.initErr = E.New("outbound detour not found: ", d.detour) } diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index 56c5f2ad15..fe4c7c1204 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -1,21 +1,22 @@ package dialer import ( + "context" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-dns" + E "github.com/sagernet/sing/common/exceptions" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" ) -func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error) { +func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) { + router := service.FromContext[adapter.Router](ctx) if options.IsWireGuardListener { return NewDefault(router, options) } - if router == nil { - return NewDefault(nil, options) - } var ( dialer N.Dialer err error @@ -26,7 +27,14 @@ func New(router adapter.Router, options option.DialerOptions) (N.Dialer, error) return nil, err } } else { - dialer = NewDetour(router, options.Detour) + outboundManager := service.FromContext[adapter.OutboundManager](ctx) + if outboundManager == nil { + return nil, E.New("missing outbound manager") + } + dialer = NewDetour(outboundManager, options.Detour) + } + if router == nil { + return NewDefault(router, options) } if options.Detour == "" { dialer = NewResolveDialer( diff --git a/common/dialer/router.go b/common/dialer/router.go index 2531607753..3edce65b09 100644 --- a/common/dialer/router.go +++ b/common/dialer/router.go @@ -9,30 +9,22 @@ import ( N "github.com/sagernet/sing/common/network" ) -type RouterDialer struct { - router adapter.Router +type DefaultOutboundDialer struct { + outboundManager adapter.OutboundManager } -func NewRouter(router adapter.Router) N.Dialer { - return &RouterDialer{router: router} +func NewDefaultOutbound(outboundManager adapter.OutboundManager) N.Dialer { + return &DefaultOutboundDialer{outboundManager: outboundManager} } -func (d *RouterDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - dialer, err := d.router.DefaultOutbound(network) - if err != nil { - return nil, err - } - return dialer.DialContext(ctx, network, destination) +func (d *DefaultOutboundDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + return d.outboundManager.Default().DialContext(ctx, network, destination) } -func (d *RouterDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - dialer, err := d.router.DefaultOutbound(N.NetworkUDP) - if err != nil { - return nil, err - } - return dialer.ListenPacket(ctx, destination) +func (d *DefaultOutboundDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + return d.outboundManager.Default().ListenPacket(ctx, destination) } -func (d *RouterDialer) Upstream() any { - return d.router +func (d *DefaultOutboundDialer) Upstream() any { + return d.outboundManager.Default() } diff --git a/common/settings/proxy_darwin.go b/common/settings/proxy_darwin.go index f03658a858..2d86fa2d9a 100644 --- a/common/settings/proxy_darwin.go +++ b/common/settings/proxy_darwin.go @@ -12,6 +12,7 @@ import ( M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/shell" "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" ) type DarwinSystemProxy struct { @@ -24,7 +25,7 @@ type DarwinSystemProxy struct { } func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*DarwinSystemProxy, error) { - interfaceMonitor := adapter.RouterFromContext(ctx).InterfaceMonitor() + interfaceMonitor := service.FromContext[adapter.Router](ctx).InterfaceMonitor() if interfaceMonitor == nil { return nil, E.New("missing interface monitor") } diff --git a/common/tls/ech_client.go b/common/tls/ech_client.go index 19022d410c..0ae3997aba 100644 --- a/common/tls/ech_client.go +++ b/common/tls/ech_client.go @@ -19,6 +19,7 @@ import ( "github.com/sagernet/sing-dns" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/ntp" + "github.com/sagernet/sing/service" mDNS "github.com/miekg/dns" ) @@ -214,7 +215,7 @@ func fetchECHClientConfig(ctx context.Context) func(_ context.Context, serverNam }, }, } - response, err := adapter.RouterFromContext(ctx).Exchange(ctx, message) + response, err := service.FromContext[adapter.Router](ctx).Exchange(ctx, message) if err != nil { return nil, err } diff --git a/common/tls/reality_server.go b/common/tls/reality_server.go index 6395f65201..069f27b975 100644 --- a/common/tls/reality_server.go +++ b/common/tls/reality_server.go @@ -11,7 +11,6 @@ import ( "time" "github.com/sagernet/reality" - "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" @@ -102,7 +101,7 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb tlsConfig.ShortIds[shortID] = true } - handshakeDialer, err := dialer.New(adapter.RouterFromContext(ctx), options.Reality.Handshake.DialerOptions) + handshakeDialer, err := dialer.New(ctx, options.Reality.Handshake.DialerOptions) if err != nil { return nil, err } diff --git a/experimental/clashapi.go b/experimental/clashapi.go index 872d9b9956..4ad07c8b88 100644 --- a/experimental/clashapi.go +++ b/experimental/clashapi.go @@ -12,7 +12,7 @@ import ( "github.com/sagernet/sing/common" ) -type ClashServerConstructor = func(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) +type ClashServerConstructor = func(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) var clashServerConstructor ClashServerConstructor @@ -20,11 +20,11 @@ func RegisterClashServerConstructor(constructor ClashServerConstructor) { clashServerConstructor = constructor } -func NewClashServer(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { +func NewClashServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { if clashServerConstructor == nil { return nil, os.ErrInvalid } - return clashServerConstructor(ctx, router, logFactory, options) + return clashServerConstructor(ctx, logFactory, options) } func CalculateClashModeList(options option.Options) []string { diff --git a/experimental/clashapi/api_meta_group.go b/experimental/clashapi/api_meta_group.go index 531311f4fe..c5c07ba6a3 100644 --- a/experimental/clashapi/api_meta_group.go +++ b/experimental/clashapi/api_meta_group.go @@ -23,7 +23,7 @@ func groupRouter(server *Server) http.Handler { r := chi.NewRouter() r.Get("/", getGroups(server)) r.Route("/{name}", func(r chi.Router) { - r.Use(parseProxyName, findProxyByName(server.router)) + r.Use(parseProxyName, findProxyByName(server)) r.Get("/", getGroup(server)) r.Get("/delay", getGroupDelay(server)) }) @@ -32,7 +32,7 @@ func groupRouter(server *Server) http.Handler { func getGroups(server *Server) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - groups := common.Map(common.Filter(server.router.Outbounds(), func(it adapter.Outbound) bool { + groups := common.Map(common.Filter(server.outboundManager.Outbounds(), func(it adapter.Outbound) bool { _, isGroup := it.(adapter.OutboundGroup) return isGroup }), func(it adapter.Outbound) *badjson.JSONObject { @@ -86,7 +86,7 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) result, err = urlTestGroup.URLTest(ctx) } else { outbounds := common.FilterNotNil(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { - itOutbound, _ := server.router.Outbound(it) + itOutbound, _ := server.outboundManager.Outbound(it) return itOutbound })) b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10)) @@ -100,7 +100,7 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) continue } checked[realTag] = true - p, loaded := server.router.Outbound(realTag) + p, loaded := server.outboundManager.Outbound(realTag) if !loaded { continue } diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go index 4a9564ee86..8d8ecb3845 100644 --- a/experimental/clashapi/proxies.go +++ b/experimental/clashapi/proxies.go @@ -23,10 +23,10 @@ import ( func proxyRouter(server *Server, router adapter.Router) http.Handler { r := chi.NewRouter() - r.Get("/", getProxies(server, router)) + r.Get("/", getProxies(server)) r.Route("/{name}", func(r chi.Router) { - r.Use(parseProxyName, findProxyByName(router)) + r.Use(parseProxyName, findProxyByName(server)) r.Get("/", getProxy(server)) r.Get("/delay", getProxyDelay(server)) r.Put("/", updateProxy) @@ -42,11 +42,11 @@ func parseProxyName(next http.Handler) http.Handler { }) } -func findProxyByName(router adapter.Router) func(next http.Handler) http.Handler { +func findProxyByName(server *Server) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { name := r.Context().Value(CtxKeyProxyName).(string) - proxy, exist := router.Outbound(name) + proxy, exist := server.outboundManager.Outbound(name) if !exist { render.Status(r, http.StatusNotFound) render.JSON(w, r, ErrNotFound) @@ -83,10 +83,10 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject { return &info } -func getProxies(server *Server, router adapter.Router) func(w http.ResponseWriter, r *http.Request) { +func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var proxyMap badjson.JSONObject - outbounds := common.Filter(router.Outbounds(), func(detour adapter.Outbound) bool { + outbounds := common.Filter(server.outboundManager.Outbounds(), func(detour adapter.Outbound) bool { return detour.Tag() != "" }) @@ -100,12 +100,7 @@ func getProxies(server *Server, router adapter.Router) func(w http.ResponseWrite allProxies = append(allProxies, detour.Tag()) } - var defaultTag string - if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { - defaultTag = defaultOutbound.Tag() - } else { - defaultTag = allProxies[0] - } + defaultTag := server.outboundManager.Default().Tag() sort.SliceStable(allProxies, func(i, j int) bool { return allProxies[i] == defaultTag diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 462f76b57d..89f33de348 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -40,15 +40,16 @@ func init() { var _ adapter.ClashServer = (*Server)(nil) type Server struct { - ctx context.Context - router adapter.Router - logger log.Logger - httpServer *http.Server - trafficManager *trafficontrol.Manager - urlTestHistory *urltest.HistoryStorage - mode string - modeList []string - modeUpdateHook chan<- struct{} + ctx context.Context + router adapter.Router + outboundManager adapter.OutboundManager + logger log.Logger + httpServer *http.Server + trafficManager *trafficontrol.Manager + urlTestHistory *urltest.HistoryStorage + mode string + modeList []string + modeUpdateHook chan<- struct{} externalController bool externalUI string @@ -56,13 +57,14 @@ type Server struct { externalUIDownloadDetour string } -func NewServer(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { +func NewServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { trafficManager := trafficontrol.NewManager() chiRouter := chi.NewRouter() - server := &Server{ - ctx: ctx, - router: router, - logger: logFactory.NewLogger("clash-api"), + s := &Server{ + ctx: ctx, + router: service.FromContext[adapter.Router](ctx), + outboundManager: service.FromContext[adapter.OutboundManager](ctx), + logger: logFactory.NewLogger("clash-api"), httpServer: &http.Server{ Addr: options.ExternalController, Handler: chiRouter, @@ -73,18 +75,18 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ externalUIDownloadURL: options.ExternalUIDownloadURL, externalUIDownloadDetour: options.ExternalUIDownloadDetour, } - server.urlTestHistory = service.PtrFromContext[urltest.HistoryStorage](ctx) - if server.urlTestHistory == nil { - server.urlTestHistory = urltest.NewHistoryStorage() + s.urlTestHistory = service.PtrFromContext[urltest.HistoryStorage](ctx) + if s.urlTestHistory == nil { + s.urlTestHistory = urltest.NewHistoryStorage() } defaultMode := "Rule" if options.DefaultMode != "" { defaultMode = options.DefaultMode } - if !common.Contains(server.modeList, defaultMode) { - server.modeList = append([]string{defaultMode}, server.modeList...) + if !common.Contains(s.modeList, defaultMode) { + s.modeList = append([]string{defaultMode}, s.modeList...) } - server.mode = defaultMode + s.mode = defaultMode //goland:noinspection GoDeprecation //nolint:staticcheck if options.StoreMode || options.StoreSelected || options.StoreFakeIP || options.CacheFile != "" || options.CacheID != "" { @@ -108,27 +110,27 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ r.Get("/logs", getLogs(logFactory)) r.Get("/traffic", traffic(trafficManager)) r.Get("/version", version) - r.Mount("/configs", configRouter(server, logFactory)) - r.Mount("/proxies", proxyRouter(server, router)) - r.Mount("/rules", ruleRouter(router)) - r.Mount("/connections", connectionRouter(router, trafficManager)) + r.Mount("/configs", configRouter(s, logFactory)) + r.Mount("/proxies", proxyRouter(s, s.router)) + r.Mount("/rules", ruleRouter(s.router)) + r.Mount("/connections", connectionRouter(s.router, trafficManager)) r.Mount("/providers/proxies", proxyProviderRouter()) r.Mount("/providers/rules", ruleProviderRouter()) r.Mount("/script", scriptRouter()) r.Mount("/profile", profileRouter()) r.Mount("/cache", cacheRouter(ctx)) - r.Mount("/dns", dnsRouter(router)) + r.Mount("/dns", dnsRouter(s.router)) - server.setupMetaAPI(r) + s.setupMetaAPI(r) }) if options.ExternalUI != "" { - server.externalUI = filemanager.BasePath(ctx, os.ExpandEnv(options.ExternalUI)) + s.externalUI = filemanager.BasePath(ctx, os.ExpandEnv(options.ExternalUI)) chiRouter.Group(func(r chi.Router) { r.Get("/ui", http.RedirectHandler("/ui/", http.StatusMovedPermanently).ServeHTTP) - r.Handle("/ui/*", http.StripPrefix("/ui/", http.FileServer(http.Dir(server.externalUI)))) + r.Handle("/ui/*", http.StripPrefix("/ui/", http.FileServer(http.Dir(s.externalUI)))) }) } - return server, nil + return s, nil } func (s *Server) PreStart() error { @@ -232,12 +234,12 @@ func (s *Server) TrafficManager() *trafficontrol.Manager { } func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule) (net.Conn, adapter.Tracker) { - tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.router, matchedRule) + tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.outboundManager, matchedRule) return tracker, tracker } func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule) (N.PacketConn, adapter.Tracker) { - tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.router, matchedRule) + tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.outboundManager, matchedRule) return tracker, tracker } diff --git a/experimental/clashapi/server_resources.go b/experimental/clashapi/server_resources.go index a5c79e0cf2..e5b28e300c 100644 --- a/experimental/clashapi/server_resources.go +++ b/experimental/clashapi/server_resources.go @@ -15,7 +15,6 @@ import ( "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service/filemanager" ) @@ -45,16 +44,13 @@ func (s *Server) downloadExternalUI() error { s.logger.Info("downloading external ui") var detour adapter.Outbound if s.externalUIDownloadDetour != "" { - outbound, loaded := s.router.Outbound(s.externalUIDownloadDetour) + outbound, loaded := s.outboundManager.Outbound(s.externalUIDownloadDetour) if !loaded { return E.New("detour outbound not found: ", s.externalUIDownloadDetour) } detour = outbound } else { - outbound, err := s.router.DefaultOutbound(N.NetworkTCP) - if err != nil { - return err - } + outbound := s.outboundManager.Default() detour = outbound } httpClient := &http.Client{ diff --git a/experimental/clashapi/trafficontrol/tracker.go b/experimental/clashapi/trafficontrol/tracker.go index 9c18abebf6..df5437fafa 100644 --- a/experimental/clashapi/trafficontrol/tracker.go +++ b/experimental/clashapi/trafficontrol/tracker.go @@ -124,7 +124,7 @@ func (tt *TCPConn) WriterReplaceable() bool { return true } -func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundContext, router adapter.Router, rule adapter.Rule) *TCPConn { +func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundContext, outboundManager adapter.OutboundManager, rule adapter.Rule) *TCPConn { id, _ := uuid.NewV4() var ( chain []string @@ -138,11 +138,11 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundCont } if routeAction, isRouteAction := action.(*R.RuleActionRoute); isRouteAction { next = routeAction.Outbound - } else if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil { - next = defaultOutbound.Tag() + } else { + next = outboundManager.Default().Tag() } for { - detour, loaded := router.Outbound(next) + detour, loaded := outboundManager.Outbound(next) if !loaded { break } @@ -213,7 +213,7 @@ func (ut *UDPConn) WriterReplaceable() bool { return true } -func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.InboundContext, router adapter.Router, rule adapter.Rule) *UDPConn { +func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.InboundContext, outboundManager adapter.OutboundManager, rule adapter.Rule) *UDPConn { id, _ := uuid.NewV4() var ( chain []string @@ -227,11 +227,11 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.Inbound } if routeAction, isRouteAction := action.(*R.RuleActionRoute); isRouteAction { next = routeAction.Outbound - } else if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil { - next = defaultOutbound.Tag() + } else { + next = outboundManager.Default().Tag() } for { - detour, loaded := router.Outbound(next) + detour, loaded := outboundManager.Outbound(next) if !loaded { break } diff --git a/protocol/direct/inbound.go b/protocol/direct/inbound.go index 568a72cbd0..8415e21fd8 100644 --- a/protocol/direct/inbound.go +++ b/protocol/direct/inbound.go @@ -76,21 +76,6 @@ func (i *Inbound) Close() error { return i.listener.Close() } -func (i *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - switch i.overrideOption { - case 1: - metadata.Destination = i.overrideDestination - case 2: - destination := i.overrideDestination - destination.Port = metadata.Destination.Port - metadata.Destination = destination - case 3: - metadata.Destination.Port = i.overrideDestination.Port - } - i.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) - return i.router.RouteConnection(ctx, conn, metadata) -} - func (i *Inbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { var destination M.Socksaddr switch i.overrideOption { @@ -107,9 +92,21 @@ func (i *Inbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { } func (i *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + switch i.overrideOption { + case 1: + metadata.Destination = i.overrideDestination + case 2: + destination := i.overrideDestination + destination.Port = metadata.Destination.Port + metadata.Destination = destination + case 3: + metadata.Destination.Port = i.overrideDestination.Port + } i.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) metadata.Inbound = i.Tag() metadata.InboundType = i.Type() + metadata.InboundDetour = i.listener.ListenOptions().Detour + metadata.InboundOptions = i.listener.ListenOptions().InboundOptions i.router.RouteConnectionEx(ctx, conn, metadata, onClose) } @@ -119,6 +116,8 @@ func (i *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, var metadata adapter.InboundContext metadata.Inbound = i.Tag() metadata.InboundType = i.Type() + metadata.InboundDetour = i.listener.ListenOptions().Detour + metadata.InboundOptions = i.listener.ListenOptions().InboundOptions metadata.Source = source metadata.Destination = destination metadata.OriginDestination = i.listener.UDPAddr() diff --git a/protocol/direct/outbound.go b/protocol/direct/outbound.go index 32c1ed8fda..27b334c9a4 100644 --- a/protocol/direct/outbound.go +++ b/protocol/direct/outbound.go @@ -12,7 +12,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing-dns" + dns "github.com/sagernet/sing-dns" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" @@ -39,7 +39,7 @@ type Outbound struct { func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DirectOutboundOptions) (adapter.Outbound, error) { options.UDPFragmentDefault = true - outboundDialer, err := dialer.New(router, options.DialerOptions) + outboundDialer, err := dialer.New(ctx, options.DialerOptions) if err != nil { return nil, err } diff --git a/protocol/group/selector.go b/protocol/group/selector.go index 8ade27a943..08db74c09e 100644 --- a/protocol/group/selector.go +++ b/protocol/group/selector.go @@ -26,7 +26,7 @@ var _ adapter.OutboundGroup = (*Selector)(nil) type Selector struct { outbound.Adapter ctx context.Context - router adapter.Router + outboundManager adapter.OutboundManager logger logger.ContextLogger tags []string defaultTag string @@ -40,7 +40,7 @@ func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextL outbound := &Selector{ Adapter: outbound.NewAdapter(C.TypeSelector, nil, tag, options.Outbounds), ctx: ctx, - router: router, + outboundManager: service.FromContext[adapter.OutboundManager](ctx), logger: logger, tags: options.Outbounds, defaultTag: options.Default, @@ -63,7 +63,7 @@ func (s *Selector) Network() []string { func (s *Selector) Start() error { for i, tag := range s.tags { - detour, loaded := s.router.Outbound(tag) + detour, loaded := s.outboundManager.Outbound(tag) if !loaded { return E.New("outbound ", i, " not found: ", tag) } diff --git a/protocol/group/urltest.go b/protocol/group/urltest.go index ccdf809d9b..4d76a31c6a 100644 --- a/protocol/group/urltest.go +++ b/protocol/group/urltest.go @@ -36,6 +36,7 @@ type URLTest struct { outbound.Adapter ctx context.Context router adapter.Router + outboundManager adapter.OutboundManager logger log.ContextLogger tags []string link string @@ -51,6 +52,7 @@ func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLo Adapter: outbound.NewAdapter(C.TypeURLTest, []string{N.NetworkTCP, N.NetworkUDP}, tag, options.Outbounds), ctx: ctx, router: router, + outboundManager: service.FromContext[adapter.OutboundManager](ctx), logger: logger, tags: options.Outbounds, link: options.URL, @@ -68,7 +70,7 @@ func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLo func (s *URLTest) Start() error { outbounds := make([]adapter.Outbound, 0, len(s.tags)) for i, tag := range s.tags { - detour, loaded := s.router.Outbound(tag) + detour, loaded := s.outboundManager.Outbound(tag) if !loaded { return E.New("outbound ", i, " not found: ", tag) } @@ -77,6 +79,7 @@ func (s *URLTest) Start() error { group, err := NewURLTestGroup( s.ctx, s.router, + s.outboundManager, s.logger, outbounds, s.link, @@ -190,6 +193,7 @@ func (s *URLTest) InterfaceUpdated() { type URLTestGroup struct { ctx context.Context router adapter.Router + outboundManager adapter.OutboundManager logger log.Logger outbounds []adapter.Outbound link string @@ -214,6 +218,7 @@ type URLTestGroup struct { func NewURLTestGroup( ctx context.Context, router adapter.Router, + outboundManager adapter.OutboundManager, logger log.Logger, outbounds []adapter.Outbound, link string, @@ -244,6 +249,7 @@ func NewURLTestGroup( return &URLTestGroup{ ctx: ctx, router: router, + outboundManager: outboundManager, logger: logger, outbounds: outbounds, link: link, @@ -385,7 +391,7 @@ func (g *URLTestGroup) urlTest(ctx context.Context, force bool) (map[string]uint continue } checked[realTag] = true - p, loaded := g.router.Outbound(realTag) + p, loaded := g.outboundManager.Outbound(realTag) if !loaded { continue } diff --git a/protocol/http/inbound.go b/protocol/http/inbound.go index 87ed9a1089..bb8fe8c7dd 100644 --- a/protocol/http/inbound.go +++ b/protocol/http/inbound.go @@ -73,7 +73,7 @@ func (h *Inbound) Start() error { func (h *Inbound) Close() error { return common.Close( - &h.listener, + h.listener, h.tlsConfig, ) } @@ -82,7 +82,11 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a err := h.newConnection(ctx, conn, metadata, onClose) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + if E.IsClosedOrCanceled(err) { + h.logger.DebugContext(ctx, "connection closed: ", err) + } else { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } } } @@ -98,6 +102,10 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada } func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions user, loaded := auth.UserFromContext[string](ctx) if !loaded { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) @@ -110,6 +118,10 @@ func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata } func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions user, loaded := auth.UserFromContext[string](ctx) if !loaded { h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) diff --git a/protocol/http/outbound.go b/protocol/http/outbound.go index 4c930591b1..81fd024669 100644 --- a/protocol/http/outbound.go +++ b/protocol/http/outbound.go @@ -30,7 +30,7 @@ type Outbound struct { } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPOutboundOptions) (adapter.Outbound, error) { - outboundDialer, err := dialer.New(router, options.DialerOptions) + outboundDialer, err := dialer.New(ctx, options.DialerOptions) if err != nil { return nil, err } diff --git a/protocol/hysteria/inbound.go b/protocol/hysteria/inbound.go index f1f6da3ef6..00defa92ab 100644 --- a/protocol/hysteria/inbound.go +++ b/protocol/hysteria/inbound.go @@ -17,6 +17,7 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) @@ -85,7 +86,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo XPlusPassword: options.Obfs, TLSConfig: tlsConfig, UDPTimeout: udpTimeout, - Handler: adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, nil), + Handler: inbound, // Legacy options @@ -117,12 +118,16 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return inbound, nil } -func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) + var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() metadata.InboundDetour = h.listener.ListenOptions().Detour metadata.InboundOptions = h.listener.ListenOptions().InboundOptions + metadata.OriginDestination = h.listener.UDPAddr() + metadata.Source = source + metadata.Destination = destination h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { @@ -131,16 +136,19 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada } else { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) } - return h.router.RouteConnection(ctx, conn, metadata) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } -func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) + var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() metadata.InboundDetour = h.listener.ListenOptions().Detour metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.OriginDestination = h.listener.UDPAddr() + metadata.Source = source + metadata.Destination = destination h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { @@ -149,7 +157,7 @@ func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, me } else { h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) } - return h.router.RoutePacketConnection(ctx, conn, metadata) + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) Start() error { @@ -168,7 +176,7 @@ func (h *Inbound) Start() error { func (h *Inbound) Close() error { return common.Close( - &h.listener, + h.listener, h.tlsConfig, common.PtrOrNil(h.service), ) diff --git a/protocol/hysteria/outbound.go b/protocol/hysteria/outbound.go index 6df32eaf71..89e6cc1089 100644 --- a/protocol/hysteria/outbound.go +++ b/protocol/hysteria/outbound.go @@ -47,7 +47,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL if err != nil { return nil, err } - outboundDialer, err := dialer.New(router, options.DialerOptions) + outboundDialer, err := dialer.New(ctx, options.DialerOptions) if err != nil { return nil, err } diff --git a/protocol/hysteria2/inbound.go b/protocol/hysteria2/inbound.go index cbf811097f..03cd8d2d52 100644 --- a/protocol/hysteria2/inbound.go +++ b/protocol/hysteria2/inbound.go @@ -20,6 +20,7 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) @@ -108,7 +109,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo TLSConfig: tlsConfig, IgnoreClientBandwidth: options.IgnoreClientBandwidth, UDPTimeout: udpTimeout, - Handler: adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, nil), + Handler: inbound, MasqueradeHandler: masqueradeHandler, }) if err != nil { @@ -128,12 +129,16 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return inbound, nil } -func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) + var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() metadata.InboundDetour = h.listener.ListenOptions().Detour metadata.InboundOptions = h.listener.ListenOptions().InboundOptions + metadata.OriginDestination = h.listener.UDPAddr() + metadata.Source = source + metadata.Destination = destination h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { @@ -142,16 +147,19 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada } else { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) } - return h.router.RouteConnection(ctx, conn, metadata) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } -func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) + var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() metadata.InboundDetour = h.listener.ListenOptions().Detour metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.OriginDestination = h.listener.UDPAddr() + metadata.Source = source + metadata.Destination = destination h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { @@ -160,7 +168,7 @@ func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, me } else { h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) } - return h.router.RoutePacketConnection(ctx, conn, metadata) + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) Start() error { @@ -179,7 +187,7 @@ func (h *Inbound) Start() error { func (h *Inbound) Close() error { return common.Close( - &h.listener, + h.listener, h.tlsConfig, common.PtrOrNil(h.service), ) diff --git a/protocol/hysteria2/outbound.go b/protocol/hysteria2/outbound.go index 5ebc6c91f3..4cabb4751c 100644 --- a/protocol/hysteria2/outbound.go +++ b/protocol/hysteria2/outbound.go @@ -59,7 +59,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, E.New("unknown obfs type: ", options.Obfs.Type) } } - outboundDialer, err := dialer.New(router, options.DialerOptions) + outboundDialer, err := dialer.New(ctx, options.DialerOptions) if err != nil { return nil, err } diff --git a/protocol/mixed/inbound.go b/protocol/mixed/inbound.go index e57b791fec..4f48144086 100644 --- a/protocol/mixed/inbound.go +++ b/protocol/mixed/inbound.go @@ -66,7 +66,11 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a err := h.newConnection(ctx, conn, metadata, onClose) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + if E.IsClosedOrCanceled(err) { + h.logger.DebugContext(ctx, "connection closed: ", err) + } else { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } } } @@ -85,6 +89,10 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada } func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions user, loaded := auth.UserFromContext[string](ctx) if !loaded { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) @@ -97,6 +105,10 @@ func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata } func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions user, loaded := auth.UserFromContext[string](ctx) if !loaded { h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) diff --git a/protocol/redirect/redirect.go b/protocol/redirect/redirect.go index 71e1fced1a..23bfad3eb4 100644 --- a/protocol/redirect/redirect.go +++ b/protocol/redirect/redirect.go @@ -59,6 +59,8 @@ func (h *Redirect) NewConnectionEx(ctx context.Context, conn net.Conn, metadata } metadata.Inbound = h.Tag() metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.Destination = M.SocksaddrFromNetIP(destination) h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) h.router.RouteConnectionEx(ctx, conn, metadata, onClose) diff --git a/protocol/redirect/tproxy.go b/protocol/redirect/tproxy.go index dee40ec5a8..825b9f0085 100644 --- a/protocol/redirect/tproxy.go +++ b/protocol/redirect/tproxy.go @@ -90,6 +90,8 @@ func (t *TProxy) Close() error { } func (t *TProxy) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = t.Tag() + metadata.InboundType = t.Type() metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr()).Unwrap() t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) t.router.RouteConnectionEx(ctx, conn, metadata, onClose) @@ -101,6 +103,8 @@ func (t *TProxy) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, s var metadata adapter.InboundContext metadata.Inbound = t.Tag() metadata.InboundType = t.Type() + metadata.InboundDetour = t.listener.ListenOptions().Detour + metadata.InboundOptions = t.listener.ListenOptions().InboundOptions metadata.Source = source metadata.Destination = destination metadata.OriginDestination = t.listener.UDPAddr() diff --git a/protocol/shadowsocks/inbound.go b/protocol/shadowsocks/inbound.go index b23516d995..84ad43fcb0 100644 --- a/protocol/shadowsocks/inbound.go +++ b/protocol/shadowsocks/inbound.go @@ -105,7 +105,11 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + if E.IsClosedOrCanceled(err) { + h.logger.DebugContext(ctx, "connection closed: ", err) + } else { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } } } @@ -120,6 +124,8 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) metadata.Inbound = h.Tag() metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions return h.router.RouteConnection(ctx, conn, metadata) } diff --git a/protocol/shadowsocks/inbound_multi.go b/protocol/shadowsocks/inbound_multi.go index 0e1efedfc2..a76075efe0 100644 --- a/protocol/shadowsocks/inbound_multi.go +++ b/protocol/shadowsocks/inbound_multi.go @@ -113,7 +113,11 @@ func (h *MultiInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metad err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + if E.IsClosedOrCanceled(err) { + h.logger.DebugContext(ctx, "connection closed: ", err) + } else { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } } } @@ -138,6 +142,8 @@ func (h *MultiInbound) newConnection(ctx context.Context, conn net.Conn, metadat h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) metadata.Inbound = h.Tag() metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions return h.router.RouteConnection(ctx, conn, metadata) } diff --git a/protocol/shadowsocks/inbound_relay.go b/protocol/shadowsocks/inbound_relay.go index 5818ca29f1..f7ec2b7703 100644 --- a/protocol/shadowsocks/inbound_relay.go +++ b/protocol/shadowsocks/inbound_relay.go @@ -98,7 +98,11 @@ func (h *RelayInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metad err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + if E.IsClosedOrCanceled(err) { + h.logger.DebugContext(ctx, "connection closed: ", err) + } else { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } } } @@ -123,6 +127,8 @@ func (h *RelayInbound) newConnection(ctx context.Context, conn net.Conn, metadat h.logger.InfoContext(ctx, "[", destination, "] inbound connection to ", metadata.Destination) metadata.Inbound = h.Tag() metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions return h.router.RouteConnection(ctx, conn, metadata) } diff --git a/protocol/shadowsocks/outbound.go b/protocol/shadowsocks/outbound.go index 73b3838503..8771fa8e99 100644 --- a/protocol/shadowsocks/outbound.go +++ b/protocol/shadowsocks/outbound.go @@ -44,7 +44,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL if err != nil { return nil, err } - outboundDialer, err := dialer.New(router, options.DialerOptions) + outboundDialer, err := dialer.New(ctx, options.DialerOptions) if err != nil { return nil, err } diff --git a/protocol/shadowtls/inbound.go b/protocol/shadowtls/inbound.go index 6887e838a5..1be422decc 100644 --- a/protocol/shadowtls/inbound.go +++ b/protocol/shadowtls/inbound.go @@ -16,6 +16,7 @@ import ( "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) @@ -46,7 +47,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if options.Version > 1 { handshakeForServerName = make(map[string]shadowtls.HandshakeConfig) for serverName, serverOptions := range options.HandshakeForServerName { - handshakeDialer, err := dialer.New(router, serverOptions.DialerOptions) + handshakeDialer, err := dialer.New(ctx, serverOptions.DialerOptions) if err != nil { return nil, err } @@ -56,7 +57,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } } } - handshakeDialer, err := dialer.New(router, options.Handshake.DialerOptions) + handshakeDialer, err := dialer.New(ctx, options.Handshake.DialerOptions) if err != nil { return nil, err } @@ -72,7 +73,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo }, HandshakeForServerName: handshakeForServerName, StrictMode: options.StrictMode, - Handler: adapter.NewUpstreamContextHandler(inbound.newConnection, nil, nil), + Handler: (*inboundHandler)(inbound), Logger: logger, }) if err != nil { @@ -97,24 +98,33 @@ func (h *Inbound) Close() error { return h.listener.Close() } -func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata)) +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + err := h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, metadata.Source, metadata.Destination, onClose) + N.CloseOnHandshakeFailure(conn, onClose, err) + if err != nil { + if E.IsClosedOrCanceled(err) { + h.logger.DebugContext(ctx, "connection closed: ", err) + } else { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } + } } -func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +type inboundHandler Inbound + +func (h *inboundHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions + metadata.Source = source + metadata.Destination = destination if userName, _ := auth.UserFromContext[string](ctx); userName != "" { metadata.User = userName h.logger.InfoContext(ctx, "[", userName, "] inbound connection to ", metadata.Destination) } else { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) } - return h.router.RouteConnection(ctx, conn, metadata) -} - -func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - err := h.NewConnection(ctx, conn, metadata) - N.CloseOnHandshakeFailure(conn, onClose, err) - if err != nil { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) - } + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } diff --git a/protocol/shadowtls/outbound.go b/protocol/shadowtls/outbound.go index 7d46a8f682..e979dba270 100644 --- a/protocol/shadowtls/outbound.go +++ b/protocol/shadowtls/outbound.go @@ -68,7 +68,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL tlsHandshakeFunc = shadowtls.DefaultTLSHandshakeFunc(options.Password, stdTLSConfig) } } - outboundDialer, err := dialer.New(router, options.DialerOptions) + outboundDialer, err := dialer.New(ctx, options.DialerOptions) if err != nil { return nil, err } diff --git a/protocol/socks/inbound.go b/protocol/socks/inbound.go index 29649a88df..fddacb21fb 100644 --- a/protocol/socks/inbound.go +++ b/protocol/socks/inbound.go @@ -62,11 +62,19 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a err := socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, metadata.Destination, onClose) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + if E.IsClosedOrCanceled(err) { + h.logger.DebugContext(ctx, "connection closed: ", err) + } else { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } } } func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions user, loaded := auth.UserFromContext[string](ctx) if !loaded { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) @@ -79,6 +87,10 @@ func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata } func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions user, loaded := auth.UserFromContext[string](ctx) if !loaded { h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) diff --git a/protocol/socks/outbound.go b/protocol/socks/outbound.go index 0194800a7c..dbb5ab6177 100644 --- a/protocol/socks/outbound.go +++ b/protocol/socks/outbound.go @@ -46,7 +46,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL if err != nil { return nil, err } - outboundDialer, err := dialer.New(router, options.DialerOptions) + outboundDialer, err := dialer.New(ctx, options.DialerOptions) if err != nil { return nil, err } diff --git a/protocol/ssh/outbound.go b/protocol/ssh/outbound.go index 62a2a8d9b5..1dfc1f6d68 100644 --- a/protocol/ssh/outbound.go +++ b/protocol/ssh/outbound.go @@ -49,7 +49,7 @@ type Outbound struct { } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SSHOutboundOptions) (adapter.Outbound, error) { - outboundDialer, err := dialer.New(router, options.DialerOptions) + outboundDialer, err := dialer.New(ctx, options.DialerOptions) if err != nil { return nil, err } diff --git a/protocol/tor/outbound.go b/protocol/tor/outbound.go index 89a295b822..3d2170115d 100644 --- a/protocol/tor/outbound.go +++ b/protocol/tor/outbound.go @@ -75,7 +75,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL } startConf.TorrcFile = torrcFile } - outboundDialer, err := dialer.New(router, options.DialerOptions) + outboundDialer, err := dialer.New(ctx, options.DialerOptions) if err != nil { return nil, err } diff --git a/protocol/trojan/inbound.go b/protocol/trojan/inbound.go index 010ae8baf2..2dc1bb9421 100644 --- a/protocol/trojan/inbound.go +++ b/protocol/trojan/inbound.go @@ -149,7 +149,7 @@ func (h *Inbound) Start() error { func (h *Inbound) Close() error { return common.Close( - &h.listener, + h.listener, h.tlsConfig, h.transport, ) @@ -170,11 +170,19 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a err := h.NewConnection(ctx, conn, metadata) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + if E.IsClosedOrCanceled(err) { + h.logger.DebugContext(ctx, "connection closed: ", err) + } else { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } } } func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid @@ -207,12 +215,20 @@ func (h *Inbound) fallbackConnection(ctx context.Context, conn net.Conn, metadat } fallbackAddr = h.fallbackAddr } + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions h.logger.InfoContext(ctx, "fallback connection to ", fallbackAddr) metadata.Destination = fallbackAddr return h.router.RouteConnection(ctx, conn, metadata) } func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid @@ -233,10 +249,6 @@ type inboundTransportHandler Inbound func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { var metadata adapter.InboundContext - metadata.Inbound = h.Tag() - metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.Source = source metadata.Destination = destination h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) diff --git a/protocol/trojan/outbound.go b/protocol/trojan/outbound.go index f64c48c3c5..68b0069043 100644 --- a/protocol/trojan/outbound.go +++ b/protocol/trojan/outbound.go @@ -38,7 +38,7 @@ type Outbound struct { } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TrojanOutboundOptions) (adapter.Outbound, error) { - outboundDialer, err := dialer.New(router, options.DialerOptions) + outboundDialer, err := dialer.New(ctx, options.DialerOptions) if err != nil { return nil, err } diff --git a/protocol/tuic/inbound.go b/protocol/tuic/inbound.go index 33de10d5cb..496079c194 100644 --- a/protocol/tuic/inbound.go +++ b/protocol/tuic/inbound.go @@ -17,6 +17,7 @@ import ( "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/gofrs/uuid/v5" @@ -71,7 +72,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo ZeroRTTHandshake: options.ZeroRTTHandshake, Heartbeat: time.Duration(options.Heartbeat), UDPTimeout: udpTimeout, - Handler: adapter.NewUpstreamHandler(adapter.InboundContext{}, inbound.newConnection, inbound.newPacketConnection, nil), + Handler: inbound, }) if err != nil { return nil, err @@ -99,12 +100,16 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return inbound, nil } -func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) + var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() metadata.InboundDetour = h.listener.ListenOptions().Detour metadata.InboundOptions = h.listener.ListenOptions().InboundOptions + metadata.OriginDestination = h.listener.UDPAddr() + metadata.Source = source + metadata.Destination = destination h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { @@ -113,16 +118,19 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada } else { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) } - return h.router.RouteConnection(ctx, conn, metadata) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } -func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { ctx = log.ContextWithNewID(ctx) + var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() metadata.InboundDetour = h.listener.ListenOptions().Detour metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.OriginDestination = h.listener.UDPAddr() + metadata.Source = source + metadata.Destination = destination h.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) userID, _ := auth.UserFromContext[int](ctx) if userName := h.userNameList[userID]; userName != "" { @@ -131,7 +139,7 @@ func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, me } else { h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) } - return h.router.RoutePacketConnection(ctx, conn, metadata) + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } func (h *Inbound) Start() error { @@ -150,7 +158,7 @@ func (h *Inbound) Start() error { func (h *Inbound) Close() error { return common.Close( - &h.listener, + h.listener, h.tlsConfig, common.PtrOrNil(h.server), ) diff --git a/protocol/tuic/outbound.go b/protocol/tuic/outbound.go index 691d1658de..177f21fc95 100644 --- a/protocol/tuic/outbound.go +++ b/protocol/tuic/outbound.go @@ -60,7 +60,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL case "quic": tuicUDPStream = true } - outboundDialer, err := dialer.New(router, options.DialerOptions) + outboundDialer, err := dialer.New(ctx, options.DialerOptions) if err != nil { return nil, err } diff --git a/protocol/vless/inbound.go b/protocol/vless/inbound.go index 0641549b0a..f5dfeabb6b 100644 --- a/protocol/vless/inbound.go +++ b/protocol/vless/inbound.go @@ -129,7 +129,7 @@ func (h *Inbound) Start() error { func (h *Inbound) Close() error { return common.Close( h.service, - &h.listener, + h.listener, h.tlsConfig, h.transport, ) @@ -150,11 +150,19 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a err := h.NewConnection(ctx, conn, metadata) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + if E.IsClosedOrCanceled(err) { + h.logger.DebugContext(ctx, "connection closed: ", err) + } else { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } } } func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid @@ -170,6 +178,10 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada } func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid @@ -196,10 +208,6 @@ type inboundTransportHandler Inbound func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { var metadata adapter.InboundContext - metadata.Inbound = h.Tag() - metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.Source = source metadata.Destination = destination h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) diff --git a/protocol/vless/outbound.go b/protocol/vless/outbound.go index 1074549e8d..de655230e0 100644 --- a/protocol/vless/outbound.go +++ b/protocol/vless/outbound.go @@ -41,7 +41,7 @@ type Outbound struct { } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VLESSOutboundOptions) (adapter.Outbound, error) { - outboundDialer, err := dialer.New(router, options.DialerOptions) + outboundDialer, err := dialer.New(ctx, options.DialerOptions) if err != nil { return nil, err } diff --git a/protocol/vmess/inbound.go b/protocol/vmess/inbound.go index 1c80f376e1..88ad222764 100644 --- a/protocol/vmess/inbound.go +++ b/protocol/vmess/inbound.go @@ -143,7 +143,7 @@ func (h *Inbound) Start() error { func (h *Inbound) Close() error { return common.Close( h.service, - &h.listener, + h.listener, h.tlsConfig, h.transport, ) @@ -164,11 +164,19 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a err := h.NewConnection(ctx, conn, metadata) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + if E.IsClosedOrCanceled(err) { + h.logger.DebugContext(ctx, "connection closed: ", err) + } else { + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) + } } } func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid @@ -184,6 +192,10 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada } func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + metadata.InboundDetour = h.listener.ListenOptions().Detour + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { return os.ErrInvalid @@ -210,10 +222,6 @@ type inboundTransportHandler Inbound func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { var metadata adapter.InboundContext - metadata.Inbound = h.Tag() - metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.Source = source metadata.Destination = destination h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) diff --git a/protocol/vmess/outbound.go b/protocol/vmess/outbound.go index 759ea8baa4..1e84639f47 100644 --- a/protocol/vmess/outbound.go +++ b/protocol/vmess/outbound.go @@ -41,7 +41,7 @@ type Outbound struct { } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.VMessOutboundOptions) (adapter.Outbound, error) { - outboundDialer, err := dialer.New(router, options.DialerOptions) + outboundDialer, err := dialer.New(ctx, options.DialerOptions) if err != nil { return nil, err } diff --git a/protocol/wireguard/outbound.go b/protocol/wireguard/outbound.go index 901f8782d6..79e8873257 100644 --- a/protocol/wireguard/outbound.go +++ b/protocol/wireguard/outbound.go @@ -78,7 +78,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL options.IsWireGuardListener = true outbound.useStdNetBind = true } - listener, err := dialer.New(router, options.DialerOptions) + listener, err := dialer.New(ctx, options.DialerOptions) if err != nil { return nil, err } diff --git a/route/dns.go b/route/dns.go index 98ea8d6b34..b69be5a279 100644 --- a/route/dns.go +++ b/route/dns.go @@ -77,7 +77,6 @@ func exchangeDNSPacket(ctx context.Context, router *Router, conn N.PacketConn, b return err } err = conn.WritePacket(responseBuffer, destination) - responseBuffer.Release() return err } diff --git a/route/router.go b/route/router.go index 82b1f4b50d..12255b4d7b 100644 --- a/route/router.go +++ b/route/router.go @@ -87,7 +87,7 @@ type Router struct { v2rayServer adapter.V2RayServer platformInterface platform.Interface needWIFIState bool - needPackageManager bool + enforcePackageManager bool wifiState adapter.WIFIState started bool } @@ -123,7 +123,7 @@ func NewRouter( pauseManager: service.FromContext[pause.Manager](ctx), platformInterface: service.FromContext[platform.Interface](ctx), needWIFIState: hasRule(options.Rules, isWIFIRule) || hasDNSRule(dnsOptions.Rules, isWIFIDNSRule), - needPackageManager: common.Any(inbounds, func(inbound option.Inbound) bool { + enforcePackageManager: common.Any(inbounds, func(inbound option.Inbound) bool { if tunOptions, isTUN := inbound.Options.(*option.TunInboundOptions); isTUN && tunOptions.AutoRoute { return true } @@ -191,7 +191,8 @@ func NewRouter( transportTags[i] = tag transportTagMap[tag] = true } - ctx = adapter.ContextWithRouter(ctx, router) + ctx = service.ContextWith[adapter.Router](ctx, router) + outboundManager := service.FromContext[adapter.OutboundManager](ctx) for { lastLen := len(dummyTransportMap) for i, server := range dnsOptions.Servers { @@ -201,9 +202,9 @@ func NewRouter( } var detour N.Dialer if server.Detour == "" { - detour = dialer.NewRouter(router) + detour = dialer.NewDefaultOutbound(outboundManager) } else { - detour = dialer.NewDetour(router, server.Detour) + detour = dialer.NewDetour(outboundManager, server.Detour) } var serverProtocol string switch server.Address { @@ -327,7 +328,7 @@ func NewRouter( } usePlatformDefaultInterfaceMonitor := router.platformInterface != nil && router.platformInterface.UsePlatformDefaultInterfaceMonitor() - needInterfaceMonitor := options.AutoDetectInterface || common.Any(inbounds, func(inbound option.Inbound) bool { + enforceInterfaceMonitor := options.AutoDetectInterface || common.Any(inbounds, func(inbound option.Inbound) bool { if httpMixedOptions, isHTTPMixed := inbound.Options.(*option.HTTPMixedInboundOptions); isHTTPMixed && httpMixedOptions.SetSystemProxy { return true } @@ -339,7 +340,7 @@ func NewRouter( if !usePlatformDefaultInterfaceMonitor { networkMonitor, err := tun.NewNetworkUpdateMonitor(router.logger) - if !((err != nil && !needInterfaceMonitor) || errors.Is(err, os.ErrInvalid)) { + if !((err != nil && !enforceInterfaceMonitor) || errors.Is(err, os.ErrInvalid)) { if err != nil { return nil, err } @@ -362,7 +363,7 @@ func NewRouter( } if ntpOptions.Enabled { - ntpDialer, err := dialer.New(router, ntpOptions.DialerOptions) + ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions) if err != nil { return nil, E.Cause(err, "create NTP service") } @@ -380,73 +381,6 @@ func NewRouter( return router, nil } -func (r *Router) Initialize(inbounds []adapter.Inbound, outbounds []adapter.Outbound, defaultOutbound func() adapter.Outbound) error { - inboundByTag := make(map[string]adapter.Inbound) - for _, inbound := range inbounds { - inboundByTag[inbound.Tag()] = inbound - } - outboundByTag := make(map[string]adapter.Outbound) - for _, detour := range outbounds { - outboundByTag[detour.Tag()] = detour - } - var defaultOutboundForConnection adapter.Outbound - var defaultOutboundForPacketConnection adapter.Outbound - if r.defaultDetour != "" { - detour, loaded := outboundByTag[r.defaultDetour] - if !loaded { - return E.New("default detour not found: ", r.defaultDetour) - } - if common.Contains(detour.Network(), N.NetworkTCP) { - defaultOutboundForConnection = detour - } - if common.Contains(detour.Network(), N.NetworkUDP) { - defaultOutboundForPacketConnection = detour - } - } - if defaultOutboundForConnection == nil { - for _, detour := range outbounds { - if common.Contains(detour.Network(), N.NetworkTCP) { - defaultOutboundForConnection = detour - break - } - } - } - if defaultOutboundForPacketConnection == nil { - for _, detour := range outbounds { - if common.Contains(detour.Network(), N.NetworkUDP) { - defaultOutboundForPacketConnection = detour - break - } - } - } - if defaultOutboundForConnection == nil || defaultOutboundForPacketConnection == nil { - detour := defaultOutbound() - if defaultOutboundForConnection == nil { - defaultOutboundForConnection = detour - } - if defaultOutboundForPacketConnection == nil { - defaultOutboundForPacketConnection = detour - } - outbounds = append(outbounds, detour) - outboundByTag[detour.Tag()] = detour - } - r.inboundByTag = inboundByTag - r.outbounds = outbounds - r.defaultOutboundForConnection = defaultOutboundForConnection - r.defaultOutboundForPacketConnection = defaultOutboundForPacketConnection - r.outboundByTag = outboundByTag - for i, rule := range r.rules { - routeAction, isRoute := rule.Action().(*R.RuleActionRoute) - if !isRoute { - continue - } - if _, loaded := outboundByTag[routeAction.Outbound]; !loaded { - return E.New("outbound not found for rule[", i, "]: ", routeAction.Outbound) - } - } - return nil -} - func (r *Router) Outbounds() []adapter.Outbound { if !r.started { return nil @@ -454,140 +388,240 @@ func (r *Router) Outbounds() []adapter.Outbound { return r.outbounds } -func (r *Router) PreStart() error { +func (r *Router) Start(stage adapter.StartStage) error { monitor := taskmonitor.New(r.logger, C.StartTimeout) - if r.interfaceMonitor != nil { - monitor.Start("initialize interface monitor") - err := r.interfaceMonitor.Start() - monitor.Finish() - if err != nil { - return err - } - } - if r.networkMonitor != nil { - monitor.Start("initialize network monitor") - err := r.networkMonitor.Start() - monitor.Finish() - if err != nil { - return err + switch stage { + case adapter.StartStateInitialize: + if r.interfaceMonitor != nil { + monitor.Start("initialize interface monitor") + err := r.interfaceMonitor.Start() + monitor.Finish() + if err != nil { + return err + } } - } - if r.fakeIPStore != nil { - monitor.Start("initialize fakeip store") - err := r.fakeIPStore.Start() - monitor.Finish() - if err != nil { - return err + if r.networkMonitor != nil { + monitor.Start("initialize network monitor") + err := r.networkMonitor.Start() + monitor.Finish() + if err != nil { + return err + } } - } - return nil -} - -func (r *Router) Start() error { - monitor := taskmonitor.New(r.logger, C.StartTimeout) - if r.needGeoIPDatabase { - monitor.Start("initialize geoip database") - err := r.prepareGeoIPDatabase() - monitor.Finish() - if err != nil { - return err + if r.fakeIPStore != nil { + monitor.Start("initialize fakeip store") + err := r.fakeIPStore.Start() + monitor.Finish() + if err != nil { + return err + } } - } - if r.needGeositeDatabase { - monitor.Start("initialize geosite database") - err := r.prepareGeositeDatabase() - monitor.Finish() - if err != nil { - return err + case adapter.StartStateStart: + if r.needGeoIPDatabase { + monitor.Start("initialize geoip database") + err := r.prepareGeoIPDatabase() + monitor.Finish() + if err != nil { + return err + } } - } - if r.needGeositeDatabase { - for _, rule := range r.rules { - err := rule.UpdateGeosite() + if r.needGeositeDatabase { + monitor.Start("initialize geosite database") + err := r.prepareGeositeDatabase() + monitor.Finish() if err != nil { - r.logger.Error("failed to initialize geosite: ", err) + return err } } - for _, rule := range r.dnsRules { - err := rule.UpdateGeosite() + if r.needGeositeDatabase { + for _, rule := range r.rules { + err := rule.UpdateGeosite() + if err != nil { + r.logger.Error("failed to initialize geosite: ", err) + } + } + for _, rule := range r.dnsRules { + err := rule.UpdateGeosite() + if err != nil { + r.logger.Error("failed to initialize geosite: ", err) + } + } + err := common.Close(r.geositeReader) if err != nil { - r.logger.Error("failed to initialize geosite: ", err) + return err } + r.geositeCache = nil + r.geositeReader = nil } - err := common.Close(r.geositeReader) - if err != nil { - return err + + if runtime.GOOS == "windows" { + powerListener, err := winpowrprof.NewEventListener(r.notifyWindowsPowerEvent) + if err == nil { + r.powerListener = powerListener + } else { + r.logger.Warn("initialize power listener: ", err) + } } - r.geositeCache = nil - r.geositeReader = nil - } - if runtime.GOOS == "windows" { - powerListener, err := winpowrprof.NewEventListener(r.notifyWindowsPowerEvent) - if err == nil { - r.powerListener = powerListener - } else { - r.logger.Warn("initialize power listener: ", err) + if r.powerListener != nil { + monitor.Start("start power listener") + err := r.powerListener.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "start power listener") + } } - } - if r.powerListener != nil { - monitor.Start("start power listener") - err := r.powerListener.Start() + monitor.Start("initialize DNS client") + r.dnsClient.Start() monitor.Finish() - if err != nil { - return E.Cause(err, "start power listener") - } - } - monitor.Start("initialize DNS client") - r.dnsClient.Start() - monitor.Finish() + if C.IsAndroid && r.platformInterface == nil { + monitor.Start("initialize package manager") + packageManager, err := tun.NewPackageManager(tun.PackageManagerOptions{ + Callback: r, + Logger: r.logger, + }) + monitor.Finish() + if err != nil { + return E.Cause(err, "create package manager") + } + if r.enforcePackageManager { + monitor.Start("start package manager") + err = packageManager.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "start package manager") + } + } + r.packageManager = packageManager + } - if C.IsAndroid && r.platformInterface == nil { - monitor.Start("initialize package manager") - packageManager, err := tun.NewPackageManager(tun.PackageManagerOptions{ - Callback: r, - Logger: r.logger, - }) - monitor.Finish() - if err != nil { - return E.Cause(err, "create package manager") + for i, rule := range r.dnsRules { + monitor.Start("initialize DNS rule[", i, "]") + err := rule.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "initialize DNS rule[", i, "]") + } } - if r.needPackageManager { - monitor.Start("start package manager") - err = packageManager.Start() + for i, transport := range r.transports { + monitor.Start("initialize DNS transport[", i, "]") + err := transport.Start() monitor.Finish() if err != nil { - return E.Cause(err, "start package manager") + return E.Cause(err, "initialize DNS server[", i, "]") } } - r.packageManager = packageManager - } - - for i, rule := range r.dnsRules { - monitor.Start("initialize DNS rule[", i, "]") - err := rule.Start() - monitor.Finish() - if err != nil { - return E.Cause(err, "initialize DNS rule[", i, "]") + if r.timeService != nil { + monitor.Start("initialize time service") + err := r.timeService.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "initialize time service") + } } - } - for i, transport := range r.transports { - monitor.Start("initialize DNS transport[", i, "]") - err := transport.Start() - monitor.Finish() - if err != nil { - return E.Cause(err, "initialize DNS server[", i, "]") + case adapter.StartStatePostStart: + var cacheContext *adapter.HTTPStartContext + if len(r.ruleSets) > 0 { + monitor.Start("initialize rule-set") + cacheContext = adapter.NewHTTPStartContext() + var ruleSetStartGroup task.Group + for i, ruleSet := range r.ruleSets { + ruleSetInPlace := ruleSet + ruleSetStartGroup.Append0(func(ctx context.Context) error { + err := ruleSetInPlace.StartContext(ctx, cacheContext) + if err != nil { + return E.Cause(err, "initialize rule-set[", i, "]") + } + return nil + }) + } + ruleSetStartGroup.Concurrency(5) + ruleSetStartGroup.FastFail() + err := ruleSetStartGroup.Run(r.ctx) + monitor.Finish() + if err != nil { + return err + } } - } - if r.timeService != nil { - monitor.Start("initialize time service") - err := r.timeService.Start() - monitor.Finish() - if err != nil { - return E.Cause(err, "initialize time service") + if cacheContext != nil { + cacheContext.Close() + } + needFindProcess := r.needFindProcess + needWIFIState := r.needWIFIState + for _, ruleSet := range r.ruleSets { + metadata := ruleSet.Metadata() + if metadata.ContainsProcessRule { + needFindProcess = true + } + if metadata.ContainsWIFIRule { + needWIFIState = true + } + } + if C.IsAndroid && r.platformInterface == nil && !r.enforcePackageManager { + if needFindProcess { + monitor.Start("start package manager") + err := r.packageManager.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "start package manager") + } + } else { + r.packageManager = nil + } } + if needFindProcess { + if r.platformInterface != nil { + r.processSearcher = r.platformInterface + } else { + monitor.Start("initialize process searcher") + searcher, err := process.NewSearcher(process.Config{ + Logger: r.logger, + PackageManager: r.packageManager, + }) + monitor.Finish() + if err != nil { + if err != os.ErrInvalid { + r.logger.Warn(E.Cause(err, "create process searcher")) + } + } else { + r.processSearcher = searcher + } + } + } + if needWIFIState && r.platformInterface != nil { + monitor.Start("initialize WIFI state") + r.needWIFIState = true + r.interfaceMonitor.RegisterCallback(func(_ int) { + r.updateWIFIState() + }) + r.updateWIFIState() + monitor.Finish() + } + for i, rule := range r.rules { + monitor.Start("initialize rule[", i, "]") + err := rule.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "initialize rule[", i, "]") + } + } + for _, ruleSet := range r.ruleSets { + monitor.Start("post start rule_set[", ruleSet.Name(), "]") + err := ruleSet.PostStart() + monitor.Finish() + if err != nil { + return E.Cause(err, "post start rule_set[", ruleSet.Name(), "]") + } + } + r.started = true + return nil + case adapter.StartStateStarted: + for _, ruleSet := range r.ruleSetMap { + ruleSet.Cleanup() + } + runtime.GC() } return nil } @@ -668,113 +702,6 @@ func (r *Router) Close() error { return err } -func (r *Router) PostStart() error { - monitor := taskmonitor.New(r.logger, C.StopTimeout) - var cacheContext *adapter.HTTPStartContext - if len(r.ruleSets) > 0 { - monitor.Start("initialize rule-set") - cacheContext = adapter.NewHTTPStartContext() - var ruleSetStartGroup task.Group - for i, ruleSet := range r.ruleSets { - ruleSetInPlace := ruleSet - ruleSetStartGroup.Append0(func(ctx context.Context) error { - err := ruleSetInPlace.StartContext(ctx, cacheContext) - if err != nil { - return E.Cause(err, "initialize rule-set[", i, "]") - } - return nil - }) - } - ruleSetStartGroup.Concurrency(5) - ruleSetStartGroup.FastFail() - err := ruleSetStartGroup.Run(r.ctx) - monitor.Finish() - if err != nil { - return err - } - } - if cacheContext != nil { - cacheContext.Close() - } - needFindProcess := r.needFindProcess - needWIFIState := r.needWIFIState - for _, ruleSet := range r.ruleSets { - metadata := ruleSet.Metadata() - if metadata.ContainsProcessRule { - needFindProcess = true - } - if metadata.ContainsWIFIRule { - needWIFIState = true - } - } - if C.IsAndroid && r.platformInterface == nil && !r.needPackageManager { - if needFindProcess { - monitor.Start("start package manager") - err := r.packageManager.Start() - monitor.Finish() - if err != nil { - return E.Cause(err, "start package manager") - } - } else { - r.packageManager = nil - } - } - if needFindProcess { - if r.platformInterface != nil { - r.processSearcher = r.platformInterface - } else { - monitor.Start("initialize process searcher") - searcher, err := process.NewSearcher(process.Config{ - Logger: r.logger, - PackageManager: r.packageManager, - }) - monitor.Finish() - if err != nil { - if err != os.ErrInvalid { - r.logger.Warn(E.Cause(err, "create process searcher")) - } - } else { - r.processSearcher = searcher - } - } - } - if needWIFIState && r.platformInterface != nil { - monitor.Start("initialize WIFI state") - r.needWIFIState = true - r.interfaceMonitor.RegisterCallback(func(_ int) { - r.updateWIFIState() - }) - r.updateWIFIState() - monitor.Finish() - } - for i, rule := range r.rules { - monitor.Start("initialize rule[", i, "]") - err := rule.Start() - monitor.Finish() - if err != nil { - return E.Cause(err, "initialize rule[", i, "]") - } - } - for _, ruleSet := range r.ruleSets { - monitor.Start("post start rule_set[", ruleSet.Name(), "]") - err := ruleSet.PostStart() - monitor.Finish() - if err != nil { - return E.Cause(err, "post start rule_set[", ruleSet.Name(), "]") - } - } - r.started = true - return nil -} - -func (r *Router) Cleanup() error { - for _, ruleSet := range r.ruleSetMap { - ruleSet.Cleanup() - } - runtime.GC() - return nil -} - func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { outbound, loaded := r.outboundByTag[tag] return outbound, loaded diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index 0bf45ba25f..00579c18d5 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -22,7 +22,7 @@ import ( N "github.com/sagernet/sing/common/network" ) -func NewRuleAction(router adapter.Router, logger logger.ContextLogger, action option.RuleAction) (adapter.RuleAction, error) { +func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action option.RuleAction) (adapter.RuleAction, error) { switch action.Action { case "": return nil, nil @@ -36,7 +36,7 @@ func NewRuleAction(router adapter.Router, logger logger.ContextLogger, action op UDPConnect: action.RouteOptionsOptions.UDPConnect, }, nil case C.RuleActionTypeDirect: - directDialer, err := dialer.New(router, option.DialerOptions(action.DirectOptions)) + directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions)) if err != nil { return nil, err } diff --git a/route/rule/rule_default.go b/route/rule/rule_default.go index 566c816e11..a12c63ef85 100644 --- a/route/rule/rule_default.go +++ b/route/rule/rule_default.go @@ -52,7 +52,7 @@ type RuleItem interface { } func NewDefaultRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.DefaultRule) (*DefaultRule, error) { - action, err := NewRuleAction(router, logger, options.RuleAction) + action, err := NewRuleAction(ctx, logger, options.RuleAction) if err != nil { return nil, E.Cause(err, "action") } @@ -254,7 +254,7 @@ type LogicalRule struct { } func NewLogicalRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { - action, err := NewRuleAction(router, logger, options.RuleAction) + action, err := NewRuleAction(ctx, logger, options.RuleAction) if err != nil { return nil, E.Cause(err, "action") } diff --git a/route/rule/rule_set_remote.go b/route/rule/rule_set_remote.go index f055771827..fb58169140 100644 --- a/route/rule/rule_set_remote.go +++ b/route/rule/rule_set_remote.go @@ -33,23 +33,24 @@ import ( var _ adapter.RuleSet = (*RemoteRuleSet)(nil) type RemoteRuleSet struct { - ctx context.Context - cancel context.CancelFunc - router adapter.Router - logger logger.ContextLogger - options option.RuleSet - metadata adapter.RuleSetMetadata - updateInterval time.Duration - dialer N.Dialer - rules []adapter.HeadlessRule - lastUpdated time.Time - lastEtag string - updateTicker *time.Ticker - cacheFile adapter.CacheFile - pauseManager pause.Manager - callbackAccess sync.Mutex - callbacks list.List[adapter.RuleSetUpdateCallback] - refs atomic.Int32 + ctx context.Context + cancel context.CancelFunc + router adapter.Router + outboundManager adapter.OutboundManager + logger logger.ContextLogger + options option.RuleSet + metadata adapter.RuleSetMetadata + updateInterval time.Duration + dialer N.Dialer + rules []adapter.HeadlessRule + lastUpdated time.Time + lastEtag string + updateTicker *time.Ticker + cacheFile adapter.CacheFile + pauseManager pause.Manager + callbackAccess sync.Mutex + callbacks list.List[adapter.RuleSetUpdateCallback] + refs atomic.Int32 } func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { @@ -61,13 +62,14 @@ func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger. updateInterval = 24 * time.Hour } return &RemoteRuleSet{ - ctx: ctx, - cancel: cancel, - router: router, - logger: logger, - options: options, - updateInterval: updateInterval, - pauseManager: service.FromContext[pause.Manager](ctx), + ctx: ctx, + cancel: cancel, + router: router, + outboundManager: service.FromContext[adapter.OutboundManager](ctx), + logger: logger, + options: options, + updateInterval: updateInterval, + pauseManager: service.FromContext[pause.Manager](ctx), } } @@ -83,17 +85,13 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext *adapter. s.cacheFile = service.FromContext[adapter.CacheFile](s.ctx) var dialer N.Dialer if s.options.RemoteOptions.DownloadDetour != "" { - outbound, loaded := s.router.Outbound(s.options.RemoteOptions.DownloadDetour) + outbound, loaded := s.outboundManager.Outbound(s.options.RemoteOptions.DownloadDetour) if !loaded { return E.New("download_detour not found: ", s.options.RemoteOptions.DownloadDetour) } dialer = outbound } else { - outbound, err := s.router.DefaultOutbound(N.NetworkTCP) - if err != nil { - return err - } - dialer = outbound + dialer = s.outboundManager.Default() } s.dialer = dialer if s.cacheFile != nil { diff --git a/transport/dhcp/server.go b/transport/dhcp/server.go index 8325c37b2f..d5603e9ed5 100644 --- a/transport/dhcp/server.go +++ b/transport/dhcp/server.go @@ -23,6 +23,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/task" "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" "github.com/insomniacslk/dhcp/dhcpv4" mDNS "github.com/miekg/dns" @@ -53,7 +54,7 @@ func NewTransport(options dns.TransportOptions) (*Transport, error) { if linkURL.Host == "" { return nil, E.New("missing interface name for DHCP") } - router := adapter.RouterFromContext(options.Context) + router := service.FromContext[adapter.Router](options.Context) if router == nil { return nil, E.New("missing router in context") } diff --git a/transport/fakeip/server.go b/transport/fakeip/server.go index 5e0c7eef02..d1bbb2aadd 100644 --- a/transport/fakeip/server.go +++ b/transport/fakeip/server.go @@ -9,6 +9,7 @@ import ( "github.com/sagernet/sing-dns" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" + "github.com/sagernet/sing/service" mDNS "github.com/miekg/dns" ) @@ -32,7 +33,7 @@ type Transport struct { } func NewTransport(options dns.TransportOptions) (*Transport, error) { - router := adapter.RouterFromContext(options.Context) + router := service.FromContext[adapter.Router](options.Context) if router == nil { return nil, E.New("missing router in context") } From f2ddc5883b158903b7008925e22e3c8587a343c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 10 Nov 2024 12:11:21 +0800 Subject: [PATCH 15/49] refactor: Modular network manager --- adapter/inbound/manager.go | 4 +- adapter/network.go | 23 ++ adapter/outbound/manager.go | 20 +- adapter/router.go | 19 +- box.go | 56 +-- cmd/sing-box/cmd_rule_set_match.go | 3 +- common/dialer/default.go | 24 +- common/dialer/dialer.go | 25 +- common/settings/proxy_darwin.go | 2 +- experimental/libbox/command_group.go | 4 +- experimental/libbox/command_select.go | 2 +- experimental/libbox/command_urltest.go | 4 +- experimental/libbox/config.go | 2 +- experimental/libbox/monitor.go | 2 +- experimental/libbox/platform/interface.go | 4 +- experimental/libbox/service.go | 10 +- experimental/libbox/service_pause.go | 2 +- include/clashapi_stub.go | 2 +- protocol/direct/loopback_detect.go | 16 +- protocol/tun/inbound.go | 24 +- protocol/wireguard/outbound.go | 2 +- route/geo_resources.go | 10 +- route/network.go | 334 ++++++++++++++++ route/route.go | 34 +- route/router.go | 456 +++------------------- route/rule/rule_default.go | 19 +- route/rule/rule_dns.go | 19 +- route/rule/rule_headless.go | 24 +- route/rule/rule_item_wifi_bssid.go | 12 +- route/rule/rule_item_wifi_ssid.go | 12 +- route/rule/rule_set.go | 6 +- route/rule/rule_set_local.go | 8 +- route/rule/rule_set_remote.go | 6 +- transport/dhcp/server.go | 25 +- transport/wireguard/device_system.go | 4 +- 35 files changed, 614 insertions(+), 605 deletions(-) create mode 100644 adapter/network.go create mode 100644 route/network.go diff --git a/adapter/inbound/manager.go b/adapter/inbound/manager.go index d2be4b578c..69a3ad46b0 100644 --- a/adapter/inbound/manager.go +++ b/adapter/inbound/manager.go @@ -52,13 +52,13 @@ func (m *Manager) Start(stage adapter.StartStage) error { func (m *Manager) Close() error { m.access.Lock() + defer m.access.Unlock() if !m.started { - panic("not started") + return nil } m.started = false inbounds := m.inbounds m.inbounds = nil - m.access.Unlock() monitor := taskmonitor.New(m.logger, C.StopTimeout) var err error for _, inbound := range inbounds { diff --git a/adapter/network.go b/adapter/network.go new file mode 100644 index 0000000000..0ce2741185 --- /dev/null +++ b/adapter/network.go @@ -0,0 +1,23 @@ +package adapter + +import ( + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/control" +) + +type NetworkManager interface { + NewService + InterfaceFinder() control.InterfaceFinder + UpdateInterfaces() error + DefaultInterface() string + AutoDetectInterface() bool + AutoDetectInterfaceFunc() control.Func + DefaultMark() uint32 + RegisterAutoRedirectOutputMark(mark uint32) error + AutoRedirectOutputMark() uint32 + NetworkMonitor() tun.NetworkUpdateMonitor + InterfaceMonitor() tun.DefaultInterfaceMonitor + PackageManager() tun.PackageManager + WIFIState() WIFIState + ResetNetwork() +} diff --git a/adapter/outbound/manager.go b/adapter/outbound/manager.go index b3e1a170bf..10a89a1c9b 100644 --- a/adapter/outbound/manager.go +++ b/adapter/outbound/manager.go @@ -48,16 +48,17 @@ func (m *Manager) Initialize(defaultOutboundFallback adapter.Outbound) { func (m *Manager) Start(stage adapter.StartStage) error { m.access.Lock() - defer m.access.Unlock() if m.started && m.stage >= stage { panic("already started") } m.started = true m.stage = stage + outbounds := m.outbounds + m.access.Unlock() if stage == adapter.StartStateStart { - m.startOutbounds() + return m.startOutbounds(outbounds) } else { - for _, outbound := range m.outbounds { + for _, outbound := range outbounds { err := adapter.LegacyStart(outbound, stage) if err != nil { return E.Cause(err, stage.Action(), " outbound/", outbound.Type(), "[", outbound.Tag(), "]") @@ -67,13 +68,13 @@ func (m *Manager) Start(stage adapter.StartStage) error { return nil } -func (m *Manager) startOutbounds() error { +func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error { monitor := taskmonitor.New(m.logger, C.StartTimeout) started := make(map[string]bool) for { canContinue := false startOne: - for _, outboundToStart := range m.outbounds { + for _, outboundToStart := range outbounds { outboundTag := outboundToStart.Tag() if started[outboundTag] { continue @@ -97,13 +98,13 @@ func (m *Manager) startOutbounds() error { } } } - if len(started) == len(m.outbounds) { + if len(started) == len(outbounds) { break } if canContinue { continue } - currentOutbound := common.Find(m.outbounds, func(it adapter.Outbound) bool { + currentOutbound := common.Find(outbounds, func(it adapter.Outbound) bool { return !started[it.Tag()] }) var lintOutbound func(oTree []string, oCurrent adapter.Outbound) error @@ -114,7 +115,9 @@ func (m *Manager) startOutbounds() error { if common.Contains(oTree, problemOutboundTag) { return E.New("circular outbound dependency: ", strings.Join(oTree, " -> "), " -> ", problemOutboundTag) } + m.access.Lock() problemOutbound := m.outboundByTag[problemOutboundTag] + m.access.Unlock() if problemOutbound == nil { return E.New("dependency[", problemOutboundTag, "] not found for outbound[", oCurrent.Tag(), "]") } @@ -129,7 +132,8 @@ func (m *Manager) Close() error { monitor := taskmonitor.New(m.logger, C.StopTimeout) m.access.Lock() if !m.started { - panic("not started") + m.access.Unlock() + return nil } m.started = false outbounds := m.outbounds diff --git a/adapter/router.go b/adapter/router.go index b8ac51f573..40a461a79e 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -10,8 +10,6 @@ import ( "github.com/sagernet/sing-box/common/geoip" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-dns" - "github.com/sagernet/sing-tun" - "github.com/sagernet/sing/common/control" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/x/list" @@ -31,28 +29,13 @@ type Router interface { GeoIPReader() *geoip.Reader LoadGeosite(code string) (Rule, error) - RuleSet(tag string) (RuleSet, bool) - NeedWIFIState() bool Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error) ClearDNSCache() - - InterfaceFinder() control.InterfaceFinder - UpdateInterfaces() error - DefaultInterface() string - AutoDetectInterface() bool - AutoDetectInterfaceFunc() control.Func - DefaultMark() uint32 - RegisterAutoRedirectOutputMark(mark uint32) error - AutoRedirectOutputMark() uint32 - NetworkMonitor() tun.NetworkUpdateMonitor - InterfaceMonitor() tun.DefaultInterfaceMonitor - PackageManager() tun.PackageManager - WIFIState() WIFIState Rules() []Rule ClashServer() ClashServer @@ -61,7 +44,7 @@ type Router interface { V2RayServer() V2RayServer SetV2RayServer(server V2RayServer) - ResetNetwork() error + ResetNetwork() } // Deprecated: Use ConnectionRouterEx instead. diff --git a/box.go b/box.go index d44e8ed058..8eac0dfa6c 100644 --- a/box.go +++ b/box.go @@ -34,6 +34,7 @@ type Box struct { router adapter.Router inbound *inbound.Manager outbound *outbound.Manager + network *route.NetworkManager logFactory log.Factory logger log.ContextLogger preServices1 map[string]adapter.Service @@ -109,20 +110,18 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "create log factory") } routeOptions := common.PtrValueOrDefault(options.Route) - inboundManager := inbound.NewManager(logFactory.NewLogger("inbound-manager"), inboundRegistry) - outboundManager := outbound.NewManager(logFactory.NewLogger("outbound-manager"), outboundRegistry, routeOptions.Final) + inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry) + outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, routeOptions.Final) ctx = service.ContextWith[adapter.InboundManager](ctx, inboundManager) ctx = service.ContextWith[adapter.OutboundManager](ctx, outboundManager) - router, err := route.NewRouter( - ctx, - logFactory, - routeOptions, - common.PtrValueOrDefault(options.DNS), - common.PtrValueOrDefault(options.NTP), - options.Inbounds, - ) + networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions) if err != nil { - return nil, E.Cause(err, "parse route options") + return nil, E.Cause(err, "initialize network manager") + } + ctx = service.ContextWith[adapter.NetworkManager](ctx, networkManager) + router, err := route.NewRouter(ctx, logFactory, routeOptions, common.PtrValueOrDefault(options.DNS), common.PtrValueOrDefault(options.NTP)) + if err != nil { + return nil, E.Cause(err, "initialize router") } for i, inboundOptions := range options.Inbounds { var tag string @@ -177,11 +176,8 @@ func New(options Options) (*Box, error) { option.DirectOutboundOptions{}, ), )) - if err != nil { - return nil, err - } if platformInterface != nil { - err = platformInterface.Initialize(ctx, router) + err = platformInterface.Initialize(networkManager) if err != nil { return nil, E.Cause(err, "initialize platform interface") } @@ -219,6 +215,7 @@ func New(options Options) (*Box, error) { router: router, inbound: inboundManager, outbound: outboundManager, + network: networkManager, createdAt: createdAt, logFactory: logFactory, logger: logFactory.Logger(), @@ -295,6 +292,10 @@ func (s *Box) preStart() error { } } } + err = s.network.Start(adapter.StartStateInitialize) + if err != nil { + return E.Cause(err, "initialize network manager") + } err = s.router.Start(adapter.StartStateInitialize) if err != nil { return E.Cause(err, "initialize router") @@ -303,6 +304,10 @@ func (s *Box) preStart() error { if err != nil { return err } + err = s.network.Start(adapter.StartStateStart) + if err != nil { + return err + } return s.router.Start(adapter.StartStateStart) } @@ -337,6 +342,10 @@ func (s *Box) start() error { if err != nil { return err } + err = s.network.Start(adapter.StartStatePostStart) + if err != nil { + return err + } err = s.router.Start(adapter.StartStatePostStart) if err != nil { return err @@ -345,6 +354,10 @@ func (s *Box) start() error { if err != nil { return err } + err = s.network.Start(adapter.StartStateStarted) + if err != nil { + return err + } err = s.router.Start(adapter.StartStateStarted) if err != nil { return err @@ -378,13 +391,8 @@ func (s *Box) Close() error { } errors = E.Errors(errors, s.inbound.Close()) errors = E.Errors(errors, s.outbound.Close()) - monitor.Start("close router") - if err := common.Close(s.router); err != nil { - errors = E.Append(errors, err, func(err error) error { - return E.Cause(err, "close router") - }) - } - monitor.Finish() + errors = E.Errors(errors, s.network.Close()) + errors = E.Errors(errors, s.router.Close()) for serviceName, service := range s.preServices1 { monitor.Start("close ", serviceName) errors = E.Append(errors, service.Close(), func(err error) error { @@ -415,6 +423,10 @@ func (s *Box) Outbound() adapter.OutboundManager { return s.outbound } +func (s *Box) Network() adapter.NetworkManager { + return s.network +} + func (s *Box) Router() adapter.Router { return s.router } diff --git a/cmd/sing-box/cmd_rule_set_match.go b/cmd/sing-box/cmd_rule_set_match.go index b96ff22cf9..f5016117d7 100644 --- a/cmd/sing-box/cmd_rule_set_match.go +++ b/cmd/sing-box/cmd_rule_set_match.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "io" "os" @@ -83,7 +84,7 @@ func ruleSetMatch(sourcePath string, domain string) error { } for i, ruleOptions := range plainRuleSet.Rules { var currentRule adapter.HeadlessRule - currentRule, err = rule.NewHeadlessRule(nil, ruleOptions) + currentRule, err = rule.NewHeadlessRule(context.Background(), ruleOptions) if err != nil { return E.Cause(err, "parse rule_set.rules.[", i, "]") } diff --git a/common/dialer/default.go b/common/dialer/default.go index 0c8cee18fd..daf7995037 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -29,31 +29,31 @@ type DefaultDialer struct { isWireGuardListener bool } -func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDialer, error) { +func NewDefault(networkManager adapter.NetworkManager, options option.DialerOptions) (*DefaultDialer, error) { var dialer net.Dialer var listener net.ListenConfig if options.BindInterface != "" { var interfaceFinder control.InterfaceFinder - if router != nil { - interfaceFinder = router.InterfaceFinder() + if networkManager != nil { + interfaceFinder = networkManager.InterfaceFinder() } else { interfaceFinder = control.NewDefaultInterfaceFinder() } bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1) dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) - } else if router != nil && router.AutoDetectInterface() { - bindFunc := router.AutoDetectInterfaceFunc() + } else if networkManager != nil && networkManager.AutoDetectInterface() { + bindFunc := networkManager.AutoDetectInterfaceFunc() dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) - } else if router != nil && router.DefaultInterface() != "" { - bindFunc := control.BindToInterface(router.InterfaceFinder(), router.DefaultInterface(), -1) + } else if networkManager != nil && networkManager.DefaultInterface() != "" { + bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), networkManager.DefaultInterface(), -1) dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) } var autoRedirectOutputMark uint32 - if router != nil { - autoRedirectOutputMark = router.AutoRedirectOutputMark() + if networkManager != nil { + autoRedirectOutputMark = networkManager.AutoRedirectOutputMark() } if autoRedirectOutputMark > 0 { dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark)) @@ -65,9 +65,9 @@ func NewDefault(router adapter.Router, options option.DialerOptions) (*DefaultDi if autoRedirectOutputMark > 0 { return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `routing_mark`") } - } else if router != nil && router.DefaultMark() > 0 { - dialer.Control = control.Append(dialer.Control, control.RoutingMark(router.DefaultMark())) - listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark())) + } else if networkManager != nil && networkManager.DefaultMark() > 0 { + dialer.Control = control.Append(dialer.Control, control.RoutingMark(networkManager.DefaultMark())) + listener.Control = control.Append(listener.Control, control.RoutingMark(networkManager.DefaultMark())) if autoRedirectOutputMark > 0 { return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `default_mark`") } diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index fe4c7c1204..047a25146b 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -13,16 +13,16 @@ import ( ) func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) { - router := service.FromContext[adapter.Router](ctx) + networkManager := service.FromContext[adapter.NetworkManager](ctx) if options.IsWireGuardListener { - return NewDefault(router, options) + return NewDefault(networkManager, options) } var ( dialer N.Dialer err error ) if options.Detour == "" { - dialer, err = NewDefault(router, options) + dialer, err = NewDefault(networkManager, options) if err != nil { return nil, err } @@ -33,16 +33,19 @@ func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) { } dialer = NewDetour(outboundManager, options.Detour) } - if router == nil { - return NewDefault(router, options) + if networkManager == nil { + return NewDefault(networkManager, options) } if options.Detour == "" { - dialer = NewResolveDialer( - router, - dialer, - options.Detour == "" && !options.TCPFastOpen, - dns.DomainStrategy(options.DomainStrategy), - time.Duration(options.FallbackDelay)) + router := service.FromContext[adapter.Router](ctx) + if router != nil { + dialer = NewResolveDialer( + router, + dialer, + options.Detour == "" && !options.TCPFastOpen, + dns.DomainStrategy(options.DomainStrategy), + time.Duration(options.FallbackDelay)) + } } return dialer, nil } diff --git a/common/settings/proxy_darwin.go b/common/settings/proxy_darwin.go index 2d86fa2d9a..17e82cf59f 100644 --- a/common/settings/proxy_darwin.go +++ b/common/settings/proxy_darwin.go @@ -25,7 +25,7 @@ type DarwinSystemProxy struct { } func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*DarwinSystemProxy, error) { - interfaceMonitor := service.FromContext[adapter.Router](ctx).InterfaceMonitor() + interfaceMonitor := service.FromContext[adapter.NetworkManager](ctx).InterfaceMonitor() if interfaceMonitor == nil { return nil, E.New("missing interface monitor") } diff --git a/experimental/libbox/command_group.go b/experimental/libbox/command_group.go index 0915f56b83..684cac62ce 100644 --- a/experimental/libbox/command_group.go +++ b/experimental/libbox/command_group.go @@ -109,7 +109,7 @@ func readGroups(reader io.Reader) (OutboundGroupIterator, error) { func writeGroups(writer io.Writer, boxService *BoxService) error { historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx) cacheFile := service.FromContext[adapter.CacheFile](boxService.ctx) - outbounds := boxService.instance.Router().Outbounds() + outbounds := boxService.instance.Outbound().Outbounds() var iGroups []adapter.OutboundGroup for _, it := range outbounds { if group, isGroup := it.(adapter.OutboundGroup); isGroup { @@ -130,7 +130,7 @@ func writeGroups(writer io.Writer, boxService *BoxService) error { } for _, itemTag := range iGroup.All() { - itemOutbound, isLoaded := boxService.instance.Router().Outbound(itemTag) + itemOutbound, isLoaded := boxService.instance.Outbound().Outbound(itemTag) if !isLoaded { continue } diff --git a/experimental/libbox/command_select.go b/experimental/libbox/command_select.go index f352005d11..6dd74a2d48 100644 --- a/experimental/libbox/command_select.go +++ b/experimental/libbox/command_select.go @@ -43,7 +43,7 @@ func (s *CommandServer) handleSelectOutbound(conn net.Conn) error { if service == nil { return writeError(conn, E.New("service not ready")) } - outboundGroup, isLoaded := service.instance.Router().Outbound(groupTag) + outboundGroup, isLoaded := service.instance.Outbound().Outbound(groupTag) if !isLoaded { return writeError(conn, E.New("selector not found: ", groupTag)) } diff --git a/experimental/libbox/command_urltest.go b/experimental/libbox/command_urltest.go index c72ded8a3c..c30d996e6b 100644 --- a/experimental/libbox/command_urltest.go +++ b/experimental/libbox/command_urltest.go @@ -41,7 +41,7 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { if serviceNow == nil { return nil } - abstractOutboundGroup, isLoaded := serviceNow.instance.Router().Outbound(groupTag) + abstractOutboundGroup, isLoaded := serviceNow.instance.Outbound().Outbound(groupTag) if !isLoaded { return writeError(conn, E.New("outbound group not found: ", groupTag)) } @@ -55,7 +55,7 @@ func (s *CommandServer) handleURLTest(conn net.Conn) error { } else { historyStorage := service.PtrFromContext[urltest.HistoryStorage](serviceNow.ctx) outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { - itOutbound, _ := serviceNow.instance.Router().Outbound(it) + itOutbound, _ := serviceNow.instance.Outbound().Outbound(it) return itOutbound }), func(it adapter.Outbound) bool { if it == nil { diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index cc8057f49d..3310b09a58 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -50,7 +50,7 @@ func CheckConfig(configContent string) error { type platformInterfaceStub struct{} -func (s *platformInterfaceStub) Initialize(ctx context.Context, router adapter.Router) error { +func (s *platformInterfaceStub) Initialize(networkManager adapter.NetworkManager) error { return nil } diff --git a/experimental/libbox/monitor.go b/experimental/libbox/monitor.go index eb56a314af..00a3e0cf6c 100644 --- a/experimental/libbox/monitor.go +++ b/experimental/libbox/monitor.go @@ -123,7 +123,7 @@ func (m *platformDefaultInterfaceMonitor) updateDefaultInterface(interfaceName s err = m.updateInterfaces() } if err == nil { - err = m.router.UpdateInterfaces() + err = m.networkManager.UpdateInterfaces() } if err != nil { m.logger.Error(E.Cause(err, "update interfaces")) diff --git a/experimental/libbox/platform/interface.go b/experimental/libbox/platform/interface.go index 3dc000814a..16eab5abcc 100644 --- a/experimental/libbox/platform/interface.go +++ b/experimental/libbox/platform/interface.go @@ -1,8 +1,6 @@ package platform import ( - "context" - "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/process" "github.com/sagernet/sing-box/option" @@ -12,7 +10,7 @@ import ( ) type Interface interface { - Initialize(ctx context.Context, router adapter.Router) error + Initialize(networkManager adapter.NetworkManager) error UsePlatformAutoDetectInterfaceControl() bool AutoDetectInterfaceControl(fd int) error OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 24fbdb4c94..e3d2f95474 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -117,13 +117,13 @@ var ( ) type platformInterfaceWrapper struct { - iif PlatformInterface - useProcFS bool - router adapter.Router + iif PlatformInterface + useProcFS bool + networkManager adapter.NetworkManager } -func (w *platformInterfaceWrapper) Initialize(ctx context.Context, router adapter.Router) error { - w.router = router +func (w *platformInterfaceWrapper) Initialize(networkManager adapter.NetworkManager) error { + w.networkManager = networkManager return nil } diff --git a/experimental/libbox/service_pause.go b/experimental/libbox/service_pause.go index c4aa8daaff..a7599a767a 100644 --- a/experimental/libbox/service_pause.go +++ b/experimental/libbox/service_pause.go @@ -29,5 +29,5 @@ func (s *BoxService) Wake() { } func (s *BoxService) ResetNetwork() { - _ = s.instance.Router().ResetNetwork() + s.instance.Router().ResetNetwork() } diff --git a/include/clashapi_stub.go b/include/clashapi_stub.go index 26e199b314..e7d5304fa1 100644 --- a/include/clashapi_stub.go +++ b/include/clashapi_stub.go @@ -13,7 +13,7 @@ import ( ) func init() { - experimental.RegisterClashServerConstructor(func(ctx context.Context, router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { + experimental.RegisterClashServerConstructor(func(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) { return nil, E.New(`clash api is not included in this build, rebuild with -tags with_clash_api`) }) } diff --git a/protocol/direct/loopback_detect.go b/protocol/direct/loopback_detect.go index 5a184e692a..4bc1be3b04 100644 --- a/protocol/direct/loopback_detect.go +++ b/protocol/direct/loopback_detect.go @@ -11,18 +11,18 @@ import ( ) type loopBackDetector struct { - router adapter.Router + networkManager adapter.NetworkManager connAccess sync.RWMutex packetConnAccess sync.RWMutex connMap map[netip.AddrPort]netip.AddrPort packetConnMap map[uint16]uint16 } -func newLoopBackDetector(router adapter.Router) *loopBackDetector { +func newLoopBackDetector(networkManager adapter.NetworkManager) *loopBackDetector { return &loopBackDetector{ - router: router, - connMap: make(map[netip.AddrPort]netip.AddrPort), - packetConnMap: make(map[uint16]uint16), + networkManager: networkManager, + connMap: make(map[netip.AddrPort]netip.AddrPort), + packetConnMap: make(map[uint16]uint16), } } @@ -33,7 +33,7 @@ func (l *loopBackDetector) NewConn(conn net.Conn) net.Conn { } if udpConn, isUDPConn := conn.(abstractUDPConn); isUDPConn { if !source.Addr().IsLoopback() { - _, err := l.router.InterfaceFinder().InterfaceByAddr(source.Addr()) + _, err := l.networkManager.InterfaceFinder().InterfaceByAddr(source.Addr()) if err != nil { return conn } @@ -59,7 +59,7 @@ func (l *loopBackDetector) NewPacketConn(conn N.NetPacketConn, destination M.Soc return conn } if !source.Addr().IsLoopback() { - _, err := l.router.InterfaceFinder().InterfaceByAddr(source.Addr()) + _, err := l.networkManager.InterfaceFinder().InterfaceByAddr(source.Addr()) if err != nil { return conn } @@ -82,7 +82,7 @@ func (l *loopBackDetector) CheckPacketConn(source netip.AddrPort, local netip.Ad return false } if !source.Addr().IsLoopback() { - _, err := l.router.InterfaceFinder().InterfaceByAddr(source.Addr()) + _, err := l.networkManager.InterfaceFinder().InterfaceByAddr(source.Addr()) if err != nil { return false } diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index f2476223ce..302afb578d 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -36,10 +36,11 @@ func RegisterInbound(registry *inbound.Registry) { } type Inbound struct { - tag string - ctx context.Context - router adapter.Router - logger log.ContextLogger + tag string + ctx context.Context + router adapter.Router + networkManager adapter.NetworkManager + logger log.ContextLogger // Deprecated inboundOptions option.InboundOptions tunOptions tun.Options @@ -168,11 +169,12 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if outputMark == 0 { outputMark = tun.DefaultAutoRedirectOutputMark } - + networkManager := service.FromContext[adapter.NetworkManager](ctx) inbound := &Inbound{ tag: tag, ctx: ctx, router: router, + networkManager: networkManager, logger: logger, inboundOptions: options.InboundOptions, tunOptions: tun.Options{ @@ -198,7 +200,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo IncludeAndroidUser: options.IncludeAndroidUser, IncludePackage: options.IncludePackage, ExcludePackage: options.ExcludePackage, - InterfaceMonitor: router.InterfaceMonitor(), + InterfaceMonitor: networkManager.InterfaceMonitor(), }, endpointIndependentNat: options.EndpointIndependentNat, udpTimeout: udpTimeout, @@ -216,8 +218,8 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo Context: ctx, Handler: (*autoRedirectHandler)(inbound), Logger: logger, - NetworkMonitor: router.NetworkMonitor(), - InterfaceFinder: router.InterfaceFinder(), + NetworkMonitor: networkManager.NetworkMonitor(), + InterfaceFinder: networkManager.InterfaceFinder(), TableName: "sing-box", DisableNFTables: dErr == nil && disableNFTables, RouteAddressSet: &inbound.routeAddressSet, @@ -248,7 +250,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } if markMode { inbound.tunOptions.AutoRedirectMarkMode = true - err = router.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark) + err = networkManager.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark) if err != nil { return nil, err } @@ -300,7 +302,7 @@ func (t *Inbound) Tag() string { func (t *Inbound) Start() error { if C.IsAndroid && t.platformInterface == nil { - t.tunOptions.BuildAndroidRules(t.router.PackageManager()) + t.tunOptions.BuildAndroidRules(t.networkManager.PackageManager()) } if t.tunOptions.Name == "" { t.tunOptions.Name = tun.CalculateInterfaceName("") @@ -338,7 +340,7 @@ func (t *Inbound) Start() error { Handler: t, Logger: t.logger, ForwarderBindInterface: forwarderBindInterface, - InterfaceFinder: t.router.InterfaceFinder(), + InterfaceFinder: t.networkManager.InterfaceFinder(), IncludeAllNetworks: includeAllNetworks, }) if err != nil { diff --git a/protocol/wireguard/outbound.go b/protocol/wireguard/outbound.go index 79e8873257..12e53e591a 100644 --- a/protocol/wireguard/outbound.go +++ b/protocol/wireguard/outbound.go @@ -100,7 +100,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL if !options.SystemInterface && tun.WithGVisor { wireTunDevice, err = wireguard.NewStackDevice(options.LocalAddress, mtu) } else { - wireTunDevice, err = wireguard.NewSystemDevice(router, options.InterfaceName, options.LocalAddress, mtu, options.GSO) + wireTunDevice, err = wireguard.NewSystemDevice(service.FromContext[adapter.NetworkManager](ctx), options.InterfaceName, options.LocalAddress, mtu, options.GSO) } if err != nil { return nil, E.Cause(err, "create WireGuard device") diff --git a/route/geo_resources.go b/route/geo_resources.go index 91c06796bc..c5a45ffdee 100644 --- a/route/geo_resources.go +++ b/route/geo_resources.go @@ -33,7 +33,7 @@ func (r *Router) LoadGeosite(code string) (adapter.Rule, error) { if err != nil { return nil, err } - rule, err = R.NewDefaultRule(r.ctx, r, nil, geosite.Compile(items)) + rule, err = R.NewDefaultRule(r.ctx, nil, geosite.Compile(items)) if err != nil { return nil, err } @@ -145,13 +145,13 @@ func (r *Router) downloadGeoIPDatabase(savePath string) error { r.logger.Info("downloading geoip database") var detour adapter.Outbound if r.geoIPOptions.DownloadDetour != "" { - outbound, loaded := r.Outbound(r.geoIPOptions.DownloadDetour) + outbound, loaded := r.outboundManager.Outbound(r.geoIPOptions.DownloadDetour) if !loaded { return E.New("detour outbound not found: ", r.geoIPOptions.DownloadDetour) } detour = outbound } else { - detour = r.defaultOutboundForConnection + detour = r.outboundManager.Default() } if parentDir := filepath.Dir(savePath); parentDir != "" { @@ -200,13 +200,13 @@ func (r *Router) downloadGeositeDatabase(savePath string) error { r.logger.Info("downloading geosite database") var detour adapter.Outbound if r.geositeOptions.DownloadDetour != "" { - outbound, loaded := r.Outbound(r.geositeOptions.DownloadDetour) + outbound, loaded := r.outboundManager.Outbound(r.geositeOptions.DownloadDetour) if !loaded { return E.New("detour outbound not found: ", r.geositeOptions.DownloadDetour) } detour = outbound } else { - detour = r.defaultOutboundForConnection + detour = r.outboundManager.Default() } if parentDir := filepath.Dir(savePath); parentDir != "" { diff --git a/route/network.go b/route/network.go new file mode 100644 index 0000000000..912b6621e7 --- /dev/null +++ b/route/network.go @@ -0,0 +1,334 @@ +package route + +import ( + "context" + "errors" + "net/netip" + "os" + "runtime" + "syscall" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/conntrack" + "github.com/sagernet/sing-box/common/taskmonitor" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/libbox/platform" + "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" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/winpowrprof" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/pause" +) + +var _ adapter.NetworkManager = (*NetworkManager)(nil) + +type NetworkManager struct { + logger logger.ContextLogger + interfaceFinder *control.DefaultInterfaceFinder + autoDetectInterface bool + defaultInterface string + defaultMark uint32 + autoRedirectOutputMark uint32 + networkMonitor tun.NetworkUpdateMonitor + interfaceMonitor tun.DefaultInterfaceMonitor + packageManager tun.PackageManager + powerListener winpowrprof.EventListener + pauseManager pause.Manager + platformInterface platform.Interface + outboundManager adapter.OutboundManager + wifiState adapter.WIFIState + started bool +} + +func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOptions option.RouteOptions) (*NetworkManager, error) { + nm := &NetworkManager{ + logger: logger, + interfaceFinder: control.NewDefaultInterfaceFinder(), + autoDetectInterface: routeOptions.AutoDetectInterface, + defaultInterface: routeOptions.DefaultInterface, + defaultMark: routeOptions.DefaultMark, + pauseManager: service.FromContext[pause.Manager](ctx), + platformInterface: service.FromContext[platform.Interface](ctx), + outboundManager: service.FromContext[adapter.OutboundManager](ctx), + } + usePlatformDefaultInterfaceMonitor := nm.platformInterface != nil && nm.platformInterface.UsePlatformDefaultInterfaceMonitor() + enforceInterfaceMonitor := routeOptions.AutoDetectInterface + if !usePlatformDefaultInterfaceMonitor { + networkMonitor, err := tun.NewNetworkUpdateMonitor(logger) + if !((err != nil && !enforceInterfaceMonitor) || errors.Is(err, os.ErrInvalid)) { + if err != nil { + return nil, E.Cause(err, "create network monitor") + } + nm.networkMonitor = networkMonitor + interfaceMonitor, err := tun.NewDefaultInterfaceMonitor(nm.networkMonitor, logger, tun.DefaultInterfaceMonitorOptions{ + InterfaceFinder: nm.interfaceFinder, + OverrideAndroidVPN: routeOptions.OverrideAndroidVPN, + UnderNetworkExtension: nm.platformInterface != nil && nm.platformInterface.UnderNetworkExtension(), + }) + if err != nil { + return nil, E.New("auto_detect_interface unsupported on current platform") + } + interfaceMonitor.RegisterCallback(nm.notifyNetworkUpdate) + nm.interfaceMonitor = interfaceMonitor + } + } else { + interfaceMonitor := nm.platformInterface.CreateDefaultInterfaceMonitor(logger) + interfaceMonitor.RegisterCallback(nm.notifyNetworkUpdate) + nm.interfaceMonitor = interfaceMonitor + } + return nm, nil +} + +func (r *NetworkManager) Start(stage adapter.StartStage) error { + monitor := taskmonitor.New(r.logger, C.StartTimeout) + switch stage { + case adapter.StartStateInitialize: + if r.interfaceMonitor != nil { + monitor.Start("initialize interface monitor") + err := r.interfaceMonitor.Start() + monitor.Finish() + if err != nil { + return err + } + } + if r.networkMonitor != nil { + monitor.Start("initialize network monitor") + err := r.networkMonitor.Start() + monitor.Finish() + if err != nil { + return err + } + } + case adapter.StartStateStart: + if runtime.GOOS == "windows" { + powerListener, err := winpowrprof.NewEventListener(r.notifyWindowsPowerEvent) + if err == nil { + r.powerListener = powerListener + } else { + r.logger.Warn("initialize power listener: ", err) + } + } + if r.powerListener != nil { + monitor.Start("start power listener") + err := r.powerListener.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "start power listener") + } + } + if C.IsAndroid && r.platformInterface == nil { + monitor.Start("initialize package manager") + packageManager, err := tun.NewPackageManager(tun.PackageManagerOptions{ + Callback: r, + Logger: r.logger, + }) + monitor.Finish() + if err != nil { + return E.Cause(err, "create package manager") + } + monitor.Start("start package manager") + err = packageManager.Start() + monitor.Finish() + if err != nil { + r.logger.Warn("initialize package manager: ", err) + } else { + r.packageManager = packageManager + } + } + case adapter.StartStatePostStart: + r.started = true + } + return nil +} + +func (r *NetworkManager) Close() error { + monitor := taskmonitor.New(r.logger, C.StopTimeout) + var err error + if r.interfaceMonitor != nil { + monitor.Start("close interface monitor") + err = E.Append(err, r.interfaceMonitor.Close(), func(err error) error { + return E.Cause(err, "close interface monitor") + }) + monitor.Finish() + } + if r.networkMonitor != nil { + monitor.Start("close network monitor") + err = E.Append(err, r.networkMonitor.Close(), func(err error) error { + return E.Cause(err, "close network monitor") + }) + monitor.Finish() + } + if r.packageManager != nil { + monitor.Start("close package manager") + err = E.Append(err, r.packageManager.Close(), func(err error) error { + return E.Cause(err, "close package manager") + }) + monitor.Finish() + } + if r.powerListener != nil { + monitor.Start("close power listener") + err = E.Append(err, r.powerListener.Close(), func(err error) error { + return E.Cause(err, "close power listener") + }) + monitor.Finish() + } + return nil +} + +func (r *NetworkManager) InterfaceFinder() control.InterfaceFinder { + return r.interfaceFinder +} + +func (r *NetworkManager) UpdateInterfaces() error { + if r.platformInterface == nil || !r.platformInterface.UsePlatformInterfaceGetter() { + return r.interfaceFinder.Update() + } else { + interfaces, err := r.platformInterface.Interfaces() + if err != nil { + return err + } + r.interfaceFinder.UpdateInterfaces(interfaces) + return nil + } +} + +func (r *NetworkManager) DefaultInterface() string { + return r.defaultInterface +} + +func (r *NetworkManager) AutoDetectInterface() bool { + return r.autoDetectInterface +} + +func (r *NetworkManager) AutoDetectInterfaceFunc() control.Func { + if r.platformInterface != nil && r.platformInterface.UsePlatformAutoDetectInterfaceControl() { + return func(network, address string, conn syscall.RawConn) error { + return control.Raw(conn, func(fd uintptr) error { + return r.platformInterface.AutoDetectInterfaceControl(int(fd)) + }) + } + } else { + if r.interfaceMonitor == nil { + return nil + } + return control.BindToInterfaceFunc(r.interfaceFinder, func(network string, address string) (interfaceName string, interfaceIndex int, err error) { + remoteAddr := M.ParseSocksaddr(address).Addr + if C.IsLinux { + interfaceName, interfaceIndex = r.interfaceMonitor.DefaultInterface(remoteAddr) + if interfaceIndex == -1 { + err = tun.ErrNoRoute + } + } else { + interfaceIndex = r.interfaceMonitor.DefaultInterfaceIndex(remoteAddr) + if interfaceIndex == -1 { + err = tun.ErrNoRoute + } + } + return + }) + } +} + +func (r *NetworkManager) DefaultMark() uint32 { + return r.defaultMark +} + +func (r *NetworkManager) RegisterAutoRedirectOutputMark(mark uint32) error { + if r.autoRedirectOutputMark > 0 { + return E.New("only one auto-redirect can be configured") + } + r.autoRedirectOutputMark = mark + return nil +} + +func (r *NetworkManager) AutoRedirectOutputMark() uint32 { + return r.autoRedirectOutputMark +} + +func (r *NetworkManager) NetworkMonitor() tun.NetworkUpdateMonitor { + return r.networkMonitor +} + +func (r *NetworkManager) InterfaceMonitor() tun.DefaultInterfaceMonitor { + return r.interfaceMonitor +} + +func (r *NetworkManager) PackageManager() tun.PackageManager { + return r.packageManager +} + +func (r *NetworkManager) WIFIState() adapter.WIFIState { + return r.wifiState +} + +func (r *NetworkManager) ResetNetwork() { + conntrack.Close() + + for _, outbound := range r.outboundManager.Outbounds() { + listener, isListener := outbound.(adapter.InterfaceUpdateListener) + if isListener { + listener.InterfaceUpdated() + } + } +} + +func (r *NetworkManager) notifyNetworkUpdate(event int) { + if event == tun.EventNoRoute { + r.pauseManager.NetworkPause() + r.logger.Error("missing default interface") + } else { + r.pauseManager.NetworkWake() + if C.IsAndroid && r.platformInterface == nil { + var vpnStatus string + if r.interfaceMonitor.AndroidVPNEnabled() { + vpnStatus = "enabled" + } else { + vpnStatus = "disabled" + } + r.logger.Info("updated default interface ", r.interfaceMonitor.DefaultInterfaceName(netip.IPv4Unspecified()), ", index ", r.interfaceMonitor.DefaultInterfaceIndex(netip.IPv4Unspecified()), ", vpn ", vpnStatus) + } else { + r.logger.Info("updated default interface ", r.interfaceMonitor.DefaultInterfaceName(netip.IPv4Unspecified()), ", index ", r.interfaceMonitor.DefaultInterfaceIndex(netip.IPv4Unspecified())) + } + if r.platformInterface != nil { + state := r.platformInterface.ReadWIFIState() + if state != r.wifiState { + r.wifiState = state + if state.SSID == "" && state.BSSID == "" { + r.logger.Info("updated WIFI state: disconnected") + } else { + r.logger.Info("updated WIFI state: SSID=", state.SSID, ", BSSID=", state.BSSID) + } + } + } + } + + if !r.started { + return + } + + r.ResetNetwork() +} + +func (r *NetworkManager) notifyWindowsPowerEvent(event int) { + switch event { + case winpowrprof.EVENT_SUSPEND: + r.pauseManager.DevicePause() + r.ResetNetwork() + case winpowrprof.EVENT_RESUME: + if !r.pauseManager.IsDevicePaused() { + return + } + fallthrough + case winpowrprof.EVENT_RESUME_AUTOMATIC: + r.pauseManager.DeviceWake() + r.ResetNetwork() + } +} + +func (r *NetworkManager) OnPackagesUpdated(packages int, sharedUsers int) { + r.logger.Info("updated packages list: ", packages, " packages, ", sharedUsers, " shared users") +} diff --git a/route/route.go b/route/route.go index 91d7f3c9bf..d55b4ec170 100644 --- a/route/route.go +++ b/route/route.go @@ -58,8 +58,8 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad if metadata.LastInbound == metadata.InboundDetour { return E.New("routing loop on detour: ", metadata.InboundDetour) } - detour := r.inboundByTag[metadata.InboundDetour] - if detour == nil { + detour, loaded := r.inboundManager.Get(metadata.InboundDetour) + if !loaded { return E.New("inbound detour not found: ", metadata.InboundDetour) } injectable, isInjectable := detour.(adapter.TCPInjectableInbound) @@ -100,7 +100,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad if selectedRule != nil { switch action := selectedRule.Action().(type) { case *rule.RuleActionRoute: - selectedOutbound, loaded := r.Outbound(action.Outbound) + selectedOutbound, loaded := r.outboundManager.Outbound(action.Outbound) if !loaded { buf.ReleaseMulti(buffers) return E.New("outbound not found: ", action.Outbound) @@ -128,13 +128,14 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad } } if selectedRule == nil { - if r.defaultOutboundForConnection == nil { + defaultOutbound := r.outboundManager.Default() + if !common.Contains(defaultOutbound.Network(), N.NetworkTCP) { buf.ReleaseMulti(buffers) - return E.New("missing default outbound with TCP support") + return E.New("TCP is not supported by default outbound: ", defaultOutbound.Tag()) } - selectedDialer = r.defaultOutboundForConnection - selectedTag = r.defaultOutboundForConnection.Tag() - selectedDescription = F.ToString("outbound/", r.defaultOutboundForConnection.Type(), "[", r.defaultOutboundForConnection.Tag(), "]") + selectedDialer = defaultOutbound + selectedTag = defaultOutbound.Tag() + selectedDescription = F.ToString("outbound/", defaultOutbound.Type(), "[", defaultOutbound.Tag(), "]") } for _, buffer := range buffers { @@ -217,8 +218,8 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m if metadata.LastInbound == metadata.InboundDetour { return E.New("routing loop on detour: ", metadata.InboundDetour) } - detour := r.inboundByTag[metadata.InboundDetour] - if detour == nil { + detour, loaded := r.inboundManager.Get(metadata.InboundDetour) + if !loaded { return E.New("inbound detour not found: ", metadata.InboundDetour) } injectable, isInjectable := detour.(adapter.UDPInjectableInbound) @@ -254,7 +255,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m if selectedRule != nil { switch action := selectedRule.Action().(type) { case *rule.RuleActionRoute: - selectedOutbound, loaded := r.Outbound(action.Outbound) + selectedOutbound, loaded := r.outboundManager.Outbound(action.Outbound) if !loaded { N.ReleaseMultiPacketBuffer(packetBuffers) return E.New("outbound not found: ", action.Outbound) @@ -279,13 +280,14 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m } } if selectedRule == nil || selectReturn { - if r.defaultOutboundForPacketConnection == nil { + defaultOutbound := r.outboundManager.Default() + if !common.Contains(defaultOutbound.Network(), N.NetworkUDP) { N.ReleaseMultiPacketBuffer(packetBuffers) - return E.New("missing default outbound with UDP support") + return E.New("UDP is not supported by outbound: ", defaultOutbound.Tag()) } - selectedDialer = r.defaultOutboundForPacketConnection - selectedTag = r.defaultOutboundForPacketConnection.Tag() - selectedDescription = F.ToString("outbound/", r.defaultOutboundForPacketConnection.Type(), "[", r.defaultOutboundForPacketConnection.Tag(), "]") + selectedDialer = defaultOutbound + selectedTag = defaultOutbound.Tag() + selectedDescription = F.ToString("outbound/", defaultOutbound.Type(), "[", defaultOutbound.Tag(), "]") } for _, buffer := range packetBuffers { conn = bufio.NewCachedPacketConn(conn, buffer.Buffer, buffer.Destination) diff --git a/route/router.go b/route/router.go index 12255b4d7b..62b374473f 100644 --- a/route/router.go +++ b/route/router.go @@ -2,17 +2,14 @@ package route import ( "context" - "errors" "net/netip" "net/url" "os" "runtime" "strings" - "syscall" "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/conntrack" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/geoip" "github.com/sagernet/sing-box/common/geosite" @@ -25,16 +22,13 @@ import ( R "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing-box/transport/fakeip" "github.com/sagernet/sing-dns" - "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/task" - "github.com/sagernet/sing/common/winpowrprof" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" ) @@ -42,69 +36,50 @@ import ( var _ adapter.Router = (*Router)(nil) type Router struct { - ctx context.Context - logger log.ContextLogger - dnsLogger log.ContextLogger - inboundByTag map[string]adapter.Inbound - outbounds []adapter.Outbound - outboundByTag map[string]adapter.Outbound - rules []adapter.Rule - defaultDetour string - defaultOutboundForConnection adapter.Outbound - defaultOutboundForPacketConnection adapter.Outbound - needGeoIPDatabase bool - needGeositeDatabase bool - geoIPOptions option.GeoIPOptions - geositeOptions option.GeositeOptions - geoIPReader *geoip.Reader - geositeReader *geosite.Reader - geositeCache map[string]adapter.Rule - needFindProcess bool - dnsClient *dns.Client - defaultDomainStrategy dns.DomainStrategy - dnsRules []adapter.DNSRule - ruleSets []adapter.RuleSet - ruleSetMap map[string]adapter.RuleSet - defaultTransport dns.Transport - transports []dns.Transport - transportMap map[string]dns.Transport - transportDomainStrategy map[dns.Transport]dns.DomainStrategy - dnsReverseMapping *DNSReverseMapping - fakeIPStore adapter.FakeIPStore - interfaceFinder *control.DefaultInterfaceFinder - autoDetectInterface bool - defaultInterface string - defaultMark uint32 - autoRedirectOutputMark uint32 - networkMonitor tun.NetworkUpdateMonitor - interfaceMonitor tun.DefaultInterfaceMonitor - packageManager tun.PackageManager - powerListener winpowrprof.EventListener - processSearcher process.Searcher - timeService *ntp.Service - pauseManager pause.Manager - clashServer adapter.ClashServer - v2rayServer adapter.V2RayServer - platformInterface platform.Interface - needWIFIState bool - enforcePackageManager bool - wifiState adapter.WIFIState - started bool -} - -func NewRouter( - ctx context.Context, - logFactory log.Factory, - options option.RouteOptions, - dnsOptions option.DNSOptions, - ntpOptions option.NTPOptions, - inbounds []option.Inbound, -) (*Router, error) { + ctx context.Context + logger log.ContextLogger + dnsLogger log.ContextLogger + inboundManager adapter.InboundManager + outboundManager adapter.OutboundManager + networkManager adapter.NetworkManager + rules []adapter.Rule + needGeoIPDatabase bool + needGeositeDatabase bool + geoIPOptions option.GeoIPOptions + geositeOptions option.GeositeOptions + geoIPReader *geoip.Reader + geositeReader *geosite.Reader + geositeCache map[string]adapter.Rule + needFindProcess bool + dnsClient *dns.Client + defaultDomainStrategy dns.DomainStrategy + dnsRules []adapter.DNSRule + ruleSets []adapter.RuleSet + ruleSetMap map[string]adapter.RuleSet + defaultTransport dns.Transport + transports []dns.Transport + transportMap map[string]dns.Transport + transportDomainStrategy map[dns.Transport]dns.DomainStrategy + dnsReverseMapping *DNSReverseMapping + fakeIPStore adapter.FakeIPStore + processSearcher process.Searcher + timeService *ntp.Service + pauseManager pause.Manager + clashServer adapter.ClashServer + v2rayServer adapter.V2RayServer + platformInterface platform.Interface + needWIFIState bool + started bool +} + +func NewRouter(ctx context.Context, logFactory log.Factory, options option.RouteOptions, dnsOptions option.DNSOptions, ntpOptions option.NTPOptions) (*Router, error) { router := &Router{ ctx: ctx, logger: logFactory.NewLogger("router"), dnsLogger: logFactory.NewLogger("dns"), - outboundByTag: make(map[string]adapter.Outbound), + inboundManager: service.FromContext[adapter.InboundManager](ctx), + outboundManager: service.FromContext[adapter.OutboundManager](ctx), + networkManager: service.FromContext[adapter.NetworkManager](ctx), rules: make([]adapter.Rule, 0, len(options.Rules)), dnsRules: make([]adapter.DNSRule, 0, len(dnsOptions.Rules)), ruleSetMap: make(map[string]adapter.RuleSet), @@ -114,22 +89,12 @@ func NewRouter( geositeOptions: common.PtrValueOrDefault(options.Geosite), geositeCache: make(map[string]adapter.Rule), needFindProcess: hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess, - defaultDetour: options.Final, defaultDomainStrategy: dns.DomainStrategy(dnsOptions.Strategy), - interfaceFinder: control.NewDefaultInterfaceFinder(), - autoDetectInterface: options.AutoDetectInterface, - defaultInterface: options.DefaultInterface, - defaultMark: options.DefaultMark, pauseManager: service.FromContext[pause.Manager](ctx), platformInterface: service.FromContext[platform.Interface](ctx), needWIFIState: hasRule(options.Rules, isWIFIRule) || hasDNSRule(dnsOptions.Rules, isWIFIDNSRule), - enforcePackageManager: common.Any(inbounds, func(inbound option.Inbound) bool { - if tunOptions, isTUN := inbound.Options.(*option.TunInboundOptions); isTUN && tunOptions.AutoRoute { - return true - } - return false - }), } + ctx = service.ContextWith[adapter.Router](ctx, router) router.dnsClient = dns.NewClient(dns.ClientOptions{ DisableCache: dnsOptions.DNSClientOptions.DisableCache, DisableExpire: dnsOptions.DNSClientOptions.DisableExpire, @@ -147,14 +112,14 @@ func NewRouter( Logger: router.dnsLogger, }) for i, ruleOptions := range options.Rules { - routeRule, err := R.NewRule(ctx, router, router.logger, ruleOptions, true) + routeRule, err := R.NewRule(ctx, router.logger, ruleOptions, true) if err != nil { return nil, E.Cause(err, "parse rule[", i, "]") } router.rules = append(router.rules, routeRule) } for i, dnsRuleOptions := range dnsOptions.Rules { - dnsRule, err := R.NewDNSRule(ctx, router, router.logger, dnsRuleOptions, true) + dnsRule, err := R.NewDNSRule(ctx, router.logger, dnsRuleOptions, true) if err != nil { return nil, E.Cause(err, "parse dns rule[", i, "]") } @@ -164,7 +129,7 @@ func NewRouter( if _, exists := router.ruleSetMap[ruleSetOptions.Tag]; exists { return nil, E.New("duplicate rule-set tag: ", ruleSetOptions.Tag) } - ruleSet, err := R.NewRuleSet(ctx, router, router.logger, ruleSetOptions) + ruleSet, err := R.NewRuleSet(ctx, router.logger, ruleSetOptions) if err != nil { return nil, E.Cause(err, "parse rule-set[", i, "]") } @@ -191,7 +156,6 @@ func NewRouter( transportTags[i] = tag transportTagMap[tag] = true } - ctx = service.ContextWith[adapter.Router](ctx, router) outboundManager := service.FromContext[adapter.OutboundManager](ctx) for { lastLen := len(dummyTransportMap) @@ -298,7 +262,7 @@ func NewRouter( Context: ctx, Name: "local", Address: "local", - Dialer: common.Must1(dialer.NewDefault(router, option.DialerOptions{})), + Dialer: common.Must1(dialer.NewDefault(router.networkManager, option.DialerOptions{})), }))) } defaultTransport = transports[0] @@ -327,41 +291,6 @@ func NewRouter( router.fakeIPStore = fakeip.NewStore(ctx, router.logger, inet4Range, inet6Range) } - usePlatformDefaultInterfaceMonitor := router.platformInterface != nil && router.platformInterface.UsePlatformDefaultInterfaceMonitor() - enforceInterfaceMonitor := options.AutoDetectInterface || common.Any(inbounds, func(inbound option.Inbound) bool { - if httpMixedOptions, isHTTPMixed := inbound.Options.(*option.HTTPMixedInboundOptions); isHTTPMixed && httpMixedOptions.SetSystemProxy { - return true - } - if tunOptions, isTUN := inbound.Options.(*option.TunInboundOptions); isTUN && tunOptions.AutoRoute { - return true - } - return false - }) - - if !usePlatformDefaultInterfaceMonitor { - networkMonitor, err := tun.NewNetworkUpdateMonitor(router.logger) - if !((err != nil && !enforceInterfaceMonitor) || errors.Is(err, os.ErrInvalid)) { - if err != nil { - return nil, err - } - router.networkMonitor = networkMonitor - interfaceMonitor, err := tun.NewDefaultInterfaceMonitor(router.networkMonitor, router.logger, tun.DefaultInterfaceMonitorOptions{ - InterfaceFinder: router.interfaceFinder, - OverrideAndroidVPN: options.OverrideAndroidVPN, - UnderNetworkExtension: router.platformInterface != nil && router.platformInterface.UnderNetworkExtension(), - }) - if err != nil { - return nil, E.New("auto_detect_interface unsupported on current platform") - } - interfaceMonitor.RegisterCallback(router.notifyNetworkUpdate) - router.interfaceMonitor = interfaceMonitor - } - } else { - interfaceMonitor := router.platformInterface.CreateDefaultInterfaceMonitor(router.logger) - interfaceMonitor.RegisterCallback(router.notifyNetworkUpdate) - router.interfaceMonitor = interfaceMonitor - } - if ntpOptions.Enabled { ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions) if err != nil { @@ -381,33 +310,10 @@ func NewRouter( return router, nil } -func (r *Router) Outbounds() []adapter.Outbound { - if !r.started { - return nil - } - return r.outbounds -} - func (r *Router) Start(stage adapter.StartStage) error { monitor := taskmonitor.New(r.logger, C.StartTimeout) switch stage { case adapter.StartStateInitialize: - if r.interfaceMonitor != nil { - monitor.Start("initialize interface monitor") - err := r.interfaceMonitor.Start() - monitor.Finish() - if err != nil { - return err - } - } - if r.networkMonitor != nil { - monitor.Start("initialize network monitor") - err := r.networkMonitor.Start() - monitor.Finish() - if err != nil { - return err - } - } if r.fakeIPStore != nil { monitor.Start("initialize fakeip store") err := r.fakeIPStore.Start() @@ -454,49 +360,10 @@ func (r *Router) Start(stage adapter.StartStage) error { r.geositeReader = nil } - if runtime.GOOS == "windows" { - powerListener, err := winpowrprof.NewEventListener(r.notifyWindowsPowerEvent) - if err == nil { - r.powerListener = powerListener - } else { - r.logger.Warn("initialize power listener: ", err) - } - } - - if r.powerListener != nil { - monitor.Start("start power listener") - err := r.powerListener.Start() - monitor.Finish() - if err != nil { - return E.Cause(err, "start power listener") - } - } - monitor.Start("initialize DNS client") r.dnsClient.Start() monitor.Finish() - if C.IsAndroid && r.platformInterface == nil { - monitor.Start("initialize package manager") - packageManager, err := tun.NewPackageManager(tun.PackageManagerOptions{ - Callback: r, - Logger: r.logger, - }) - monitor.Finish() - if err != nil { - return E.Cause(err, "create package manager") - } - if r.enforcePackageManager { - monitor.Start("start package manager") - err = packageManager.Start() - monitor.Finish() - if err != nil { - return E.Cause(err, "start package manager") - } - } - r.packageManager = packageManager - } - for i, rule := range r.dnsRules { monitor.Start("initialize DNS rule[", i, "]") err := rule.Start() @@ -549,26 +416,13 @@ func (r *Router) Start(stage adapter.StartStage) error { cacheContext.Close() } needFindProcess := r.needFindProcess - needWIFIState := r.needWIFIState for _, ruleSet := range r.ruleSets { metadata := ruleSet.Metadata() if metadata.ContainsProcessRule { needFindProcess = true } if metadata.ContainsWIFIRule { - needWIFIState = true - } - } - if C.IsAndroid && r.platformInterface == nil && !r.enforcePackageManager { - if needFindProcess { - monitor.Start("start package manager") - err := r.packageManager.Start() - monitor.Finish() - if err != nil { - return E.Cause(err, "start package manager") - } - } else { - r.packageManager = nil + r.needWIFIState = true } } if needFindProcess { @@ -578,7 +432,7 @@ func (r *Router) Start(stage adapter.StartStage) error { monitor.Start("initialize process searcher") searcher, err := process.NewSearcher(process.Config{ Logger: r.logger, - PackageManager: r.packageManager, + PackageManager: r.networkManager.PackageManager(), }) monitor.Finish() if err != nil { @@ -590,15 +444,6 @@ func (r *Router) Start(stage adapter.StartStage) error { } } } - if needWIFIState && r.platformInterface != nil { - monitor.Start("initialize WIFI state") - r.needWIFIState = true - r.interfaceMonitor.RegisterCallback(func(_ int) { - r.updateWIFIState() - }) - r.updateWIFIState() - monitor.Finish() - } for i, rule := range r.rules { monitor.Start("initialize rule[", i, "]") err := rule.Start() @@ -657,34 +502,6 @@ func (r *Router) Close() error { }) monitor.Finish() } - if r.interfaceMonitor != nil { - monitor.Start("close interface monitor") - err = E.Append(err, r.interfaceMonitor.Close(), func(err error) error { - return E.Cause(err, "close interface monitor") - }) - monitor.Finish() - } - if r.networkMonitor != nil { - monitor.Start("close network monitor") - err = E.Append(err, r.networkMonitor.Close(), func(err error) error { - return E.Cause(err, "close network monitor") - }) - monitor.Finish() - } - if r.packageManager != nil { - monitor.Start("close package manager") - err = E.Append(err, r.packageManager.Close(), func(err error) error { - return E.Cause(err, "close package manager") - }) - monitor.Finish() - } - if r.powerListener != nil { - monitor.Start("close power listener") - err = E.Append(err, r.powerListener.Close(), func(err error) error { - return E.Cause(err, "close power listener") - }) - monitor.Finish() - } if r.timeService != nil { monitor.Start("close time service") err = E.Append(err, r.timeService.Close(), func(err error) error { @@ -702,25 +519,6 @@ func (r *Router) Close() error { return err } -func (r *Router) Outbound(tag string) (adapter.Outbound, bool) { - outbound, loaded := r.outboundByTag[tag] - return outbound, loaded -} - -func (r *Router) DefaultOutbound(network string) (adapter.Outbound, error) { - if network == N.NetworkTCP { - if r.defaultOutboundForConnection == nil { - return nil, E.New("missing default outbound for TCP connections") - } - return r.defaultOutboundForConnection, nil - } else { - if r.defaultOutboundForPacketConnection == nil { - return nil, E.New("missing default outbound for UDP connections") - } - return r.defaultOutboundForPacketConnection, nil - } -} - func (r *Router) FakeIPStore() adapter.FakeIPStore { return r.fakeIPStore } @@ -734,96 +532,10 @@ func (r *Router) NeedWIFIState() bool { return r.needWIFIState } -func (r *Router) InterfaceFinder() control.InterfaceFinder { - return r.interfaceFinder -} - -func (r *Router) UpdateInterfaces() error { - if r.platformInterface == nil || !r.platformInterface.UsePlatformInterfaceGetter() { - return r.interfaceFinder.Update() - } else { - interfaces, err := r.platformInterface.Interfaces() - if err != nil { - return err - } - r.interfaceFinder.UpdateInterfaces(interfaces) - return nil - } -} - -func (r *Router) AutoDetectInterface() bool { - return r.autoDetectInterface -} - -func (r *Router) AutoDetectInterfaceFunc() control.Func { - if r.platformInterface != nil && r.platformInterface.UsePlatformAutoDetectInterfaceControl() { - return func(network, address string, conn syscall.RawConn) error { - return control.Raw(conn, func(fd uintptr) error { - return r.platformInterface.AutoDetectInterfaceControl(int(fd)) - }) - } - } else { - if r.interfaceMonitor == nil { - return nil - } - return control.BindToInterfaceFunc(r.InterfaceFinder(), func(network string, address string) (interfaceName string, interfaceIndex int, err error) { - remoteAddr := M.ParseSocksaddr(address).Addr - if C.IsLinux { - interfaceName, interfaceIndex = r.InterfaceMonitor().DefaultInterface(remoteAddr) - if interfaceIndex == -1 { - err = tun.ErrNoRoute - } - } else { - interfaceIndex = r.InterfaceMonitor().DefaultInterfaceIndex(remoteAddr) - if interfaceIndex == -1 { - err = tun.ErrNoRoute - } - } - return - }) - } -} - -func (r *Router) RegisterAutoRedirectOutputMark(mark uint32) error { - if r.autoRedirectOutputMark > 0 { - return E.New("only one auto-redirect can be configured") - } - r.autoRedirectOutputMark = mark - return nil -} - -func (r *Router) AutoRedirectOutputMark() uint32 { - return r.autoRedirectOutputMark -} - -func (r *Router) DefaultInterface() string { - return r.defaultInterface -} - -func (r *Router) DefaultMark() uint32 { - return r.defaultMark -} - func (r *Router) Rules() []adapter.Rule { return r.rules } -func (r *Router) WIFIState() adapter.WIFIState { - return r.wifiState -} - -func (r *Router) NetworkMonitor() tun.NetworkUpdateMonitor { - return r.networkMonitor -} - -func (r *Router) InterfaceMonitor() tun.DefaultInterfaceMonitor { - return r.interfaceMonitor -} - -func (r *Router) PackageManager() tun.PackageManager { - return r.packageManager -} - func (r *Router) ClashServer() adapter.ClashServer { return r.clashServer } @@ -840,10 +552,6 @@ func (r *Router) SetV2RayServer(server adapter.V2RayServer) { r.v2rayServer = server } -func (r *Router) OnPackagesUpdated(packages int, sharedUsers int) { - r.logger.Info("updated packages list: ", packages, " packages, ", sharedUsers, " shared users") -} - func (r *Router) NewError(ctx context.Context, err error) { common.Close(err) if E.IsClosedOrCanceled(err) { @@ -853,75 +561,9 @@ func (r *Router) NewError(ctx context.Context, err error) { r.logger.ErrorContext(ctx, err) } -func (r *Router) notifyNetworkUpdate(event int) { - if event == tun.EventNoRoute { - r.pauseManager.NetworkPause() - r.logger.Error("missing default interface") - } else { - r.pauseManager.NetworkWake() - if C.IsAndroid && r.platformInterface == nil { - var vpnStatus string - if r.interfaceMonitor.AndroidVPNEnabled() { - vpnStatus = "enabled" - } else { - vpnStatus = "disabled" - } - r.logger.Info("updated default interface ", r.interfaceMonitor.DefaultInterfaceName(netip.IPv4Unspecified()), ", index ", r.interfaceMonitor.DefaultInterfaceIndex(netip.IPv4Unspecified()), ", vpn ", vpnStatus) - } else { - r.logger.Info("updated default interface ", r.interfaceMonitor.DefaultInterfaceName(netip.IPv4Unspecified()), ", index ", r.interfaceMonitor.DefaultInterfaceIndex(netip.IPv4Unspecified())) - } - } - - if !r.started { - return - } - - _ = r.ResetNetwork() -} - -func (r *Router) ResetNetwork() error { - conntrack.Close() - - for _, outbound := range r.outbounds { - listener, isListener := outbound.(adapter.InterfaceUpdateListener) - if isListener { - listener.InterfaceUpdated() - } - } - +func (r *Router) ResetNetwork() { + r.networkManager.ResetNetwork() for _, transport := range r.transports { transport.Reset() } - return nil -} - -func (r *Router) updateWIFIState() { - if r.platformInterface == nil { - return - } - state := r.platformInterface.ReadWIFIState() - if state != r.wifiState { - r.wifiState = state - if state.SSID == "" && state.BSSID == "" { - r.logger.Info("updated WIFI state: disconnected") - } else { - r.logger.Info("updated WIFI state: SSID=", state.SSID, ", BSSID=", state.BSSID) - } - } -} - -func (r *Router) notifyWindowsPowerEvent(event int) { - switch event { - case winpowrprof.EVENT_SUSPEND: - r.pauseManager.DevicePause() - _ = r.ResetNetwork() - case winpowrprof.EVENT_RESUME: - if !r.pauseManager.IsDevicePaused() { - return - } - fallthrough - case winpowrprof.EVENT_RESUME_AUTOMATIC: - r.pauseManager.DeviceWake() - _ = r.ResetNetwork() - } } diff --git a/route/rule/rule_default.go b/route/rule/rule_default.go index a12c63ef85..33a8e16c35 100644 --- a/route/rule/rule_default.go +++ b/route/rule/rule_default.go @@ -9,9 +9,10 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/service" ) -func NewRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.Rule, checkOutbound bool) (adapter.Rule, error) { +func NewRule(ctx context.Context, logger log.ContextLogger, options option.Rule, checkOutbound bool) (adapter.Rule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { @@ -23,7 +24,7 @@ func NewRule(ctx context.Context, router adapter.Router, logger log.ContextLogge return nil, E.New("missing outbound field") } } - return NewDefaultRule(ctx, router, logger, options.DefaultOptions) + return NewDefaultRule(ctx, logger, options.DefaultOptions) case C.RuleTypeLogical: if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") @@ -34,7 +35,7 @@ func NewRule(ctx context.Context, router adapter.Router, logger log.ContextLogge return nil, E.New("missing outbound field") } } - return NewLogicalRule(ctx, router, logger, options.LogicalOptions) + return NewLogicalRule(ctx, logger, options.LogicalOptions) default: return nil, E.New("unknown rule type: ", options.Type) } @@ -51,7 +52,7 @@ type RuleItem interface { String() string } -func NewDefaultRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.DefaultRule) (*DefaultRule, error) { +func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options option.DefaultRule) (*DefaultRule, error) { action, err := NewRuleAction(ctx, logger, options.RuleAction) if err != nil { return nil, E.Cause(err, "action") @@ -62,6 +63,8 @@ func NewDefaultRule(ctx context.Context, router adapter.Router, logger log.Conte action: action, }, } + router := service.FromContext[adapter.Router](ctx) + networkManager := service.FromContext[adapter.NetworkManager](ctx) if len(options.Inbound) > 0 { item := NewInboundRule(options.Inbound) rule.items = append(rule.items, item) @@ -221,12 +224,12 @@ func NewDefaultRule(ctx context.Context, router adapter.Router, logger log.Conte rule.allItems = append(rule.allItems, item) } if len(options.WIFISSID) > 0 { - item := NewWIFISSIDItem(router, options.WIFISSID) + item := NewWIFISSIDItem(networkManager, options.WIFISSID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.WIFIBSSID) > 0 { - item := NewWIFIBSSIDItem(router, options.WIFIBSSID) + item := NewWIFIBSSIDItem(networkManager, options.WIFIBSSID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } @@ -253,7 +256,7 @@ type LogicalRule struct { abstractLogicalRule } -func NewLogicalRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { +func NewLogicalRule(ctx context.Context, logger log.ContextLogger, options option.LogicalRule) (*LogicalRule, error) { action, err := NewRuleAction(ctx, logger, options.RuleAction) if err != nil { return nil, E.Cause(err, "action") @@ -274,7 +277,7 @@ func NewLogicalRule(ctx context.Context, router adapter.Router, logger log.Conte return nil, E.New("unknown logical mode: ", options.Mode) } for i, subOptions := range options.Rules { - subRule, err := NewRule(ctx, router, logger, subOptions, false) + subRule, err := NewRule(ctx, logger, subOptions, false) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go index 2218f6a30c..df5f3f335c 100644 --- a/route/rule/rule_dns.go +++ b/route/rule/rule_dns.go @@ -10,9 +10,10 @@ import ( "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/service" ) -func NewDNSRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.DNSRule, checkServer bool) (adapter.DNSRule, error) { +func NewDNSRule(ctx context.Context, logger log.ContextLogger, options option.DNSRule, checkServer bool) (adapter.DNSRule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { @@ -24,7 +25,7 @@ func NewDNSRule(ctx context.Context, router adapter.Router, logger log.ContextLo return nil, E.New("missing server field") } } - return NewDefaultDNSRule(ctx, router, logger, options.DefaultOptions) + return NewDefaultDNSRule(ctx, logger, options.DefaultOptions) case C.RuleTypeLogical: if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") @@ -35,7 +36,7 @@ func NewDNSRule(ctx context.Context, router adapter.Router, logger log.ContextLo return nil, E.New("missing server field") } } - return NewLogicalDNSRule(ctx, router, logger, options.LogicalOptions) + return NewLogicalDNSRule(ctx, logger, options.LogicalOptions) default: return nil, E.New("unknown rule type: ", options.Type) } @@ -47,7 +48,7 @@ type DefaultDNSRule struct { abstractDefaultRule } -func NewDefaultDNSRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) { +func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) { rule := &DefaultDNSRule{ abstractDefaultRule: abstractDefaultRule{ invert: options.Invert, @@ -59,6 +60,8 @@ func NewDefaultDNSRule(ctx context.Context, router adapter.Router, logger log.Co rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + router := service.FromContext[adapter.Router](ctx) + networkManager := service.FromContext[adapter.NetworkManager](ctx) if options.IPVersion > 0 { switch options.IPVersion { case 4, 6: @@ -218,12 +221,12 @@ func NewDefaultDNSRule(ctx context.Context, router adapter.Router, logger log.Co rule.allItems = append(rule.allItems, item) } if len(options.WIFISSID) > 0 { - item := NewWIFISSIDItem(router, options.WIFISSID) + item := NewWIFISSIDItem(networkManager, options.WIFISSID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } if len(options.WIFIBSSID) > 0 { - item := NewWIFIBSSIDItem(router, options.WIFIBSSID) + item := NewWIFIBSSIDItem(networkManager, options.WIFIBSSID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } @@ -282,7 +285,7 @@ type LogicalDNSRule struct { abstractLogicalRule } -func NewLogicalDNSRule(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { +func NewLogicalDNSRule(ctx context.Context, logger log.ContextLogger, options option.LogicalDNSRule) (*LogicalDNSRule, error) { r := &LogicalDNSRule{ abstractLogicalRule: abstractLogicalRule{ rules: make([]adapter.HeadlessRule, len(options.Rules)), @@ -299,7 +302,7 @@ func NewLogicalDNSRule(ctx context.Context, router adapter.Router, logger log.Co return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewDNSRule(ctx, router, logger, subRule, false) + rule, err := NewDNSRule(ctx, logger, subRule, false) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } diff --git a/route/rule/rule_headless.go b/route/rule/rule_headless.go index 9ea357af8c..99488b2049 100644 --- a/route/rule/rule_headless.go +++ b/route/rule/rule_headless.go @@ -1,24 +1,27 @@ package rule import ( + "context" + "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/service" ) -func NewHeadlessRule(router adapter.Router, options option.HeadlessRule) (adapter.HeadlessRule, error) { +func NewHeadlessRule(ctx context.Context, options option.HeadlessRule) (adapter.HeadlessRule, error) { switch options.Type { case "", C.RuleTypeDefault: if !options.DefaultOptions.IsValid() { return nil, E.New("missing conditions") } - return NewDefaultHeadlessRule(router, options.DefaultOptions) + return NewDefaultHeadlessRule(ctx, options.DefaultOptions) case C.RuleTypeLogical: if !options.LogicalOptions.IsValid() { return nil, E.New("missing conditions") } - return NewLogicalHeadlessRule(router, options.LogicalOptions) + return NewLogicalHeadlessRule(ctx, options.LogicalOptions) default: return nil, E.New("unknown rule type: ", options.Type) } @@ -30,7 +33,8 @@ type DefaultHeadlessRule struct { abstractDefaultRule } -func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) { +func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessRule) (*DefaultHeadlessRule, error) { + networkManager := service.FromContext[adapter.NetworkManager](ctx) rule := &DefaultHeadlessRule{ abstractDefaultRule{ invert: options.Invert, @@ -137,15 +141,15 @@ func NewDefaultHeadlessRule(router adapter.Router, options option.DefaultHeadles rule.allItems = append(rule.allItems, item) } if len(options.WIFISSID) > 0 { - if router != nil { - item := NewWIFISSIDItem(router, options.WIFISSID) + if networkManager != nil { + item := NewWIFISSIDItem(networkManager, options.WIFISSID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } } if len(options.WIFIBSSID) > 0 { - if router != nil { - item := NewWIFIBSSIDItem(router, options.WIFIBSSID) + if networkManager != nil { + item := NewWIFIBSSIDItem(networkManager, options.WIFIBSSID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } @@ -168,7 +172,7 @@ type LogicalHeadlessRule struct { abstractLogicalRule } -func NewLogicalHeadlessRule(router adapter.Router, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) { +func NewLogicalHeadlessRule(ctx context.Context, options option.LogicalHeadlessRule) (*LogicalHeadlessRule, error) { r := &LogicalHeadlessRule{ abstractLogicalRule{ rules: make([]adapter.HeadlessRule, len(options.Rules)), @@ -184,7 +188,7 @@ func NewLogicalHeadlessRule(router adapter.Router, options option.LogicalHeadles return nil, E.New("unknown logical mode: ", options.Mode) } for i, subRule := range options.Rules { - rule, err := NewHeadlessRule(router, subRule) + rule, err := NewHeadlessRule(ctx, subRule) if err != nil { return nil, E.Cause(err, "sub rule[", i, "]") } diff --git a/route/rule/rule_item_wifi_bssid.go b/route/rule/rule_item_wifi_bssid.go index ae94bd6df2..8f887322e5 100644 --- a/route/rule/rule_item_wifi_bssid.go +++ b/route/rule/rule_item_wifi_bssid.go @@ -10,12 +10,12 @@ import ( var _ RuleItem = (*WIFIBSSIDItem)(nil) type WIFIBSSIDItem struct { - bssidList []string - bssidMap map[string]bool - router adapter.Router + bssidList []string + bssidMap map[string]bool + networkManager adapter.NetworkManager } -func NewWIFIBSSIDItem(router adapter.Router, bssidList []string) *WIFIBSSIDItem { +func NewWIFIBSSIDItem(networkManager adapter.NetworkManager, bssidList []string) *WIFIBSSIDItem { bssidMap := make(map[string]bool) for _, bssid := range bssidList { bssidMap[bssid] = true @@ -23,12 +23,12 @@ func NewWIFIBSSIDItem(router adapter.Router, bssidList []string) *WIFIBSSIDItem return &WIFIBSSIDItem{ bssidList, bssidMap, - router, + networkManager, } } func (r *WIFIBSSIDItem) Match(metadata *adapter.InboundContext) bool { - return r.bssidMap[r.router.WIFIState().BSSID] + return r.bssidMap[r.networkManager.WIFIState().BSSID] } func (r *WIFIBSSIDItem) String() string { diff --git a/route/rule/rule_item_wifi_ssid.go b/route/rule/rule_item_wifi_ssid.go index 3a928f77e5..ab9fdd884c 100644 --- a/route/rule/rule_item_wifi_ssid.go +++ b/route/rule/rule_item_wifi_ssid.go @@ -10,12 +10,12 @@ import ( var _ RuleItem = (*WIFISSIDItem)(nil) type WIFISSIDItem struct { - ssidList []string - ssidMap map[string]bool - router adapter.Router + ssidList []string + ssidMap map[string]bool + networkManager adapter.NetworkManager } -func NewWIFISSIDItem(router adapter.Router, ssidList []string) *WIFISSIDItem { +func NewWIFISSIDItem(networkManager adapter.NetworkManager, ssidList []string) *WIFISSIDItem { ssidMap := make(map[string]bool) for _, ssid := range ssidList { ssidMap[ssid] = true @@ -23,12 +23,12 @@ func NewWIFISSIDItem(router adapter.Router, ssidList []string) *WIFISSIDItem { return &WIFISSIDItem{ ssidList, ssidMap, - router, + networkManager, } } func (r *WIFISSIDItem) Match(metadata *adapter.InboundContext) bool { - return r.ssidMap[r.router.WIFIState().SSID] + return r.ssidMap[r.networkManager.WIFIState().SSID] } func (r *WIFISSIDItem) String() string { diff --git a/route/rule/rule_set.go b/route/rule/rule_set.go index cdd0fc0af2..c0f307b764 100644 --- a/route/rule/rule_set.go +++ b/route/rule/rule_set.go @@ -13,12 +13,12 @@ import ( "go4.org/netipx" ) -func NewRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { +func NewRuleSet(ctx context.Context, logger logger.ContextLogger, options option.RuleSet) (adapter.RuleSet, error) { switch options.Type { case C.RuleSetTypeInline, C.RuleSetTypeLocal, "": - return NewLocalRuleSet(ctx, router, logger, options) + return NewLocalRuleSet(ctx, logger, options) case C.RuleSetTypeRemote: - return NewRemoteRuleSet(ctx, router, logger, options), nil + return NewRemoteRuleSet(ctx, logger, options), nil default: return nil, E.New("unknown rule-set type: ", options.Type) } diff --git a/route/rule/rule_set_local.go b/route/rule/rule_set_local.go index 9186454e68..efbc525ee6 100644 --- a/route/rule/rule_set_local.go +++ b/route/rule/rule_set_local.go @@ -26,7 +26,7 @@ import ( var _ adapter.RuleSet = (*LocalRuleSet)(nil) type LocalRuleSet struct { - router adapter.Router + ctx context.Context logger logger.Logger tag string rules []adapter.HeadlessRule @@ -36,9 +36,9 @@ type LocalRuleSet struct { refs atomic.Int32 } -func NewLocalRuleSet(ctx context.Context, router adapter.Router, logger logger.Logger, options option.RuleSet) (*LocalRuleSet, error) { +func NewLocalRuleSet(ctx context.Context, logger logger.Logger, options option.RuleSet) (*LocalRuleSet, error) { ruleSet := &LocalRuleSet{ - router: router, + ctx: ctx, logger: logger, tag: options.Tag, fileFormat: options.Format, @@ -130,7 +130,7 @@ func (s *LocalRuleSet) reloadRules(headlessRules []option.HeadlessRule) error { rules := make([]adapter.HeadlessRule, len(headlessRules)) var err error for i, ruleOptions := range headlessRules { - rules[i], err = NewHeadlessRule(s.router, ruleOptions) + rules[i], err = NewHeadlessRule(s.ctx, ruleOptions) if err != nil { return E.Cause(err, "parse rule_set.rules.[", i, "]") } diff --git a/route/rule/rule_set_remote.go b/route/rule/rule_set_remote.go index fb58169140..830e19f7cf 100644 --- a/route/rule/rule_set_remote.go +++ b/route/rule/rule_set_remote.go @@ -35,7 +35,6 @@ var _ adapter.RuleSet = (*RemoteRuleSet)(nil) type RemoteRuleSet struct { ctx context.Context cancel context.CancelFunc - router adapter.Router outboundManager adapter.OutboundManager logger logger.ContextLogger options option.RuleSet @@ -53,7 +52,7 @@ type RemoteRuleSet struct { refs atomic.Int32 } -func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { +func NewRemoteRuleSet(ctx context.Context, logger logger.ContextLogger, options option.RuleSet) *RemoteRuleSet { ctx, cancel := context.WithCancel(ctx) var updateInterval time.Duration if options.RemoteOptions.UpdateInterval > 0 { @@ -64,7 +63,6 @@ func NewRemoteRuleSet(ctx context.Context, router adapter.Router, logger logger. return &RemoteRuleSet{ ctx: ctx, cancel: cancel, - router: router, outboundManager: service.FromContext[adapter.OutboundManager](ctx), logger: logger, options: options, @@ -180,7 +178,7 @@ func (s *RemoteRuleSet) loadBytes(content []byte) error { } rules := make([]adapter.HeadlessRule, len(plainRuleSet.Rules)) for i, ruleOptions := range plainRuleSet.Rules { - rules[i], err = NewHeadlessRule(s.router, ruleOptions) + rules[i], err = NewHeadlessRule(s.ctx, ruleOptions) if err != nil { return E.Cause(err, "parse rule_set.rules.[", i, "]") } diff --git a/transport/dhcp/server.go b/transport/dhcp/server.go index d5603e9ed5..dfe33d86c2 100644 --- a/transport/dhcp/server.go +++ b/transport/dhcp/server.go @@ -38,6 +38,7 @@ func init() { type Transport struct { options dns.TransportOptions router adapter.Router + networkManager adapter.NetworkManager interfaceName string autoInterface bool interfaceCallback *list.Element[tun.DefaultInterfaceUpdateCallback] @@ -54,15 +55,11 @@ func NewTransport(options dns.TransportOptions) (*Transport, error) { if linkURL.Host == "" { return nil, E.New("missing interface name for DHCP") } - router := service.FromContext[adapter.Router](options.Context) - if router == nil { - return nil, E.New("missing router in context") - } transport := &Transport{ - options: options, - router: router, - interfaceName: linkURL.Host, - autoInterface: linkURL.Host == "auto", + options: options, + networkManager: service.FromContext[adapter.NetworkManager](options.Context), + interfaceName: linkURL.Host, + autoInterface: linkURL.Host == "auto", } return transport, nil } @@ -77,7 +74,7 @@ func (t *Transport) Start() error { return err } if t.autoInterface { - t.interfaceCallback = t.router.InterfaceMonitor().RegisterCallback(t.interfaceUpdated) + t.interfaceCallback = t.networkManager.InterfaceMonitor().RegisterCallback(t.interfaceUpdated) } return nil } @@ -93,7 +90,7 @@ func (t *Transport) Close() error { transport.Close() } if t.interfaceCallback != nil { - t.router.InterfaceMonitor().UnregisterCallback(t.interfaceCallback) + t.networkManager.InterfaceMonitor().UnregisterCallback(t.interfaceCallback) } return nil } @@ -125,10 +122,10 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, func (t *Transport) fetchInterface() (*net.Interface, error) { interfaceName := t.interfaceName if t.autoInterface { - if t.router.InterfaceMonitor() == nil { + if t.networkManager.InterfaceMonitor() == nil { return nil, E.New("missing monitor for auto DHCP, set route.auto_detect_interface") } - interfaceName = t.router.InterfaceMonitor().DefaultInterfaceName(netip.Addr{}) + interfaceName = t.networkManager.InterfaceMonitor().DefaultInterfaceName(netip.Addr{}) } if interfaceName == "" { return nil, E.New("missing default interface") @@ -177,7 +174,7 @@ func (t *Transport) interfaceUpdated(int) { func (t *Transport) fetchServers0(ctx context.Context, iface *net.Interface) error { var listener net.ListenConfig - listener.Control = control.Append(listener.Control, control.BindToInterface(t.router.InterfaceFinder(), iface.Name, iface.Index)) + listener.Control = control.Append(listener.Control, control.BindToInterface(t.networkManager.InterfaceFinder(), iface.Name, iface.Index)) listener.Control = control.Append(listener.Control, control.ReuseAddr()) listenAddr := "0.0.0.0:68" if runtime.GOOS == "linux" || runtime.GOOS == "android" { @@ -255,7 +252,7 @@ func (t *Transport) recreateServers(iface *net.Interface, serverAddrs []netip.Ad return it.String() }), ","), "]") } - serverDialer := common.Must1(dialer.NewDefault(t.router, option.DialerOptions{ + serverDialer := common.Must1(dialer.NewDefault(t.networkManager, option.DialerOptions{ BindInterface: iface.Name, UDPFragmentDefault: true, })) diff --git a/transport/wireguard/device_system.go b/transport/wireguard/device_system.go index 36c270f9d3..8a54a75ef9 100644 --- a/transport/wireguard/device_system.go +++ b/transport/wireguard/device_system.go @@ -34,7 +34,7 @@ type SystemDevice struct { closeOnce sync.Once } -func NewSystemDevice(router adapter.Router, interfaceName string, localPrefixes []netip.Prefix, mtu uint32, gso bool) (*SystemDevice, error) { +func NewSystemDevice(networkManager adapter.NetworkManager, interfaceName string, localPrefixes []netip.Prefix, mtu uint32, gso bool) (*SystemDevice, error) { var inet4Addresses []netip.Prefix var inet6Addresses []netip.Prefix for _, prefixes := range localPrefixes { @@ -49,7 +49,7 @@ func NewSystemDevice(router adapter.Router, interfaceName string, localPrefixes } return &SystemDevice{ - dialer: common.Must1(dialer.NewDefault(router, option.DialerOptions{ + dialer: common.Must1(dialer.NewDefault(networkManager, option.DialerOptions{ BindInterface: interfaceName, })), name: interfaceName, From daf38a84e14a1b29ef9fb174629b78fff3d037ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 10 Nov 2024 16:46:59 +0800 Subject: [PATCH 16/49] refactor: Extract services form router --- adapter/experimental.go | 30 +-- adapter/inbound.go | 2 +- adapter/inbound/manager.go | 4 +- adapter/lifecycle.go | 35 ++- adapter/lifecycle_legacy.go | 34 ++- adapter/network.go | 2 +- adapter/outbound.go | 2 +- adapter/outbound/manager.go | 4 +- adapter/router.go | 13 +- box.go | 240 +++++++----------- experimental/cachefile/cache.go | 21 +- experimental/clashapi/server.go | 81 +++--- .../clashapi/trafficontrol/tracker.go | 34 +-- experimental/libbox/command_clash_mode.go | 14 +- .../libbox/command_close_connection.go | 6 +- experimental/libbox/command_connections.go | 6 +- experimental/libbox/command_server.go | 2 +- experimental/libbox/command_status.go | 10 +- experimental/libbox/service.go | 8 +- experimental/v2rayapi/server.go | 11 +- experimental/v2rayapi/stats.go | 12 +- protocol/group/urltest.go | 29 +-- route/route.go | 77 ++---- route/router.go | 66 +---- route/rule/rule_default.go | 2 +- route/rule/rule_dns.go | 2 +- route/rule/rule_item_clash_mode.go | 23 +- 27 files changed, 310 insertions(+), 460 deletions(-) diff --git a/adapter/experimental.go b/adapter/experimental.go index bee24c4f8a..f22ff9b22a 100644 --- a/adapter/experimental.go +++ b/adapter/experimental.go @@ -4,28 +4,28 @@ import ( "bytes" "context" "encoding/binary" - "net" "time" "github.com/sagernet/sing-box/common/urltest" "github.com/sagernet/sing-dns" - N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/varbin" ) type ClashServer interface { - Service - LegacyPreStarter + LifecycleService + ConnectionTracker Mode() string ModeList() []string HistoryStorage() *urltest.HistoryStorage - RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker) - RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker) +} + +type V2RayServer interface { + LifecycleService + StatsService() ConnectionTracker } type CacheFile interface { - Service - LegacyPreStarter + LifecycleService StoreFakeIP() bool FakeIPStorage @@ -94,10 +94,6 @@ func (s *SavedRuleSet) UnmarshalBinary(data []byte) error { return nil } -type Tracker interface { - Leave() -} - type OutboundGroup interface { Outbound Now() string @@ -115,13 +111,3 @@ func OutboundTag(detour Outbound) string { } return detour.Tag() } - -type V2RayServer interface { - Service - StatsService() V2RayStatsService -} - -type V2RayStatsService interface { - RoutedConnection(inbound string, outbound string, user string, conn net.Conn) net.Conn - RoutedPacketConnection(inbound string, outbound string, user string, conn N.PacketConn) N.PacketConn -} diff --git a/adapter/inbound.go b/adapter/inbound.go index d80e59f7b9..7932237d03 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -32,7 +32,7 @@ type InboundRegistry interface { } type InboundManager interface { - NewService + Lifecycle Inbounds() []Inbound Get(tag string) (Inbound, bool) Remove(tag string) error diff --git a/adapter/inbound/manager.go b/adapter/inbound/manager.go index 69a3ad46b0..d2be0f365e 100644 --- a/adapter/inbound/manager.go +++ b/adapter/inbound/manager.go @@ -44,7 +44,7 @@ func (m *Manager) Start(stage adapter.StartStage) error { for _, inbound := range m.inbounds { err := adapter.LegacyStart(inbound, stage) if err != nil { - return E.Cause(err, stage.Action(), " inbound/", inbound.Type(), "[", inbound.Tag(), "]") + return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]") } } return nil @@ -118,7 +118,7 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log. for _, stage := range adapter.ListStartStages { err = adapter.LegacyStart(inbound, stage) if err != nil { - return E.Cause(err, stage.Action(), " inbound/", inbound.Type(), "[", inbound.Tag(), "]") + return E.Cause(err, stage, " inbound/", inbound.Type(), "[", inbound.Tag(), "]") } } } diff --git a/adapter/lifecycle.go b/adapter/lifecycle.go index 85de425d5a..aff9fadbac 100644 --- a/adapter/lifecycle.go +++ b/adapter/lifecycle.go @@ -1,5 +1,7 @@ package adapter +import E "github.com/sagernet/sing/common/exceptions" + type StartStage uint8 const ( @@ -16,7 +18,7 @@ var ListStartStages = []StartStage{ StartStateStarted, } -func (s StartStage) Action() string { +func (s StartStage) String() string { switch s { case StartStateInitialize: return "initialize" @@ -25,17 +27,38 @@ func (s StartStage) Action() string { case StartStatePostStart: return "post-start" case StartStateStarted: - return "start-after-started" + return "finish-start" default: panic("unknown stage") } } -type NewService interface { - NewStarter +type Lifecycle interface { + Start(stage StartStage) error Close() error } -type NewStarter interface { - Start(stage StartStage) error +type LifecycleService interface { + Name() string + Lifecycle +} + +func Start(stage StartStage, services ...Lifecycle) error { + for _, service := range services { + err := service.Start(stage) + if err != nil { + return err + } + } + return nil +} + +func StartNamed(stage StartStage, services []LifecycleService) error { + for _, service := range services { + err := service.Start(stage) + if err != nil { + return E.Cause(err, stage.String(), " ", service.Name()) + } + } + return nil } diff --git a/adapter/lifecycle_legacy.go b/adapter/lifecycle_legacy.go index 5968131bb4..0c8c75daed 100644 --- a/adapter/lifecycle_legacy.go +++ b/adapter/lifecycle_legacy.go @@ -1,13 +1,5 @@ package adapter -type LegacyPreStarter interface { - PreStart() error -} - -type LegacyPostStarter interface { - PostStart() error -} - func LegacyStart(starter any, stage StartStage) error { switch stage { case StartStateInitialize: @@ -22,7 +14,7 @@ func LegacyStart(starter any, stage StartStage) error { }); isStarter { return starter.Start() } - case StartStatePostStart: + case StartStateStarted: if postStarter, isPostStarter := starter.(interface { PostStart() error }); isPostStarter { @@ -31,3 +23,27 @@ func LegacyStart(starter any, stage StartStage) error { } return nil } + +type lifecycleServiceWrapper struct { + Service + name string +} + +func NewLifecycleService(service Service, name string) LifecycleService { + return &lifecycleServiceWrapper{ + Service: service, + name: name, + } +} + +func (l *lifecycleServiceWrapper) Name() string { + return l.name +} + +func (l *lifecycleServiceWrapper) Start(stage StartStage) error { + return LegacyStart(l.Service, stage) +} + +func (l *lifecycleServiceWrapper) Close() error { + return l.Service.Close() +} diff --git a/adapter/network.go b/adapter/network.go index 0ce2741185..533bfced22 100644 --- a/adapter/network.go +++ b/adapter/network.go @@ -6,7 +6,7 @@ import ( ) type NetworkManager interface { - NewService + Lifecycle InterfaceFinder() control.InterfaceFinder UpdateInterfaces() error DefaultInterface() string diff --git a/adapter/outbound.go b/adapter/outbound.go index b170398a96..2c2b109100 100644 --- a/adapter/outbound.go +++ b/adapter/outbound.go @@ -24,7 +24,7 @@ type OutboundRegistry interface { } type OutboundManager interface { - NewService + Lifecycle Outbounds() []Outbound Outbound(tag string) (Outbound, bool) Default() Outbound diff --git a/adapter/outbound/manager.go b/adapter/outbound/manager.go index 10a89a1c9b..84a105c59e 100644 --- a/adapter/outbound/manager.go +++ b/adapter/outbound/manager.go @@ -61,7 +61,7 @@ func (m *Manager) Start(stage adapter.StartStage) error { for _, outbound := range outbounds { err := adapter.LegacyStart(outbound, stage) if err != nil { - return E.Cause(err, stage.Action(), " outbound/", outbound.Type(), "[", outbound.Tag(), "]") + return E.Cause(err, stage, " outbound/", outbound.Type(), "[", outbound.Tag(), "]") } } } @@ -234,7 +234,7 @@ func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log. for _, stage := range adapter.ListStartStages { err = adapter.LegacyStart(outbound, stage) if err != nil { - return E.Cause(err, stage.Action(), " outbound/", outbound.Type(), "[", outbound.Tag(), "]") + return E.Cause(err, stage, " outbound/", outbound.Type(), "[", outbound.Tag(), "]") } } } diff --git a/adapter/router.go b/adapter/router.go index 40a461a79e..6dd39357b4 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -19,7 +19,7 @@ import ( ) type Router interface { - NewService + Lifecycle FakeIPStore() FakeIPStore @@ -38,15 +38,16 @@ type Router interface { ClearDNSCache() Rules() []Rule - ClashServer() ClashServer - SetClashServer(server ClashServer) - - V2RayServer() V2RayServer - SetV2RayServer(server V2RayServer) + SetTracker(tracker ConnectionTracker) ResetNetwork() } +type ConnectionTracker interface { + RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) net.Conn + RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule, matchOutbound Outbound) N.PacketConn +} + // Deprecated: Use ConnectionRouterEx instead. type ConnectionRouter interface { RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error diff --git a/box.go b/box.go index 8eac0dfa6c..3b69617f64 100644 --- a/box.go +++ b/box.go @@ -11,6 +11,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/outbound" + "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/taskmonitor" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental" @@ -23,6 +24,7 @@ import ( "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" ) @@ -30,17 +32,15 @@ import ( var _ adapter.Service = (*Box)(nil) type Box struct { - createdAt time.Time - router adapter.Router - inbound *inbound.Manager - outbound *outbound.Manager - network *route.NetworkManager - logFactory log.Factory - logger log.ContextLogger - preServices1 map[string]adapter.Service - preServices2 map[string]adapter.Service - postServices map[string]adapter.Service - done chan struct{} + createdAt time.Time + logFactory log.Factory + logger log.ContextLogger + network *route.NetworkManager + router *route.Router + inbound *inbound.Manager + outbound *outbound.Manager + services []adapter.LifecycleService + done chan struct{} } type Options struct { @@ -49,7 +49,11 @@ type Options struct { PlatformLogWriter log.PlatformWriter } -func Context(ctx context.Context, inboundRegistry adapter.InboundRegistry, outboundRegistry adapter.OutboundRegistry) context.Context { +func Context( + ctx context.Context, + inboundRegistry adapter.InboundRegistry, + outboundRegistry adapter.OutboundRegistry, +) context.Context { if service.FromContext[option.InboundOptionsRegistry](ctx) == nil || service.FromContext[adapter.InboundRegistry](ctx) == nil { ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry) @@ -70,14 +74,17 @@ func New(options Options) (*Box, error) { ctx = context.Background() } ctx = service.ContextWithDefaultRegistry(ctx) + inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx) if inboundRegistry == nil { return nil, E.New("missing inbound registry in context") } + outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx) if outboundRegistry == nil { return nil, E.New("missing outbound registry in context") } + ctx = pause.WithDefaultManager(ctx) experimentalOptions := common.PtrValueOrDefault(options.Experimental) applyDebugOptions(common.PtrValueOrDefault(experimentalOptions.Debug)) @@ -109,17 +116,19 @@ func New(options Options) (*Box, error) { if err != nil { return nil, E.Cause(err, "create log factory") } + routeOptions := common.PtrValueOrDefault(options.Route) inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry) outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, routeOptions.Final) - ctx = service.ContextWith[adapter.InboundManager](ctx, inboundManager) - ctx = service.ContextWith[adapter.OutboundManager](ctx, outboundManager) + service.MustRegister[adapter.InboundManager](ctx, inboundManager) + service.MustRegister[adapter.OutboundManager](ctx, outboundManager) + networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions) if err != nil { return nil, E.Cause(err, "initialize network manager") } - ctx = service.ContextWith[adapter.NetworkManager](ctx, networkManager) - router, err := route.NewRouter(ctx, logFactory, routeOptions, common.PtrValueOrDefault(options.DNS), common.PtrValueOrDefault(options.NTP)) + service.MustRegister[adapter.NetworkManager](ctx, networkManager) + router, err := route.NewRouter(ctx, logFactory, routeOptions, common.PtrValueOrDefault(options.DNS)) if err != nil { return nil, E.Cause(err, "initialize router") } @@ -182,47 +191,61 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "initialize platform interface") } } - preServices1 := make(map[string]adapter.Service) - preServices2 := make(map[string]adapter.Service) - postServices := make(map[string]adapter.Service) + var services []adapter.LifecycleService if needCacheFile { - cacheFile := service.FromContext[adapter.CacheFile](ctx) - if cacheFile == nil { - cacheFile = cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) - service.MustRegister[adapter.CacheFile](ctx, cacheFile) - } - preServices1["cache file"] = cacheFile + cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile)) + service.MustRegister[adapter.CacheFile](ctx, cacheFile) + services = append(services, cacheFile) } if needClashAPI { clashAPIOptions := common.PtrValueOrDefault(experimentalOptions.ClashAPI) clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options) clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions) if err != nil { - return nil, E.Cause(err, "create clash api server") + return nil, E.Cause(err, "create clash-server") } - router.SetClashServer(clashServer) - preServices2["clash api"] = clashServer + router.SetTracker(clashServer) + service.MustRegister[adapter.ClashServer](ctx, clashServer) + services = append(services, clashServer) } if needV2RayAPI { v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(experimentalOptions.V2RayAPI)) if err != nil { - return nil, E.Cause(err, "create v2ray api server") + return nil, E.Cause(err, "create v2ray-server") + } + if v2rayServer.StatsService() != nil { + router.SetTracker(v2rayServer.StatsService()) + services = append(services, v2rayServer) + service.MustRegister[adapter.V2RayServer](ctx, v2rayServer) + } + } + ntpOptions := common.PtrValueOrDefault(options.NTP) + if ntpOptions.Enabled { + ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions) + if err != nil { + return nil, E.Cause(err, "create NTP service") } - router.SetV2RayServer(v2rayServer) - preServices2["v2ray api"] = v2rayServer + timeService := ntp.NewService(ntp.Options{ + Context: ctx, + Dialer: ntpDialer, + Logger: logFactory.NewLogger("ntp"), + Server: ntpOptions.ServerOptions.Build(), + Interval: time.Duration(ntpOptions.Interval), + WriteToSystem: ntpOptions.WriteToSystem, + }) + service.MustRegister[ntp.TimeService](ctx, timeService) + services = append(services, adapter.NewLifecycleService(timeService, "ntp service")) } return &Box{ - router: router, - inbound: inboundManager, - outbound: outboundManager, - network: networkManager, - createdAt: createdAt, - logFactory: logFactory, - logger: logFactory.Logger(), - preServices1: preServices1, - preServices2: preServices2, - postServices: postServices, - done: make(chan struct{}), + network: networkManager, + router: router, + inbound: inboundManager, + outbound: outboundManager, + createdAt: createdAt, + logFactory: logFactory, + logger: logFactory.Logger(), + services: services, + done: make(chan struct{}), }, nil } @@ -272,43 +295,19 @@ func (s *Box) preStart() error { if err != nil { return E.Cause(err, "start logger") } - for serviceName, service := range s.preServices1 { - if preService, isPreService := service.(adapter.LegacyPreStarter); isPreService { - monitor.Start("pre-start ", serviceName) - err := preService.PreStart() - monitor.Finish() - if err != nil { - return E.Cause(err, "pre-start ", serviceName) - } - } - } - for serviceName, service := range s.preServices2 { - if preService, isPreService := service.(adapter.LegacyPreStarter); isPreService { - monitor.Start("pre-start ", serviceName) - err := preService.PreStart() - monitor.Finish() - if err != nil { - return E.Cause(err, "pre-start ", serviceName) - } - } - } - err = s.network.Start(adapter.StartStateInitialize) - if err != nil { - return E.Cause(err, "initialize network manager") - } - err = s.router.Start(adapter.StartStateInitialize) + err = adapter.StartNamed(adapter.StartStateInitialize, s.services) // cache-file clash-api v2ray-api if err != nil { - return E.Cause(err, "initialize router") + return err } - err = s.outbound.Start(adapter.StartStateStart) + err = adapter.Start(adapter.StartStateInitialize, s.network, s.router, s.outbound, s.inbound) if err != nil { return err } - err = s.network.Start(adapter.StartStateStart) + err = adapter.Start(adapter.StartStateStart, s.outbound, s.network, s.router) if err != nil { return err } - return s.router.Start(adapter.StartStateStart) + return nil } func (s *Box) start() error { @@ -316,57 +315,27 @@ func (s *Box) start() error { if err != nil { return err } - for serviceName, service := range s.preServices1 { - err = service.Start() - if err != nil { - return E.Cause(err, "start ", serviceName) - } - } - for serviceName, service := range s.preServices2 { - err = service.Start() - if err != nil { - return E.Cause(err, "start ", serviceName) - } - } - err = s.inbound.Start(adapter.StartStateStart) + err = adapter.StartNamed(adapter.StartStateStart, s.services) if err != nil { return err } - for serviceName, service := range s.postServices { - err := service.Start() - if err != nil { - return E.Cause(err, "start ", serviceName) - } - } - err = s.outbound.Start(adapter.StartStatePostStart) - if err != nil { - return err - } - err = s.network.Start(adapter.StartStatePostStart) - if err != nil { - return err - } - err = s.router.Start(adapter.StartStatePostStart) - if err != nil { - return err - } - err = s.inbound.Start(adapter.StartStatePostStart) + err = s.inbound.Start(adapter.StartStateStart) if err != nil { return err } - err = s.network.Start(adapter.StartStateStarted) + err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.router, s.inbound) if err != nil { return err } - err = s.router.Start(adapter.StartStateStarted) + err = adapter.StartNamed(adapter.StartStatePostStart, s.services) if err != nil { return err } - err = s.outbound.Start(adapter.StartStateStarted) + err = adapter.Start(adapter.StartStateStarted, s.network, s.router, s.outbound, s.inbound) if err != nil { return err } - err = s.inbound.Start(adapter.StartStateStarted) + err = adapter.StartNamed(adapter.StartStateStarted, s.services) if err != nil { return err } @@ -380,47 +349,18 @@ func (s *Box) Close() error { default: close(s.done) } - monitor := taskmonitor.New(s.logger, C.StopTimeout) - var errors error - for serviceName, service := range s.postServices { - monitor.Start("close ", serviceName) - errors = E.Append(errors, service.Close(), func(err error) error { - return E.Cause(err, "close ", serviceName) - }) - monitor.Finish() - } - errors = E.Errors(errors, s.inbound.Close()) - errors = E.Errors(errors, s.outbound.Close()) - errors = E.Errors(errors, s.network.Close()) - errors = E.Errors(errors, s.router.Close()) - for serviceName, service := range s.preServices1 { - monitor.Start("close ", serviceName) - errors = E.Append(errors, service.Close(), func(err error) error { - return E.Cause(err, "close ", serviceName) - }) - monitor.Finish() - } - for serviceName, service := range s.preServices2 { - monitor.Start("close ", serviceName) - errors = E.Append(errors, service.Close(), func(err error) error { - return E.Cause(err, "close ", serviceName) + err := common.Close( + s.inbound, s.outbound, s.router, s.network, + ) + for _, lifecycleService := range s.services { + err = E.Append(err, lifecycleService.Close(), func(err error) error { + return E.Cause(err, "close ", lifecycleService.Name()) }) - monitor.Finish() } - if err := common.Close(s.logFactory); err != nil { - errors = E.Append(errors, err, func(err error) error { - return E.Cause(err, "close logger") - }) - } - return errors -} - -func (s *Box) Inbound() adapter.InboundManager { - return s.inbound -} - -func (s *Box) Outbound() adapter.OutboundManager { - return s.outbound + err = E.Append(err, s.logFactory.Close(), func(err error) error { + return E.Cause(err, "close logger") + }) + return err } func (s *Box) Network() adapter.NetworkManager { @@ -430,3 +370,11 @@ func (s *Box) Network() adapter.NetworkManager { func (s *Box) Router() adapter.Router { return s.router } + +func (s *Box) Inbound() adapter.InboundManager { + return s.inbound +} + +func (s *Box) Outbound() adapter.OutboundManager { + return s.outbound +} diff --git a/experimental/cachefile/cache.go b/experimental/cachefile/cache.go index 1027588fc9..498b9474fd 100644 --- a/experimental/cachefile/cache.go +++ b/experimental/cachefile/cache.go @@ -93,7 +93,18 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile { } } -func (c *CacheFile) start() error { +func (c *CacheFile) Name() string { + return "cache-file" +} + +func (c *CacheFile) Dependencies() []string { + return nil +} + +func (c *CacheFile) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateInitialize { + return nil + } const fileMode = 0o666 options := bbolt.Options{Timeout: time.Second} var ( @@ -151,14 +162,6 @@ func (c *CacheFile) start() error { return nil } -func (c *CacheFile) PreStart() error { - return c.start() -} - -func (c *CacheFile) Start() error { - return nil -} - func (c *CacheFile) Close() error { if c.DB == nil { return nil diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 89f33de348..106bc73ff7 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -133,45 +133,50 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op return s, nil } -func (s *Server) PreStart() error { - cacheFile := service.FromContext[adapter.CacheFile](s.ctx) - if cacheFile != nil { - mode := cacheFile.LoadMode() - if common.Any(s.modeList, func(it string) bool { - return strings.EqualFold(it, mode) - }) { - s.mode = mode - } - } - return nil +func (s *Server) Name() string { + return "clash server" } -func (s *Server) Start() error { - if s.externalController { - s.checkAndDownloadExternalUI() - var ( - listener net.Listener - err error - ) - for i := 0; i < 3; i++ { - listener, err = net.Listen("tcp", s.httpServer.Addr) - if runtime.GOOS == "android" && errors.Is(err, syscall.EADDRINUSE) { - time.Sleep(100 * time.Millisecond) - continue +func (s *Server) Start(stage adapter.StartStage) error { + switch stage { + case adapter.StartStateStart: + cacheFile := service.FromContext[adapter.CacheFile](s.ctx) + if cacheFile != nil { + mode := cacheFile.LoadMode() + if common.Any(s.modeList, func(it string) bool { + return strings.EqualFold(it, mode) + }) { + s.mode = mode } - break - } - if err != nil { - return E.Cause(err, "external controller listen error") } - s.logger.Info("restful api listening at ", listener.Addr()) - go func() { - err = s.httpServer.Serve(listener) - if err != nil && !errors.Is(err, http.ErrServerClosed) { - s.logger.Error("external controller serve error: ", err) + case adapter.StartStateStarted: + if s.externalController { + s.checkAndDownloadExternalUI() + var ( + listener net.Listener + err error + ) + for i := 0; i < 3; i++ { + listener, err = net.Listen("tcp", s.httpServer.Addr) + if runtime.GOOS == "android" && errors.Is(err, syscall.EADDRINUSE) { + time.Sleep(100 * time.Millisecond) + continue + } + break + } + if err != nil { + return E.Cause(err, "external controller listen error") } - }() + s.logger.Info("restful api listening at ", listener.Addr()) + go func() { + err = s.httpServer.Serve(listener) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + s.logger.Error("external controller serve error: ", err) + } + }() + } } + return nil } @@ -233,14 +238,12 @@ func (s *Server) TrafficManager() *trafficontrol.Manager { return s.trafficManager } -func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule) (net.Conn, adapter.Tracker) { - tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.outboundManager, matchedRule) - return tracker, tracker +func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn { + return trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.outboundManager, matchedRule, matchOutbound) } -func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule) (N.PacketConn, adapter.Tracker) { - tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.outboundManager, matchedRule) - return tracker, tracker +func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) N.PacketConn { + return trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.outboundManager, matchedRule, matchOutbound) } func authentication(serverSecret string) func(next http.Handler) http.Handler { diff --git a/experimental/clashapi/trafficontrol/tracker.go b/experimental/clashapi/trafficontrol/tracker.go index df5437fafa..e324be206e 100644 --- a/experimental/clashapi/trafficontrol/tracker.go +++ b/experimental/clashapi/trafficontrol/tracker.go @@ -5,7 +5,6 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - R "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common/bufio" @@ -88,7 +87,6 @@ func (t TrackerMetadata) MarshalJSON() ([]byte, error) { } type Tracker interface { - adapter.Tracker Metadata() TrackerMetadata Close() error } @@ -108,10 +106,6 @@ func (tt *TCPConn) Close() error { return tt.ExtendedConn.Close() } -func (tt *TCPConn) Leave() { - tt.manager.Leave(tt) -} - func (tt *TCPConn) Upstream() any { return tt.ExtendedConn } @@ -124,7 +118,7 @@ func (tt *TCPConn) WriterReplaceable() bool { return true } -func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundContext, outboundManager adapter.OutboundManager, rule adapter.Rule) *TCPConn { +func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundContext, outboundManager adapter.OutboundManager, matchRule adapter.Rule, matchOutbound adapter.Outbound) *TCPConn { id, _ := uuid.NewV4() var ( chain []string @@ -132,12 +126,8 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundCont outbound string outboundType string ) - var action adapter.RuleAction - if rule != nil { - action = rule.Action() - } - if routeAction, isRouteAction := action.(*R.RuleActionRoute); isRouteAction { - next = routeAction.Outbound + if matchOutbound != nil { + next = matchOutbound.Tag() } else { next = outboundManager.Default().Tag() } @@ -172,7 +162,7 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundCont Upload: upload, Download: download, Chain: common.Reverse(chain), - Rule: rule, + Rule: matchRule, Outbound: outbound, OutboundType: outboundType, }, @@ -197,10 +187,6 @@ func (ut *UDPConn) Close() error { return ut.PacketConn.Close() } -func (ut *UDPConn) Leave() { - ut.manager.Leave(ut) -} - func (ut *UDPConn) Upstream() any { return ut.PacketConn } @@ -213,7 +199,7 @@ func (ut *UDPConn) WriterReplaceable() bool { return true } -func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.InboundContext, outboundManager adapter.OutboundManager, rule adapter.Rule) *UDPConn { +func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.InboundContext, outboundManager adapter.OutboundManager, matchRule adapter.Rule, matchOutbound adapter.Outbound) *UDPConn { id, _ := uuid.NewV4() var ( chain []string @@ -221,12 +207,8 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.Inbound outbound string outboundType string ) - var action adapter.RuleAction - if rule != nil { - action = rule.Action() - } - if routeAction, isRouteAction := action.(*R.RuleActionRoute); isRouteAction { - next = routeAction.Outbound + if matchOutbound != nil { + next = matchOutbound.Tag() } else { next = outboundManager.Default().Tag() } @@ -261,7 +243,7 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.Inbound Upload: upload, Download: download, Chain: common.Reverse(chain), - Rule: rule, + Rule: matchRule, Outbound: outbound, OutboundType: outboundType, }, diff --git a/experimental/libbox/command_clash_mode.go b/experimental/libbox/command_clash_mode.go index 1b6eb47085..af69047fd3 100644 --- a/experimental/libbox/command_clash_mode.go +++ b/experimental/libbox/command_clash_mode.go @@ -38,11 +38,7 @@ func (s *CommandServer) handleSetClashMode(conn net.Conn) error { if service == nil { return writeError(conn, E.New("service not ready")) } - clashServer := service.instance.Router().ClashServer() - if clashServer == nil { - return writeError(conn, E.New("Clash API disabled")) - } - clashServer.(*clashapi.Server).SetMode(newMode) + service.clashServer.(*clashapi.Server).SetMode(newMode) return writeError(conn, nil) } @@ -69,18 +65,14 @@ func (s *CommandServer) handleModeConn(conn net.Conn) error { return ctx.Err() } } - clashServer := s.service.instance.Router().ClashServer() - if clashServer == nil { - return binary.Write(conn, binary.BigEndian, uint16(0)) - } - err := writeClashModeList(conn, clashServer) + err := writeClashModeList(conn, s.service.clashServer) if err != nil { return err } for { select { case <-s.modeUpdate: - err = varbin.Write(conn, binary.BigEndian, clashServer.Mode()) + err = varbin.Write(conn, binary.BigEndian, s.service.clashServer.Mode()) if err != nil { return err } diff --git a/experimental/libbox/command_close_connection.go b/experimental/libbox/command_close_connection.go index a2b05e5619..46f7023fa1 100644 --- a/experimental/libbox/command_close_connection.go +++ b/experimental/libbox/command_close_connection.go @@ -45,11 +45,7 @@ func (s *CommandServer) handleCloseConnection(conn net.Conn) error { if service == nil { return writeError(conn, E.New("service not ready")) } - clashServer := service.instance.Router().ClashServer() - if clashServer == nil { - return writeError(conn, E.New("Clash API disabled")) - } - targetConn := clashServer.(*clashapi.Server).TrafficManager().Connection(uuid.FromStringOrNil(connId)) + targetConn := service.clashServer.(*clashapi.Server).TrafficManager().Connection(uuid.FromStringOrNil(connId)) if targetConn == nil { return writeError(conn, E.New("connection already closed")) } diff --git a/experimental/libbox/command_connections.go b/experimental/libbox/command_connections.go index b51c735249..39d9303cb5 100644 --- a/experimental/libbox/command_connections.go +++ b/experimental/libbox/command_connections.go @@ -49,11 +49,7 @@ func (s *CommandServer) handleConnectionsConn(conn net.Conn) error { for { service := s.service if service != nil { - clashServer := service.instance.Router().ClashServer() - if clashServer == nil { - return E.New("Clash API disabled") - } - trafficManager = clashServer.(*clashapi.Server).TrafficManager() + trafficManager = service.clashServer.(*clashapi.Server).TrafficManager() break } select { diff --git a/experimental/libbox/command_server.go b/experimental/libbox/command_server.go index 26b4aa79b1..798a52bddf 100644 --- a/experimental/libbox/command_server.go +++ b/experimental/libbox/command_server.go @@ -60,7 +60,7 @@ func NewCommandServer(handler CommandServerHandler, maxLines int32) *CommandServ func (s *CommandServer) SetService(newService *BoxService) { if newService != nil { service.PtrFromContext[urltest.HistoryStorage](newService.ctx).SetHook(s.urlTestUpdate) - newService.instance.Router().ClashServer().(*clashapi.Server).SetModeUpdateHook(s.modeUpdate) + newService.clashServer.(*clashapi.Server).SetModeUpdateHook(s.modeUpdate) } s.service = newService s.notifyURLTestUpdate() diff --git a/experimental/libbox/command_status.go b/experimental/libbox/command_status.go index a6280d0f7c..f8709ef04e 100644 --- a/experimental/libbox/command_status.go +++ b/experimental/libbox/command_status.go @@ -31,12 +31,10 @@ func (s *CommandServer) readStatus() StatusMessage { message.ConnectionsOut = int32(conntrack.Count()) if s.service != nil { - if clashServer := s.service.instance.Router().ClashServer(); clashServer != nil { - message.TrafficAvailable = true - trafficManager := clashServer.(*clashapi.Server).TrafficManager() - message.UplinkTotal, message.DownlinkTotal = trafficManager.Total() - message.ConnectionsIn = int32(trafficManager.ConnectionsLen()) - } + message.TrafficAvailable = true + trafficManager := s.service.clashServer.(*clashapi.Server).TrafficManager() + message.UplinkTotal, message.DownlinkTotal = trafficManager.Total() + message.ConnectionsIn = int32(trafficManager.ConnectionsLen()) } return message diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index e3d2f95474..9b7181c8b8 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -34,17 +34,18 @@ import ( type BoxService struct { ctx context.Context cancel context.CancelFunc + urlTestHistoryStorage *urltest.HistoryStorage instance *box.Box + clashServer adapter.ClashServer pauseManager pause.Manager - urlTestHistoryStorage *urltest.HistoryStorage servicePauseFields } func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) { ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry()) - ctx = service.ContextWith[deprecated.Manager](ctx, new(deprecatedManager)) ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID) + service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager)) options, err := parseConfig(ctx, configContent) if err != nil { return nil, err @@ -54,7 +55,7 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box urlTestHistoryStorage := urltest.NewHistoryStorage() ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage) platformWrapper := &platformInterfaceWrapper{iif: platformInterface, useProcFS: platformInterface.UseProcFS()} - ctx = service.ContextWith[platform.Interface](ctx, platformWrapper) + service.MustRegister[platform.Interface](ctx, platformWrapper) instance, err := box.New(box.Options{ Context: ctx, Options: options, @@ -71,6 +72,7 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box instance: instance, urlTestHistoryStorage: urlTestHistoryStorage, pauseManager: service.FromContext[pause.Manager](ctx), + clashServer: service.FromContext[adapter.ClashServer](ctx), }, nil } diff --git a/experimental/v2rayapi/server.go b/experimental/v2rayapi/server.go index 8b4b43855e..8ebae1c4f7 100644 --- a/experimental/v2rayapi/server.go +++ b/experimental/v2rayapi/server.go @@ -44,7 +44,14 @@ func NewServer(logger log.Logger, options option.V2RayAPIOptions) (adapter.V2Ray return server, nil } -func (s *Server) Start() error { +func (s *Server) Name() string { + return "v2ray server" +} + +func (s *Server) Start(stage adapter.StartStage) error { + if stage != adapter.StartStatePostStart { + return nil + } listener, err := net.Listen("tcp", s.listen) if err != nil { return err @@ -70,6 +77,6 @@ func (s *Server) Close() error { ) } -func (s *Server) StatsService() adapter.V2RayStatsService { +func (s *Server) StatsService() adapter.ConnectionTracker { return s.statsService } diff --git a/experimental/v2rayapi/stats.go b/experimental/v2rayapi/stats.go index 38b9a301fd..6c44518fd5 100644 --- a/experimental/v2rayapi/stats.go +++ b/experimental/v2rayapi/stats.go @@ -22,7 +22,7 @@ func init() { } var ( - _ adapter.V2RayStatsService = (*StatsService)(nil) + _ adapter.ConnectionTracker = (*StatsService)(nil) _ StatsServiceServer = (*StatsService)(nil) ) @@ -60,7 +60,10 @@ func NewStatsService(options option.V2RayStatsServiceOptions) *StatsService { } } -func (s *StatsService) RoutedConnection(inbound string, outbound string, user string, conn net.Conn) net.Conn { +func (s *StatsService) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn { + inbound := metadata.Inbound + user := metadata.User + outbound := matchOutbound.Tag() var readCounter []*atomic.Int64 var writeCounter []*atomic.Int64 countInbound := inbound != "" && s.inbounds[inbound] @@ -86,7 +89,10 @@ func (s *StatsService) RoutedConnection(inbound string, outbound string, user st return bufio.NewInt64CounterConn(conn, readCounter, writeCounter) } -func (s *StatsService) RoutedPacketConnection(inbound string, outbound string, user string, conn N.PacketConn) N.PacketConn { +func (s *StatsService) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) N.PacketConn { + inbound := metadata.Inbound + user := metadata.User + outbound := matchOutbound.Tag() var readCounter []*atomic.Int64 var writeCounter []*atomic.Int64 countInbound := inbound != "" && s.inbounds[inbound] diff --git a/protocol/group/urltest.go b/protocol/group/urltest.go index 4d76a31c6a..f1a84b5044 100644 --- a/protocol/group/urltest.go +++ b/protocol/group/urltest.go @@ -76,18 +76,7 @@ func (s *URLTest) Start() error { } outbounds = append(outbounds, detour) } - group, err := NewURLTestGroup( - s.ctx, - s.router, - s.outboundManager, - s.logger, - outbounds, - s.link, - s.interval, - s.tolerance, - s.idleTimeout, - s.interruptExternalConnections, - ) + group, err := NewURLTestGroup(s.ctx, s.outboundManager, s.logger, outbounds, s.link, s.interval, s.tolerance, s.idleTimeout, s.interruptExternalConnections) if err != nil { return err } @@ -215,18 +204,7 @@ type URLTestGroup struct { lastActive atomic.TypedValue[time.Time] } -func NewURLTestGroup( - ctx context.Context, - router adapter.Router, - outboundManager adapter.OutboundManager, - logger log.Logger, - outbounds []adapter.Outbound, - link string, - interval time.Duration, - tolerance uint16, - idleTimeout time.Duration, - interruptExternalConnections bool, -) (*URLTestGroup, error) { +func NewURLTestGroup(ctx context.Context, outboundManager adapter.OutboundManager, logger log.Logger, outbounds []adapter.Outbound, link string, interval time.Duration, tolerance uint16, idleTimeout time.Duration, interruptExternalConnections bool) (*URLTestGroup, error) { if interval == 0 { interval = C.DefaultURLTestInterval } @@ -241,14 +219,13 @@ func NewURLTestGroup( } var history *urltest.HistoryStorage if history = service.PtrFromContext[urltest.HistoryStorage](ctx); history != nil { - } else if clashServer := router.ClashServer(); clashServer != nil { + } else if clashServer := service.FromContext[adapter.ClashServer](ctx); clashServer != nil { history = clashServer.HistoryStorage() } else { history = urltest.NewHistoryStorage() } return &URLTestGroup{ ctx: ctx, - router: router, outboundManager: outboundManager, logger: logger, outbounds: outbounds, diff --git a/route/route.go b/route/route.go index d55b4ec170..1c4da4b7ad 100644 --- a/route/route.go +++ b/route/route.go @@ -91,16 +91,12 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad if err != nil { return err } - var ( - // selectedOutbound adapter.Outbound - selectedDialer N.Dialer - selectedTag string - selectedDescription string - ) + var selectedOutbound adapter.Outbound if selectedRule != nil { switch action := selectedRule.Action().(type) { case *rule.RuleActionRoute: - selectedOutbound, loaded := r.outboundManager.Outbound(action.Outbound) + var loaded bool + selectedOutbound, loaded = r.outboundManager.Outbound(action.Outbound) if !loaded { buf.ReleaseMulti(buffers) return E.New("outbound not found: ", action.Outbound) @@ -109,12 +105,6 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad buf.ReleaseMulti(buffers) return E.New("TCP is not supported by outbound: ", selectedOutbound.Tag()) } - selectedDialer = selectedOutbound - selectedTag = selectedOutbound.Tag() - selectedDescription = F.ToString("outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]") - case *rule.RuleActionDirect: - selectedDialer = action.Dialer - selectedDescription = action.String() case *rule.RuleActionReject: buf.ReleaseMulti(buffers) N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx)) @@ -133,25 +123,16 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad buf.ReleaseMulti(buffers) return E.New("TCP is not supported by default outbound: ", defaultOutbound.Tag()) } - selectedDialer = defaultOutbound - selectedTag = defaultOutbound.Tag() - selectedDescription = F.ToString("outbound/", defaultOutbound.Type(), "[", defaultOutbound.Tag(), "]") + selectedOutbound = defaultOutbound } for _, buffer := range buffers { conn = bufio.NewCachedConn(conn, buffer) } - if r.clashServer != nil { - trackerConn, tracker := r.clashServer.RoutedConnection(ctx, conn, metadata, selectedRule) - defer tracker.Leave() - conn = trackerConn + if r.tracker != nil { + conn = r.tracker.RoutedConnection(ctx, conn, metadata, selectedRule, selectedOutbound) } - if r.v2rayServer != nil { - if statsService := r.v2rayServer.StatsService(); statsService != nil { - conn = statsService.RoutedConnection(metadata.Inbound, selectedTag, metadata.User, conn) - } - } - legacyOutbound, isLegacy := selectedDialer.(adapter.ConnectionHandler) + legacyOutbound, isLegacy := selectedOutbound.(adapter.ConnectionHandler) if isLegacy { err = legacyOutbound.NewConnection(ctx, conn, metadata) if err != nil { @@ -159,7 +140,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad if onClose != nil { onClose(err) } - return E.Cause(err, selectedDescription) + return E.Cause(err, F.ToString("outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]")) } else { if onClose != nil { onClose(nil) @@ -168,13 +149,13 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad return nil } // TODO - err = outbound.NewConnection(ctx, selectedDialer, conn, metadata) + err = outbound.NewConnection(ctx, selectedOutbound, conn, metadata) if err != nil { conn.Close() if onClose != nil { onClose(err) } - return E.Cause(err, selectedDescription) + return E.Cause(err, F.ToString("outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]")) } else { if onClose != nil { onClose(nil) @@ -246,16 +227,13 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m if err != nil { return err } - var ( - selectedDialer N.Dialer - selectedTag string - selectedDescription string - ) + var selectedOutbound adapter.Outbound var selectReturn bool if selectedRule != nil { switch action := selectedRule.Action().(type) { case *rule.RuleActionRoute: - selectedOutbound, loaded := r.outboundManager.Outbound(action.Outbound) + var loaded bool + selectedOutbound, loaded = r.outboundManager.Outbound(action.Outbound) if !loaded { N.ReleaseMultiPacketBuffer(packetBuffers) return E.New("outbound not found: ", action.Outbound) @@ -264,12 +242,6 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m N.ReleaseMultiPacketBuffer(packetBuffers) return E.New("UDP is not supported by outbound: ", selectedOutbound.Tag()) } - selectedDialer = selectedOutbound - selectedTag = selectedOutbound.Tag() - selectedDescription = F.ToString("outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]") - case *rule.RuleActionDirect: - selectedDialer = action.Dialer - selectedDescription = action.String() case *rule.RuleActionReject: N.ReleaseMultiPacketBuffer(packetBuffers) N.CloseOnHandshakeFailure(conn, onClose, action.Error(ctx)) @@ -285,41 +257,32 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m N.ReleaseMultiPacketBuffer(packetBuffers) return E.New("UDP is not supported by outbound: ", defaultOutbound.Tag()) } - selectedDialer = defaultOutbound - selectedTag = defaultOutbound.Tag() - selectedDescription = F.ToString("outbound/", defaultOutbound.Type(), "[", defaultOutbound.Tag(), "]") + selectedOutbound = defaultOutbound } for _, buffer := range packetBuffers { conn = bufio.NewCachedPacketConn(conn, buffer.Buffer, buffer.Destination) N.PutPacketBuffer(buffer) } - if r.clashServer != nil { - trackerConn, tracker := r.clashServer.RoutedPacketConnection(ctx, conn, metadata, selectedRule) - defer tracker.Leave() - conn = trackerConn - } - if r.v2rayServer != nil { - if statsService := r.v2rayServer.StatsService(); statsService != nil { - conn = statsService.RoutedPacketConnection(metadata.Inbound, selectedTag, metadata.User, conn) - } + if r.tracker != nil { + conn = r.tracker.RoutedPacketConnection(ctx, conn, metadata, selectedRule, selectedOutbound) } if metadata.FakeIP { conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination) } - legacyOutbound, isLegacy := selectedDialer.(adapter.PacketConnectionHandler) + legacyOutbound, isLegacy := selectedOutbound.(adapter.PacketConnectionHandler) if isLegacy { err = legacyOutbound.NewPacketConnection(ctx, conn, metadata) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { - return E.Cause(err, selectedDescription) + return E.Cause(err, F.ToString("outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]")) } return nil } // TODO - err = outbound.NewPacketConnection(ctx, selectedDialer, conn, metadata) + err = outbound.NewPacketConnection(ctx, selectedOutbound, conn, metadata) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { - return E.Cause(err, selectedDescription) + return E.Cause(err, F.ToString("outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]")) } return nil } diff --git a/route/router.go b/route/router.go index 62b374473f..1f760a86a0 100644 --- a/route/router.go +++ b/route/router.go @@ -27,7 +27,6 @@ import ( F "github.com/sagernet/sing/common/format" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/ntp" "github.com/sagernet/sing/common/task" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" @@ -63,16 +62,14 @@ type Router struct { dnsReverseMapping *DNSReverseMapping fakeIPStore adapter.FakeIPStore processSearcher process.Searcher - timeService *ntp.Service pauseManager pause.Manager - clashServer adapter.ClashServer - v2rayServer adapter.V2RayServer + tracker adapter.ConnectionTracker platformInterface platform.Interface needWIFIState bool started bool } -func NewRouter(ctx context.Context, logFactory log.Factory, options option.RouteOptions, dnsOptions option.DNSOptions, ntpOptions option.NTPOptions) (*Router, error) { +func NewRouter(ctx context.Context, logFactory log.Factory, options option.RouteOptions, dnsOptions option.DNSOptions) (*Router, error) { router := &Router{ ctx: ctx, logger: logFactory.NewLogger("router"), @@ -94,7 +91,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route platformInterface: service.FromContext[platform.Interface](ctx), needWIFIState: hasRule(options.Rules, isWIFIRule) || hasDNSRule(dnsOptions.Rules, isWIFIDNSRule), } - ctx = service.ContextWith[adapter.Router](ctx, router) + service.MustRegister[adapter.Router](ctx, router) router.dnsClient = dns.NewClient(dns.ClientOptions{ DisableCache: dnsOptions.DNSClientOptions.DisableCache, DisableExpire: dnsOptions.DNSClientOptions.DisableExpire, @@ -290,23 +287,6 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route } router.fakeIPStore = fakeip.NewStore(ctx, router.logger, inet4Range, inet6Range) } - - if ntpOptions.Enabled { - ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions) - if err != nil { - return nil, E.Cause(err, "create NTP service") - } - timeService := ntp.NewService(ntp.Options{ - Context: ctx, - Dialer: ntpDialer, - Logger: logFactory.NewLogger("ntp"), - Server: ntpOptions.ServerOptions.Build(), - Interval: time.Duration(ntpOptions.Interval), - WriteToSystem: ntpOptions.WriteToSystem, - }) - service.MustRegister[ntp.TimeService](ctx, timeService) - router.timeService = timeService - } return router, nil } @@ -380,14 +360,6 @@ func (r *Router) Start(stage adapter.StartStage) error { return E.Cause(err, "initialize DNS server[", i, "]") } } - if r.timeService != nil { - monitor.Start("initialize time service") - err := r.timeService.Start() - monitor.Finish() - if err != nil { - return E.Cause(err, "initialize time service") - } - } case adapter.StartStatePostStart: var cacheContext *adapter.HTTPStartContext if len(r.ruleSets) > 0 { @@ -502,13 +474,6 @@ func (r *Router) Close() error { }) monitor.Finish() } - if r.timeService != nil { - monitor.Start("close time service") - err = E.Append(err, r.timeService.Close(), func(err error) error { - return E.Cause(err, "close time service") - }) - monitor.Finish() - } if r.fakeIPStore != nil { monitor.Start("close fakeip store") err = E.Append(err, r.fakeIPStore.Close(), func(err error) error { @@ -536,29 +501,8 @@ func (r *Router) Rules() []adapter.Rule { return r.rules } -func (r *Router) ClashServer() adapter.ClashServer { - return r.clashServer -} - -func (r *Router) SetClashServer(server adapter.ClashServer) { - r.clashServer = server -} - -func (r *Router) V2RayServer() adapter.V2RayServer { - return r.v2rayServer -} - -func (r *Router) SetV2RayServer(server adapter.V2RayServer) { - r.v2rayServer = server -} - -func (r *Router) NewError(ctx context.Context, err error) { - common.Close(err) - if E.IsClosedOrCanceled(err) { - r.logger.DebugContext(ctx, "connection closed: ", err) - return - } - r.logger.ErrorContext(ctx, err) +func (r *Router) SetTracker(tracker adapter.ConnectionTracker) { + r.tracker = tracker } func (r *Router) ResetNetwork() { diff --git a/route/rule/rule_default.go b/route/rule/rule_default.go index 33a8e16c35..12d9e96ae8 100644 --- a/route/rule/rule_default.go +++ b/route/rule/rule_default.go @@ -219,7 +219,7 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio rule.allItems = append(rule.allItems, item) } if options.ClashMode != "" { - item := NewClashModeItem(router, options.ClashMode) + item := NewClashModeItem(ctx, options.ClashMode) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go index df5f3f335c..1ec652b22a 100644 --- a/route/rule/rule_dns.go +++ b/route/rule/rule_dns.go @@ -216,7 +216,7 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op rule.allItems = append(rule.allItems, item) } if options.ClashMode != "" { - item := NewClashModeItem(router, options.ClashMode) + item := NewClashModeItem(ctx, options.ClashMode) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } diff --git a/route/rule/rule_item_clash_mode.go b/route/rule/rule_item_clash_mode.go index aa5126cbde..fe2347a06f 100644 --- a/route/rule/rule_item_clash_mode.go +++ b/route/rule/rule_item_clash_mode.go @@ -1,31 +1,38 @@ package rule import ( + "context" "strings" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/service" ) var _ RuleItem = (*ClashModeItem)(nil) type ClashModeItem struct { - router adapter.Router - mode string + ctx context.Context + clashServer adapter.ClashServer + mode string } -func NewClashModeItem(router adapter.Router, mode string) *ClashModeItem { +func NewClashModeItem(ctx context.Context, mode string) *ClashModeItem { return &ClashModeItem{ - router: router, - mode: mode, + ctx: ctx, + mode: mode, } } +func (r *ClashModeItem) Start() error { + r.clashServer = service.FromContext[adapter.ClashServer](r.ctx) + return nil +} + func (r *ClashModeItem) Match(metadata *adapter.InboundContext) bool { - clashServer := r.router.ClashServer() - if clashServer == nil { + if r.clashServer == nil { return false } - return strings.EqualFold(clashServer.Mode(), r.mode) + return strings.EqualFold(r.clashServer.Mode(), r.mode) } func (r *ClashModeItem) String() string { From 72239dcbd3a93bb378432d8fc82c6ff5295d4613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 11 Nov 2024 16:23:45 +0800 Subject: [PATCH 17/49] refactor: Platform Interfaces --- adapter/network.go | 19 ++ adapter/router.go | 9 - common/settings/proxy_darwin.go | 7 +- constant/network.go | 8 + constant/os.go | 2 +- experimental/libbox/config.go | 18 +- experimental/libbox/link_flags_stub.go | 2 +- ...link_flags_linux.go => link_flags_unix.go} | 2 + experimental/libbox/monitor.go | 161 ++++------------ experimental/libbox/platform.go | 16 +- experimental/libbox/platform/interface.go | 5 +- experimental/libbox/service.go | 55 ++++-- protocol/direct/loopback_detect.go | 6 +- route/network.go | 179 +++++++++++++----- transport/dhcp/server.go | 21 +- 15 files changed, 263 insertions(+), 247 deletions(-) create mode 100644 constant/network.go rename experimental/libbox/{link_flags_linux.go => link_flags_unix.go} (97%) diff --git a/adapter/network.go b/adapter/network.go index 533bfced22..6c09a0a3e1 100644 --- a/adapter/network.go +++ b/adapter/network.go @@ -9,6 +9,8 @@ type NetworkManager interface { Lifecycle InterfaceFinder() control.InterfaceFinder UpdateInterfaces() error + DefaultNetworkInterface() *NetworkInterface + NetworkInterfaces() []NetworkInterface DefaultInterface() string AutoDetectInterface() bool AutoDetectInterfaceFunc() control.Func @@ -21,3 +23,20 @@ type NetworkManager interface { WIFIState() WIFIState ResetNetwork() } + +type InterfaceUpdateListener interface { + InterfaceUpdated() +} + +type WIFIState struct { + SSID string + BSSID string +} + +type NetworkInterface struct { + control.Interface + Type string + DNSServers []string + Expensive bool + Constrained bool +} diff --git a/adapter/router.go b/adapter/router.go index 6dd39357b4..a637e5068d 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -119,12 +119,3 @@ func (c *HTTPStartContext) Close() { client.CloseIdleConnections() } } - -type InterfaceUpdateListener interface { - InterfaceUpdated() -} - -type WIFIState struct { - SSID string - BSSID string -} diff --git a/common/settings/proxy_darwin.go b/common/settings/proxy_darwin.go index 17e82cf59f..3c06a85369 100644 --- a/common/settings/proxy_darwin.go +++ b/common/settings/proxy_darwin.go @@ -2,7 +2,6 @@ package settings import ( "context" - "net/netip" "strconv" "strings" @@ -77,14 +76,14 @@ func (p *DarwinSystemProxy) update(event int) { } func (p *DarwinSystemProxy) update0() error { - newInterfaceName := p.monitor.DefaultInterfaceName(netip.IPv4Unspecified()) - if p.interfaceName == newInterfaceName { + newInterface := p.monitor.DefaultInterface() + if p.interfaceName == newInterface.Name { return nil } if p.interfaceName != "" { _ = p.Disable() } - p.interfaceName = newInterfaceName + p.interfaceName = newInterface.Name interfaceDisplayName, err := getInterfaceDisplayName(p.interfaceName) if err != nil { return err diff --git a/constant/network.go b/constant/network.go new file mode 100644 index 0000000000..f5ac2a4e47 --- /dev/null +++ b/constant/network.go @@ -0,0 +1,8 @@ +package constant + +const ( + InterfaceTypeWIFI = "wifi" + InterfaceTypeCellular = "cellular" + InterfaceTypeEthernet = "ethernet" + InterfaceTypeOther = "other" +) diff --git a/constant/os.go b/constant/os.go index 63c6c41cd2..6142767c9c 100644 --- a/constant/os.go +++ b/constant/os.go @@ -6,7 +6,7 @@ import ( const IsAndroid = goos.IsAndroid == 1 -const IsDarwin = goos.IsDarwin == 1 +const IsDarwin = goos.IsDarwin == 1 || goos.IsIos == 1 const IsDragonfly = goos.IsDragonfly == 1 diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index 3310b09a58..02d5b884b5 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -74,11 +74,7 @@ func (s *platformInterfaceStub) CreateDefaultInterfaceMonitor(logger logger.Logg return (*interfaceMonitorStub)(nil) } -func (s *platformInterfaceStub) UsePlatformInterfaceGetter() bool { - return true -} - -func (s *platformInterfaceStub) Interfaces() ([]control.Interface, error) { +func (s *platformInterfaceStub) Interfaces() ([]adapter.NetworkInterface, error) { return nil, os.ErrInvalid } @@ -111,16 +107,8 @@ func (s *interfaceMonitorStub) Close() error { return os.ErrInvalid } -func (s *interfaceMonitorStub) DefaultInterfaceName(destination netip.Addr) string { - return "" -} - -func (s *interfaceMonitorStub) DefaultInterfaceIndex(destination netip.Addr) int { - return -1 -} - -func (s *interfaceMonitorStub) DefaultInterface(destination netip.Addr) (string, int) { - return "", -1 +func (s *interfaceMonitorStub) DefaultInterface() *control.Interface { + return nil } func (s *interfaceMonitorStub) OverrideAndroidVPN() bool { diff --git a/experimental/libbox/link_flags_stub.go b/experimental/libbox/link_flags_stub.go index 306754f3f2..ce3d1eb805 100644 --- a/experimental/libbox/link_flags_stub.go +++ b/experimental/libbox/link_flags_stub.go @@ -1,4 +1,4 @@ -//go:build !linux +//go:build !unix package libbox diff --git a/experimental/libbox/link_flags_linux.go b/experimental/libbox/link_flags_unix.go similarity index 97% rename from experimental/libbox/link_flags_linux.go rename to experimental/libbox/link_flags_unix.go index aa9ad46d20..04f41d641f 100644 --- a/experimental/libbox/link_flags_linux.go +++ b/experimental/libbox/link_flags_unix.go @@ -1,3 +1,5 @@ +//go:build unix + package libbox import ( diff --git a/experimental/libbox/monitor.go b/experimental/libbox/monitor.go index 00a3e0cf6c..d144fa07e4 100644 --- a/experimental/libbox/monitor.go +++ b/experimental/libbox/monitor.go @@ -1,15 +1,10 @@ package libbox import ( - "net" - "net/netip" - "sync" - "github.com/sagernet/sing-tun" - "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" - M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/x/list" ) @@ -20,19 +15,9 @@ var ( type platformDefaultInterfaceMonitor struct { *platformInterfaceWrapper - networkAddresses []networkAddress - defaultInterfaceName string - defaultInterfaceIndex int - element *list.Element[tun.NetworkUpdateCallback] - access sync.Mutex - callbacks list.List[tun.DefaultInterfaceUpdateCallback] - logger logger.Logger -} - -type networkAddress struct { - interfaceName string - interfaceIndex int - addresses []netip.Prefix + element *list.Element[tun.NetworkUpdateCallback] + callbacks list.List[tun.DefaultInterfaceUpdateCallback] + logger logger.Logger } func (m *platformDefaultInterfaceMonitor) Start() error { @@ -43,37 +28,10 @@ func (m *platformDefaultInterfaceMonitor) Close() error { return m.iif.CloseDefaultInterfaceMonitor(m) } -func (m *platformDefaultInterfaceMonitor) DefaultInterfaceName(destination netip.Addr) string { - for _, address := range m.networkAddresses { - for _, prefix := range address.addresses { - if prefix.Contains(destination) { - return address.interfaceName - } - } - } - return m.defaultInterfaceName -} - -func (m *platformDefaultInterfaceMonitor) DefaultInterfaceIndex(destination netip.Addr) int { - for _, address := range m.networkAddresses { - for _, prefix := range address.addresses { - if prefix.Contains(destination) { - return address.interfaceIndex - } - } - } - return m.defaultInterfaceIndex -} - -func (m *platformDefaultInterfaceMonitor) DefaultInterface(destination netip.Addr) (string, int) { - for _, address := range m.networkAddresses { - for _, prefix := range address.addresses { - if prefix.Contains(destination) { - return address.interfaceName, address.interfaceIndex - } - } - } - return m.defaultInterfaceName, m.defaultInterfaceIndex +func (m *platformDefaultInterfaceMonitor) DefaultInterface() *control.Interface { + m.defaultInterfaceAccess.Lock() + defer m.defaultInterfaceAccess.Unlock() + return m.defaultInterface } func (m *platformDefaultInterfaceMonitor) OverrideAndroidVPN() bool { @@ -85,104 +43,57 @@ func (m *platformDefaultInterfaceMonitor) AndroidVPNEnabled() bool { } func (m *platformDefaultInterfaceMonitor) RegisterCallback(callback tun.DefaultInterfaceUpdateCallback) *list.Element[tun.DefaultInterfaceUpdateCallback] { - m.access.Lock() - defer m.access.Unlock() + m.defaultInterfaceAccess.Lock() + defer m.defaultInterfaceAccess.Unlock() return m.callbacks.PushBack(callback) } func (m *platformDefaultInterfaceMonitor) UnregisterCallback(element *list.Element[tun.DefaultInterfaceUpdateCallback]) { - m.access.Lock() - defer m.access.Unlock() + m.defaultInterfaceAccess.Lock() + defer m.defaultInterfaceAccess.Unlock() m.callbacks.Remove(element) } -func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(interfaceName string, interfaceIndex32 int32) { +func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(interfaceName string, interfaceIndex32 int32, isExpensive bool, isConstrained bool) { if sFixAndroidStack { - go m.updateDefaultInterface(interfaceName, interfaceIndex32) + go m.updateDefaultInterface(interfaceName, interfaceIndex32, isExpensive, isConstrained) } else { - m.updateDefaultInterface(interfaceName, interfaceIndex32) + m.updateDefaultInterface(interfaceName, interfaceIndex32, isExpensive, isConstrained) } } -func (m *platformDefaultInterfaceMonitor) updateDefaultInterface(interfaceName string, interfaceIndex32 int32) { - if interfaceName == "" || interfaceIndex32 == -1 { - m.defaultInterfaceName = "" - m.defaultInterfaceIndex = -1 - m.access.Lock() +func (m *platformDefaultInterfaceMonitor) updateDefaultInterface(interfaceName string, interfaceIndex32 int32, isExpensive bool, isConstrained bool) { + m.isExpensive = isExpensive + m.isConstrained = isConstrained + err := m.networkManager.UpdateInterfaces() + if err != nil { + m.logger.Error(E.Cause(err, "update interfaces")) + } + m.defaultInterfaceAccess.Lock() + if interfaceIndex32 == -1 { + m.defaultInterface = nil callbacks := m.callbacks.Array() - m.access.Unlock() + m.defaultInterfaceAccess.Unlock() for _, callback := range callbacks { - callback(tun.EventNoRoute) + callback(tun.EventInterfaceUpdate) } return } - var err error - if m.iif.UsePlatformInterfaceGetter() { - err = m.updateInterfacesPlatform() - } else { - err = m.updateInterfaces() - } - if err == nil { - err = m.networkManager.UpdateInterfaces() - } + oldInterface := m.defaultInterface + newInterface, err := m.networkManager.InterfaceFinder().ByIndex(int(interfaceIndex32)) if err != nil { - m.logger.Error(E.Cause(err, "update interfaces")) + m.defaultInterfaceAccess.Unlock() + m.logger.Error(E.Cause(err, "find updated interface: ", interfaceName)) + return } - interfaceIndex := int(interfaceIndex32) - if m.defaultInterfaceName == interfaceName && m.defaultInterfaceIndex == interfaceIndex { + m.defaultInterface = newInterface + if oldInterface != nil && oldInterface.Name == m.defaultInterface.Name && oldInterface.Index == m.defaultInterface.Index { + m.defaultInterfaceAccess.Unlock() return } - m.defaultInterfaceName = interfaceName - m.defaultInterfaceIndex = interfaceIndex - m.access.Lock() callbacks := m.callbacks.Array() - m.access.Unlock() + m.defaultInterfaceAccess.Unlock() for _, callback := range callbacks { callback(tun.EventInterfaceUpdate) } } - -func (m *platformDefaultInterfaceMonitor) updateInterfaces() error { - interfaces, err := net.Interfaces() - if err != nil { - return err - } - var addresses []networkAddress - for _, iif := range interfaces { - var netAddresses []net.Addr - netAddresses, err = iif.Addrs() - if err != nil { - return err - } - var address networkAddress - address.interfaceName = iif.Name - address.interfaceIndex = iif.Index - address.addresses = common.Map(common.FilterIsInstance(netAddresses, func(it net.Addr) (*net.IPNet, bool) { - value, loaded := it.(*net.IPNet) - return value, loaded - }), func(it *net.IPNet) netip.Prefix { - bits, _ := it.Mask.Size() - return netip.PrefixFrom(M.AddrFromIP(it.IP), bits) - }) - addresses = append(addresses, address) - } - m.networkAddresses = addresses - return nil -} - -func (m *platformDefaultInterfaceMonitor) updateInterfacesPlatform() error { - interfaces, err := m.Interfaces() - if err != nil { - return err - } - var addresses []networkAddress - for _, iif := range interfaces { - var address networkAddress - address.interfaceName = iif.Name - address.interfaceIndex = iif.Index - // address.addresses = common.Map(iif.Addresses, netip.MustParsePrefix) - addresses = append(addresses, address) - } - m.networkAddresses = addresses - return nil -} diff --git a/experimental/libbox/platform.go b/experimental/libbox/platform.go index 1976ad70fe..a61243458c 100644 --- a/experimental/libbox/platform.go +++ b/experimental/libbox/platform.go @@ -1,6 +1,7 @@ package libbox import ( + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" ) @@ -13,10 +14,8 @@ type PlatformInterface interface { FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (int32, error) PackageNameByUid(uid int32) (string, error) UIDByPackageName(packageName string) (int32, error) - UsePlatformDefaultInterfaceMonitor() bool StartDefaultInterfaceMonitor(listener InterfaceUpdateListener) error CloseDefaultInterfaceMonitor(listener InterfaceUpdateListener) error - UsePlatformInterfaceGetter() bool GetInterfaces() (NetworkInterfaceIterator, error) UnderNetworkExtension() bool IncludeAllNetworks() bool @@ -31,15 +30,26 @@ type TunInterface interface { } type InterfaceUpdateListener interface { - UpdateDefaultInterface(interfaceName string, interfaceIndex int32) + UpdateDefaultInterface(interfaceName string, interfaceIndex int32, isExpensive bool, isConstrained bool) } +const ( + InterfaceTypeWIFI = C.InterfaceTypeWIFI + InterfaceTypeCellular = C.InterfaceTypeCellular + InterfaceTypeEthernet = C.InterfaceTypeEthernet + InterfaceTypeOther = C.InterfaceTypeOther +) + type NetworkInterface struct { Index int32 MTU int32 Name string Addresses StringIterator Flags int32 + + Type string + DNSServer StringIterator + Metered bool } type WIFIState struct { diff --git a/experimental/libbox/platform/interface.go b/experimental/libbox/platform/interface.go index 16eab5abcc..ef37daea22 100644 --- a/experimental/libbox/platform/interface.go +++ b/experimental/libbox/platform/interface.go @@ -5,7 +5,6 @@ import ( "github.com/sagernet/sing-box/common/process" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" - "github.com/sagernet/sing/common/control" "github.com/sagernet/sing/common/logger" ) @@ -14,10 +13,8 @@ type Interface interface { UsePlatformAutoDetectInterfaceControl() bool AutoDetectInterfaceControl(fd int) error OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) - UsePlatformDefaultInterfaceMonitor() bool CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor - UsePlatformInterfaceGetter() bool - Interfaces() ([]control.Interface, error) + Interfaces() ([]adapter.NetworkInterface, error) UnderNetworkExtension() bool IncludeAllNetworks() bool ClearDNSCache() diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 9b7181c8b8..28ae03d3c0 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -6,6 +6,7 @@ import ( "os" "runtime" runtimeDebug "runtime/debug" + "sync" "syscall" "time" @@ -54,7 +55,10 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box ctx, cancel := context.WithCancel(ctx) urlTestHistoryStorage := urltest.NewHistoryStorage() ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage) - platformWrapper := &platformInterfaceWrapper{iif: platformInterface, useProcFS: platformInterface.UseProcFS()} + platformWrapper := &platformInterfaceWrapper{ + iif: platformInterface, + useProcFS: platformInterface.UseProcFS(), + } service.MustRegister[platform.Interface](ctx, platformWrapper) instance, err := box.New(box.Options{ Context: ctx, @@ -119,9 +123,14 @@ var ( ) type platformInterfaceWrapper struct { - iif PlatformInterface - useProcFS bool - networkManager adapter.NetworkManager + iif PlatformInterface + useProcFS bool + networkManager adapter.NetworkManager + myTunName string + defaultInterfaceAccess sync.Mutex + defaultInterface *control.Interface + isExpensive bool + isConstrained bool } func (w *platformInterfaceWrapper) Initialize(networkManager adapter.NetworkManager) error { @@ -161,38 +170,42 @@ func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions return nil, E.Cause(err, "dup tun file descriptor") } options.FileDescriptor = dupFd + w.myTunName = options.Name return tun.New(*options) } -func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool { - return w.iif.UsePlatformDefaultInterfaceMonitor() -} - func (w *platformInterfaceWrapper) CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor { return &platformDefaultInterfaceMonitor{ platformInterfaceWrapper: w, - defaultInterfaceIndex: -1, logger: logger, } } -func (w *platformInterfaceWrapper) UsePlatformInterfaceGetter() bool { - return w.iif.UsePlatformInterfaceGetter() -} - -func (w *platformInterfaceWrapper) Interfaces() ([]control.Interface, error) { +func (w *platformInterfaceWrapper) Interfaces() ([]adapter.NetworkInterface, error) { interfaceIterator, err := w.iif.GetInterfaces() if err != nil { return nil, err } - var interfaces []control.Interface + var interfaces []adapter.NetworkInterface for _, netInterface := range iteratorToArray[*NetworkInterface](interfaceIterator) { - interfaces = append(interfaces, control.Interface{ - Index: int(netInterface.Index), - MTU: int(netInterface.MTU), - Name: netInterface.Name, - Addresses: common.Map(iteratorToArray[string](netInterface.Addresses), netip.MustParsePrefix), - Flags: linkFlags(uint32(netInterface.Flags)), + if netInterface.Name == w.myTunName { + continue + } + w.defaultInterfaceAccess.Lock() + isDefault := w.defaultInterface != nil && int(netInterface.Index) == w.defaultInterface.Index + w.defaultInterfaceAccess.Unlock() + interfaces = append(interfaces, adapter.NetworkInterface{ + Interface: control.Interface{ + Index: int(netInterface.Index), + MTU: int(netInterface.MTU), + Name: netInterface.Name, + Addresses: common.Map(iteratorToArray[string](netInterface.Addresses), netip.MustParsePrefix), + Flags: linkFlags(uint32(netInterface.Flags)), + }, + Type: netInterface.Type, + DNSServers: iteratorToArray[string](netInterface.DNSServer), + Expensive: netInterface.Metered || isDefault && w.isExpensive, + Constrained: isDefault && w.isConstrained, }) } return interfaces, nil diff --git a/protocol/direct/loopback_detect.go b/protocol/direct/loopback_detect.go index 4bc1be3b04..7a62164ef5 100644 --- a/protocol/direct/loopback_detect.go +++ b/protocol/direct/loopback_detect.go @@ -33,7 +33,7 @@ func (l *loopBackDetector) NewConn(conn net.Conn) net.Conn { } if udpConn, isUDPConn := conn.(abstractUDPConn); isUDPConn { if !source.Addr().IsLoopback() { - _, err := l.networkManager.InterfaceFinder().InterfaceByAddr(source.Addr()) + _, err := l.networkManager.InterfaceFinder().ByAddr(source.Addr()) if err != nil { return conn } @@ -59,7 +59,7 @@ func (l *loopBackDetector) NewPacketConn(conn N.NetPacketConn, destination M.Soc return conn } if !source.Addr().IsLoopback() { - _, err := l.networkManager.InterfaceFinder().InterfaceByAddr(source.Addr()) + _, err := l.networkManager.InterfaceFinder().ByAddr(source.Addr()) if err != nil { return conn } @@ -82,7 +82,7 @@ func (l *loopBackDetector) CheckPacketConn(source netip.AddrPort, local netip.Ad return false } if !source.Addr().IsLoopback() { - _, err := l.networkManager.InterfaceFinder().InterfaceByAddr(source.Addr()) + _, err := l.networkManager.InterfaceFinder().ByAddr(source.Addr()) if err != nil { return false } diff --git a/route/network.go b/route/network.go index 912b6621e7..d4b8adcf41 100644 --- a/route/network.go +++ b/route/network.go @@ -3,9 +3,10 @@ package route import ( "context" "errors" - "net/netip" + "net" "os" "runtime" + "strings" "syscall" "github.com/sagernet/sing-box/adapter" @@ -15,33 +16,41 @@ import ( "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/winpowrprof" "github.com/sagernet/sing/service" "github.com/sagernet/sing/service/pause" + + "golang.org/x/exp/slices" ) var _ adapter.NetworkManager = (*NetworkManager)(nil) type NetworkManager struct { - logger logger.ContextLogger - interfaceFinder *control.DefaultInterfaceFinder + logger logger.ContextLogger + interfaceFinder *control.DefaultInterfaceFinder + networkInterfaces atomic.TypedValue[[]adapter.NetworkInterface] + autoDetectInterface bool defaultInterface string defaultMark uint32 autoRedirectOutputMark uint32 - networkMonitor tun.NetworkUpdateMonitor - interfaceMonitor tun.DefaultInterfaceMonitor - packageManager tun.PackageManager - powerListener winpowrprof.EventListener - pauseManager pause.Manager - platformInterface platform.Interface - outboundManager adapter.OutboundManager - wifiState adapter.WIFIState - started bool + + networkMonitor tun.NetworkUpdateMonitor + interfaceMonitor tun.DefaultInterfaceMonitor + packageManager tun.PackageManager + powerListener winpowrprof.EventListener + pauseManager pause.Manager + platformInterface platform.Interface + outboundManager adapter.OutboundManager + wifiState adapter.WIFIState + started bool } func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOptions option.RouteOptions) (*NetworkManager, error) { @@ -55,7 +64,7 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOp platformInterface: service.FromContext[platform.Interface](ctx), outboundManager: service.FromContext[adapter.OutboundManager](ctx), } - usePlatformDefaultInterfaceMonitor := nm.platformInterface != nil && nm.platformInterface.UsePlatformDefaultInterfaceMonitor() + usePlatformDefaultInterfaceMonitor := nm.platformInterface != nil enforceInterfaceMonitor := routeOptions.AutoDetectInterface if !usePlatformDefaultInterfaceMonitor { networkMonitor, err := tun.NewNetworkUpdateMonitor(logger) @@ -87,17 +96,17 @@ func (r *NetworkManager) Start(stage adapter.StartStage) error { monitor := taskmonitor.New(r.logger, C.StartTimeout) switch stage { case adapter.StartStateInitialize: - if r.interfaceMonitor != nil { - monitor.Start("initialize interface monitor") - err := r.interfaceMonitor.Start() + if r.networkMonitor != nil { + monitor.Start("initialize network monitor") + err := r.networkMonitor.Start() monitor.Finish() if err != nil { return err } } - if r.networkMonitor != nil { - monitor.Start("initialize network monitor") - err := r.networkMonitor.Start() + if r.interfaceMonitor != nil { + monitor.Start("initialize interface monitor") + err := r.interfaceMonitor.Start() monitor.Finish() if err != nil { return err @@ -148,20 +157,6 @@ func (r *NetworkManager) Start(stage adapter.StartStage) error { func (r *NetworkManager) Close() error { monitor := taskmonitor.New(r.logger, C.StopTimeout) var err error - if r.interfaceMonitor != nil { - monitor.Start("close interface monitor") - err = E.Append(err, r.interfaceMonitor.Close(), func(err error) error { - return E.Cause(err, "close interface monitor") - }) - monitor.Finish() - } - if r.networkMonitor != nil { - monitor.Start("close network monitor") - err = E.Append(err, r.networkMonitor.Close(), func(err error) error { - return E.Cause(err, "close network monitor") - }) - monitor.Finish() - } if r.packageManager != nil { monitor.Start("close package manager") err = E.Append(err, r.packageManager.Close(), func(err error) error { @@ -176,6 +171,20 @@ func (r *NetworkManager) Close() error { }) monitor.Finish() } + if r.interfaceMonitor != nil { + monitor.Start("close interface monitor") + err = E.Append(err, r.interfaceMonitor.Close(), func(err error) error { + return E.Cause(err, "close interface monitor") + }) + monitor.Finish() + } + if r.networkMonitor != nil { + monitor.Start("close network monitor") + err = E.Append(err, r.networkMonitor.Close(), func(err error) error { + return E.Cause(err, "close network monitor") + }) + monitor.Finish() + } return nil } @@ -184,18 +193,75 @@ func (r *NetworkManager) InterfaceFinder() control.InterfaceFinder { } func (r *NetworkManager) UpdateInterfaces() error { - if r.platformInterface == nil || !r.platformInterface.UsePlatformInterfaceGetter() { + if r.platformInterface == nil { return r.interfaceFinder.Update() } else { interfaces, err := r.platformInterface.Interfaces() if err != nil { return err } - r.interfaceFinder.UpdateInterfaces(interfaces) + if C.IsDarwin { + err = r.interfaceFinder.Update() + if err != nil { + return err + } + // NEInterface only provides name,index and type + interfaces = common.Map(interfaces, func(it adapter.NetworkInterface) adapter.NetworkInterface { + iif, _ := r.interfaceFinder.ByIndex(it.Index) + if iif != nil { + it.Interface = *iif + } + return it + }) + } else { + r.interfaceFinder.UpdateInterfaces(common.Map(interfaces, func(it adapter.NetworkInterface) control.Interface { return it.Interface })) + } + oldInterfaces := r.networkInterfaces.Load() + newInterfaces := common.Filter(interfaces, func(it adapter.NetworkInterface) bool { + return it.Flags&net.FlagUp != 0 + }) + r.networkInterfaces.Store(newInterfaces) + if len(newInterfaces) > 0 && !slices.EqualFunc(oldInterfaces, newInterfaces, func(oldInterface adapter.NetworkInterface, newInterface adapter.NetworkInterface) bool { + return oldInterface.Interface.Index == newInterface.Interface.Index && + oldInterface.Interface.Name == newInterface.Interface.Name && + oldInterface.Interface.Flags == newInterface.Interface.Flags && + oldInterface.Type == newInterface.Type && + oldInterface.Expensive == newInterface.Expensive && + oldInterface.Constrained == newInterface.Constrained + }) { + r.logger.Info("updated available networks: ", strings.Join(common.Map(newInterfaces, func(it adapter.NetworkInterface) string { + var options []string + options = append(options, F.ToString(it.Type)) + if it.Expensive { + options = append(options, "expensive") + } + if it.Constrained { + options = append(options, "constrained") + } + return F.ToString(it.Name, " (", strings.Join(options, ", "), ")") + }), ", ")) + } return nil } } +func (r *NetworkManager) DefaultNetworkInterface() *adapter.NetworkInterface { + iif := r.interfaceMonitor.DefaultInterface() + if iif == nil { + return nil + } + for _, it := range r.networkInterfaces.Load() { + if it.Interface.Index == iif.Index { + return &it + } + } + return &adapter.NetworkInterface{Interface: *iif} +} + +func (r *NetworkManager) NetworkInterfaces() []adapter.NetworkInterface { + return r.networkInterfaces.Load() +} + func (r *NetworkManager) DefaultInterface() string { return r.defaultInterface } @@ -217,18 +283,17 @@ func (r *NetworkManager) AutoDetectInterfaceFunc() control.Func { } return control.BindToInterfaceFunc(r.interfaceFinder, func(network string, address string) (interfaceName string, interfaceIndex int, err error) { remoteAddr := M.ParseSocksaddr(address).Addr - if C.IsLinux { - interfaceName, interfaceIndex = r.interfaceMonitor.DefaultInterface(remoteAddr) - if interfaceIndex == -1 { - err = tun.ErrNoRoute - } - } else { - interfaceIndex = r.interfaceMonitor.DefaultInterfaceIndex(remoteAddr) - if interfaceIndex == -1 { - err = tun.ErrNoRoute + if remoteAddr.IsValid() { + iif, err := r.interfaceFinder.ByAddr(remoteAddr) + if err == nil { + return iif.Name, iif.Index, nil } } - return + defaultInterface := r.interfaceMonitor.DefaultInterface() + if defaultInterface == nil { + return "", -1, tun.ErrNoRoute + } + return defaultInterface.Name, defaultInterface.Index, nil }) } } @@ -282,6 +347,12 @@ func (r *NetworkManager) notifyNetworkUpdate(event int) { r.logger.Error("missing default interface") } else { r.pauseManager.NetworkWake() + defaultInterface := r.DefaultNetworkInterface() + if defaultInterface == nil { + panic("invalid interface context") + } + var options []string + options = append(options, F.ToString("index ", defaultInterface.Index)) if C.IsAndroid && r.platformInterface == nil { var vpnStatus string if r.interfaceMonitor.AndroidVPNEnabled() { @@ -289,17 +360,24 @@ func (r *NetworkManager) notifyNetworkUpdate(event int) { } else { vpnStatus = "disabled" } - r.logger.Info("updated default interface ", r.interfaceMonitor.DefaultInterfaceName(netip.IPv4Unspecified()), ", index ", r.interfaceMonitor.DefaultInterfaceIndex(netip.IPv4Unspecified()), ", vpn ", vpnStatus) + options = append(options, "vpn "+vpnStatus) } else { - r.logger.Info("updated default interface ", r.interfaceMonitor.DefaultInterfaceName(netip.IPv4Unspecified()), ", index ", r.interfaceMonitor.DefaultInterfaceIndex(netip.IPv4Unspecified())) + if defaultInterface.Type != "" { + options = append(options, F.ToString("type ", defaultInterface.Type)) + } + if defaultInterface.Expensive { + options = append(options, "expensive") + } + if defaultInterface.Constrained { + options = append(options, "constrained") + } } + r.logger.Info("updated default interface ", defaultInterface.Name, ", ", strings.Join(options, ", ")) if r.platformInterface != nil { state := r.platformInterface.ReadWIFIState() if state != r.wifiState { r.wifiState = state - if state.SSID == "" && state.BSSID == "" { - r.logger.Info("updated WIFI state: disconnected") - } else { + if state.SSID != "" { r.logger.Info("updated WIFI state: SSID=", state.SSID, ", BSSID=", state.BSSID) } } @@ -309,7 +387,6 @@ func (r *NetworkManager) notifyNetworkUpdate(event int) { if !r.started { return } - r.ResetNetwork() } diff --git a/transport/dhcp/server.go b/transport/dhcp/server.go index dfe33d86c2..9a06ac1716 100644 --- a/transport/dhcp/server.go +++ b/transport/dhcp/server.go @@ -119,18 +119,19 @@ func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, return nil, err } -func (t *Transport) fetchInterface() (*net.Interface, error) { - interfaceName := t.interfaceName +func (t *Transport) fetchInterface() (*control.Interface, error) { if t.autoInterface { if t.networkManager.InterfaceMonitor() == nil { return nil, E.New("missing monitor for auto DHCP, set route.auto_detect_interface") } - interfaceName = t.networkManager.InterfaceMonitor().DefaultInterfaceName(netip.Addr{}) - } - if interfaceName == "" { - return nil, E.New("missing default interface") + defaultInterface := t.networkManager.InterfaceMonitor().DefaultInterface() + if defaultInterface == nil { + return nil, E.New("missing default interface") + } + return defaultInterface, nil + } else { + return t.networkManager.InterfaceFinder().ByName(t.interfaceName) } - return net.InterfaceByName(interfaceName) } func (t *Transport) fetchServers() error { @@ -172,7 +173,7 @@ func (t *Transport) interfaceUpdated(int) { } } -func (t *Transport) fetchServers0(ctx context.Context, iface *net.Interface) error { +func (t *Transport) fetchServers0(ctx context.Context, iface *control.Interface) error { var listener net.ListenConfig listener.Control = control.Append(listener.Control, control.BindToInterface(t.networkManager.InterfaceFinder(), iface.Name, iface.Index)) listener.Control = control.Append(listener.Control, control.ReuseAddr()) @@ -206,7 +207,7 @@ func (t *Transport) fetchServers0(ctx context.Context, iface *net.Interface) err return group.Run(ctx) } -func (t *Transport) fetchServersResponse(iface *net.Interface, packetConn net.PacketConn, transactionID dhcpv4.TransactionID) error { +func (t *Transport) fetchServersResponse(iface *control.Interface, packetConn net.PacketConn, transactionID dhcpv4.TransactionID) error { buffer := buf.NewSize(dhcpv4.MaxMessageSize) defer buffer.Release() @@ -246,7 +247,7 @@ func (t *Transport) fetchServersResponse(iface *net.Interface, packetConn net.Pa } } -func (t *Transport) recreateServers(iface *net.Interface, serverAddrs []netip.Addr) error { +func (t *Transport) recreateServers(iface *control.Interface, serverAddrs []netip.Addr) error { if len(serverAddrs) > 0 { t.options.Logger.Info("dhcp: updated DNS servers from ", iface.Name, ": [", strings.Join(common.Map(serverAddrs, func(it netip.Addr) string { return it.String() From f2780d0713fcaf8725618f3b571eb6743a126408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 11 Nov 2024 16:32:34 +0800 Subject: [PATCH 18/49] Merge route options to route actions --- experimental/deprecated/constants.go | 10 ------ option/rule_action.go | 43 ++++++------------------- route/rule/rule_action.go | 48 +++++++++++++++++++++------- 3 files changed, 45 insertions(+), 56 deletions(-) diff --git a/experimental/deprecated/constants.go b/experimental/deprecated/constants.go index 8f7eee11ff..918b698df9 100644 --- a/experimental/deprecated/constants.go +++ b/experimental/deprecated/constants.go @@ -104,15 +104,6 @@ var OptionInboundOptions = Note{ MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions", } -var OptionLegacyDNSRouteOptions = Note{ - Name: "legacy-dns-route-options", - Description: "legacy dns route options", - DeprecatedVersion: "1.11.0", - ScheduledVersion: "1.12.0", - EnvName: "LEGACY_DNS_ROUTE_OPTIONS", - MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-legacy-dns-route-options-to-rule-actions", -} - var Options = []Note{ OptionBadMatchSource, OptionGEOIP, @@ -120,5 +111,4 @@ var Options = []Note{ OptionTUNAddressX, OptionSpecialOutbounds, OptionInboundOptions, - OptionLegacyDNSRouteOptions, } diff --git a/option/rule_action.go b/option/rule_action.go index 3b4e8edb5f..9bc1303933 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -7,8 +7,7 @@ import ( "time" C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/experimental/deprecated" - dns "github.com/sagernet/sing-dns" + "github.com/sagernet/sing-dns" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/json" "github.com/sagernet/sing/common/json/badjson" @@ -137,18 +136,10 @@ func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) e return badjson.UnmarshallExcludedContext(ctx, data, (*_DNSRuleAction)(r), v) } -type _RouteActionOptions struct { - Outbound string `json:"outbound,omitempty"` -} - -type RouteActionOptions _RouteActionOptions - -func (r *RouteActionOptions) UnmarshalJSON(data []byte) error { - err := json.Unmarshal(data, (*_RouteActionOptions)(r)) - if err != nil { - return err - } - return nil +type RouteActionOptions struct { + Outbound string `json:"outbound,omitempty"` + UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` + UDPConnect bool `json:"udp_connect,omitempty"` } type _RouteOptionsActionOptions struct { @@ -169,29 +160,13 @@ func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error { return nil } -type _DNSRouteActionOptions struct { - Server string `json:"server,omitempty"` - // Deprecated: Use DNSRouteOptionsActionOptions instead. - DisableCache bool `json:"disable_cache,omitempty"` - // Deprecated: Use DNSRouteOptionsActionOptions instead. - RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` - // Deprecated: Use DNSRouteOptionsActionOptions instead. +type DNSRouteActionOptions struct { + Server string `json:"server,omitempty"` + DisableCache bool `json:"disable_cache,omitempty"` + RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } -type DNSRouteActionOptions _DNSRouteActionOptions - -func (r *DNSRouteActionOptions) UnmarshalJSONContext(ctx context.Context, data []byte) error { - err := json.Unmarshal(data, (*_DNSRouteActionOptions)(r)) - if err != nil { - return err - } - if r.DisableCache || r.RewriteTTL != nil || r.ClientSubnet != nil { - deprecated.Report(ctx, deprecated.OptionLegacyDNSRouteOptions) - } - return nil -} - type _DNSRouteOptionsActionOptions struct { DisableCache bool `json:"disable_cache,omitempty"` RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"` diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index 00579c18d5..d44e36eeac 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -29,6 +29,10 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti case C.RuleActionTypeRoute: return &RuleActionRoute{ Outbound: action.RouteOptions.Outbound, + RuleActionRouteOptions: RuleActionRouteOptions{ + UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping, + UDPConnect: action.RouteOptions.UDPConnect, + }, }, nil case C.RuleActionTypeRouteOptions: return &RuleActionRouteOptions{ @@ -85,10 +89,12 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) return nil case C.RuleActionTypeRoute: return &RuleActionDNSRoute{ - Server: action.RouteOptions.Server, - DisableCache: action.RouteOptions.DisableCache, - RewriteTTL: action.RouteOptions.RewriteTTL, - ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), + Server: action.RouteOptions.Server, + RuleActionDNSRouteOptions: RuleActionDNSRouteOptions{ + DisableCache: action.RouteOptions.DisableCache, + RewriteTTL: action.RouteOptions.RewriteTTL, + ClientSubnet: netip.Prefix(common.PtrValueOrDefault(action.RouteOptions.ClientSubnet)), + }, } case C.RuleActionTypeRouteOptions: return &RuleActionDNSRouteOptions{ @@ -109,6 +115,7 @@ func NewDNSRuleAction(logger logger.ContextLogger, action option.DNSRuleAction) type RuleActionRoute struct { Outbound string + RuleActionRouteOptions } func (r *RuleActionRoute) Type() string { @@ -116,7 +123,15 @@ func (r *RuleActionRoute) Type() string { } func (r *RuleActionRoute) String() string { - return F.ToString("route(", r.Outbound, ")") + var descriptions []string + descriptions = append(descriptions, r.Outbound) + if r.UDPDisableDomainUnmapping { + descriptions = append(descriptions, "udp-disable-domain-unmapping") + } + if r.UDPConnect { + descriptions = append(descriptions, "udp-connect") + } + return F.ToString("route(", strings.Join(descriptions, ","), ")") } type RuleActionRouteOptions struct { @@ -140,10 +155,8 @@ func (r *RuleActionRouteOptions) String() string { } type RuleActionDNSRoute struct { - Server string - DisableCache bool - RewriteTTL *uint32 - ClientSubnet netip.Prefix + Server string + RuleActionDNSRouteOptions } func (r *RuleActionDNSRoute) Type() string { @@ -151,7 +164,18 @@ func (r *RuleActionDNSRoute) Type() string { } func (r *RuleActionDNSRoute) String() string { - return F.ToString("route(", r.Server, ")") + var descriptions []string + descriptions = append(descriptions, r.Server) + if r.DisableCache { + descriptions = append(descriptions, "disable-cache") + } + if r.RewriteTTL != nil { + descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL)) + } + if r.ClientSubnet.IsValid() { + descriptions = append(descriptions, F.ToString("client-subnet=", r.ClientSubnet)) + } + return F.ToString("route(", strings.Join(descriptions, ","), ")") } type RuleActionDNSRouteOptions struct { @@ -170,10 +194,10 @@ func (r *RuleActionDNSRouteOptions) String() string { descriptions = append(descriptions, "disable-cache") } if r.RewriteTTL != nil { - descriptions = append(descriptions, F.ToString("rewrite-ttl(", *r.RewriteTTL, ")")) + descriptions = append(descriptions, F.ToString("rewrite-ttl=", *r.RewriteTTL)) } if r.ClientSubnet.IsValid() { - descriptions = append(descriptions, F.ToString("client-subnet(", r.ClientSubnet, ")")) + descriptions = append(descriptions, F.ToString("client-subnet=", r.ClientSubnet)) } return F.ToString("route-options(", strings.Join(descriptions, ","), ")") } From 7819f134890f0276f9c0b4affaf9ade8a56a37e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 11 Nov 2024 16:30:25 +0800 Subject: [PATCH 19/49] Add `network_[type/is_expensive/is_constrained]` rule items --- .../convertor/adguard/convertor_test.go | 7 +- common/srs/binary.go | 30 ++ constant/rule.go | 3 +- docs/configuration/dns/rule.md | 43 ++- docs/configuration/dns/rule.zh.md | 91 ++++-- docs/configuration/route/rule.md | 43 ++- docs/configuration/route/rule.zh.md | 50 +++- docs/configuration/rule-set/adguard.zh.md | 70 +++++ docs/configuration/rule-set/headless-rule.md | 48 ++++ .../rule-set/headless-rule.zh.md | 258 ++++++++++++++++++ docs/configuration/rule-set/index.md | 2 +- docs/configuration/rule-set/index.zh.md | 117 ++++++++ docs/configuration/rule-set/source-format.md | 21 +- .../rule-set/source-format.zh.md | 46 ++++ option/rule.go | 3 + option/rule_dns.go | 3 + option/rule_set.go | 47 ++-- route/rule/rule_default.go | 15 + route/rule/rule_dns.go | 15 + route/rule/rule_headless.go | 25 +- .../rule/rule_item_network_is_constrained.go | 29 ++ route/rule/rule_item_network_is_expensive.go | 29 ++ route/rule/rule_item_network_type.go | 39 +++ 23 files changed, 967 insertions(+), 67 deletions(-) create mode 100644 docs/configuration/rule-set/adguard.zh.md create mode 100644 docs/configuration/rule-set/headless-rule.zh.md create mode 100644 docs/configuration/rule-set/index.zh.md create mode 100644 docs/configuration/rule-set/source-format.zh.md create mode 100644 route/rule/rule_item_network_is_constrained.go create mode 100644 route/rule/rule_item_network_is_expensive.go create mode 100644 route/rule/rule_item_network_type.go diff --git a/cmd/sing-box/internal/convertor/adguard/convertor_test.go b/cmd/sing-box/internal/convertor/adguard/convertor_test.go index 6098485cf0..212c21703a 100644 --- a/cmd/sing-box/internal/convertor/adguard/convertor_test.go +++ b/cmd/sing-box/internal/convertor/adguard/convertor_test.go @@ -1,6 +1,7 @@ package adguard import ( + "context" "strings" "testing" @@ -26,7 +27,7 @@ example.arpa `)) require.NoError(t, err) require.Len(t, rules, 1) - rule, err := rule.NewHeadlessRule(nil, rules[0]) + rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) require.NoError(t, err) matchDomain := []string{ "example.org", @@ -85,7 +86,7 @@ func TestHosts(t *testing.T) { `)) require.NoError(t, err) require.Len(t, rules, 1) - rule, err := rule.NewHeadlessRule(nil, rules[0]) + rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) require.NoError(t, err) matchDomain := []string{ "google.com", @@ -115,7 +116,7 @@ www.example.org `)) require.NoError(t, err) require.Len(t, rules, 1) - rule, err := rule.NewHeadlessRule(nil, rules[0]) + rule, err := rule.NewHeadlessRule(context.Background(), rules[0]) require.NoError(t, err) matchDomain := []string{ "example.com", diff --git a/common/srs/binary.go b/common/srs/binary.go index dd670fc83e..fbed78adc5 100644 --- a/common/srs/binary.go +++ b/common/srs/binary.go @@ -38,6 +38,9 @@ const ( ruleItemWIFIBSSID ruleItemAdGuardDomain ruleItemProcessPathRegex + ruleItemNetworkType + ruleItemNetworkIsExpensive + ruleItemNetworkIsConstrained ruleItemFinal uint8 = 0xFF ) @@ -222,6 +225,12 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea return } rule.AdGuardDomainMatcher = matcher + case ruleItemNetworkType: + rule.NetworkType, err = readRuleItemString(reader) + case ruleItemNetworkIsExpensive: + rule.NetworkIsExpensive = true + case ruleItemNetworkIsConstrained: + rule.NetworkIsConstrained = true case ruleItemFinal: err = binary.Read(reader, binary.BigEndian, &rule.Invert) return @@ -336,6 +345,27 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen return err } } + if len(rule.NetworkType) > 0 { + if generateVersion < C.RuleSetVersion3 { + return E.New("network_type rule item is only supported in version 3 or later") + } + err = writeRuleItemString(writer, ruleItemNetworkType, rule.NetworkType) + if err != nil { + return err + } + } + if rule.NetworkIsExpensive { + err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsExpensive) + if err != nil { + return err + } + } + if rule.NetworkIsConstrained { + err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsConstrained) + if err != nil { + return err + } + } if len(rule.WIFISSID) > 0 { err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID) if err != nil { diff --git a/constant/rule.go b/constant/rule.go index b1f91c608c..c4a778384f 100644 --- a/constant/rule.go +++ b/constant/rule.go @@ -21,7 +21,8 @@ const ( const ( RuleSetVersion1 = 1 + iota RuleSetVersion2 - RuleSetVersionCurrent = RuleSetVersion2 + RuleSetVersion3 + RuleSetVersionCurrent = RuleSetVersion3 ) const ( diff --git a/docs/configuration/dns/rule.md b/docs/configuration/dns/rule.md index 5cb24e8197..1f04b2999d 100644 --- a/docs/configuration/dns/rule.md +++ b/docs/configuration/dns/rule.md @@ -8,7 +8,10 @@ icon: material/new-box :material-alert: [server](#server) :material-alert: [disable_cache](#disable_cache) :material-alert: [rewrite_ttl](#rewrite_ttl) - :material-alert: [client_subnet](#client_subnet) + :material-alert: [client_subnet](#client_subnet) + :material-plus: [network_type](#network_type) + :material-plus: [network_is_expensive](#network_is_expensive) + :material-plus: [network_is_constrained](#network_is_constrained) !!! quote "Changes in sing-box 1.10.0" @@ -125,6 +128,11 @@ icon: material/new-box 1000 ], "clash_mode": "direct", + "network_type": [ + "wifi" + ], + "network_is_expensive": false, + "network_is_constrained": false, "wifi_ssid": [ "My WIFI" ], @@ -310,6 +318,39 @@ Match user id. Match Clash mode. +#### network_type + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Match network type. + +Available values: `wifi`, `cellular`, `ethernet` and `other`. + +#### network_is_expensive + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Match if network is considered Metered (on Android) or considered expensive, +such as Cellular or a Personal Hotspot (on Apple platforms). + +#### network_is_constrained + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Apple platforms. + +Match if network is in Low Data Mode. + #### wifi_ssid !!! quote "" diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index 205b01ae00..e904f8cd02 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -2,6 +2,17 @@ icon: material/new-box --- +!!! quote "sing-box 1.11.0 中的更改" + + :material-plus: [action](#action) + :material-alert: [server](#server) + :material-alert: [disable_cache](#disable_cache) + :material-alert: [rewrite_ttl](#rewrite_ttl) + :material-alert: [client_subnet](#client_subnet) + :material-plus: [network_type](#network_type) + :material-plus: [network_is_expensive](#network_is_expensive) + :material-plus: [network_is_constrained](#network_is_constrained) + !!! quote "sing-box 1.10.0 中的更改" :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) @@ -117,6 +128,11 @@ icon: material/new-box 1000 ], "clash_mode": "direct", + "network_type": [ + "wifi" + ], + "network_is_expensive": false, + "network_is_constrained": false, "wifi_ssid": [ "My WIFI" ], @@ -135,17 +151,15 @@ icon: material/new-box "outbound": [ "direct" ], - "server": "local", - "disable_cache": false, - "client_subnet": "127.0.0.1/24" + "action": "route", + "server": "local" }, { "type": "logical", "mode": "and", "rules": [], - "server": "local", - "disable_cache": false, - "client_subnet": "127.0.0.1/24" + "action": "route", + "server": "local" } ] } @@ -304,6 +318,39 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 匹配 Clash 模式。 +#### network_type + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配网络类型。 + +Available values: `wifi`, `cellular`, `ethernet` and `other`. + +#### network_is_expensive + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配如果网络被视为计费 (在 Android) 或被视为昂贵, +像蜂窝网络或个人热点 (在 Apple 平台)。 + +#### network_is_constrained + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Apple 平台图形客户端中支持。 + +匹配如果网络在低数据模式下。 + #### wifi_ssid !!! quote "" @@ -352,29 +399,35 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 `any` 可作为值用于匹配任意出站。 -#### server +#### action ==必填== -目标 DNS 服务器的标签。 +参阅 [规则动作](../rule_action/)。 + +#### server + +!!! failure "已在 sing-box 1.11.0 废弃" + + 已移动到 [DNS 规则动作](../rule_action#route). #### disable_cache -在此查询中禁用缓存。 +!!! failure "已在 sing-box 1.11.0 废弃" + + 已移动到 [DNS 规则动作](../rule_action#route). #### rewrite_ttl -重写 DNS 回应中的 TTL。 +!!! failure "已在 sing-box 1.11.0 废弃" -#### client_subnet + 已移动到 [DNS 规则动作](../rule_action#route). -!!! question "自 sing-box 1.9.0 起" - -默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。 +#### client_subnet -如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。 +!!! failure "已在 sing-box 1.11.0 废弃" -将覆盖 `dns.client_subnet` 与 `servers.[].client_subnet`。 + 已移动到 [DNS 规则动作](../rule_action#route). ### 地址筛选字段 @@ -420,8 +473,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。 #### mode +==必填== + `and` 或 `or` #### rules -包括的规则。 +==必填== + +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/route/rule.md b/docs/configuration/route/rule.md index fe40d56518..43954a78c9 100644 --- a/docs/configuration/route/rule.md +++ b/docs/configuration/route/rule.md @@ -5,7 +5,10 @@ icon: material/new-box !!! quote "Changes in sing-box 1.11.0" :material-plus: [action](#action) - :material-alert: [outbound](#outbound) + :material-alert: [outbound](#outbound) + :material-plus: [network_type](#network_type) + :material-plus: [network_is_expensive](#network_is_expensive) + :material-plus: [network_is_constrained](#network_is_constrained) !!! quote "Changes in sing-box 1.10.0" @@ -120,6 +123,11 @@ icon: material/new-box 1000 ], "clash_mode": "direct", + "network_type": [ + "wifi" + ], + "network_is_expensive": false, + "network_is_constrained": false, "wifi_ssid": [ "My WIFI" ], @@ -322,6 +330,39 @@ Match user id. Match Clash mode. +#### network_type + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Match network type. + +Available values: `wifi`, `cellular`, `ethernet` and `other`. + +#### network_is_expensive + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Match if network is considered Metered (on Android) or considered expensive, +such as Cellular or a Personal Hotspot (on Apple platforms). + +#### network_is_constrained + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Apple platforms. + +Match if network is in Low Data Mode. + #### wifi_ssid !!! quote "" diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index 316339f677..e5c5f01776 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -5,7 +5,10 @@ icon: material/new-box !!! quote "sing-box 1.11.0 中的更改" :material-plus: [action](#action) - :material-alert: [outbound](#outbound) + :material-alert: [outbound](#outbound) + :material-plus: [network_type](#network_type) + :material-plus: [network_is_expensive](#network_is_expensive) + :material-plus: [network_is_constrained](#network_is_constrained) !!! quote "sing-box 1.10.0 中的更改" @@ -13,7 +16,6 @@ icon: material/new-box :material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source) :material-plus: [process_path_regex](#process_path_regex) - !!! quote "sing-box 1.8.0 中的更改" :material-plus: [rule_set](#rule_set) @@ -118,6 +120,11 @@ icon: material/new-box 1000 ], "clash_mode": "direct", + "network_type": [ + "wifi" + ], + "network_is_expensive": false, + "network_is_constrained": false, "wifi_ssid": [ "My WIFI" ], @@ -153,7 +160,7 @@ icon: material/new-box 当内容只有一项时,可以忽略 JSON 数组 [] 标签。 -### Default Fields +### 默认字段 !!! note "" @@ -320,6 +327,39 @@ icon: material/new-box 匹配 Clash 模式。 +#### network_type + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配网络类型。 + +Available values: `wifi`, `cellular`, `ethernet` and `other`. + +#### network_is_expensive + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配如果网络被视为计费 (在 Android) 或被视为昂贵, +像蜂窝网络或个人热点 (在 Apple 平台)。 + +#### network_is_constrained + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Apple 平台图形客户端中支持。 + +匹配如果网络在低数据模式下。 + #### wifi_ssid !!! quote "" @@ -366,13 +406,13 @@ icon: material/new-box ==必填== -参阅 [规则行动](../rule_action/)。 +参阅 [规则动作](../rule_action/)。 #### outbound !!! failure "已在 sing-box 1.11.0 废弃" - 已移动到 [规则行动](../rule_action#route). + 已移动到 [规则动作](../rule_action#route). ### 逻辑字段 diff --git a/docs/configuration/rule-set/adguard.zh.md b/docs/configuration/rule-set/adguard.zh.md new file mode 100644 index 0000000000..b08ab34b4d --- /dev/null +++ b/docs/configuration/rule-set/adguard.zh.md @@ -0,0 +1,70 @@ +--- +icon: material/new-box +--- + +# AdGuard DNS Filter + +!!! question "自 sing-box 1.10.0 起" + +sing-box 支持其他项目的一些规则集格式,这些格式无法完全转换为 sing-box, +目前只有 AdGuard DNS Filter。 + +这些格式不直接作为源格式支持, +而是需要将它们转换为二进制规则集。 + +## 转换 + +使用 `sing-box rule-set convert --type adguard [--output .srs] .txt` 以转换为二进制规则集。 + +## 性能 + +AdGuard 将所有规则保存在内存中并按顺序匹配, +而 sing-box 选择高性能和较小的内存使用量。 +作为权衡,您无法知道匹配了哪个规则项。 + +## 兼容性 + +[AdGuardSDNSFilter](https://github.com/AdguardTeam/AdGuardSDNSFilter) +中的几乎所有规则以及 [adguard-filter-list](https://github.com/ppfeufer/adguard-filter-list) +中列出的规则集中的规则均受支持。 + +## 支持的格式 + +### AdGuard Filter + +#### 基本规则语法 + +| 语法 | 支持 | +|--------|------------------| +| `@@` | :material-check: | +| `\|\|` | :material-check: | +| `\|` | :material-check: | +| `^` | :material-check: | +| `*` | :material-check: | + +#### 主机语法 + +| 语法 | 示例 | 支持 | +|-------------|--------------------------|--------------------------| +| Scheme | `https://` | :material-alert: Ignored | +| Domain Host | `example.org` | :material-check: | +| IP Host | `1.1.1.1`, `10.0.0.` | :material-close: | +| Regexp | `/regexp/` | :material-check: | +| Port | `example.org:80` | :material-close: | +| Path | `example.org/path/ad.js` | :material-close: | + +#### 描述符语法 + +| 描述符 | 支持 | +|-----------------------|--------------------------| +| `$important` | :material-check: | +| `$dnsrewrite=0.0.0.0` | :material-alert: Ignored | +| 任何其他描述符 | :material-close: | + +### Hosts + +只有 IP 地址为 `0.0.0.0` 的条目将被接受。 + +### 简易 + +当所有行都是有效域时,它们被视为简单的逐行域规则, 与 hosts 一样,只匹配完全相同的域。 \ No newline at end of file diff --git a/docs/configuration/rule-set/headless-rule.md b/docs/configuration/rule-set/headless-rule.md index 891b127840..bdad22f0dc 100644 --- a/docs/configuration/rule-set/headless-rule.md +++ b/docs/configuration/rule-set/headless-rule.md @@ -1,3 +1,13 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.11.0" + + :material-plus: [network_type](#network_type) + :material-plus: [network_is_expensive](#network_is_expensive) + :material-plus: [network_is_constrained](#network_is_constrained) + ### Structure !!! question "Since sing-box 1.8.0" @@ -63,6 +73,11 @@ "package_name": [ "com.termux" ], + "network_type": [ + "wifi" + ], + "network_is_expensive": false, + "network_is_constrained": false, "wifi_ssid": [ "My WIFI" ], @@ -177,6 +192,39 @@ Match process path using regular expression. Match android package name. +#### network_type + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Match network type. + +Available values: `wifi`, `cellular`, `ethernet` and `other`. + +#### network_is_expensive + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms. + +Match if network is considered Metered (on Android) or considered expensive, +such as Cellular or a Personal Hotspot (on Apple platforms). + +#### network_is_constrained + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Apple platforms. + +Match if network is in Low Data Mode. + #### wifi_ssid !!! quote "" diff --git a/docs/configuration/rule-set/headless-rule.zh.md b/docs/configuration/rule-set/headless-rule.zh.md new file mode 100644 index 0000000000..c5281504f6 --- /dev/null +++ b/docs/configuration/rule-set/headless-rule.zh.md @@ -0,0 +1,258 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.11.0 中的更改" + + :material-plus: [network_type](#network_type) + :material-alert: [network_is_expensive](#network_is_expensive) + :material-alert: [network_is_constrained](#network_is_constrained) + +### 结构 + +!!! question "自 sing-box 1.8.0 起" + +```json +{ + "rules": [ + { + "query_type": [ + "A", + "HTTPS", + 32768 + ], + "network": [ + "tcp" + ], + "domain": [ + "test.com" + ], + "domain_suffix": [ + ".cn" + ], + "domain_keyword": [ + "test" + ], + "domain_regex": [ + "^stun\\..+" + ], + "source_ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "ip_cidr": [ + "10.0.0.0/24", + "192.168.0.1" + ], + "source_port": [ + 12345 + ], + "source_port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "port": [ + 80, + 443 + ], + "port_range": [ + "1000:2000", + ":3000", + "4000:" + ], + "process_name": [ + "curl" + ], + "process_path": [ + "/usr/bin/curl" + ], + "process_path_regex": [ + "^/usr/bin/.+" + ], + "package_name": [ + "com.termux" + ], + "network_type": [ + "wifi" + ], + "network_is_expensive": false, + "network_is_constrained": false, + "wifi_ssid": [ + "My WIFI" + ], + "wifi_bssid": [ + "00:00:00:00:00:00" + ], + "invert": false + }, + { + "type": "logical", + "mode": "and", + "rules": [], + "invert": false + } + ] +} +``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签。 + +### Default Fields + +!!! note "" + + 默认规则使用以下匹配逻辑: + (`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `ip_cidr`) && + (`port` || `port_range`) && + (`source_port` || `source_port_range`) && + `other fields` + +#### query_type + +DNS 查询类型。值可以为整数或者类型名称字符串。 + +#### network + +`tcp` 或 `udp`。 + +#### domain + +匹配完整域名。 + +#### domain_suffix + +匹配域名后缀。 + +#### domain_keyword + +匹配域名关键字。 + +#### domain_regex + +匹配域名正则表达式。 + +#### source_ip_cidr + +匹配源 IP CIDR。 + +#### ip_cidr + +匹配 IP CIDR。 + +#### source_port + +匹配源端口。 + +#### source_port_range + +匹配源端口范围。 + +#### port + +匹配端口。 + +#### port_range + +匹配端口范围。 + +#### process_name + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS。 + +匹配进程名称。 + +#### process_path + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +匹配进程路径。 + +#### process_path_regex + +!!! question "自 sing-box 1.10.0 起" + +!!! quote "" + + 仅支持 Linux、Windows 和 macOS. + +使用正则表达式匹配进程路径。 + +#### package_name + +匹配 Android 应用包名。 + +#### network_type + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配网络类型。 + +Available values: `wifi`, `cellular`, `ethernet` and `other`. + +#### network_is_expensive + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配如果网络被视为计费 (在 Android) 或被视为昂贵, +像蜂窝网络或个人热点 (在 Apple 平台)。 + +#### network_is_constrained + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Apple 平台图形客户端中支持。 + +匹配如果网络在低数据模式下。 + +#### wifi_ssid + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +匹配 WiFi SSID。 + +#### wifi_bssid + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持。 + +#### invert + +反选匹配结果。 + +### 逻辑字段 + +#### type + +`logical` + +#### mode + +==必填== + +`and` 或 `or` + +#### rules + +==必填== + +包括的规则。 \ No newline at end of file diff --git a/docs/configuration/rule-set/index.md b/docs/configuration/rule-set/index.md index dfe71d9e39..bed3fb5466 100644 --- a/docs/configuration/rule-set/index.md +++ b/docs/configuration/rule-set/index.md @@ -74,7 +74,7 @@ Tag of rule-set. ==Required== -List of [Headless Rule](./headless-rule.md/). +List of [Headless Rule](../headless-rule/). ### Local or Remote Fields diff --git a/docs/configuration/rule-set/index.zh.md b/docs/configuration/rule-set/index.zh.md new file mode 100644 index 0000000000..083c06bd3a --- /dev/null +++ b/docs/configuration/rule-set/index.zh.md @@ -0,0 +1,117 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.10.0 中的更改" + + :material-plus: `type: inline` + +# 规则集 + +!!! question "自 sing-box 1.8.0 起" + +### 结构 + +=== "内联" + + !!! question "自 sing-box 1.10.0 起" + + ```json + { + "type": "inline", // 可选 + "tag": "", + "rules": [] + } + ``` + +=== "本地文件" + + ```json + { + "type": "local", + "tag": "", + "format": "source", // or binary + "path": "" + } + ``` + +=== "远程文件" + + !!! info "" + + 远程规则集将被缓存如果 `experimental.cache_file.enabled` 已启用。 + + ```json + { + "type": "remote", + "tag": "", + "format": "source", // or binary + "url": "", + "download_detour": "", // 可选 + "update_interval": "" // 可选 + } + ``` + +### 字段 + +#### type + +==必填== + +规则集类型, `local` 或 `remote`。 + +#### tag + +==必填== + +规则集的标签。 + +### 内联字段 + +!!! question "自 sing-box 1.10.0 起" + +#### rules + +==必填== + +一组 [无头规则](../headless-rule/). + +### 本地或远程字段 + +#### format + +==必填== + +规则集格式, `source` 或 `binary`。 + +### 本地字段 + +#### path + +==必填== + +!!! note "" + + 自 sing-box 1.10.0 起,文件更改时将自动重新加载。 + +规则集的文件路径。 + +### 远程字段 + +#### url + +==必填== + +规则集的下载 URL。 + +#### download_detour + +用于下载规则集的出站的标签。 + +如果为空,将使用默认出站。 + +#### update_interval + +规则集的更新间隔。 + +默认使用 `1d`。 diff --git a/docs/configuration/rule-set/source-format.md b/docs/configuration/rule-set/source-format.md index 8c46cbf02b..43e8ea4535 100644 --- a/docs/configuration/rule-set/source-format.md +++ b/docs/configuration/rule-set/source-format.md @@ -4,6 +4,10 @@ icon: material/new-box # Source Format +!!! quote "Changes in sing-box 1.11.0" + + :material-plus: version `3` + !!! quote "Changes in sing-box 1.10.0" :material-plus: version `2` @@ -14,7 +18,7 @@ icon: material/new-box ```json { - "version": 2, + "version": 3, "rules": [] } ``` @@ -29,19 +33,14 @@ Use `sing-box rule-set compile [--output .srs] .json` to c ==Required== -Version of rule-set, one of `1` or `2`. - -* 1: Initial rule-set version, since sing-box 1.8.0. -* 2: Optimized memory usages of `domain_suffix` rules. - -The new rule-set version `2` does not make any changes to the format, only affecting `binary` rule-sets compiled by command `rule-set compile` - -Since 1.10.0, the optimization is always applied to `source` rule-sets even if version is set to `1`. +Version of rule-set. -It is recommended to upgrade to `2` after sing-box 1.10.0 becomes a stable version. +* 1: sing-box 1.8.0: Initial rule-set version. +* 2: sing-box 1.10.0: Optimized memory usages of `domain_suffix` rules in binary rule-sets. +* 3: sing-box 1.11.0: Added `network_type`, `network_is_expensive` and `network_is_constrainted` rule items. #### rules ==Required== -List of [Headless Rule](./headless-rule.md/). +List of [Headless Rule](../headless-rule/). diff --git a/docs/configuration/rule-set/source-format.zh.md b/docs/configuration/rule-set/source-format.zh.md new file mode 100644 index 0000000000..0e8fc0cb96 --- /dev/null +++ b/docs/configuration/rule-set/source-format.zh.md @@ -0,0 +1,46 @@ +--- +icon: material/new-box +--- + +# 源文件格式 + +!!! quote "sing-box 1.11.0 中的更改" + + :material-plus: version `3` + +!!! quote "sing-box 1.10.0 中的更改" + + :material-plus: version `2` + +!!! question "自 sing-box 1.8.0 起" + +### 结构 + +```json +{ + "version": 3, + "rules": [] +} +``` + +### 编译 + +使用 `sing-box rule-set compile [--output .srs] .json` 以编译源文件为二进制规则集。 + +### 字段 + +#### version + +==必填== + +规则集版本。 + +* 1: sing-box 1.8.0: 初始规则集版本。 +* 2: sing-box 1.10.0: 优化了二进制规则集中 `domain_suffix` 规则的内存使用。 +* 3: sing-box 1.11.0: 添加了 `network_type`、 `network_is_expensive` 和 `network_is_constrainted` 规则项。 + +#### rules + +==必填== + +一组 [无头规则](../headless-rule/). diff --git a/option/rule.go b/option/rule.go index d5ff9925a6..82a53f75a4 100644 --- a/option/rule.go +++ b/option/rule.go @@ -95,6 +95,9 @@ type RawDefaultRule struct { User badoption.Listable[string] `json:"user,omitempty"` UserID badoption.Listable[int32] `json:"user_id,omitempty"` ClashMode string `json:"clash_mode,omitempty"` + NetworkType badoption.Listable[string] `json:"network_type,omitempty"` + NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` + NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` diff --git a/option/rule_dns.go b/option/rule_dns.go index 6c9755fda8..33dc816c53 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -97,6 +97,9 @@ type RawDefaultDNSRule struct { UserID badoption.Listable[int32] `json:"user_id,omitempty"` Outbound badoption.Listable[string] `json:"outbound,omitempty"` ClashMode string `json:"clash_mode,omitempty"` + NetworkType badoption.Listable[string] `json:"network_type,omitempty"` + NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` + NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` diff --git a/option/rule_set.go b/option/rule_set.go index fbb527ca55..9ccca4759b 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -146,25 +146,28 @@ func (r HeadlessRule) IsValid() bool { } type DefaultHeadlessRule struct { - QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` - Network badoption.Listable[string] `json:"network,omitempty"` - Domain badoption.Listable[string] `json:"domain,omitempty"` - DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` - SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` - IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` - SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` - Port badoption.Listable[uint16] `json:"port,omitempty"` - PortRange badoption.Listable[string] `json:"port_range,omitempty"` - ProcessName badoption.Listable[string] `json:"process_name,omitempty"` - ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` - ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` - PackageName badoption.Listable[string] `json:"package_name,omitempty"` - WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` - Invert bool `json:"invert,omitempty"` + QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` + Network badoption.Listable[string] `json:"network,omitempty"` + Domain badoption.Listable[string] `json:"domain,omitempty"` + DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` + SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` + SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` + Port badoption.Listable[uint16] `json:"port,omitempty"` + PortRange badoption.Listable[string] `json:"port_range,omitempty"` + ProcessName badoption.Listable[string] `json:"process_name,omitempty"` + ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` + ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` + PackageName badoption.Listable[string] `json:"package_name,omitempty"` + NetworkType badoption.Listable[string] `json:"network_type,omitempty"` + NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` + NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` + WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` + Invert bool `json:"invert,omitempty"` DomainMatcher *domain.Matcher `json:"-"` SourceIPSet *netipx.IPSet `json:"-"` @@ -200,7 +203,7 @@ type PlainRuleSetCompat _PlainRuleSetCompat func (r PlainRuleSetCompat) MarshalJSON() ([]byte, error) { var v any switch r.Version { - case C.RuleSetVersion1, C.RuleSetVersion2: + case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3: v = r.Options default: return nil, E.New("unknown rule-set version: ", r.Version) @@ -215,7 +218,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { } var v any switch r.Version { - case C.RuleSetVersion1, C.RuleSetVersion2: + case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3: v = &r.Options case 0: return E.New("missing rule-set version") @@ -231,7 +234,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { func (r PlainRuleSetCompat) Upgrade() (PlainRuleSet, error) { switch r.Version { - case C.RuleSetVersion1, C.RuleSetVersion2: + case C.RuleSetVersion1, C.RuleSetVersion2, C.RuleSetVersion3: default: return PlainRuleSet{}, E.New("unknown rule-set version: " + F.ToString(r.Version)) } diff --git a/route/rule/rule_default.go b/route/rule/rule_default.go index 12d9e96ae8..a5d47b4445 100644 --- a/route/rule/rule_default.go +++ b/route/rule/rule_default.go @@ -223,6 +223,21 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.NetworkType) > 0 { + item := NewNetworkTypeItem(networkManager, options.NetworkType) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsExpensive { + item := NewNetworkIsExpensiveItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsConstrained { + item := NewNetworkIsConstrainedItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if len(options.WIFISSID) > 0 { item := NewWIFISSIDItem(networkManager, options.WIFISSID) rule.items = append(rule.items, item) diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go index 1ec652b22a..4ac08a5a44 100644 --- a/route/rule/rule_dns.go +++ b/route/rule/rule_dns.go @@ -220,6 +220,21 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } + if len(options.NetworkType) > 0 { + item := NewNetworkTypeItem(networkManager, options.NetworkType) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsExpensive { + item := NewNetworkIsExpensiveItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsConstrained { + item := NewNetworkIsConstrainedItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } if len(options.WIFISSID) > 0 { item := NewWIFISSIDItem(networkManager, options.WIFISSID) rule.items = append(rule.items, item) diff --git a/route/rule/rule_headless.go b/route/rule/rule_headless.go index 99488b2049..7f2dc5fca7 100644 --- a/route/rule/rule_headless.go +++ b/route/rule/rule_headless.go @@ -140,18 +140,33 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } - if len(options.WIFISSID) > 0 { - if networkManager != nil { + if networkManager != nil { + if len(options.NetworkType) > 0 { + item := NewNetworkTypeItem(networkManager, options.NetworkType) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsExpensive { + item := NewNetworkIsExpensiveItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if options.NetworkIsConstrained { + item := NewNetworkIsConstrainedItem(networkManager) + rule.items = append(rule.items, item) + rule.allItems = append(rule.allItems, item) + } + if len(options.WIFISSID) > 0 { item := NewWIFISSIDItem(networkManager, options.WIFISSID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) + } - } - if len(options.WIFIBSSID) > 0 { - if networkManager != nil { + if len(options.WIFIBSSID) > 0 { item := NewWIFIBSSIDItem(networkManager, options.WIFIBSSID) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) + } } if len(options.AdGuardDomain) > 0 { diff --git a/route/rule/rule_item_network_is_constrained.go b/route/rule/rule_item_network_is_constrained.go new file mode 100644 index 0000000000..e0368b753f --- /dev/null +++ b/route/rule/rule_item_network_is_constrained.go @@ -0,0 +1,29 @@ +package rule + +import ( + "github.com/sagernet/sing-box/adapter" +) + +var _ RuleItem = (*NetworkIsConstrainedItem)(nil) + +type NetworkIsConstrainedItem struct { + networkManager adapter.NetworkManager +} + +func NewNetworkIsConstrainedItem(networkManager adapter.NetworkManager) *NetworkIsConstrainedItem { + return &NetworkIsConstrainedItem{ + networkManager: networkManager, + } +} + +func (r *NetworkIsConstrainedItem) Match(metadata *adapter.InboundContext) bool { + networkInterface := r.networkManager.DefaultNetworkInterface() + if networkInterface == nil { + return false + } + return networkInterface.Constrained +} + +func (r *NetworkIsConstrainedItem) String() string { + return "network_is_expensive=true" +} diff --git a/route/rule/rule_item_network_is_expensive.go b/route/rule/rule_item_network_is_expensive.go new file mode 100644 index 0000000000..83e4f96f88 --- /dev/null +++ b/route/rule/rule_item_network_is_expensive.go @@ -0,0 +1,29 @@ +package rule + +import ( + "github.com/sagernet/sing-box/adapter" +) + +var _ RuleItem = (*NetworkIsExpensiveItem)(nil) + +type NetworkIsExpensiveItem struct { + networkManager adapter.NetworkManager +} + +func NewNetworkIsExpensiveItem(networkManager adapter.NetworkManager) *NetworkIsExpensiveItem { + return &NetworkIsExpensiveItem{ + networkManager: networkManager, + } +} + +func (r *NetworkIsExpensiveItem) Match(metadata *adapter.InboundContext) bool { + networkInterface := r.networkManager.DefaultNetworkInterface() + if networkInterface == nil { + return false + } + return networkInterface.Expensive +} + +func (r *NetworkIsExpensiveItem) String() string { + return "network_is_expensive=true" +} diff --git a/route/rule/rule_item_network_type.go b/route/rule/rule_item_network_type.go new file mode 100644 index 0000000000..8ebdb25e9d --- /dev/null +++ b/route/rule/rule_item_network_type.go @@ -0,0 +1,39 @@ +package rule + +import ( + "strings" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" +) + +var _ RuleItem = (*NetworkTypeItem)(nil) + +type NetworkTypeItem struct { + networkManager adapter.NetworkManager + networkType []string +} + +func NewNetworkTypeItem(networkManager adapter.NetworkManager, networkType []string) *NetworkTypeItem { + return &NetworkTypeItem{ + networkManager: networkManager, + networkType: networkType, + } +} + +func (r *NetworkTypeItem) Match(metadata *adapter.InboundContext) bool { + networkInterface := r.networkManager.DefaultNetworkInterface() + if networkInterface == nil { + return false + } + return common.Contains(r.networkType, networkInterface.Type) +} + +func (r *NetworkTypeItem) String() string { + if len(r.networkType) == 1 { + return F.ToString("network_type=", r.networkType[0]) + } else { + return F.ToString("network_type=", "["+strings.Join(F.MapToString(r.networkType), " ")+"]") + } +} From 65cb225a2cdcc9c17cf596be60cf9db9fceb219c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 11 Nov 2024 17:24:15 +0800 Subject: [PATCH 20/49] documentation: Merge route options to route actions --- docs/configuration/dns/rule_action.md | 32 ++++++------- docs/configuration/dns/rule_action.zh.md | 31 ++++++------- docs/configuration/route/rule_action.md | 28 ++++++------ docs/configuration/route/rule_action.zh.md | 25 ++++++----- docs/deprecated.md | 8 ---- docs/deprecated.zh.md | 7 --- docs/migration.md | 52 ---------------------- docs/migration.zh.md | 52 ---------------------- 8 files changed, 55 insertions(+), 180 deletions(-) diff --git a/docs/configuration/dns/rule_action.md b/docs/configuration/dns/rule_action.md index 8943b65351..cccc139679 100644 --- a/docs/configuration/dns/rule_action.md +++ b/docs/configuration/dns/rule_action.md @@ -12,8 +12,6 @@ icon: material/new-box { "action": "route", // default "server": "", - - // for compatibility "disable_cache": false, "rewrite_ttl": 0, "client_subnet": null @@ -28,23 +26,6 @@ icon: material/new-box Tag of target server. -#### disable_cache/rewrite_ttl/client_subnet - -!!! failure "Deprecated in sing-box 1.11.0" - - Legacy route options is deprecated and will be removed in sing-box 1.12.0, check [Migration](/migration/#migrate-legacy-dns-route-options-to-rule-actions). - -### route-options - -```json -{ - "action": "route-options", - "disable_cache": false, - "rewrite_ttl": null, - "client_subnet": null -} -``` - #### disable_cache Disable cache and save cache in this query. @@ -61,6 +42,19 @@ If value is an IP address instead of prefix, `/32` or `/128` will be appended au Will overrides `dns.client_subnet` and `servers.[].client_subnet`. +### route-options + +```json +{ + "action": "route-options", + "disable_cache": false, + "rewrite_ttl": null, + "client_subnet": null +} +``` + +`route-options` set options for routing. + ### reject ```json diff --git a/docs/configuration/dns/rule_action.zh.md b/docs/configuration/dns/rule_action.zh.md index 8a9dc07eea..8fad238169 100644 --- a/docs/configuration/dns/rule_action.zh.md +++ b/docs/configuration/dns/rule_action.zh.md @@ -28,24 +28,6 @@ icon: material/new-box 目标 DNS 服务器的标签。 -#### disable_cache/rewrite_ttl/client_subnet - -!!! failure "自 sing-box 1.11.0 起" - - 旧的路由选项已弃用,且将在 sing-box 1.12.0 中移除,参阅 [迁移指南](/migration/#migrate-legacy-dns-route-options-to-rule-actions). - -### route-options - -```json -{ - "action": "route-options", - "disable_cache": false, - "rewrite_ttl": null, - "client_subnet": null -} -``` - - #### disable_cache 在此查询中禁用缓存。 @@ -62,6 +44,19 @@ icon: material/new-box 将覆盖 `dns.client_subnet` 与 `servers.[].client_subnet`。 +### route-options + +```json +{ + "action": "route-options", + "disable_cache": false, + "rewrite_ttl": null, + "client_subnet": null +} +``` + +`route-options` 为路由设置选项。 + ### reject ```json diff --git a/docs/configuration/route/rule_action.md b/docs/configuration/route/rule_action.md index 843d756366..7804f5869b 100644 --- a/docs/configuration/route/rule_action.md +++ b/docs/configuration/route/rule_action.md @@ -13,7 +13,9 @@ icon: material/new-box ```json { "action": "route", // default - "outbound": "" + "outbound": "", + "udp_disable_domain_unmapping": false, + "udp_connect": false } ``` @@ -25,18 +27,6 @@ icon: material/new-box Tag of target outbound. -### route-options - -```json -{ - "action": "route-options", - "udp_disable_domain_unmapping": false, - "udp_connect": false -} -``` - -`route-options` set options for routing. - #### udp_disable_domain_unmapping If enabled, for UDP proxy requests addressed to a domain, @@ -49,6 +39,18 @@ do not support receiving UDP packets with domain addresses, such as Surge. If enabled, attempts to connect UDP connection to the destination instead of listen. +### route-options + +```json +{ + "action": "route-options", + "udp_disable_domain_unmapping": false, + "udp_connect": false +} +``` + +`route-options` set options for routing. + ### reject ```json diff --git a/docs/configuration/route/rule_action.zh.md b/docs/configuration/route/rule_action.zh.md index ae16d85fde..103a04bd5c 100644 --- a/docs/configuration/route/rule_action.zh.md +++ b/docs/configuration/route/rule_action.zh.md @@ -14,7 +14,8 @@ icon: material/new-box { "action": "route", // 默认 "outbound": "", - "udp_disable_domain_unmapping": false + "udp_disable_domain_unmapping": false, + "udp_connect": false } ``` @@ -26,16 +27,6 @@ icon: material/new-box 目标出站的标签。 -### route-options - -```json -{ - "action": "route-options", - "udp_disable_domain_unmapping": false, - "udp_connect": false -} -``` - #### udp_disable_domain_unmapping 如果启用,对于地址为域的 UDP 代理请求,将在响应中发送原始包地址而不是映射的域。 @@ -46,6 +37,18 @@ icon: material/new-box 如果启用,将尝试将 UDP 连接 connect 到目标而不是 listen。 +### route-options + +```json +{ + "action": "route-options", + "udp_disable_domain_unmapping": false, + "udp_connect": false +} +``` + +`route-options` 为路由设置选项。 + ### reject ```json diff --git a/docs/deprecated.md b/docs/deprecated.md index f057319ade..d57c93ade4 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -22,14 +22,6 @@ check [Migration](../migration/#migrate-legacy-inbound-fields-to-rule-actions). Old fields will be removed in sing-box 1.13.0. -#### Legacy DNS route options - -Legacy DNS route options (`disable_cache`, `rewrite_ttl`, `client_subnet`) are deprecated -and can be replaced by rule actions, -check [Migration](../migration/#migrate-legacy-dns-route-options-to-rule-actions). - -Old fields will be removed in sing-box 1.12.0. - ## 1.10.0 #### TUN address fields are merged diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index 2f7b28f7bd..76a426d279 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -20,13 +20,6 @@ icon: material/delete-alert 旧字段将在 sing-box 1.13.0 中被移除。 -#### 旧的 DNS 路由参数 - -旧的 DNS 路由参数(`disable_cache`、`rewrite_ttl`、`client_subnet`)已废弃且可以通过规则动作替代, -参阅 [迁移指南](/migration/#migrate-legacy-dns-route-options-to-rule-actions)。 - -旧字段将在 sing-box 1.12.0 中被移除。 - ## 1.10.0 #### Match source 规则项已重命名 diff --git a/docs/migration.md b/docs/migration.md index 9207db5bc9..8f01c4f031 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -156,58 +156,6 @@ Inbound fields are deprecated and can be replaced by rule actions. } ``` -### Migrate legacy DNS route options to rule actions - -Legacy DNS route options are deprecated and can be replaced by rule actions. - -!!! info "References" - - [DNS Rule](/configuration/dns/rule/) / - [DNS Rule Action](/configuration/dns/rule_action/) - -=== ":material-card-remove: Deprecated" - - ```json - { - "dns": { - "rules": [ - { - ..., - - "server": "local", - "disable_cache": true, - "rewrite_ttl": 600, - "client_subnet": "1.1.1.1/24" - } - ] - } - } - ``` - -=== ":material-card-multiple: New" - - ```json - { - "dns": { - "rules": [ - { - ..., - - "action": "route-options", - "disable_cache": true, - "rewrite_ttl": 600, - "client_subnet": "1.1.1.1/24" - }, - { - ..., - - "server": "local" - } - ] - } - } - ``` - ## 1.10.0 ### TUN address fields are merged diff --git a/docs/migration.zh.md b/docs/migration.zh.md index f51860f720..dc62f37003 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -156,58 +156,6 @@ icon: material/arrange-bring-forward } ``` -### 迁移旧的 DNS 路由选项到规则动作 - -旧的 DNS 路由选项已被弃用,且可以被规则动作替代。 - -!!! info "参考" - - [DNS 规则](/zh/configuration/dns/rule/) / - [DNS 规则动作](/zh/configuration/dns/rule_action/) - -=== ":material-card-remove: 弃用的" - - ```json - { - "dns": { - "rules": [ - { - ..., - - "server": "local", - "disable_cache": true, - "rewrite_ttl": 600, - "client_subnet": "1.1.1.1/24" - } - ] - } - } - ``` - -=== ":material-card-multiple: 新的" - - ```json - { - "dns": { - "rules": [ - { - ..., - - "action": "route-options", - "disable_cache": true, - "rewrite_ttl": 600, - "client_subnet": "1.1.1.1/24" - }, - { - ..., - - "server": "local" - } - ] - } - } - ``` - ## 1.10.0 ### TUN 地址字段已合并 From 53e227a31891a39132a4cb36ec79cfe1774b10a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 12 Nov 2024 19:37:10 +0800 Subject: [PATCH 21/49] Add multi network dialing --- adapter/inbound.go | 4 + adapter/network.go | 14 +- adapter/outbound/default.go | 116 ++-------- common/dialer/default.go | 208 ++++++++++++----- common/dialer/default_go1.20.go | 4 + common/dialer/default_nongo1.20.go | 4 + common/dialer/default_parallel_interface.go | 241 ++++++++++++++++++++ common/dialer/default_parallel_network.go | 122 ++++++++++ common/dialer/dialer.go | 36 +++ common/dialer/resolve.go | 89 +++++++- common/settings/proxy_darwin.go | 10 +- constant/network.go | 42 ++++ docs/configuration/route/index.md | 45 +++- docs/configuration/route/index.zh.md | 30 ++- docs/configuration/route/rule_action.md | 29 ++- docs/configuration/route/rule_action.zh.md | 29 ++- docs/configuration/shared/dial.md | 57 ++++- docs/configuration/shared/dial.zh.md | 59 ++++- experimental/libbox/monitor.go | 4 +- option/outbound.go | 32 +-- option/route.go | 24 +- option/rule_action.go | 14 +- option/types.go | 21 ++ protocol/direct/outbound.go | 131 +++++++++-- protocol/socks/outbound.go | 21 -- protocol/wireguard/outbound.go | 13 -- route/network.go | 121 ++++++---- route/route.go | 4 + route/rule/rule_action.go | 6 + transport/dhcp/server.go | 2 +- 30 files changed, 1182 insertions(+), 350 deletions(-) create mode 100644 common/dialer/default_parallel_interface.go create mode 100644 common/dialer/default_parallel_network.go diff --git a/adapter/inbound.go b/adapter/inbound.go index 7932237d03..33b1b4d164 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -3,8 +3,10 @@ package adapter import ( "context" "net/netip" + "time" "github.com/sagernet/sing-box/common/process" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" M "github.com/sagernet/sing/common/metadata" @@ -66,6 +68,8 @@ type InboundContext struct { InboundOptions option.InboundOptions UDPDisableDomainUnmapping bool UDPConnect bool + NetworkStrategy C.NetworkStrategy + FallbackDelay time.Duration DNSServer string diff --git a/adapter/network.go b/adapter/network.go index 6c09a0a3e1..dd924c339f 100644 --- a/adapter/network.go +++ b/adapter/network.go @@ -1,6 +1,9 @@ package adapter import ( + "time" + + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/control" ) @@ -11,10 +14,10 @@ type NetworkManager interface { UpdateInterfaces() error DefaultNetworkInterface() *NetworkInterface NetworkInterfaces() []NetworkInterface - DefaultInterface() string AutoDetectInterface() bool AutoDetectInterfaceFunc() control.Func - DefaultMark() uint32 + ProtectFunc() control.Func + DefaultOptions() NetworkOptions RegisterAutoRedirectOutputMark(mark uint32) error AutoRedirectOutputMark() uint32 NetworkMonitor() tun.NetworkUpdateMonitor @@ -24,6 +27,13 @@ type NetworkManager interface { ResetNetwork() } +type NetworkOptions struct { + DefaultNetworkStrategy C.NetworkStrategy + DefaultFallbackDelay time.Duration + DefaultInterface string + DefaultMark uint32 +} + type InterfaceUpdateListener interface { InterfaceUpdated() } diff --git a/adapter/outbound/default.go b/adapter/outbound/default.go index bb58ff54d9..84be8abaf1 100644 --- a/adapter/outbound/default.go +++ b/adapter/outbound/default.go @@ -8,8 +8,8 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-dns" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" @@ -25,35 +25,11 @@ func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata a var outConn net.Conn var err error if len(metadata.DestinationAddresses) > 0 { - outConn, err = N.DialSerial(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses) - } else { - outConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination) - } - if err != nil { - return N.ReportHandshakeFailure(conn, err) - } - err = N.ReportConnHandshakeSuccess(conn, outConn) - if err != nil { - outConn.Close() - return err - } - return CopyEarlyConn(ctx, conn, outConn) -} - -func NewDirectConnection(ctx context.Context, router adapter.Router, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, domainStrategy dns.DomainStrategy) error { - defer conn.Close() - ctx = adapter.WithContext(ctx, &metadata) - var outConn net.Conn - var err error - if len(metadata.DestinationAddresses) > 0 { - outConn, err = N.DialSerial(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses) - } else if metadata.Destination.IsFqdn() { - var destinationAddresses []netip.Addr - destinationAddresses, err = router.Lookup(ctx, metadata.Destination.Fqdn, domainStrategy) - if err != nil { - return N.ReportHandshakeFailure(conn, err) + if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer { + outConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.FallbackDelay) + } else { + outConn, err = N.DialSerial(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses) } - outConn, err = N.DialSerial(ctx, this, N.NetworkTCP, metadata.Destination, destinationAddresses) } else { outConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination) } @@ -79,7 +55,11 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, ) if metadata.UDPConnect { if len(metadata.DestinationAddresses) > 0 { - outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses) + if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer { + outConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.FallbackDelay) + } else { + outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses) + } } else { outConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination) } @@ -93,7 +73,11 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, } } else { if len(metadata.DestinationAddresses) > 0 { - outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses) + if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer { + outPacketConn, destinationAddress, err = dialer.ListenSerialNetworkPacket(ctx, parallelDialer, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.FallbackDelay) + } else { + outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses) + } } else { outPacketConn, err = this.ListenPacket(ctx, metadata.Destination) } @@ -129,76 +113,6 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, return bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(outPacketConn)) } -func NewDirectPacketConnection(ctx context.Context, router adapter.Router, this N.Dialer, conn N.PacketConn, metadata adapter.InboundContext, domainStrategy dns.DomainStrategy) error { - defer conn.Close() - ctx = adapter.WithContext(ctx, &metadata) - var ( - outPacketConn net.PacketConn - outConn net.Conn - destinationAddress netip.Addr - err error - ) - if metadata.UDPConnect { - if len(metadata.DestinationAddresses) > 0 { - outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses) - } else if metadata.Destination.IsFqdn() { - var destinationAddresses []netip.Addr - destinationAddresses, err = router.Lookup(ctx, metadata.Destination.Fqdn, domainStrategy) - if err != nil { - return N.ReportHandshakeFailure(conn, err) - } - outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, destinationAddresses) - } else { - outConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination) - } - if err != nil { - return N.ReportHandshakeFailure(conn, err) - } - connRemoteAddr := M.AddrFromNet(outConn.RemoteAddr()) - if connRemoteAddr != metadata.Destination.Addr { - destinationAddress = connRemoteAddr - } - } else { - if len(metadata.DestinationAddresses) > 0 { - outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses) - } else if metadata.Destination.IsFqdn() { - var destinationAddresses []netip.Addr - destinationAddresses, err = router.Lookup(ctx, metadata.Destination.Fqdn, domainStrategy) - if err != nil { - return N.ReportHandshakeFailure(conn, err) - } - outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, destinationAddresses) - } else { - outPacketConn, err = this.ListenPacket(ctx, metadata.Destination) - } - if err != nil { - return N.ReportHandshakeFailure(conn, err) - } - } - err = N.ReportPacketConnHandshakeSuccess(conn, outPacketConn) - if err != nil { - outPacketConn.Close() - return err - } - if destinationAddress.IsValid() { - if metadata.Destination.IsFqdn() { - outPacketConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) - } - if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded { - natConn.UpdateDestination(destinationAddress) - } - } - switch metadata.Protocol { - case C.ProtocolSTUN: - ctx, conn = canceler.NewPacketConn(ctx, conn, C.STUNTimeout) - case C.ProtocolQUIC: - ctx, conn = canceler.NewPacketConn(ctx, conn, C.QUICTimeout) - case C.ProtocolDNS: - ctx, conn = canceler.NewPacketConn(ctx, conn, C.DNSTimeout) - } - return bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(outPacketConn)) -} - func CopyEarlyConn(ctx context.Context, conn net.Conn, serverConn net.Conn) error { if cachedReader, isCached := conn.(N.CachedReader); isCached { payload := cachedReader.ReadCached() diff --git a/common/dialer/default.go b/common/dialer/default.go index daf7995037..c7329f2e1a 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -10,66 +10,93 @@ import ( "github.com/sagernet/sing-box/common/conntrack" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) -var _ WireGuardListener = (*DefaultDialer)(nil) +var ( + _ ParallelInterfaceDialer = (*DefaultDialer)(nil) + _ WireGuardListener = (*DefaultDialer)(nil) +) type DefaultDialer struct { - dialer4 tcpDialer - dialer6 tcpDialer - udpDialer4 net.Dialer - udpDialer6 net.Dialer - udpListener net.ListenConfig - udpAddr4 string - udpAddr6 string - isWireGuardListener bool + dialer4 tcpDialer + dialer6 tcpDialer + udpDialer4 net.Dialer + udpDialer6 net.Dialer + udpListener net.ListenConfig + udpAddr4 string + udpAddr6 string + isWireGuardListener bool + networkManager adapter.NetworkManager + networkStrategy C.NetworkStrategy + networkFallbackDelay time.Duration + networkLastFallback atomic.TypedValue[time.Time] } func NewDefault(networkManager adapter.NetworkManager, options option.DialerOptions) (*DefaultDialer, error) { - var dialer net.Dialer - var listener net.ListenConfig + var ( + dialer net.Dialer + listener net.ListenConfig + interfaceFinder control.InterfaceFinder + networkStrategy C.NetworkStrategy + networkFallbackDelay time.Duration + ) + if networkManager != nil { + interfaceFinder = networkManager.InterfaceFinder() + } else { + interfaceFinder = control.NewDefaultInterfaceFinder() + } if options.BindInterface != "" { - var interfaceFinder control.InterfaceFinder - if networkManager != nil { - interfaceFinder = networkManager.InterfaceFinder() - } else { - interfaceFinder = control.NewDefaultInterfaceFinder() - } bindFunc := control.BindToInterface(interfaceFinder, options.BindInterface, -1) dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) - } else if networkManager != nil && networkManager.AutoDetectInterface() { - bindFunc := networkManager.AutoDetectInterfaceFunc() - dialer.Control = control.Append(dialer.Control, bindFunc) - listener.Control = control.Append(listener.Control, bindFunc) - } else if networkManager != nil && networkManager.DefaultInterface() != "" { - bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), networkManager.DefaultInterface(), -1) - dialer.Control = control.Append(dialer.Control, bindFunc) - listener.Control = control.Append(listener.Control, bindFunc) - } - var autoRedirectOutputMark uint32 - if networkManager != nil { - autoRedirectOutputMark = networkManager.AutoRedirectOutputMark() - } - if autoRedirectOutputMark > 0 { - dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark)) - listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark)) } if options.RoutingMark > 0 { dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark)) listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark)) + } + if networkManager != nil { + autoRedirectOutputMark := networkManager.AutoRedirectOutputMark() if autoRedirectOutputMark > 0 { - return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `routing_mark`") + if options.RoutingMark > 0 { + return nil, E.New("`routing_mark` is conflict with `tun.auto_redirect` with `tun.route_[_exclude]_address_set") + } + dialer.Control = control.Append(dialer.Control, control.RoutingMark(autoRedirectOutputMark)) + listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark)) } - } else if networkManager != nil && networkManager.DefaultMark() > 0 { - dialer.Control = control.Append(dialer.Control, control.RoutingMark(networkManager.DefaultMark())) - listener.Control = control.Append(listener.Control, control.RoutingMark(networkManager.DefaultMark())) - if autoRedirectOutputMark > 0 { - return nil, E.New("`auto_redirect` with `route_[_exclude]_address_set is conflict with `default_mark`") + } + if C.NetworkStrategy(options.NetworkStrategy) != C.NetworkStrategyDefault { + if options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil { + return nil, E.New("`network_strategy` is conflict with `bind_interface`, `inet4_bind_address` and `inet6_bind_address`") + } + networkStrategy = C.NetworkStrategy(options.NetworkStrategy) + networkFallbackDelay = time.Duration(options.NetworkFallbackDelay) + if networkManager == nil || !networkManager.AutoDetectInterface() { + return nil, E.New("`route.auto_detect_interface` is require by `network_strategy`") + } + } + if networkManager != nil && options.BindInterface == "" && options.Inet4BindAddress == nil && options.Inet6BindAddress == nil { + defaultOptions := networkManager.DefaultOptions() + if defaultOptions.DefaultInterface != "" { + bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.DefaultInterface, -1) + dialer.Control = control.Append(dialer.Control, bindFunc) + listener.Control = control.Append(listener.Control, bindFunc) + } else if networkManager.AutoDetectInterface() { + if defaultOptions.DefaultNetworkStrategy != C.NetworkStrategyDefault && C.NetworkStrategy(options.NetworkStrategy) == C.NetworkStrategyDefault { + networkStrategy = defaultOptions.DefaultNetworkStrategy + networkFallbackDelay = defaultOptions.DefaultFallbackDelay + bindFunc := networkManager.ProtectFunc() + dialer.Control = control.Append(dialer.Control, bindFunc) + listener.Control = control.Append(listener.Control, bindFunc) + } else { + bindFunc := networkManager.AutoDetectInterfaceFunc() + dialer.Control = control.Append(dialer.Control, bindFunc) + listener.Control = control.Append(listener.Control, bindFunc) + } } } if options.ReuseAddr { @@ -130,6 +157,9 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti listener.Control = control.Append(listener.Control, controlFn) } } + if networkStrategy != C.NetworkStrategyDefault && options.TCPFastOpen { + return nil, E.New("`tcp_fast_open` is conflict with `network_strategy` or `route.default_network_strategy`") + } tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen) if err != nil { return nil, err @@ -139,14 +169,17 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti return nil, err } return &DefaultDialer{ - tcpDialer4, - tcpDialer6, - udpDialer4, - udpDialer6, - listener, - udpAddr4, - udpAddr6, - options.IsWireGuardListener, + dialer4: tcpDialer4, + dialer6: tcpDialer6, + udpDialer4: udpDialer4, + udpDialer6: udpDialer6, + udpListener: listener, + udpAddr4: udpAddr4, + udpAddr6: udpAddr6, + isWireGuardListener: options.IsWireGuardListener, + networkManager: networkManager, + networkStrategy: networkStrategy, + networkFallbackDelay: networkFallbackDelay, }, nil } @@ -154,33 +187,88 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address if !address.IsValid() { return nil, E.New("invalid address") } - switch N.NetworkName(network) { - case N.NetworkUDP: + if d.networkStrategy == C.NetworkStrategyDefault { + switch N.NetworkName(network) { + case N.NetworkUDP: + if !address.IsIPv6() { + return trackConn(d.udpDialer4.DialContext(ctx, network, address.String())) + } else { + return trackConn(d.udpDialer6.DialContext(ctx, network, address.String())) + } + } if !address.IsIPv6() { - return trackConn(d.udpDialer4.DialContext(ctx, network, address.String())) + return trackConn(DialSlowContext(&d.dialer4, ctx, network, address)) } else { - return trackConn(d.udpDialer6.DialContext(ctx, network, address.String())) + return trackConn(DialSlowContext(&d.dialer6, ctx, network, address)) } + } else { + return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkFallbackDelay) + } +} + +func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network string, address M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) { + if strategy == C.NetworkStrategyDefault { + return d.DialContext(ctx, network, address) } - if !address.IsIPv6() { - return trackConn(DialSlowContext(&d.dialer4, ctx, network, address)) + if !d.networkManager.AutoDetectInterface() { + return nil, E.New("`route.auto_detect_interface` is require by `network_strategy`") + } + var dialer net.Dialer + if N.NetworkName(network) == N.NetworkTCP { + dialer = dialerFromTCPDialer(d.dialer4) + } else { + dialer = d.udpDialer4 + } + fastFallback := time.Now().Sub(d.networkLastFallback.Load()) < C.TCPTimeout + var ( + conn net.Conn + isPrimary bool + err error + ) + if !fastFallback { + conn, isPrimary, err = d.dialParallelInterface(ctx, dialer, network, address.String(), strategy, fallbackDelay) } else { - return trackConn(DialSlowContext(&d.dialer6, ctx, network, address)) + conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), strategy, fallbackDelay, d.networkLastFallback.Store) } + if err != nil { + return nil, err + } + if !fastFallback && !isPrimary { + d.networkLastFallback.Store(time.Now()) + } + return trackConn(conn, nil) } func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - if destination.IsIPv6() { - return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)) - } else if destination.IsIPv4() && !destination.Addr.IsUnspecified() { - return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4)) + if d.networkStrategy == C.NetworkStrategyDefault { + if destination.IsIPv6() { + return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)) + } else if destination.IsIPv4() && !destination.Addr.IsUnspecified() { + return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP+"4", d.udpAddr4)) + } else { + return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4)) + } } else { - return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4)) + return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkFallbackDelay) + } +} + +func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, error) { + if strategy == C.NetworkStrategyDefault { + return d.ListenPacket(ctx, destination) + } + if !d.networkManager.AutoDetectInterface() { + return nil, E.New("`route.auto_detect_interface` is require by `network_strategy`") + } + network := N.NetworkUDP + if destination.IsIPv4() && !destination.Addr.IsUnspecified() { + network += "4" } + return trackPacketConn(d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", strategy, fallbackDelay)) } func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) { - return d.udpListener.ListenPacket(context.Background(), network, address) + return d.listenSerialInterfacePacket(context.Background(), d.udpListener, network, address, d.networkStrategy, d.networkFallbackDelay) } func trackConn(conn net.Conn, err error) (net.Conn, error) { diff --git a/common/dialer/default_go1.20.go b/common/dialer/default_go1.20.go index a9f7b612b8..9dde955f86 100644 --- a/common/dialer/default_go1.20.go +++ b/common/dialer/default_go1.20.go @@ -13,3 +13,7 @@ type tcpDialer = tfo.Dialer func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) { return tfo.Dialer{Dialer: dialer, DisableTFO: !tfoEnabled}, nil } + +func dialerFromTCPDialer(dialer tcpDialer) net.Dialer { + return dialer.Dialer +} diff --git a/common/dialer/default_nongo1.20.go b/common/dialer/default_nongo1.20.go index 215024245a..b2e4638d16 100644 --- a/common/dialer/default_nongo1.20.go +++ b/common/dialer/default_nongo1.20.go @@ -16,3 +16,7 @@ func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) { } return dialer, nil } + +func dialerFromTCPDialer(dialer tcpDialer) net.Dialer { + return dialer +} diff --git a/common/dialer/default_parallel_interface.go b/common/dialer/default_parallel_interface.go new file mode 100644 index 0000000000..baf0349ec8 --- /dev/null +++ b/common/dialer/default_parallel_interface.go @@ -0,0 +1,241 @@ +package dialer + +import ( + "context" + "net" + "time" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + N "github.com/sagernet/sing/common/network" +) + +func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Dialer, network string, addr string, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, bool, error) { + primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy) + if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { + return nil, false, E.New("no available network interface") + } + if fallbackDelay == 0 { + fallbackDelay = N.DefaultFallbackDelay + } + returned := make(chan struct{}) + defer close(returned) + type dialResult struct { + net.Conn + error + primary bool + } + results := make(chan dialResult) // unbuffered + startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) { + perNetDialer := dialer + perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index)) + conn, err := perNetDialer.DialContext(ctx, network, addr) + if err != nil { + select { + case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Name, ")"), primary: primary}: + case <-returned: + } + } else { + select { + case results <- dialResult{Conn: conn}: + case <-returned: + conn.Close() + } + } + } + primaryCtx, primaryCancel := context.WithCancel(ctx) + defer primaryCancel() + for _, iif := range primaryInterfaces { + go startRacer(primaryCtx, true, iif) + } + var ( + fallbackTimer *time.Timer + fallbackChan <-chan time.Time + ) + if len(fallbackInterfaces) > 0 { + fallbackTimer = time.NewTimer(fallbackDelay) + defer fallbackTimer.Stop() + fallbackChan = fallbackTimer.C + } + var errors []error + for { + select { + case <-fallbackChan: + fallbackCtx, fallbackCancel := context.WithCancel(ctx) + defer fallbackCancel() + for _, iif := range fallbackInterfaces { + go startRacer(fallbackCtx, false, iif) + } + case res := <-results: + if res.error == nil { + return res.Conn, res.primary, nil + } + errors = append(errors, res.error) + if len(errors) == len(primaryInterfaces)+len(fallbackInterfaces) { + return nil, false, E.Errors(errors...) + } + if res.primary && fallbackTimer != nil && fallbackTimer.Stop() { + fallbackTimer.Reset(0) + } + } + } +} + +func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, dialer net.Dialer, network string, addr string, strategy C.NetworkStrategy, fallbackDelay time.Duration, resetFastFallback func(time.Time)) (net.Conn, bool, error) { + primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy) + if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { + return nil, false, E.New("no available network interface") + } + if fallbackDelay == 0 { + fallbackDelay = N.DefaultFallbackDelay + } + returned := make(chan struct{}) + defer close(returned) + type dialResult struct { + net.Conn + error + primary bool + } + startAt := time.Now() + results := make(chan dialResult) // unbuffered + startRacer := func(ctx context.Context, primary bool, iif adapter.NetworkInterface) { + perNetDialer := dialer + perNetDialer.Control = control.Append(perNetDialer.Control, control.BindToInterface(nil, iif.Name, iif.Index)) + conn, err := perNetDialer.DialContext(ctx, network, addr) + if err != nil { + select { + case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Name, ")"), primary: primary}: + case <-returned: + } + } else { + select { + case results <- dialResult{Conn: conn}: + case <-returned: + if primary && time.Since(startAt) <= fallbackDelay { + resetFastFallback(time.Time{}) + } + conn.Close() + } + } + } + for _, iif := range primaryInterfaces { + go startRacer(ctx, true, iif) + } + fallbackCtx, fallbackCancel := context.WithCancel(ctx) + defer fallbackCancel() + for _, iif := range fallbackInterfaces { + go startRacer(fallbackCtx, false, iif) + } + var errors []error + for { + select { + case res := <-results: + if res.error == nil { + return res.Conn, res.primary, nil + } + errors = append(errors, res.error) + if len(errors) == len(primaryInterfaces)+len(fallbackInterfaces) { + return nil, false, E.Errors(errors...) + } + } + } +} + +func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listener net.ListenConfig, network string, addr string, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, error) { + primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy) + if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { + return nil, E.New("no available network interface") + } + if fallbackDelay == 0 { + fallbackDelay = N.DefaultFallbackDelay + } + var errors []error + for _, primaryInterface := range primaryInterfaces { + perNetListener := listener + perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, primaryInterface.Name, primaryInterface.Index)) + conn, err := perNetListener.ListenPacket(ctx, network, addr) + if err == nil { + return conn, nil + } + errors = append(errors, E.Cause(err, "listen ", primaryInterface.Name, " (", primaryInterface.Name, ")")) + } + for _, fallbackInterface := range fallbackInterfaces { + perNetListener := listener + perNetListener.Control = control.Append(perNetListener.Control, control.BindToInterface(nil, fallbackInterface.Name, fallbackInterface.Index)) + conn, err := perNetListener.ListenPacket(ctx, network, addr) + if err == nil { + return conn, nil + } + errors = append(errors, E.Cause(err, "listen ", fallbackInterface.Name, " (", fallbackInterface.Name, ")")) + } + return nil, E.Errors(errors...) +} + +func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkStrategy) (primaryInterfaces []adapter.NetworkInterface, fallbackInterfaces []adapter.NetworkInterface) { + interfaces := networkManager.NetworkInterfaces() + switch strategy { + case C.NetworkStrategyFallback: + defaultIf := networkManager.InterfaceMonitor().DefaultInterface() + if defaultIf != nil { + for _, iif := range interfaces { + if iif.Index == defaultIf.Index { + primaryInterfaces = append(primaryInterfaces, iif) + } else { + fallbackInterfaces = append(fallbackInterfaces, iif) + } + } + } else { + primaryInterfaces = interfaces + } + case C.NetworkStrategyHybrid: + primaryInterfaces = interfaces + case C.NetworkStrategyWIFI: + for _, iif := range interfaces { + if iif.Type == C.InterfaceTypeWIFI { + primaryInterfaces = append(primaryInterfaces, iif) + } else { + fallbackInterfaces = append(fallbackInterfaces, iif) + } + } + case C.NetworkStrategyCellular: + for _, iif := range interfaces { + if iif.Type == C.InterfaceTypeCellular { + primaryInterfaces = append(primaryInterfaces, iif) + } else { + fallbackInterfaces = append(fallbackInterfaces, iif) + } + } + case C.NetworkStrategyEthernet: + for _, iif := range interfaces { + if iif.Type == C.InterfaceTypeEthernet { + primaryInterfaces = append(primaryInterfaces, iif) + } else { + fallbackInterfaces = append(fallbackInterfaces, iif) + } + } + case C.NetworkStrategyWIFIOnly: + for _, iif := range interfaces { + if iif.Type == C.InterfaceTypeWIFI { + primaryInterfaces = append(primaryInterfaces, iif) + } + } + case C.NetworkStrategyCellularOnly: + for _, iif := range interfaces { + if iif.Type == C.InterfaceTypeCellular { + primaryInterfaces = append(primaryInterfaces, iif) + } + } + case C.NetworkStrategyEthernetOnly: + for _, iif := range interfaces { + if iif.Type == C.InterfaceTypeEthernet { + primaryInterfaces = append(primaryInterfaces, iif) + } + } + default: + panic(F.ToString("unknown network strategy: ", strategy)) + } + return primaryInterfaces, fallbackInterfaces +} diff --git a/common/dialer/default_parallel_network.go b/common/dialer/default_parallel_network.go new file mode 100644 index 0000000000..f42d9330c7 --- /dev/null +++ b/common/dialer/default_parallel_network.go @@ -0,0 +1,122 @@ +package dialer + +import ( + "context" + "net" + "net/netip" + "time" + + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +func DialSerialNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) { + if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel { + return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, fallbackDelay) + } + var errors []error + for _, address := range destinationAddresses { + conn, err := dialer.DialParallelInterface(ctx, network, M.SocksaddrFrom(address, destination.Port), strategy, fallbackDelay) + if err == nil { + return conn, nil + } + errors = append(errors, err) + } + return nil, E.Errors(errors...) +} + +func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, preferIPv6 bool, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) { + if fallbackDelay == 0 { + fallbackDelay = N.DefaultFallbackDelay + } + + returned := make(chan struct{}) + defer close(returned) + + addresses4 := common.Filter(destinationAddresses, func(address netip.Addr) bool { + return address.Is4() || address.Is4In6() + }) + addresses6 := common.Filter(destinationAddresses, func(address netip.Addr) bool { + return address.Is6() && !address.Is4In6() + }) + if len(addresses4) == 0 || len(addresses6) == 0 { + return DialSerialNetwork(ctx, dialer, network, destination, destinationAddresses, strategy, fallbackDelay) + } + var primaries, fallbacks []netip.Addr + if preferIPv6 { + primaries = addresses6 + fallbacks = addresses4 + } else { + primaries = addresses4 + fallbacks = addresses6 + } + type dialResult struct { + net.Conn + error + primary bool + done bool + } + results := make(chan dialResult) // unbuffered + startRacer := func(ctx context.Context, primary bool) { + ras := primaries + if !primary { + ras = fallbacks + } + c, err := DialSerialNetwork(ctx, dialer, network, destination, ras, strategy, fallbackDelay) + select { + case results <- dialResult{Conn: c, error: err, primary: primary, done: true}: + case <-returned: + if c != nil { + c.Close() + } + } + } + var primary, fallback dialResult + primaryCtx, primaryCancel := context.WithCancel(ctx) + defer primaryCancel() + go startRacer(primaryCtx, true) + fallbackTimer := time.NewTimer(fallbackDelay) + defer fallbackTimer.Stop() + for { + select { + case <-fallbackTimer.C: + fallbackCtx, fallbackCancel := context.WithCancel(ctx) + defer fallbackCancel() + go startRacer(fallbackCtx, false) + + case res := <-results: + if res.error == nil { + return res.Conn, nil + } + if res.primary { + primary = res + } else { + fallback = res + } + if primary.done && fallback.done { + return nil, primary.error + } + if res.primary && fallbackTimer.Stop() { + fallbackTimer.Reset(0) + } + } + } +} + +func ListenSerialNetworkPacket(ctx context.Context, dialer ParallelInterfaceDialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { + if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel { + return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, fallbackDelay) + } + var errors []error + for _, address := range destinationAddresses { + conn, err := dialer.ListenSerialInterfacePacket(ctx, M.SocksaddrFrom(address, destination.Port), strategy, fallbackDelay) + if err == nil { + return conn, address, nil + } + errors = append(errors, err) + } + return nil, netip.Addr{}, E.Errors(errors...) +} diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index 047a25146b..b3305d7397 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -2,12 +2,16 @@ package dialer import ( "context" + "net" + "net/netip" "time" "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-dns" E "github.com/sagernet/sing/common/exceptions" + M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" ) @@ -49,3 +53,35 @@ func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) { } return dialer, nil } + +func NewDirect(ctx context.Context, options option.DialerOptions) (ParallelInterfaceDialer, error) { + if options.Detour != "" { + return nil, E.New("`detour` is not supported in direct context") + } + networkManager := service.FromContext[adapter.NetworkManager](ctx) + if options.IsWireGuardListener { + return NewDefault(networkManager, options) + } + dialer, err := NewDefault(networkManager, options) + if err != nil { + return nil, err + } + return NewResolveParallelInterfaceDialer( + service.FromContext[adapter.Router](ctx), + dialer, + true, + dns.DomainStrategy(options.DomainStrategy), + time.Duration(options.FallbackDelay), + ), nil +} + +type ParallelInterfaceDialer interface { + N.Dialer + DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) + ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, error) +} + +type ParallelNetworkDialer interface { + DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) + ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) +} diff --git a/common/dialer/resolve.go b/common/dialer/resolve.go index f2ee50db5a..ce17923c40 100644 --- a/common/dialer/resolve.go +++ b/common/dialer/resolve.go @@ -7,6 +7,7 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-dns" "github.com/sagernet/sing/common/bufio" @@ -14,7 +15,12 @@ import ( N "github.com/sagernet/sing/common/network" ) -type ResolveDialer struct { +var ( + _ N.Dialer = (*resolveDialer)(nil) + _ ParallelInterfaceDialer = (*resolveParallelNetworkDialer)(nil) +) + +type resolveDialer struct { dialer N.Dialer parallel bool router adapter.Router @@ -22,8 +28,8 @@ type ResolveDialer struct { fallbackDelay time.Duration } -func NewResolveDialer(router adapter.Router, dialer N.Dialer, parallel bool, strategy dns.DomainStrategy, fallbackDelay time.Duration) *ResolveDialer { - return &ResolveDialer{ +func NewResolveDialer(router adapter.Router, dialer N.Dialer, parallel bool, strategy dns.DomainStrategy, fallbackDelay time.Duration) N.Dialer { + return &resolveDialer{ dialer, parallel, router, @@ -32,7 +38,25 @@ func NewResolveDialer(router adapter.Router, dialer N.Dialer, parallel bool, str } } -func (d *ResolveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +type resolveParallelNetworkDialer struct { + resolveDialer + dialer ParallelInterfaceDialer +} + +func NewResolveParallelInterfaceDialer(router adapter.Router, dialer ParallelInterfaceDialer, parallel bool, strategy dns.DomainStrategy, fallbackDelay time.Duration) ParallelInterfaceDialer { + return &resolveParallelNetworkDialer{ + resolveDialer{ + dialer, + parallel, + router, + strategy, + fallbackDelay, + }, + dialer, + } +} + +func (d *resolveDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { if !destination.IsFqdn() { return d.dialer.DialContext(ctx, network, destination) } @@ -57,7 +81,7 @@ func (d *ResolveDialer) DialContext(ctx context.Context, network string, destina } } -func (d *ResolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { if !destination.IsFqdn() { return d.dialer.ListenPacket(ctx, destination) } @@ -82,6 +106,59 @@ func (d *ResolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil } -func (d *ResolveDialer) Upstream() any { +func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) { + if !destination.IsFqdn() { + return d.dialer.DialContext(ctx, network, destination) + } + ctx, metadata := adapter.ExtendContext(ctx) + ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) + metadata.Destination = destination + metadata.Domain = "" + var addresses []netip.Addr + var err error + if d.strategy == dns.DomainStrategyAsIS { + addresses, err = d.router.LookupDefault(ctx, destination.Fqdn) + } else { + addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy) + } + if err != nil { + return nil, err + } + if fallbackDelay == 0 { + fallbackDelay = d.fallbackDelay + } + if d.parallel { + return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, strategy, fallbackDelay) + } else { + return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, fallbackDelay) + } +} + +func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, error) { + if !destination.IsFqdn() { + return d.dialer.ListenPacket(ctx, destination) + } + ctx, metadata := adapter.ExtendContext(ctx) + ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug) + metadata.Destination = destination + metadata.Domain = "" + var addresses []netip.Addr + var err error + if d.strategy == dns.DomainStrategyAsIS { + addresses, err = d.router.LookupDefault(ctx, destination.Fqdn) + } else { + addresses, err = d.router.Lookup(ctx, destination.Fqdn, d.strategy) + } + if err != nil { + return nil, err + } + conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, fallbackDelay) + if err != nil { + return nil, err + } + return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil +} + +func (d *resolveDialer) Upstream() any { return d.dialer } diff --git a/common/settings/proxy_darwin.go b/common/settings/proxy_darwin.go index 3c06a85369..53ed0fe03c 100644 --- a/common/settings/proxy_darwin.go +++ b/common/settings/proxy_darwin.go @@ -7,6 +7,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" "github.com/sagernet/sing/common/shell" @@ -33,7 +34,7 @@ func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bo serverAddr: serverAddr, supportSOCKS: supportSOCKS, } - proxy.element = interfaceMonitor.RegisterCallback(proxy.update) + proxy.element = interfaceMonitor.RegisterCallback(proxy.routeUpdate) return proxy, nil } @@ -65,11 +66,8 @@ func (p *DarwinSystemProxy) Disable() error { return err } -func (p *DarwinSystemProxy) update(event int) { - if event&tun.EventInterfaceUpdate == 0 { - return - } - if !p.isEnabled { +func (p *DarwinSystemProxy) routeUpdate(defaultInterface *control.Interface, flags int) { + if !p.isEnabled || defaultInterface == nil { return } _ = p.update0() diff --git a/constant/network.go b/constant/network.go index f5ac2a4e47..c026b7b15b 100644 --- a/constant/network.go +++ b/constant/network.go @@ -1,8 +1,50 @@ package constant +import ( + "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" +) + const ( InterfaceTypeWIFI = "wifi" InterfaceTypeCellular = "cellular" InterfaceTypeEthernet = "ethernet" InterfaceTypeOther = "other" ) + +type NetworkStrategy int + +const ( + NetworkStrategyDefault NetworkStrategy = iota + NetworkStrategyFallback + NetworkStrategyHybrid + NetworkStrategyWIFI + NetworkStrategyCellular + NetworkStrategyEthernet + NetworkStrategyWIFIOnly + NetworkStrategyCellularOnly + NetworkStrategyEthernetOnly +) + +var ( + NetworkStrategyToString = map[NetworkStrategy]string{ + NetworkStrategyDefault: "default", + NetworkStrategyFallback: "fallback", + NetworkStrategyHybrid: "hybrid", + NetworkStrategyWIFI: "wifi", + NetworkStrategyCellular: "cellular", + NetworkStrategyEthernet: "ethernet", + NetworkStrategyWIFIOnly: "wifi_only", + NetworkStrategyCellularOnly: "cellular_only", + NetworkStrategyEthernetOnly: "ethernet_only", + } + StringToNetworkStrategy = common.ReverseMap(NetworkStrategyToString) +) + +func (s NetworkStrategy) String() string { + name, loaded := NetworkStrategyToString[s] + if !loaded { + return F.ToString(int(s)) + } + return name +} diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 507cb140b6..2b035eb426 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -1,5 +1,14 @@ +--- +icon: material/new-box +--- + # Route +!!! quote "Changes in sing-box 1.11.0" + + :material-plus: [default_network_strategy](#default_network_strategy) + :material-alert: [default_fallback_delay](#default_fallback_delay) + !!! quote "Changes in sing-box 1.8.0" :material-plus: [rule_set](#rule_set) @@ -18,16 +27,18 @@ "final": "", "auto_detect_interface": false, "override_android_vpn": false, - "default_interface": "en0", - "default_mark": 233 + "default_interface": "", + "default_mark": 0, + "default_network_strategy": "", + "default_fallback_delay": "" } } ``` ### Fields -| Key | Format | -|-----------|----------------------| +| Key | Format | +|-----------|-----------------------| | `geoip` | [GeoIP](./geoip/) | | `geosite` | [Geosite](./geosite/) | @@ -81,4 +92,28 @@ Takes no effect if `auto_detect_interface` is set. Set routing mark by default. -Takes no effect if `outbound.routing_mark` is set. \ No newline at end of file +Takes no effect if `outbound.routing_mark` is set. + +#### default_network_strategy + +!!! quote "" + + Only supported in graphical clients on Android and iOS with `auto_detect_interface` enabled. + +Strategy for selecting network interfaces. + +Takes no effect if `outbound.bind_interface`, `outbound.inet4_bind_address` or `outbound.inet6_bind_address` is set. + +Can be overrides by `outbound.network_strategy`. + +Conflicts with `default_interface`. + +See [Dial Fields](/configuration/shared/dial/#network_strategy) for available values. + +#### default_fallback_delay + +!!! quote "" + + Only supported in graphical clients on Android and iOS with `auto_detect_interface` enabled and `network_strategy` set. + +See [Dial Fields](/configuration/shared/dial/#fallback_delay) for details. \ No newline at end of file diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md index 68d4f66d91..b00237c450 100644 --- a/docs/configuration/route/index.zh.md +++ b/docs/configuration/route/index.zh.md @@ -18,8 +18,10 @@ "final": "", "auto_detect_interface": false, "override_android_vpn": false, - "default_interface": "en0", - "default_mark": 233 + "default_interface": "", + "default_mark": 0, + "default_network_strategy": "", + "default_fallback_delay": "" } } ``` @@ -82,3 +84,27 @@ 默认为出站连接设置路由标记。 如果设置了 `outbound.routing_mark` 设置,则不生效。 + +#### network_strategy + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持,并且需要 `auto_detect_interface`。 + +选择网络接口的策略。 + +当 `outbound.bind_interface`, `outbound.inet4_bind_address` 或 `outbound.inet6_bind_address` 已设置时不生效。 + +可以被 `outbound.network_strategy` 覆盖。 + +与 `default_interface` 冲突。 + +可用值请参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。 + +#### fallback_delay + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持,并且需要 `auto_detect_interface` 且 `network_strategy` 已设置。 + +详情请参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。 diff --git a/docs/configuration/route/rule_action.md b/docs/configuration/route/rule_action.md index 7804f5869b..5e9cfbc220 100644 --- a/docs/configuration/route/rule_action.md +++ b/docs/configuration/route/rule_action.md @@ -2,10 +2,6 @@ icon: material/new-box --- -# Rule Action - -!!! question "Since sing-box 1.11.0" - ## Final actions ### route @@ -14,6 +10,8 @@ icon: material/new-box { "action": "route", // default "outbound": "", + "network_strategy": "", + "fallback_delay": "", "udp_disable_domain_unmapping": false, "udp_connect": false } @@ -27,6 +25,27 @@ icon: material/new-box Tag of target outbound. +#### network_strategy + +!!! quote "" + + Only supported in graphical clients on Android and iOS with `auto_detect_interface` enabled. + +Strategy for selecting network interfaces. + +Only take effect if outbound is direct without `outbound.bind_interface`, +`outbound.inet4_bind_address` and `outbound.inet6_bind_address` set. + +See [Dial Fields](/configuration/shared/dial/#network_strategy) for available values. + +#### fallback_delay + +!!! quote "" + + Only supported in graphical clients on Android and iOS with `auto_detect_interface` enabled and `network_strategy` set. + +See [Dial Fields](/configuration/shared/dial/#fallback_delay) for details. + #### udp_disable_domain_unmapping If enabled, for UDP proxy requests addressed to a domain, @@ -44,6 +63,8 @@ If enabled, attempts to connect UDP connection to the destination instead of lis ```json { "action": "route-options", + "network_strategy": "", + "fallback_delay": "", "udp_disable_domain_unmapping": false, "udp_connect": false } diff --git a/docs/configuration/route/rule_action.zh.md b/docs/configuration/route/rule_action.zh.md index 103a04bd5c..52965fd240 100644 --- a/docs/configuration/route/rule_action.zh.md +++ b/docs/configuration/route/rule_action.zh.md @@ -2,10 +2,6 @@ icon: material/new-box --- -# 规则动作 - -!!! question "自 sing-box 1.11.0 起" - ## 最终动作 ### route @@ -14,6 +10,8 @@ icon: material/new-box { "action": "route", // 默认 "outbound": "", + "network_strategy": "", + "fallback_delay": "", "udp_disable_domain_unmapping": false, "udp_connect": false } @@ -27,6 +25,27 @@ icon: material/new-box 目标出站的标签。 +#### network_strategy + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持,并且需要 `auto_detect_interface`。 + +选择网络接口的策略。 + +仅当出站为 `direct` 且 `outbound.bind_interface`, `outbound.inet4_bind_address` +且 `outbound.inet6_bind_address` 未设置时生效。 + +可用值参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。 + +#### fallback_delay + +!!! quote "" + + 仅在 Android 与 Apple 平台图形客户端中支持,并且需要 `auto_detect_interface` 且 `network_strategy` 已设置。 + +详情参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。 + #### udp_disable_domain_unmapping 如果启用,对于地址为域的 UDP 代理请求,将在响应中发送原始包地址而不是映射的域。 @@ -42,6 +61,8 @@ icon: material/new-box ```json { "action": "route-options", + "network_strategy": "", + "fallback_delay": "", "udp_disable_domain_unmapping": false, "udp_connect": false } diff --git a/docs/configuration/shared/dial.md b/docs/configuration/shared/dial.md index 8139c7518c..e67bf61623 100644 --- a/docs/configuration/shared/dial.md +++ b/docs/configuration/shared/dial.md @@ -1,3 +1,12 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.11.0" + + :material-plus: [network_strategy](#network_strategy) + :material-alert: [fallback_delay](#fallback_delay) + ### Structure ```json @@ -13,20 +22,19 @@ "tcp_multi_path": false, "udp_fragment": false, "domain_strategy": "prefer_ipv6", + "network_strategy": "default", "fallback_delay": "300ms" } ``` ### Fields -| Field | Available Context | -|------------------------------------------------------------------------------------------------------------------------------------------|-------------------| -| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open` / `tcp_multi_path` / `udp_fragment` /`connect_timeout` | `detour` not set | - #### detour The tag of the upstream outbound. +If enabled, all other fields will be ignored. + #### bind_interface The network interface to bind to. @@ -78,7 +86,7 @@ Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". #### domain_strategy -One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`. +Available values: `prefer_ipv4`, `prefer_ipv6`, `ipv4_only`, `ipv6_only`. If set, the requested domain name will be resolved to IP before connect. @@ -87,11 +95,44 @@ If set, the requested domain name will be resolved to IP before connect. | `direct` | Domain in request | Take `inbound.domain_strategy` if not set | | others | Domain in server address | / | +#### network_strategy + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and iOS with `auto_detect_interface` enabled. + +Strategy for selecting network interfaces. + +Available values: + +- `default` (default): Connect to the default interface. +- `fallback`: Try all other interfaces when timeout. +- `hybrid`: Connect to all interfaces concurrently and choose the fastest one. +- `wifi`: Prioritize WIFI, but try all other interfaces when unavailable or timeout. +- `cellular`: Prioritize Cellular, but try all other interfaces when unavailable or timeout. +- `ethernet`: Prioritize Ethernet, but try all other interfaces when unavailable or timeout. +- `wifi_only`: Connect to WIFI only. +- `cellular_only`: Connect to Cellular only. +- `ethernet_only`: Connect to Ethernet only. + +For fallback strategies, when preferred interfaces fails or times out, +it will enter a 15s fast fallback state (upgraded to `hybrid`), +and exit immediately if recovers. + +Conflicts with `bind_interface`, `inet4_bind_address` and `inet6_bind_address`. + #### fallback_delay The length of time to wait before spawning a RFC 6555 Fast Fallback connection. -That is, is the amount of time to wait for connection to succeed before assuming + +For `domain_strategy`, is the amount of time to wait for connection to succeed before assuming that IPv4/IPv6 is misconfigured and falling back to other type of addresses. -If zero, a default delay of 300ms is used. -Only take effect when `domain_strategy` is set. \ No newline at end of file +For `network_strategy`, is the amount of time to wait for connection to succeed before falling +back to other interfaces. + +Only take effect when `domain_strategy` or `network_strategy` is set. + +`300ms` is used by default. diff --git a/docs/configuration/shared/dial.zh.md b/docs/configuration/shared/dial.zh.md index ccb61c7a44..4e20f106e8 100644 --- a/docs/configuration/shared/dial.zh.md +++ b/docs/configuration/shared/dial.zh.md @@ -1,3 +1,12 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.11.0 中的更改" + + :material-plus: [network_strategy](#network_strategy) + :material-alert: [fallback_delay](#fallback_delay) + ### 结构 ```json @@ -13,17 +22,13 @@ "tcp_multi_path": false, "udp_fragment": false, "domain_strategy": "prefer_ipv6", + "network_strategy": "", "fallback_delay": "300ms" } ``` ### 字段 -| 字段 | 可用上下文 | -|------------------------------------------------------------------------------------------------------------------------------------------|--------------| -| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open` / `tcp_mutli_path` / `udp_fragment` /`connect_timeout` | `detour` 未设置 | - - #### detour 上游出站的标签。 @@ -83,15 +88,45 @@ 如果设置,域名将在请求发出之前解析为 IP。 -| 出站 | 受影响的域名 | 默认回退值 | -|----------|--------------------------|-------------------------------------------| -| `direct` | 请求中的域名 | `inbound.domain_strategy` | -| others | 服务器地址中的域名 | / | +| 出站 | 受影响的域名 | 默认回退值 | +|----------|-----------|---------------------------| +| `direct` | 请求中的域名 | `inbound.domain_strategy` | +| others | 服务器地址中的域名 | / | + +#### network_strategy + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 iOS 平台图形客户端中支持。 + +用于选择网络接口的策略。 + +可用值: + +- `default` (默认): 连接到默认接口, +- `fallback`: 如果超时,尝试所有剩余接口。 +- `hybrid`: 同时尝试所有接口,选择最快的一个。 +- `wifi`: 优先使用 WIFI,但在不可用或超时时尝试所有其他接口。 +- `cellular`: 优先使用蜂窝数据,但在不可用或超时时尝试所有其他接口。 +- `ethernet`: 优先使用以太网,但在不可用或超时时尝试所有其他接口。 +- `wifi_only`: 仅连接到 WIFI。 +- `cellular_only`: 仅连接到蜂窝数据。 +- `ethernet_only`: 仅连接到以太网。 + +对于回退策略, 当优先使用的接口发生故障或超时时, 将进入 15 秒的快速回退状态(升级为 `hybrid`), 且恢复后立即退出。 + +与 `bind_interface`, `bind_inet4_address` 和 `bind_inet6_address` 冲突。 #### fallback_delay 在生成 RFC 6555 快速回退连接之前等待的时间长度。 -也就是说,是在假设之前等待 IPv6 成功的时间量如果设置了 "prefer_ipv4",则 IPv6 配置错误并回退到 IPv4。 -如果为零,则使用 300 毫秒的默认延迟。 -仅当 `domain_strategy` 为 `prefer_ipv4` 或 `prefer_ipv6` 时生效。 +对于 `domain_strategy`,是在假设之前等待 IPv6 成功的时间量如果设置了 "prefer_ipv4",则 IPv6 配置错误并回退到 IPv4。 + +对于 `network_strategy`,对于 `network_strategy`,是在回退到其他接口之前等待连接成功的时间。 + +仅当 `domain_strategy` 或 `network_strategy` 已设置时生效。 + +默认使用 `300ms`。 diff --git a/experimental/libbox/monitor.go b/experimental/libbox/monitor.go index d144fa07e4..eac0026d24 100644 --- a/experimental/libbox/monitor.go +++ b/experimental/libbox/monitor.go @@ -75,7 +75,7 @@ func (m *platformDefaultInterfaceMonitor) updateDefaultInterface(interfaceName s callbacks := m.callbacks.Array() m.defaultInterfaceAccess.Unlock() for _, callback := range callbacks { - callback(tun.EventInterfaceUpdate) + callback(nil, 0) } return } @@ -94,6 +94,6 @@ func (m *platformDefaultInterfaceMonitor) updateDefaultInterface(interfaceName s callbacks := m.callbacks.Array() m.defaultInterfaceAccess.Unlock() for _, callback := range callbacks { - callback(tun.EventInterfaceUpdate) + callback(newInterface, 0) } } diff --git a/option/outbound.go b/option/outbound.go index 0e2d5874da..5791802c9e 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -65,21 +65,23 @@ type DialerOptionsWrapper interface { } type DialerOptions struct { - Detour string `json:"detour,omitempty"` - BindInterface string `json:"bind_interface,omitempty"` - Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"` - Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"` - ProtectPath string `json:"protect_path,omitempty"` - RoutingMark uint32 `json:"routing_mark,omitempty"` - ReuseAddr bool `json:"reuse_addr,omitempty"` - ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"` - TCPFastOpen bool `json:"tcp_fast_open,omitempty"` - TCPMultiPath bool `json:"tcp_multi_path,omitempty"` - UDPFragment *bool `json:"udp_fragment,omitempty"` - UDPFragmentDefault bool `json:"-"` - DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` - FallbackDelay badoption.Duration `json:"fallback_delay,omitempty"` - IsWireGuardListener bool `json:"-"` + Detour string `json:"detour,omitempty"` + BindInterface string `json:"bind_interface,omitempty"` + Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"` + Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"` + ProtectPath string `json:"protect_path,omitempty"` + RoutingMark uint32 `json:"routing_mark,omitempty"` + ReuseAddr bool `json:"reuse_addr,omitempty"` + ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"` + TCPFastOpen bool `json:"tcp_fast_open,omitempty"` + TCPMultiPath bool `json:"tcp_multi_path,omitempty"` + UDPFragment *bool `json:"udp_fragment,omitempty"` + UDPFragmentDefault bool `json:"-"` + DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` + NetworkStrategy NetworkStrategy `json:"network_strategy,omitempty"` + FallbackDelay badoption.Duration `json:"fallback_delay,omitempty"` + NetworkFallbackDelay badoption.Duration `json:"network_fallback_delay,omitempty"` + IsWireGuardListener bool `json:"-"` } func (o *DialerOptions) TakeDialerOptions() DialerOptions { diff --git a/option/route.go b/option/route.go index dfd72986ff..236e56f7ba 100644 --- a/option/route.go +++ b/option/route.go @@ -1,16 +1,20 @@ package option +import "github.com/sagernet/sing/common/json/badoption" + type RouteOptions struct { - GeoIP *GeoIPOptions `json:"geoip,omitempty"` - Geosite *GeositeOptions `json:"geosite,omitempty"` - Rules []Rule `json:"rules,omitempty"` - RuleSet []RuleSet `json:"rule_set,omitempty"` - Final string `json:"final,omitempty"` - FindProcess bool `json:"find_process,omitempty"` - AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` - OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"` - DefaultInterface string `json:"default_interface,omitempty"` - DefaultMark uint32 `json:"default_mark,omitempty"` + GeoIP *GeoIPOptions `json:"geoip,omitempty"` + Geosite *GeositeOptions `json:"geosite,omitempty"` + Rules []Rule `json:"rules,omitempty"` + RuleSet []RuleSet `json:"rule_set,omitempty"` + Final string `json:"final,omitempty"` + FindProcess bool `json:"find_process,omitempty"` + AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` + OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"` + DefaultInterface string `json:"default_interface,omitempty"` + DefaultMark uint32 `json:"default_mark,omitempty"` + DefaultNetworkStrategy NetworkStrategy `json:"default_network_strategy,omitempty"` + DefaultFallbackDelay badoption.Duration `json:"default_fallback_delay,omitempty"` } type GeoIPOptions struct { diff --git a/option/rule_action.go b/option/rule_action.go index 9bc1303933..7c31ea7a42 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -137,14 +137,18 @@ func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) e } type RouteActionOptions struct { - Outbound string `json:"outbound,omitempty"` - UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` - UDPConnect bool `json:"udp_connect,omitempty"` + Outbound string `json:"outbound,omitempty"` + NetworkStrategy NetworkStrategy `json:"network_strategy,omitempty"` + FallbackDelay uint32 `json:"fallback_delay,omitempty"` + UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` + UDPConnect bool `json:"udp_connect,omitempty"` } type _RouteOptionsActionOptions struct { - UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` - UDPConnect bool `json:"udp_connect,omitempty"` + NetworkStrategy NetworkStrategy `json:"network_strategy,omitempty"` + FallbackDelay uint32 `json:"fallback_delay,omitempty"` + UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` + UDPConnect bool `json:"udp_connect,omitempty"` } type RouteOptionsActionOptions _RouteOptionsActionOptions diff --git a/option/types.go b/option/types.go index 04e3f10e23..8ed06250b5 100644 --- a/option/types.go +++ b/option/types.go @@ -3,6 +3,7 @@ package option import ( "strings" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-dns" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" @@ -150,3 +151,23 @@ func DNSQueryTypeToString(queryType uint16) string { } return F.ToString(queryType) } + +type NetworkStrategy C.NetworkStrategy + +func (n NetworkStrategy) MarshalJSON() ([]byte, error) { + return json.Marshal(C.NetworkStrategy(n).String()) +} + +func (n *NetworkStrategy) UnmarshalJSON(content []byte) error { + var value string + err := json.Unmarshal(content, &value) + if err != nil { + return err + } + strategy, loaded := C.StringToNetworkStrategy[value] + if !loaded { + return E.New("unknown network strategy: ", value) + } + *n = NetworkStrategy(strategy) + return nil +} diff --git a/protocol/direct/outbound.go b/protocol/direct/outbound.go index 27b334c9a4..4251c36633 100644 --- a/protocol/direct/outbound.go +++ b/protocol/direct/outbound.go @@ -13,6 +13,7 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" dns "github.com/sagernet/sing-dns" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" @@ -24,31 +25,38 @@ func RegisterOutbound(registry *outbound.Registry) { outbound.Register[option.DirectOutboundOptions](registry, C.TypeDirect, NewOutbound) } -var _ N.ParallelDialer = (*Outbound)(nil) +var ( + _ N.ParallelDialer = (*Outbound)(nil) + _ dialer.ParallelNetworkDialer = (*Outbound)(nil) +) type Outbound struct { outbound.Adapter - logger logger.ContextLogger - dialer N.Dialer - domainStrategy dns.DomainStrategy - fallbackDelay time.Duration - overrideOption int - overrideDestination M.Socksaddr + logger logger.ContextLogger + dialer dialer.ParallelInterfaceDialer + domainStrategy dns.DomainStrategy + fallbackDelay time.Duration + networkStrategy C.NetworkStrategy + networkFallbackDelay time.Duration + overrideOption int + overrideDestination M.Socksaddr // loopBack *loopBackDetector } func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.DirectOutboundOptions) (adapter.Outbound, error) { options.UDPFragmentDefault = true - outboundDialer, err := dialer.New(ctx, options.DialerOptions) + outboundDialer, err := dialer.NewDirect(ctx, options.DialerOptions) if err != nil { return nil, err } outbound := &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions(C.TypeDirect, []string{N.NetworkTCP, N.NetworkUDP}, tag, options.DialerOptions), - logger: logger, - domainStrategy: dns.DomainStrategy(options.DomainStrategy), - fallbackDelay: time.Duration(options.FallbackDelay), - dialer: outboundDialer, + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeDirect, []string{N.NetworkTCP, N.NetworkUDP}, tag, options.DialerOptions), + logger: logger, + domainStrategy: dns.DomainStrategy(options.DomainStrategy), + fallbackDelay: time.Duration(options.FallbackDelay), + networkStrategy: C.NetworkStrategy(options.NetworkStrategy), + networkFallbackDelay: time.Duration(options.NetworkFallbackDelay), + dialer: outboundDialer, // loopBack: newLoopBackDetector(router), } if options.ProxyProtocol != 0 { @@ -96,6 +104,37 @@ func (h *Outbound) DialContext(ctx context.Context, network string, destination return h.dialer.DialContext(ctx, network, destination) } +func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + originDestination := destination + switch h.overrideOption { + case 1: + destination = h.overrideDestination + case 2: + newDestination := h.overrideDestination + newDestination.Port = destination.Port + destination = newDestination + case 3: + destination.Port = h.overrideDestination.Port + } + if h.overrideOption == 0 { + h.logger.InfoContext(ctx, "outbound packet connection") + } else { + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + conn, err := h.dialer.ListenPacket(ctx, destination) + if err != nil { + return nil, err + } + // conn = h.loopBack.NewPacketConn(bufio.NewPacketConn(conn), destination) + if originDestination != destination { + conn = bufio.NewNATPacketConn(bufio.NewPacketConn(conn), destination, originDestination) + } + return conn, nil +} + func (h *Outbound) DialParallel(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() @@ -120,14 +159,64 @@ func (h *Outbound) DialParallel(ctx context.Context, network string, destination } else { domainStrategy = dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) } - return N.DialParallel(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == dns.DomainStrategyPreferIPv6, h.fallbackDelay) + switch domainStrategy { + case dns.DomainStrategyUseIPv4: + destinationAddresses = common.Filter(destinationAddresses, netip.Addr.Is4) + if len(destinationAddresses) == 0 { + return nil, E.New("no IPv4 address available for ", destination) + } + case dns.DomainStrategyUseIPv6: + destinationAddresses = common.Filter(destinationAddresses, netip.Addr.Is6) + if len(destinationAddresses) == 0 { + return nil, E.New("no IPv6 address available for ", destination) + } + } + return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == dns.DomainStrategyPreferIPv6, h.networkStrategy, h.fallbackDelay) } -func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (h *Outbound) DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) { + ctx, metadata := adapter.ExtendContext(ctx) + metadata.Outbound = h.Tag() + metadata.Destination = destination + switch h.overrideOption { + case 1, 2: + // override address + return h.DialContext(ctx, network, destination) + case 3: + destination.Port = h.overrideDestination.Port + } + network = N.NetworkName(network) + switch network { + case N.NetworkTCP: + h.logger.InfoContext(ctx, "outbound connection to ", destination) + case N.NetworkUDP: + h.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + var domainStrategy dns.DomainStrategy + if h.domainStrategy != dns.DomainStrategyAsIS { + domainStrategy = h.domainStrategy + } else { + domainStrategy = dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) + } + switch domainStrategy { + case dns.DomainStrategyUseIPv4: + destinationAddresses = common.Filter(destinationAddresses, netip.Addr.Is4) + if len(destinationAddresses) == 0 { + return nil, E.New("no IPv4 address available for ", destination) + } + case dns.DomainStrategyUseIPv6: + destinationAddresses = common.Filter(destinationAddresses, netip.Addr.Is6) + if len(destinationAddresses) == 0 { + return nil, E.New("no IPv6 address available for ", destination) + } + } + return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == dns.DomainStrategyPreferIPv6, networkStrategy, fallbackDelay) +} + +func (h *Outbound) ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination - originDestination := destination switch h.overrideOption { case 1: destination = h.overrideDestination @@ -143,15 +232,11 @@ func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n } else { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) } - conn, err := h.dialer.ListenPacket(ctx, destination) + conn, newDestination, err := dialer.ListenSerialNetworkPacket(ctx, h.dialer, destination, destinationAddresses, networkStrategy, fallbackDelay) if err != nil { - return nil, err - } - // conn = h.loopBack.NewPacketConn(bufio.NewPacketConn(conn), destination) - if originDestination != destination { - conn = bufio.NewNATPacketConn(bufio.NewPacketConn(conn), destination, originDestination) + return nil, netip.Addr{}, err } - return conn, nil + return conn, newDestination, nil } /*func (h *Outbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { diff --git a/protocol/socks/outbound.go b/protocol/socks/outbound.go index dbb5ab6177..70a5a5eda0 100644 --- a/protocol/socks/outbound.go +++ b/protocol/socks/outbound.go @@ -10,7 +10,6 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing-dns" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" @@ -115,23 +114,3 @@ func (h *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n h.logger.InfoContext(ctx, "outbound packet connection to ", destination) return h.client.ListenPacket(ctx, destination) } - -// TODO -// Deprecated -func (h *Outbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - if h.resolve { - return outbound.NewDirectConnection(ctx, h.router, h, conn, metadata, dns.DomainStrategyUseIPv4) - } else { - return outbound.NewConnection(ctx, h, conn, metadata) - } -} - -// TODO -// Deprecated -func (h *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - if h.resolve { - return outbound.NewDirectPacketConnection(ctx, h.router, h, conn, metadata, dns.DomainStrategyUseIPv4) - } else { - return outbound.NewPacketConnection(ctx, h, conn, metadata) - } -} diff --git a/protocol/wireguard/outbound.go b/protocol/wireguard/outbound.go index 12e53e591a..70d74140be 100644 --- a/protocol/wireguard/outbound.go +++ b/protocol/wireguard/outbound.go @@ -16,7 +16,6 @@ import ( "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/wireguard" - "github.com/sagernet/sing-dns" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" @@ -238,15 +237,3 @@ func (w *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n } return w.tunDevice.ListenPacket(ctx, destination) } - -// TODO -// Deprecated -func (w *Outbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - return outbound.NewDirectConnection(ctx, w.router, w, conn, metadata, dns.DomainStrategyAsIS) -} - -// TODO -// Deprecated -func (w *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return outbound.NewDirectPacketConnection(ctx, w.router, w, conn, metadata, dns.DomainStrategyAsIS) -} diff --git a/route/network.go b/route/network.go index d4b8adcf41..eda1a054a9 100644 --- a/route/network.go +++ b/route/network.go @@ -8,6 +8,7 @@ import ( "runtime" "strings" "syscall" + "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/conntrack" @@ -38,8 +39,7 @@ type NetworkManager struct { networkInterfaces atomic.TypedValue[[]adapter.NetworkInterface] autoDetectInterface bool - defaultInterface string - defaultMark uint32 + defaultOptions adapter.NetworkOptions autoRedirectOutputMark uint32 networkMonitor tun.NetworkUpdateMonitor @@ -58,11 +58,23 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOp logger: logger, interfaceFinder: control.NewDefaultInterfaceFinder(), autoDetectInterface: routeOptions.AutoDetectInterface, - defaultInterface: routeOptions.DefaultInterface, - defaultMark: routeOptions.DefaultMark, - pauseManager: service.FromContext[pause.Manager](ctx), - platformInterface: service.FromContext[platform.Interface](ctx), - outboundManager: service.FromContext[adapter.OutboundManager](ctx), + defaultOptions: adapter.NetworkOptions{ + DefaultInterface: routeOptions.DefaultInterface, + DefaultMark: routeOptions.DefaultMark, + DefaultNetworkStrategy: C.NetworkStrategy(routeOptions.DefaultNetworkStrategy), + DefaultFallbackDelay: time.Duration(routeOptions.DefaultFallbackDelay), + }, + pauseManager: service.FromContext[pause.Manager](ctx), + platformInterface: service.FromContext[platform.Interface](ctx), + outboundManager: service.FromContext[adapter.OutboundManager](ctx), + } + if C.NetworkStrategy(routeOptions.DefaultNetworkStrategy) != C.NetworkStrategyDefault { + if routeOptions.DefaultInterface != "" { + return nil, E.New("`default_network_strategy` is conflict with `default_interface`") + } + if !routeOptions.AutoDetectInterface { + return nil, E.New("`auto_detect_interface` is required by `default_network_strategy`") + } } usePlatformDefaultInterfaceMonitor := nm.platformInterface != nil enforceInterfaceMonitor := routeOptions.AutoDetectInterface @@ -81,12 +93,12 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOp if err != nil { return nil, E.New("auto_detect_interface unsupported on current platform") } - interfaceMonitor.RegisterCallback(nm.notifyNetworkUpdate) + interfaceMonitor.RegisterCallback(nm.notifyInterfaceUpdate) nm.interfaceMonitor = interfaceMonitor } } else { interfaceMonitor := nm.platformInterface.CreateDefaultInterfaceMonitor(logger) - interfaceMonitor.RegisterCallback(nm.notifyNetworkUpdate) + interfaceMonitor.RegisterCallback(nm.notifyInterfaceUpdate) nm.interfaceMonitor = interfaceMonitor } return nm, nil @@ -262,10 +274,6 @@ func (r *NetworkManager) NetworkInterfaces() []adapter.NetworkInterface { return r.networkInterfaces.Load() } -func (r *NetworkManager) DefaultInterface() string { - return r.defaultInterface -} - func (r *NetworkManager) AutoDetectInterface() bool { return r.autoDetectInterface } @@ -298,8 +306,19 @@ func (r *NetworkManager) AutoDetectInterfaceFunc() control.Func { } } -func (r *NetworkManager) DefaultMark() uint32 { - return r.defaultMark +func (r *NetworkManager) ProtectFunc() control.Func { + if r.platformInterface != nil && r.platformInterface.UsePlatformAutoDetectInterfaceControl() { + return func(network, address string, conn syscall.RawConn) error { + return control.Raw(conn, func(fd uintptr) error { + return r.platformInterface.AutoDetectInterfaceControl(int(fd)) + }) + } + } + return nil +} + +func (r *NetworkManager) DefaultOptions() adapter.NetworkOptions { + return r.defaultOptions } func (r *NetworkManager) RegisterAutoRedirectOutputMark(mark uint32) error { @@ -341,45 +360,47 @@ func (r *NetworkManager) ResetNetwork() { } } -func (r *NetworkManager) notifyNetworkUpdate(event int) { - if event == tun.EventNoRoute { +func (r *NetworkManager) notifyInterfaceUpdate(defaultInterface *control.Interface, flags int) { + if defaultInterface == nil { r.pauseManager.NetworkPause() r.logger.Error("missing default interface") - } else { - r.pauseManager.NetworkWake() - defaultInterface := r.DefaultNetworkInterface() - if defaultInterface == nil { - panic("invalid interface context") - } - var options []string - options = append(options, F.ToString("index ", defaultInterface.Index)) - if C.IsAndroid && r.platformInterface == nil { - var vpnStatus string - if r.interfaceMonitor.AndroidVPNEnabled() { - vpnStatus = "enabled" - } else { - vpnStatus = "disabled" - } - options = append(options, "vpn "+vpnStatus) + return + } + + r.pauseManager.NetworkWake() + var options []string + options = append(options, F.ToString("index ", defaultInterface.Index)) + if C.IsAndroid && r.platformInterface == nil { + var vpnStatus string + if r.interfaceMonitor.AndroidVPNEnabled() { + vpnStatus = "enabled" } else { - if defaultInterface.Type != "" { - options = append(options, F.ToString("type ", defaultInterface.Type)) - } - if defaultInterface.Expensive { - options = append(options, "expensive") - } - if defaultInterface.Constrained { - options = append(options, "constrained") - } + vpnStatus = "disabled" } - r.logger.Info("updated default interface ", defaultInterface.Name, ", ", strings.Join(options, ", ")) - if r.platformInterface != nil { - state := r.platformInterface.ReadWIFIState() - if state != r.wifiState { - r.wifiState = state - if state.SSID != "" { - r.logger.Info("updated WIFI state: SSID=", state.SSID, ", BSSID=", state.BSSID) - } + options = append(options, "vpn "+vpnStatus) + } else if r.platformInterface != nil { + networkInterface := common.Find(r.networkInterfaces.Load(), func(it adapter.NetworkInterface) bool { + return it.Interface.Index == defaultInterface.Index + }) + if networkInterface.Type == "" { + // race + return + } + options = append(options, F.ToString("type ", networkInterface.Type)) + if networkInterface.Expensive { + options = append(options, "expensive") + } + if networkInterface.Constrained { + options = append(options, "constrained") + } + } + r.logger.Info("updated default interface ", defaultInterface.Name, ", ", strings.Join(options, ", ")) + if r.platformInterface != nil { + state := r.platformInterface.ReadWIFIState() + if state != r.wifiState { + r.wifiState = state + if state.SSID != "" { + r.logger.Info("updated WIFI state: SSID=", state.SSID, ", BSSID=", state.BSSID) } } } diff --git a/route/route.go b/route/route.go index 1c4da4b7ad..051ee403ba 100644 --- a/route/route.go +++ b/route/route.go @@ -424,9 +424,13 @@ match: } switch action := currentRule.Action().(type) { case *rule.RuleActionRoute: + metadata.NetworkStrategy = action.NetworkStrategy + metadata.FallbackDelay = action.FallbackDelay metadata.UDPDisableDomainUnmapping = action.UDPDisableDomainUnmapping metadata.UDPConnect = action.UDPConnect case *rule.RuleActionRouteOptions: + metadata.NetworkStrategy = action.NetworkStrategy + metadata.FallbackDelay = action.FallbackDelay metadata.UDPDisableDomainUnmapping = action.UDPDisableDomainUnmapping metadata.UDPConnect = action.UDPConnect case *rule.RuleActionSniff: diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index d44e36eeac..f9b2e64108 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -30,12 +30,16 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti return &RuleActionRoute{ Outbound: action.RouteOptions.Outbound, RuleActionRouteOptions: RuleActionRouteOptions{ + NetworkStrategy: C.NetworkStrategy(action.RouteOptions.NetworkStrategy), + FallbackDelay: time.Duration(action.RouteOptions.FallbackDelay), UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping, UDPConnect: action.RouteOptions.UDPConnect, }, }, nil case C.RuleActionTypeRouteOptions: return &RuleActionRouteOptions{ + NetworkStrategy: C.NetworkStrategy(action.RouteOptionsOptions.NetworkStrategy), + FallbackDelay: time.Duration(action.RouteOptionsOptions.FallbackDelay), UDPDisableDomainUnmapping: action.RouteOptionsOptions.UDPDisableDomainUnmapping, UDPConnect: action.RouteOptionsOptions.UDPConnect, }, nil @@ -135,6 +139,8 @@ func (r *RuleActionRoute) String() string { } type RuleActionRouteOptions struct { + NetworkStrategy C.NetworkStrategy + FallbackDelay time.Duration UDPDisableDomainUnmapping bool UDPConnect bool } diff --git a/transport/dhcp/server.go b/transport/dhcp/server.go index 9a06ac1716..29c6bbe022 100644 --- a/transport/dhcp/server.go +++ b/transport/dhcp/server.go @@ -166,7 +166,7 @@ func (t *Transport) updateServers() error { } } -func (t *Transport) interfaceUpdated(int) { +func (t *Transport) interfaceUpdated(defaultInterface *control.Interface, flags int) { err := t.updateServers() if err != nil { t.options.Logger.Error("update servers: ", err) From 292fcde876baa1db15c8671efcdd844210ee1146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 12 Nov 2024 22:49:36 +0800 Subject: [PATCH 22/49] documentation: Remove unused titles --- docs/configuration/dns/fakeip.md | 2 -- docs/configuration/dns/fakeip.zh.md | 2 -- docs/configuration/dns/rule_action.md | 2 -- docs/configuration/dns/rule_action.zh.md | 2 -- docs/configuration/inbound/shadowtls.md | 4 ++-- docs/configuration/rule-set/adguard.md | 2 -- docs/configuration/rule-set/adguard.zh.md | 2 -- docs/configuration/rule-set/source-format.md | 2 -- docs/configuration/rule-set/source-format.zh.md | 2 -- docs/configuration/shared/tls.md | 2 +- docs/configuration/shared/udp-over-tcp.md | 2 -- 11 files changed, 3 insertions(+), 21 deletions(-) diff --git a/docs/configuration/dns/fakeip.md b/docs/configuration/dns/fakeip.md index 51db1f42d6..63490ac197 100644 --- a/docs/configuration/dns/fakeip.md +++ b/docs/configuration/dns/fakeip.md @@ -1,5 +1,3 @@ -# FakeIP - ### Structure ```json diff --git a/docs/configuration/dns/fakeip.zh.md b/docs/configuration/dns/fakeip.zh.md index 3d9a814a66..10c6dc68e7 100644 --- a/docs/configuration/dns/fakeip.zh.md +++ b/docs/configuration/dns/fakeip.zh.md @@ -1,5 +1,3 @@ -# FakeIP - ### 结构 ```json diff --git a/docs/configuration/dns/rule_action.md b/docs/configuration/dns/rule_action.md index cccc139679..af19131f62 100644 --- a/docs/configuration/dns/rule_action.md +++ b/docs/configuration/dns/rule_action.md @@ -2,8 +2,6 @@ icon: material/new-box --- -# DNS Rule Action - !!! question "Since sing-box 1.11.0" ### route diff --git a/docs/configuration/dns/rule_action.zh.md b/docs/configuration/dns/rule_action.zh.md index 8fad238169..219a5fd757 100644 --- a/docs/configuration/dns/rule_action.zh.md +++ b/docs/configuration/dns/rule_action.zh.md @@ -2,8 +2,6 @@ icon: material/new-box --- -# DNS 规则动作 - !!! question "自 sing-box 1.11.0 起" ### route diff --git a/docs/configuration/inbound/shadowtls.md b/docs/configuration/inbound/shadowtls.md index db010b7908..0f6684d20e 100644 --- a/docs/configuration/inbound/shadowtls.md +++ b/docs/configuration/inbound/shadowtls.md @@ -66,11 +66,11 @@ Only available in the ShadowTLS protocol 3. ==Required== -Handshake server address and [Dial options](/configuration/shared/dial/). +Handshake server address and [Dial Fields](/configuration/shared/dial/). #### handshake_for_server_name -Handshake server address and [Dial options](/configuration/shared/dial/) for specific server name. +Handshake server address and [Dial Fields](/configuration/shared/dial/) for specific server name. Only available in the ShadowTLS protocol 2/3. diff --git a/docs/configuration/rule-set/adguard.md b/docs/configuration/rule-set/adguard.md index 870972bf2f..bda7379451 100644 --- a/docs/configuration/rule-set/adguard.md +++ b/docs/configuration/rule-set/adguard.md @@ -2,8 +2,6 @@ icon: material/new-box --- -# AdGuard DNS Filter - !!! question "Since sing-box 1.10.0" sing-box supports some rule-set formats from other projects which cannot be fully translated to sing-box, diff --git a/docs/configuration/rule-set/adguard.zh.md b/docs/configuration/rule-set/adguard.zh.md index b08ab34b4d..026f2e0bea 100644 --- a/docs/configuration/rule-set/adguard.zh.md +++ b/docs/configuration/rule-set/adguard.zh.md @@ -2,8 +2,6 @@ icon: material/new-box --- -# AdGuard DNS Filter - !!! question "自 sing-box 1.10.0 起" sing-box 支持其他项目的一些规则集格式,这些格式无法完全转换为 sing-box, diff --git a/docs/configuration/rule-set/source-format.md b/docs/configuration/rule-set/source-format.md index 43e8ea4535..1dcc1d4470 100644 --- a/docs/configuration/rule-set/source-format.md +++ b/docs/configuration/rule-set/source-format.md @@ -2,8 +2,6 @@ icon: material/new-box --- -# Source Format - !!! quote "Changes in sing-box 1.11.0" :material-plus: version `3` diff --git a/docs/configuration/rule-set/source-format.zh.md b/docs/configuration/rule-set/source-format.zh.md index 0e8fc0cb96..3dacaea7c8 100644 --- a/docs/configuration/rule-set/source-format.zh.md +++ b/docs/configuration/rule-set/source-format.zh.md @@ -2,8 +2,6 @@ icon: material/new-box --- -# 源文件格式 - !!! quote "sing-box 1.11.0 中的更改" :material-plus: version `3` diff --git a/docs/configuration/shared/tls.md b/docs/configuration/shared/tls.md index 40343e18ef..7a6c9d5e08 100644 --- a/docs/configuration/shared/tls.md +++ b/docs/configuration/shared/tls.md @@ -380,7 +380,7 @@ See [DNS01 Challenge Fields](/configuration/shared/dns01_challenge/) for details ==Required== -Handshake server address and [Dial options](/configuration/shared/dial/). +Handshake server address and [Dial Fields](/configuration/shared/dial/). #### private_key diff --git a/docs/configuration/shared/udp-over-tcp.md b/docs/configuration/shared/udp-over-tcp.md index 6bd8a68a91..9c2688b94b 100644 --- a/docs/configuration/shared/udp-over-tcp.md +++ b/docs/configuration/shared/udp-over-tcp.md @@ -1,5 +1,3 @@ -# UDP over TCP - !!! warning "" It's a proprietary protocol created by SagerNet, not part of shadowsocks. From f3bf440c91995b185772ea1242ed7b9f8a4c3cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 13 Nov 2024 19:05:28 +0800 Subject: [PATCH 23/49] Refactor multi networks strategy --- adapter/inbound.go | 2 + adapter/network.go | 12 +-- adapter/outbound/default.go | 6 +- common/dialer/default.go | 63 +++++++++------ common/dialer/default_parallel_interface.go | 90 +++++++++------------ common/dialer/default_parallel_network.go | 18 ++--- common/dialer/dialer.go | 8 +- common/dialer/resolve.go | 10 +-- common/srs/binary.go | 16 +++- constant/network.go | 54 +++++++------ docs/configuration/route/index.md | 37 +++++---- docs/configuration/route/index.zh.md | 39 ++++++--- docs/configuration/route/rule_action.md | 22 ++--- docs/configuration/route/rule_action.zh.md | 22 ++--- docs/configuration/shared/dial.md | 63 +++++++++++---- docs/configuration/shared/dial.zh.md | 54 ++++++++++--- experimental/libbox/platform.go | 10 +-- experimental/libbox/service.go | 2 +- option/outbound.go | 36 +++++---- option/route.go | 26 +++--- option/rule.go | 72 ++++++++--------- option/rule_dns.go | 76 ++++++++--------- option/rule_set.go | 44 +++++----- option/types.go | 24 ++++++ protocol/direct/outbound.go | 16 ++-- route/network.go | 12 +-- route/rule/rule_action.go | 2 + route/rule/rule_default.go | 3 +- route/rule/rule_dns.go | 2 +- route/rule/rule_headless.go | 3 +- route/rule/rule_item_network_type.go | 5 +- 31 files changed, 507 insertions(+), 342 deletions(-) diff --git a/adapter/inbound.go b/adapter/inbound.go index 33b1b4d164..1093bac4e6 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -69,6 +69,8 @@ type InboundContext struct { UDPDisableDomainUnmapping bool UDPConnect bool NetworkStrategy C.NetworkStrategy + NetworkType []C.InterfaceType + FallbackNetworkType []C.InterfaceType FallbackDelay time.Duration DNSServer string diff --git a/adapter/network.go b/adapter/network.go index dd924c339f..08fc00fac0 100644 --- a/adapter/network.go +++ b/adapter/network.go @@ -28,10 +28,12 @@ type NetworkManager interface { } type NetworkOptions struct { - DefaultNetworkStrategy C.NetworkStrategy - DefaultFallbackDelay time.Duration - DefaultInterface string - DefaultMark uint32 + NetworkStrategy C.NetworkStrategy + NetworkType []C.InterfaceType + FallbackNetworkType []C.InterfaceType + FallbackDelay time.Duration + BindInterface string + RoutingMark uint32 } type InterfaceUpdateListener interface { @@ -45,7 +47,7 @@ type WIFIState struct { type NetworkInterface struct { control.Interface - Type string + Type C.InterfaceType DNSServers []string Expensive bool Constrained bool diff --git a/adapter/outbound/default.go b/adapter/outbound/default.go index 84be8abaf1..27cafb9d2f 100644 --- a/adapter/outbound/default.go +++ b/adapter/outbound/default.go @@ -26,7 +26,7 @@ func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata a var err error if len(metadata.DestinationAddresses) > 0 { if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer { - outConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.FallbackDelay) + outConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) } else { outConn, err = N.DialSerial(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses) } @@ -56,7 +56,7 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, if metadata.UDPConnect { if len(metadata.DestinationAddresses) > 0 { if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer { - outConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.FallbackDelay) + outConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) } else { outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses) } @@ -74,7 +74,7 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, } else { if len(metadata.DestinationAddresses) > 0 { if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer { - outPacketConn, destinationAddress, err = dialer.ListenSerialNetworkPacket(ctx, parallelDialer, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.FallbackDelay) + outPacketConn, destinationAddress, err = dialer.ListenSerialNetworkPacket(ctx, parallelDialer, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) } else { outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses) } diff --git a/common/dialer/default.go b/common/dialer/default.go index c7329f2e1a..5871e64007 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -10,6 +10,7 @@ import ( "github.com/sagernet/sing-box/common/conntrack" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/atomic" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" @@ -33,6 +34,8 @@ type DefaultDialer struct { isWireGuardListener bool networkManager adapter.NetworkManager networkStrategy C.NetworkStrategy + networkType []C.InterfaceType + fallbackNetworkType []C.InterfaceType networkFallbackDelay time.Duration networkLastFallback atomic.TypedValue[time.Time] } @@ -43,6 +46,8 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti listener net.ListenConfig interfaceFinder control.InterfaceFinder networkStrategy C.NetworkStrategy + networkType []C.InterfaceType + fallbackNetworkType []C.InterfaceType networkFallbackDelay time.Duration ) if networkManager != nil { @@ -56,8 +61,8 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti listener.Control = control.Append(listener.Control, bindFunc) } if options.RoutingMark > 0 { - dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark)) - listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark)) + dialer.Control = control.Append(dialer.Control, control.RoutingMark(uint32(options.RoutingMark))) + listener.Control = control.Append(listener.Control, control.RoutingMark(uint32(options.RoutingMark))) } if networkManager != nil { autoRedirectOutputMark := networkManager.AutoRedirectOutputMark() @@ -74,6 +79,8 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti return nil, E.New("`network_strategy` is conflict with `bind_interface`, `inet4_bind_address` and `inet6_bind_address`") } networkStrategy = C.NetworkStrategy(options.NetworkStrategy) + networkType = common.Map(options.NetworkType, option.InterfaceType.Build) + fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build) networkFallbackDelay = time.Duration(options.NetworkFallbackDelay) if networkManager == nil || !networkManager.AutoDetectInterface() { return nil, E.New("`route.auto_detect_interface` is require by `network_strategy`") @@ -81,23 +88,31 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti } if networkManager != nil && options.BindInterface == "" && options.Inet4BindAddress == nil && options.Inet6BindAddress == nil { defaultOptions := networkManager.DefaultOptions() - if defaultOptions.DefaultInterface != "" { - bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.DefaultInterface, -1) - dialer.Control = control.Append(dialer.Control, bindFunc) - listener.Control = control.Append(listener.Control, bindFunc) - } else if networkManager.AutoDetectInterface() { - if defaultOptions.DefaultNetworkStrategy != C.NetworkStrategyDefault && C.NetworkStrategy(options.NetworkStrategy) == C.NetworkStrategyDefault { - networkStrategy = defaultOptions.DefaultNetworkStrategy - networkFallbackDelay = defaultOptions.DefaultFallbackDelay - bindFunc := networkManager.ProtectFunc() - dialer.Control = control.Append(dialer.Control, bindFunc) - listener.Control = control.Append(listener.Control, bindFunc) - } else { - bindFunc := networkManager.AutoDetectInterfaceFunc() + if options.BindInterface == "" { + if defaultOptions.BindInterface != "" { + bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1) dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) + } else if networkManager.AutoDetectInterface() { + if defaultOptions.NetworkStrategy != C.NetworkStrategyDefault && C.NetworkStrategy(options.NetworkStrategy) == C.NetworkStrategyDefault { + networkStrategy = defaultOptions.NetworkStrategy + networkType = defaultOptions.NetworkType + fallbackNetworkType = defaultOptions.FallbackNetworkType + networkFallbackDelay = defaultOptions.FallbackDelay + bindFunc := networkManager.ProtectFunc() + dialer.Control = control.Append(dialer.Control, bindFunc) + listener.Control = control.Append(listener.Control, bindFunc) + } else { + bindFunc := networkManager.AutoDetectInterfaceFunc() + dialer.Control = control.Append(dialer.Control, bindFunc) + listener.Control = control.Append(listener.Control, bindFunc) + } } } + if options.RoutingMark == 0 && defaultOptions.RoutingMark != 0 { + dialer.Control = control.Append(dialer.Control, control.RoutingMark(defaultOptions.RoutingMark)) + listener.Control = control.Append(listener.Control, control.RoutingMark(defaultOptions.RoutingMark)) + } } if options.ReuseAddr { listener.Control = control.Append(listener.Control, control.ReuseAddr()) @@ -179,6 +194,8 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti isWireGuardListener: options.IsWireGuardListener, networkManager: networkManager, networkStrategy: networkStrategy, + networkType: networkType, + fallbackNetworkType: fallbackNetworkType, networkFallbackDelay: networkFallbackDelay, }, nil } @@ -202,11 +219,11 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address return trackConn(DialSlowContext(&d.dialer6, ctx, network, address)) } } else { - return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkFallbackDelay) + return d.DialParallelInterface(ctx, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay) } } -func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network string, address M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) { +func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network string, address M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { if strategy == C.NetworkStrategyDefault { return d.DialContext(ctx, network, address) } @@ -226,9 +243,9 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin err error ) if !fastFallback { - conn, isPrimary, err = d.dialParallelInterface(ctx, dialer, network, address.String(), strategy, fallbackDelay) + conn, isPrimary, err = d.dialParallelInterface(ctx, dialer, network, address.String(), strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } else { - conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), strategy, fallbackDelay, d.networkLastFallback.Store) + conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), strategy, interfaceType, fallbackInterfaceType, fallbackDelay, d.networkLastFallback.Store) } if err != nil { return nil, err @@ -249,11 +266,11 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4)) } } else { - return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkFallbackDelay) + return d.ListenSerialInterfacePacket(ctx, destination, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay) } } -func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, error) { +func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) { if strategy == C.NetworkStrategyDefault { return d.ListenPacket(ctx, destination) } @@ -264,11 +281,11 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina if destination.IsIPv4() && !destination.Addr.IsUnspecified() { network += "4" } - return trackPacketConn(d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", strategy, fallbackDelay)) + return trackPacketConn(d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", strategy, interfaceType, fallbackInterfaceType, fallbackDelay)) } func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) { - return d.listenSerialInterfacePacket(context.Background(), d.udpListener, network, address, d.networkStrategy, d.networkFallbackDelay) + return d.listenSerialInterfacePacket(context.Background(), d.udpListener, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay) } func trackConn(conn net.Conn, err error) (net.Conn, error) { diff --git a/common/dialer/default_parallel_interface.go b/common/dialer/default_parallel_interface.go index baf0349ec8..71d9814bd2 100644 --- a/common/dialer/default_parallel_interface.go +++ b/common/dialer/default_parallel_interface.go @@ -7,14 +7,14 @@ import ( "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/control" E "github.com/sagernet/sing/common/exceptions" - F "github.com/sagernet/sing/common/format" N "github.com/sagernet/sing/common/network" ) -func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Dialer, network string, addr string, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, bool, error) { - primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy) +func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Dialer, network string, addr string, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, bool, error) { + primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType) if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { return nil, false, E.New("no available network interface") } @@ -84,8 +84,8 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di } } -func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, dialer net.Dialer, network string, addr string, strategy C.NetworkStrategy, fallbackDelay time.Duration, resetFastFallback func(time.Time)) (net.Conn, bool, error) { - primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy) +func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, dialer net.Dialer, network string, addr string, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration, resetFastFallback func(time.Time)) (net.Conn, bool, error) { + primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType) if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { return nil, false, E.New("no available network interface") } @@ -144,8 +144,8 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d } } -func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listener net.ListenConfig, network string, addr string, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, error) { - primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy) +func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listener net.ListenConfig, network string, addr string, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) { + primaryInterfaces, fallbackInterfaces := selectInterfaces(d.networkManager, strategy, interfaceType, fallbackInterfaceType) if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { return nil, E.New("no available network interface") } @@ -174,12 +174,12 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene return nil, E.Errors(errors...) } -func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkStrategy) (primaryInterfaces []adapter.NetworkInterface, fallbackInterfaces []adapter.NetworkInterface) { +func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType) (primaryInterfaces []adapter.NetworkInterface, fallbackInterfaces []adapter.NetworkInterface) { interfaces := networkManager.NetworkInterfaces() switch strategy { - case C.NetworkStrategyFallback: - defaultIf := networkManager.InterfaceMonitor().DefaultInterface() - if defaultIf != nil { + case C.NetworkStrategyDefault: + if len(interfaceType) == 0 { + defaultIf := networkManager.InterfaceMonitor().DefaultInterface() for _, iif := range interfaces { if iif.Index == defaultIf.Index { primaryInterfaces = append(primaryInterfaces, iif) @@ -188,54 +188,36 @@ func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkS } } } else { - primaryInterfaces = interfaces + primaryInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool { + return common.Contains(interfaceType, iif.Type) + }) } case C.NetworkStrategyHybrid: - primaryInterfaces = interfaces - case C.NetworkStrategyWIFI: - for _, iif := range interfaces { - if iif.Type == C.InterfaceTypeWIFI { - primaryInterfaces = append(primaryInterfaces, iif) - } else { - fallbackInterfaces = append(fallbackInterfaces, iif) - } - } - case C.NetworkStrategyCellular: - for _, iif := range interfaces { - if iif.Type == C.InterfaceTypeCellular { - primaryInterfaces = append(primaryInterfaces, iif) - } else { - fallbackInterfaces = append(fallbackInterfaces, iif) - } - } - case C.NetworkStrategyEthernet: - for _, iif := range interfaces { - if iif.Type == C.InterfaceTypeEthernet { - primaryInterfaces = append(primaryInterfaces, iif) - } else { - fallbackInterfaces = append(fallbackInterfaces, iif) - } - } - case C.NetworkStrategyWIFIOnly: - for _, iif := range interfaces { - if iif.Type == C.InterfaceTypeWIFI { - primaryInterfaces = append(primaryInterfaces, iif) - } - } - case C.NetworkStrategyCellularOnly: - for _, iif := range interfaces { - if iif.Type == C.InterfaceTypeCellular { - primaryInterfaces = append(primaryInterfaces, iif) - } + if len(interfaceType) == 0 { + primaryInterfaces = interfaces + } else { + primaryInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool { + return common.Contains(interfaceType, iif.Type) + }) } - case C.NetworkStrategyEthernetOnly: - for _, iif := range interfaces { - if iif.Type == C.InterfaceTypeEthernet { - primaryInterfaces = append(primaryInterfaces, iif) + case C.NetworkStrategyFallback: + if len(interfaceType) == 0 { + defaultIf := networkManager.InterfaceMonitor().DefaultInterface() + for _, iif := range interfaces { + if iif.Index == defaultIf.Index { + primaryInterfaces = append(primaryInterfaces, iif) + } else { + fallbackInterfaces = append(fallbackInterfaces, iif) + } } + } else { + primaryInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool { + return common.Contains(interfaceType, iif.Type) + }) } - default: - panic(F.ToString("unknown network strategy: ", strategy)) + fallbackInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool { + return common.Contains(fallbackInterfaceType, iif.Type) + }) } return primaryInterfaces, fallbackInterfaces } diff --git a/common/dialer/default_parallel_network.go b/common/dialer/default_parallel_network.go index f42d9330c7..ea043dfdb2 100644 --- a/common/dialer/default_parallel_network.go +++ b/common/dialer/default_parallel_network.go @@ -13,13 +13,13 @@ import ( N "github.com/sagernet/sing/common/network" ) -func DialSerialNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) { +func DialSerialNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel { - return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, fallbackDelay) + return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } var errors []error for _, address := range destinationAddresses { - conn, err := dialer.DialParallelInterface(ctx, network, M.SocksaddrFrom(address, destination.Port), strategy, fallbackDelay) + conn, err := dialer.DialParallelInterface(ctx, network, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay) if err == nil { return conn, nil } @@ -28,7 +28,7 @@ func DialSerialNetwork(ctx context.Context, dialer ParallelInterfaceDialer, netw return nil, E.Errors(errors...) } -func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, preferIPv6 bool, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) { +func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, preferIPv6 bool, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { if fallbackDelay == 0 { fallbackDelay = N.DefaultFallbackDelay } @@ -43,7 +43,7 @@ func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, ne return address.Is6() && !address.Is4In6() }) if len(addresses4) == 0 || len(addresses6) == 0 { - return DialSerialNetwork(ctx, dialer, network, destination, destinationAddresses, strategy, fallbackDelay) + return DialSerialNetwork(ctx, dialer, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } var primaries, fallbacks []netip.Addr if preferIPv6 { @@ -65,7 +65,7 @@ func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, ne if !primary { ras = fallbacks } - c, err := DialSerialNetwork(ctx, dialer, network, destination, ras, strategy, fallbackDelay) + c, err := DialSerialNetwork(ctx, dialer, network, destination, ras, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) select { case results <- dialResult{Conn: c, error: err, primary: primary, done: true}: case <-returned: @@ -106,13 +106,13 @@ func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, ne } } -func ListenSerialNetworkPacket(ctx context.Context, dialer ParallelInterfaceDialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { +func ListenSerialNetworkPacket(ctx context.Context, dialer ParallelInterfaceDialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel { - return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, fallbackDelay) + return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } var errors []error for _, address := range destinationAddresses { - conn, err := dialer.ListenSerialInterfacePacket(ctx, M.SocksaddrFrom(address, destination.Port), strategy, fallbackDelay) + conn, err := dialer.ListenSerialInterfacePacket(ctx, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay) if err == nil { return conn, address, nil } diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index b3305d7397..b307a3303b 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -77,11 +77,11 @@ func NewDirect(ctx context.Context, options option.DialerOptions) (ParallelInter type ParallelInterfaceDialer interface { N.Dialer - DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) - ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, error) + DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) + ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) } type ParallelNetworkDialer interface { - DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) - ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) + DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) + ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) } diff --git a/common/dialer/resolve.go b/common/dialer/resolve.go index ce17923c40..b5d922b375 100644 --- a/common/dialer/resolve.go +++ b/common/dialer/resolve.go @@ -106,7 +106,7 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil } -func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) { +func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { if !destination.IsFqdn() { return d.dialer.DialContext(ctx, network, destination) } @@ -128,13 +128,13 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context fallbackDelay = d.fallbackDelay } if d.parallel { - return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, strategy, fallbackDelay) + return DialParallelNetwork(ctx, d.dialer, network, destination, addresses, d.strategy == dns.DomainStrategyPreferIPv6, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } else { - return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, fallbackDelay) + return DialSerialNetwork(ctx, d.dialer, network, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } } -func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, error) { +func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) { if !destination.IsFqdn() { return d.dialer.ListenPacket(ctx, destination) } @@ -152,7 +152,7 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C if err != nil { return nil, err } - conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, fallbackDelay) + conn, destinationAddress, err := ListenSerialNetworkPacket(ctx, d.dialer, destination, addresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) if err != nil { return nil, err } diff --git a/common/srs/binary.go b/common/srs/binary.go index fbed78adc5..42b4460d8d 100644 --- a/common/srs/binary.go +++ b/common/srs/binary.go @@ -226,7 +226,7 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea } rule.AdGuardDomainMatcher = matcher case ruleItemNetworkType: - rule.NetworkType, err = readRuleItemString(reader) + rule.NetworkType, err = readRuleItemUint8[option.InterfaceType](reader) case ruleItemNetworkIsExpensive: rule.NetworkIsExpensive = true case ruleItemNetworkIsConstrained: @@ -349,7 +349,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen if generateVersion < C.RuleSetVersion3 { return E.New("network_type rule item is only supported in version 3 or later") } - err = writeRuleItemString(writer, ruleItemNetworkType, rule.NetworkType) + err = writeRuleItemUint8(writer, ruleItemNetworkType, rule.NetworkType) if err != nil { return err } @@ -414,6 +414,18 @@ func writeRuleItemString(writer varbin.Writer, itemType uint8, value []string) e return varbin.Write(writer, binary.BigEndian, value) } +func readRuleItemUint8[E ~uint8](reader varbin.Reader) ([]E, error) { + return varbin.ReadValue[[]E](reader, binary.BigEndian) +} + +func writeRuleItemUint8[E ~uint8](writer varbin.Writer, itemType uint8, value []E) error { + err := writer.WriteByte(itemType) + if err != nil { + return err + } + return varbin.Write(writer, binary.BigEndian, value) +} + func readRuleItemUint16(reader varbin.Reader) ([]uint16, error) { return varbin.ReadValue[[]uint16](reader, binary.BigEndian) } diff --git a/constant/network.go b/constant/network.go index c026b7b15b..88a1dd815f 100644 --- a/constant/network.go +++ b/constant/network.go @@ -5,44 +5,52 @@ import ( F "github.com/sagernet/sing/common/format" ) +type InterfaceType uint8 + const ( - InterfaceTypeWIFI = "wifi" - InterfaceTypeCellular = "cellular" - InterfaceTypeEthernet = "ethernet" - InterfaceTypeOther = "other" + InterfaceTypeWIFI InterfaceType = iota + InterfaceTypeCellular + InterfaceTypeEthernet + InterfaceTypeOther +) + +var ( + interfaceTypeToString = map[InterfaceType]string{ + InterfaceTypeWIFI: "wifi", + InterfaceTypeCellular: "cellular", + InterfaceTypeEthernet: "ethernet", + InterfaceTypeOther: "other", + } + StringToInterfaceType = common.ReverseMap(interfaceTypeToString) ) -type NetworkStrategy int +func (t InterfaceType) String() string { + name, loaded := interfaceTypeToString[t] + if !loaded { + return F.ToString(int(t)) + } + return name +} + +type NetworkStrategy uint8 const ( NetworkStrategyDefault NetworkStrategy = iota NetworkStrategyFallback NetworkStrategyHybrid - NetworkStrategyWIFI - NetworkStrategyCellular - NetworkStrategyEthernet - NetworkStrategyWIFIOnly - NetworkStrategyCellularOnly - NetworkStrategyEthernetOnly ) var ( - NetworkStrategyToString = map[NetworkStrategy]string{ - NetworkStrategyDefault: "default", - NetworkStrategyFallback: "fallback", - NetworkStrategyHybrid: "hybrid", - NetworkStrategyWIFI: "wifi", - NetworkStrategyCellular: "cellular", - NetworkStrategyEthernet: "ethernet", - NetworkStrategyWIFIOnly: "wifi_only", - NetworkStrategyCellularOnly: "cellular_only", - NetworkStrategyEthernetOnly: "ethernet_only", + networkStrategyToString = map[NetworkStrategy]string{ + NetworkStrategyDefault: "default", + NetworkStrategyFallback: "fallback", + NetworkStrategyHybrid: "hybrid", } - StringToNetworkStrategy = common.ReverseMap(NetworkStrategyToString) + StringToNetworkStrategy = common.ReverseMap(networkStrategyToString) ) func (s NetworkStrategy) String() string { - name, loaded := NetworkStrategyToString[s] + name, loaded := networkStrategyToString[s] if !loaded { return F.ToString(int(s)) } diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 2b035eb426..58f733528e 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -7,6 +7,8 @@ icon: material/new-box !!! quote "Changes in sing-box 1.11.0" :material-plus: [default_network_strategy](#default_network_strategy) + :material-plus: [default_network_type](#default_network_type) + :material-plus: [default_fallback_network_type](#default_fallback_network_type) :material-alert: [default_fallback_delay](#default_fallback_delay) !!! quote "Changes in sing-box 1.8.0" @@ -30,17 +32,18 @@ icon: material/new-box "default_interface": "", "default_mark": 0, "default_network_strategy": "", + "default_network_type": [], + "default_fallback_network_type": [], "default_fallback_delay": "" } } ``` -### Fields +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item -| Key | Format | -|-----------|-----------------------| -| `geoip` | [GeoIP](./geoip/) | -| `geosite` | [Geosite](./geosite/) | +### Fields #### rules @@ -96,11 +99,9 @@ Takes no effect if `outbound.routing_mark` is set. #### default_network_strategy -!!! quote "" - - Only supported in graphical clients on Android and iOS with `auto_detect_interface` enabled. +!!! question "Since sing-box 1.11.0" -Strategy for selecting network interfaces. +See [Dial Fields](/configuration/shared/dial/#network_strategy) for details. Takes no effect if `outbound.bind_interface`, `outbound.inet4_bind_address` or `outbound.inet6_bind_address` is set. @@ -108,12 +109,20 @@ Can be overrides by `outbound.network_strategy`. Conflicts with `default_interface`. -See [Dial Fields](/configuration/shared/dial/#network_strategy) for available values. +#### default_network_type -#### default_fallback_delay +!!! question "Since sing-box 1.11.0" -!!! quote "" +See [Dial Fields](/configuration/shared/dial/#network_type) for details. + +#### default_fallback_network_type + +!!! question "Since sing-box 1.11.0" + +See [Dial Fields](/configuration/shared/dial/#fallback_network_type) for details. + +#### default_fallback_delay - Only supported in graphical clients on Android and iOS with `auto_detect_interface` enabled and `network_strategy` set. +!!! question "Since sing-box 1.11.0" -See [Dial Fields](/configuration/shared/dial/#fallback_delay) for details. \ No newline at end of file +See [Dial Fields](/configuration/shared/dial/#fallback_delay) for details. diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md index b00237c450..9550b4ac3b 100644 --- a/docs/configuration/route/index.zh.md +++ b/docs/configuration/route/index.zh.md @@ -1,5 +1,16 @@ +--- +icon: material/new-box +--- + # 路由 +!!! quote "sing-box 1.11.0 中的更改" + + :material-plus: [network_strategy](#network_strategy) + :material-plus: [default_network_type](#default_network_type) + :material-plus: [default_fallback_network_type](#default_fallback_network_type) + :material-alert: [default_fallback_delay](#default_fallback_delay) + !!! quote "sing-box 1.8.0 中的更改" :material-plus: [rule_set](#rule_set) @@ -26,6 +37,10 @@ } ``` +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + ### 字段 | 键 | 格式 | @@ -87,11 +102,9 @@ #### network_strategy -!!! quote "" - - 仅在 Android 与 Apple 平台图形客户端中支持,并且需要 `auto_detect_interface`。 +!!! question "自 sing-box 1.11.0 起" -选择网络接口的策略。 +详情参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。 当 `outbound.bind_interface`, `outbound.inet4_bind_address` 或 `outbound.inet6_bind_address` 已设置时不生效。 @@ -99,12 +112,20 @@ 与 `default_interface` 冲突。 -可用值请参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。 +#### default_network_type -#### fallback_delay +!!! question "自 sing-box 1.11.0 起" -!!! quote "" +详情参阅 [拨号字段](/configuration/shared/dial/#default_network_type)。 + +#### default_fallback_network_type + +!!! question "自 sing-box 1.11.0 起" + +详情参阅 [拨号字段](/configuration/shared/dial/#default_fallback_network_type)。 + +#### default_fallback_delay - 仅在 Android 与 Apple 平台图形客户端中支持,并且需要 `auto_detect_interface` 且 `network_strategy` 已设置。 +!!! question "自 sing-box 1.11.0 起" -详情请参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。 +详情参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。 diff --git a/docs/configuration/route/rule_action.md b/docs/configuration/route/rule_action.md index 5e9cfbc220..6462c84836 100644 --- a/docs/configuration/route/rule_action.md +++ b/docs/configuration/route/rule_action.md @@ -11,12 +11,18 @@ icon: material/new-box "action": "route", // default "outbound": "", "network_strategy": "", + "network_type": [], + "fallback_network_type": [], "fallback_delay": "", "udp_disable_domain_unmapping": false, "udp_connect": false } ``` +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + `route` inherits the classic rule behavior of routing connection to the specified outbound. #### outbound @@ -27,22 +33,20 @@ Tag of target outbound. #### network_strategy -!!! quote "" - - Only supported in graphical clients on Android and iOS with `auto_detect_interface` enabled. - -Strategy for selecting network interfaces. +See [Dial Fields](/configuration/shared/dial/#network_strategy) for details. Only take effect if outbound is direct without `outbound.bind_interface`, `outbound.inet4_bind_address` and `outbound.inet6_bind_address` set. -See [Dial Fields](/configuration/shared/dial/#network_strategy) for available values. +#### network_type -#### fallback_delay +See [Dial Fields](/configuration/shared/dial/#network_type) for details. + +#### fallback_network_type -!!! quote "" +See [Dial Fields](/configuration/shared/dial/#fallback_network_type) for details. - Only supported in graphical clients on Android and iOS with `auto_detect_interface` enabled and `network_strategy` set. +#### fallback_delay See [Dial Fields](/configuration/shared/dial/#fallback_delay) for details. diff --git a/docs/configuration/route/rule_action.zh.md b/docs/configuration/route/rule_action.zh.md index 52965fd240..acadb66d3c 100644 --- a/docs/configuration/route/rule_action.zh.md +++ b/docs/configuration/route/rule_action.zh.md @@ -12,6 +12,8 @@ icon: material/new-box "outbound": "", "network_strategy": "", "fallback_delay": "", + "network_type": [], + "fallback_network_type": [], "udp_disable_domain_unmapping": false, "udp_connect": false } @@ -27,22 +29,20 @@ icon: material/new-box #### network_strategy -!!! quote "" - - 仅在 Android 与 Apple 平台图形客户端中支持,并且需要 `auto_detect_interface`。 - -选择网络接口的策略。 +详情参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。 仅当出站为 `direct` 且 `outbound.bind_interface`, `outbound.inet4_bind_address` 且 `outbound.inet6_bind_address` 未设置时生效。 -可用值参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。 +#### network_type -#### fallback_delay +详情参阅 [拨号字段](/configuration/shared/dial/#network_type)。 -!!! quote "" +#### fallback_network_type - 仅在 Android 与 Apple 平台图形客户端中支持,并且需要 `auto_detect_interface` 且 `network_strategy` 已设置。 +详情参阅 [拨号字段](/configuration/shared/dial/#fallback_network_type)。 + +#### fallback_delay 详情参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。 @@ -68,6 +68,10 @@ icon: material/new-box } ``` +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + `route-options` 为路由设置选项。 ### reject diff --git a/docs/configuration/shared/dial.md b/docs/configuration/shared/dial.md index e67bf61623..5f654ae247 100644 --- a/docs/configuration/shared/dial.md +++ b/docs/configuration/shared/dial.md @@ -5,7 +5,9 @@ icon: material/new-box !!! quote "Changes in sing-box 1.11.0" :material-plus: [network_strategy](#network_strategy) - :material-alert: [fallback_delay](#fallback_delay) + :material-alert: [fallback_delay](#fallback_delay) + :material-alert: [network_type](#network_type) + :material-alert: [fallback_network_type](#fallback_network_type) ### Structure @@ -23,10 +25,16 @@ icon: material/new-box "udp_fragment": false, "domain_strategy": "prefer_ipv6", "network_strategy": "default", + "network_type": [], + "fallback_network_type": [], "fallback_delay": "300ms" } ``` +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + ### Fields #### detour @@ -101,30 +109,57 @@ If set, the requested domain name will be resolved to IP before connect. !!! quote "" - Only supported in graphical clients on Android and iOS with `auto_detect_interface` enabled. + Only supported in graphical clients on Android and Apple platforms with `auto_detect_interface` enabled. Strategy for selecting network interfaces. Available values: -- `default` (default): Connect to the default interface. -- `fallback`: Try all other interfaces when timeout. -- `hybrid`: Connect to all interfaces concurrently and choose the fastest one. -- `wifi`: Prioritize WIFI, but try all other interfaces when unavailable or timeout. -- `cellular`: Prioritize Cellular, but try all other interfaces when unavailable or timeout. -- `ethernet`: Prioritize Ethernet, but try all other interfaces when unavailable or timeout. -- `wifi_only`: Connect to WIFI only. -- `cellular_only`: Connect to Cellular only. -- `ethernet_only`: Connect to Ethernet only. +- `default` (default): Connect to default network or networks specified in `network_type` sequentially. +- `hybrid`: Connect to all networks or networks specified in `network_type` concurrently. +- `fallback`: Connect to default network or preferred networks specified in `network_type` concurrently, and try fallback networks when unavailable or timeout. -For fallback strategies, when preferred interfaces fails or times out, -it will enter a 15s fast fallback state (upgraded to `hybrid`), -and exit immediately if recovers. +For fallback, when preferred interfaces fails or times out, +it will enter a 15s fast fallback state (Connect to all preferred and fallback networks concurrently), +and exit immediately if preferred networks recover. Conflicts with `bind_interface`, `inet4_bind_address` and `inet6_bind_address`. +#### network_type + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms with `auto_detect_interface` enabled. + +Network types to use when using `default` or `hybrid` network strategy or +preferred network types to use when using `fallback` network strategy. + +Available values: `wifi`, `cellular`, `ethernet`, `other`. + +Device's default network is used by default. + +#### fallback_network_type + +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms with `auto_detect_interface` enabled. + +Fallback network types when preferred networks are unavailable or timeout when using `fallback` network strategy. + +All other networks expect preferred are used by default. + #### fallback_delay +!!! question "Since sing-box 1.11.0" + +!!! quote "" + + Only supported in graphical clients on Android and Apple platforms with `auto_detect_interface` enabled. + The length of time to wait before spawning a RFC 6555 Fast Fallback connection. For `domain_strategy`, is the amount of time to wait for connection to succeed before assuming diff --git a/docs/configuration/shared/dial.zh.md b/docs/configuration/shared/dial.zh.md index 4e20f106e8..ab83c44c69 100644 --- a/docs/configuration/shared/dial.zh.md +++ b/docs/configuration/shared/dial.zh.md @@ -5,7 +5,9 @@ icon: material/new-box !!! quote "sing-box 1.11.0 中的更改" :material-plus: [network_strategy](#network_strategy) - :material-alert: [fallback_delay](#fallback_delay) + :material-alert: [fallback_delay](#fallback_delay) + :material-alert: [network_type](#network_type) + :material-alert: [fallback_network_type](#fallback_network_type) ### 结构 @@ -23,10 +25,16 @@ icon: material/new-box "udp_fragment": false, "domain_strategy": "prefer_ipv6", "network_strategy": "", + "network_type": [], + "fallback_network_type": [], "fallback_delay": "300ms" } ``` +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + ### 字段 #### detour @@ -99,26 +107,48 @@ icon: material/new-box !!! quote "" - 仅在 Android 与 iOS 平台图形客户端中支持。 + 仅在 Android 与 iOS 平台图形客户端中支持,并且需要 `route.auto_detect_interface`。 用于选择网络接口的策略。 可用值: -- `default` (默认): 连接到默认接口, -- `fallback`: 如果超时,尝试所有剩余接口。 -- `hybrid`: 同时尝试所有接口,选择最快的一个。 -- `wifi`: 优先使用 WIFI,但在不可用或超时时尝试所有其他接口。 -- `cellular`: 优先使用蜂窝数据,但在不可用或超时时尝试所有其他接口。 -- `ethernet`: 优先使用以太网,但在不可用或超时时尝试所有其他接口。 -- `wifi_only`: 仅连接到 WIFI。 -- `cellular_only`: 仅连接到蜂窝数据。 -- `ethernet_only`: 仅连接到以太网。 +- `default`(默认值):按顺序连接默认网络或 `network_type` 中指定的网络。 +- `hybrid`:同时连接所有网络或 `network_type` 中指定的网络。 +- `fallback`:同时连接默认网络或 `network_type` 中指定的首选网络,当不可用或超时时尝试回退网络。 -对于回退策略, 当优先使用的接口发生故障或超时时, 将进入 15 秒的快速回退状态(升级为 `hybrid`), 且恢复后立即退出。 +对于回退模式,当首选接口失败或超时时, +将进入15秒的快速回退状态(同时连接所有首选和回退网络), +如果首选网络恢复,则立即退出。 与 `bind_interface`, `bind_inet4_address` 和 `bind_inet6_address` 冲突。 +#### network_type + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 iOS 平台图形客户端中支持,并且需要 `route.auto_detect_interface`。 + +当使用 `default` 或 `hybrid` 网络策略时要使用的网络类型,或当使用 `fallback` 网络策略时要使用的首选网络类型。 + +可用值:`wifi`, `cellular`, `ethernet`, `other`。 + +默认使用设备默认网络。 + +#### fallback_network_type + +!!! question "自 sing-box 1.11.0 起" + +!!! quote "" + + 仅在 Android 与 iOS 平台图形客户端中支持,并且需要 `route.auto_detect_interface`。 + +当使用 `fallback` 网络策略时,在首选网络不可用或超时的情况下要使用的回退网络类型。 + +默认使用除首选网络外的所有其他网络。 + #### fallback_delay 在生成 RFC 6555 快速回退连接之前等待的时间长度。 diff --git a/experimental/libbox/platform.go b/experimental/libbox/platform.go index a61243458c..d5951cd38f 100644 --- a/experimental/libbox/platform.go +++ b/experimental/libbox/platform.go @@ -34,10 +34,10 @@ type InterfaceUpdateListener interface { } const ( - InterfaceTypeWIFI = C.InterfaceTypeWIFI - InterfaceTypeCellular = C.InterfaceTypeCellular - InterfaceTypeEthernet = C.InterfaceTypeEthernet - InterfaceTypeOther = C.InterfaceTypeOther + InterfaceTypeWIFI = int32(C.InterfaceTypeWIFI) + InterfaceTypeCellular = int32(C.InterfaceTypeCellular) + InterfaceTypeEthernet = int32(C.InterfaceTypeEthernet) + InterfaceTypeOther = int32(C.InterfaceTypeOther) ) type NetworkInterface struct { @@ -47,7 +47,7 @@ type NetworkInterface struct { Addresses StringIterator Flags int32 - Type string + Type int32 DNSServer StringIterator Metered bool } diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 28ae03d3c0..e40cbe0367 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -202,7 +202,7 @@ func (w *platformInterfaceWrapper) Interfaces() ([]adapter.NetworkInterface, err Addresses: common.Map(iteratorToArray[string](netInterface.Addresses), netip.MustParsePrefix), Flags: linkFlags(uint32(netInterface.Flags)), }, - Type: netInterface.Type, + Type: C.InterfaceType(netInterface.Type), DNSServers: iteratorToArray[string](netInterface.DNSServer), Expensive: netInterface.Metered || isDefault && w.isExpensive, Constrained: isDefault && w.isConstrained, diff --git a/option/outbound.go b/option/outbound.go index 5791802c9e..34ef904a80 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -65,23 +65,25 @@ type DialerOptionsWrapper interface { } type DialerOptions struct { - Detour string `json:"detour,omitempty"` - BindInterface string `json:"bind_interface,omitempty"` - Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"` - Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"` - ProtectPath string `json:"protect_path,omitempty"` - RoutingMark uint32 `json:"routing_mark,omitempty"` - ReuseAddr bool `json:"reuse_addr,omitempty"` - ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"` - TCPFastOpen bool `json:"tcp_fast_open,omitempty"` - TCPMultiPath bool `json:"tcp_multi_path,omitempty"` - UDPFragment *bool `json:"udp_fragment,omitempty"` - UDPFragmentDefault bool `json:"-"` - DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` - NetworkStrategy NetworkStrategy `json:"network_strategy,omitempty"` - FallbackDelay badoption.Duration `json:"fallback_delay,omitempty"` - NetworkFallbackDelay badoption.Duration `json:"network_fallback_delay,omitempty"` - IsWireGuardListener bool `json:"-"` + Detour string `json:"detour,omitempty"` + BindInterface string `json:"bind_interface,omitempty"` + Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"` + Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"` + ProtectPath string `json:"protect_path,omitempty"` + RoutingMark FwMark `json:"routing_mark,omitempty"` + ReuseAddr bool `json:"reuse_addr,omitempty"` + ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"` + TCPFastOpen bool `json:"tcp_fast_open,omitempty"` + TCPMultiPath bool `json:"tcp_multi_path,omitempty"` + UDPFragment *bool `json:"udp_fragment,omitempty"` + UDPFragmentDefault bool `json:"-"` + DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` + NetworkStrategy NetworkStrategy `json:"network_strategy,omitempty"` + NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` + FallbackNetworkType badoption.Listable[InterfaceType] `json:"fallback_network_type,omitempty"` + FallbackDelay badoption.Duration `json:"fallback_delay,omitempty"` + NetworkFallbackDelay badoption.Duration `json:"network_fallback_delay,omitempty"` + IsWireGuardListener bool `json:"-"` } func (o *DialerOptions) TakeDialerOptions() DialerOptions { diff --git a/option/route.go b/option/route.go index 236e56f7ba..0eb1cbf1b2 100644 --- a/option/route.go +++ b/option/route.go @@ -3,18 +3,20 @@ package option import "github.com/sagernet/sing/common/json/badoption" type RouteOptions struct { - GeoIP *GeoIPOptions `json:"geoip,omitempty"` - Geosite *GeositeOptions `json:"geosite,omitempty"` - Rules []Rule `json:"rules,omitempty"` - RuleSet []RuleSet `json:"rule_set,omitempty"` - Final string `json:"final,omitempty"` - FindProcess bool `json:"find_process,omitempty"` - AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` - OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"` - DefaultInterface string `json:"default_interface,omitempty"` - DefaultMark uint32 `json:"default_mark,omitempty"` - DefaultNetworkStrategy NetworkStrategy `json:"default_network_strategy,omitempty"` - DefaultFallbackDelay badoption.Duration `json:"default_fallback_delay,omitempty"` + GeoIP *GeoIPOptions `json:"geoip,omitempty"` + Geosite *GeositeOptions `json:"geosite,omitempty"` + Rules []Rule `json:"rules,omitempty"` + RuleSet []RuleSet `json:"rule_set,omitempty"` + Final string `json:"final,omitempty"` + FindProcess bool `json:"find_process,omitempty"` + AutoDetectInterface bool `json:"auto_detect_interface,omitempty"` + OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"` + DefaultInterface string `json:"default_interface,omitempty"` + DefaultMark FwMark `json:"default_mark,omitempty"` + DefaultNetworkStrategy NetworkStrategy `json:"default_network_strategy,omitempty"` + DefaultNetworkType badoption.Listable[InterfaceType] `json:"default_network_type,omitempty"` + DefaultFallbackNetworkType badoption.Listable[InterfaceType] `json:"default_fallback_network_type,omitempty"` + DefaultFallbackDelay badoption.Duration `json:"default_fallback_delay,omitempty"` } type GeoIPOptions struct { diff --git a/option/rule.go b/option/rule.go index 82a53f75a4..b769dab831 100644 --- a/option/rule.go +++ b/option/rule.go @@ -67,42 +67,42 @@ func (r Rule) IsValid() bool { } type RawDefaultRule struct { - Inbound badoption.Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - Network badoption.Listable[string] `json:"network,omitempty"` - AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` - Protocol badoption.Listable[string] `json:"protocol,omitempty"` - Client badoption.Listable[string] `json:"client,omitempty"` - Domain badoption.Listable[string] `json:"domain,omitempty"` - DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` - Geosite badoption.Listable[string] `json:"geosite,omitempty"` - SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` - GeoIP badoption.Listable[string] `json:"geoip,omitempty"` - SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` - SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` - IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` - IPIsPrivate bool `json:"ip_is_private,omitempty"` - SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` - Port badoption.Listable[uint16] `json:"port,omitempty"` - PortRange badoption.Listable[string] `json:"port_range,omitempty"` - ProcessName badoption.Listable[string] `json:"process_name,omitempty"` - ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` - ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` - PackageName badoption.Listable[string] `json:"package_name,omitempty"` - User badoption.Listable[string] `json:"user,omitempty"` - UserID badoption.Listable[int32] `json:"user_id,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - NetworkType badoption.Listable[string] `json:"network_type,omitempty"` - NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` - NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` - WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` - RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` - RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` - Invert bool `json:"invert,omitempty"` + Inbound badoption.Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + Network badoption.Listable[string] `json:"network,omitempty"` + AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` + Protocol badoption.Listable[string] `json:"protocol,omitempty"` + Client badoption.Listable[string] `json:"client,omitempty"` + Domain badoption.Listable[string] `json:"domain,omitempty"` + DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` + Geosite badoption.Listable[string] `json:"geosite,omitempty"` + SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` + GeoIP badoption.Listable[string] `json:"geoip,omitempty"` + SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` + IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,omitempty"` + SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` + Port badoption.Listable[uint16] `json:"port,omitempty"` + PortRange badoption.Listable[string] `json:"port_range,omitempty"` + ProcessName badoption.Listable[string] `json:"process_name,omitempty"` + ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` + ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` + PackageName badoption.Listable[string] `json:"package_name,omitempty"` + User badoption.Listable[string] `json:"user,omitempty"` + UserID badoption.Listable[int32] `json:"user_id,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` + NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` + NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` + WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` + Invert bool `json:"invert,omitempty"` // Deprecated: renamed to rule_set_ip_cidr_match_source Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` diff --git a/option/rule_dns.go b/option/rule_dns.go index 33dc816c53..b437eb54d3 100644 --- a/option/rule_dns.go +++ b/option/rule_dns.go @@ -68,44 +68,44 @@ func (r DNSRule) IsValid() bool { } type RawDefaultDNSRule struct { - Inbound badoption.Listable[string] `json:"inbound,omitempty"` - IPVersion int `json:"ip_version,omitempty"` - QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` - Network badoption.Listable[string] `json:"network,omitempty"` - AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` - Protocol badoption.Listable[string] `json:"protocol,omitempty"` - Domain badoption.Listable[string] `json:"domain,omitempty"` - DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` - Geosite badoption.Listable[string] `json:"geosite,omitempty"` - SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` - GeoIP badoption.Listable[string] `json:"geoip,omitempty"` - IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` - IPIsPrivate bool `json:"ip_is_private,omitempty"` - SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` - SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` - SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` - Port badoption.Listable[uint16] `json:"port,omitempty"` - PortRange badoption.Listable[string] `json:"port_range,omitempty"` - ProcessName badoption.Listable[string] `json:"process_name,omitempty"` - ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` - ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` - PackageName badoption.Listable[string] `json:"package_name,omitempty"` - User badoption.Listable[string] `json:"user,omitempty"` - UserID badoption.Listable[int32] `json:"user_id,omitempty"` - Outbound badoption.Listable[string] `json:"outbound,omitempty"` - ClashMode string `json:"clash_mode,omitempty"` - NetworkType badoption.Listable[string] `json:"network_type,omitempty"` - NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` - NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` - WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` - RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` - RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` - RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` - Invert bool `json:"invert,omitempty"` + Inbound badoption.Listable[string] `json:"inbound,omitempty"` + IPVersion int `json:"ip_version,omitempty"` + QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` + Network badoption.Listable[string] `json:"network,omitempty"` + AuthUser badoption.Listable[string] `json:"auth_user,omitempty"` + Protocol badoption.Listable[string] `json:"protocol,omitempty"` + Domain badoption.Listable[string] `json:"domain,omitempty"` + DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` + Geosite badoption.Listable[string] `json:"geosite,omitempty"` + SourceGeoIP badoption.Listable[string] `json:"source_geoip,omitempty"` + GeoIP badoption.Listable[string] `json:"geoip,omitempty"` + IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` + IPIsPrivate bool `json:"ip_is_private,omitempty"` + SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` + SourceIPIsPrivate bool `json:"source_ip_is_private,omitempty"` + SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` + Port badoption.Listable[uint16] `json:"port,omitempty"` + PortRange badoption.Listable[string] `json:"port_range,omitempty"` + ProcessName badoption.Listable[string] `json:"process_name,omitempty"` + ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` + ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` + PackageName badoption.Listable[string] `json:"package_name,omitempty"` + User badoption.Listable[string] `json:"user,omitempty"` + UserID badoption.Listable[int32] `json:"user_id,omitempty"` + Outbound badoption.Listable[string] `json:"outbound,omitempty"` + ClashMode string `json:"clash_mode,omitempty"` + NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` + NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` + NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` + WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` + RuleSet badoption.Listable[string] `json:"rule_set,omitempty"` + RuleSetIPCIDRMatchSource bool `json:"rule_set_ip_cidr_match_source,omitempty"` + RuleSetIPCIDRAcceptEmpty bool `json:"rule_set_ip_cidr_accept_empty,omitempty"` + Invert bool `json:"invert,omitempty"` // Deprecated: renamed to rule_set_ip_cidr_match_source Deprecated_RulesetIPCIDRMatchSource bool `json:"rule_set_ipcidr_match_source,omitempty"` diff --git a/option/rule_set.go b/option/rule_set.go index 9ccca4759b..f7a5f3344f 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -146,28 +146,28 @@ func (r HeadlessRule) IsValid() bool { } type DefaultHeadlessRule struct { - QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` - Network badoption.Listable[string] `json:"network,omitempty"` - Domain badoption.Listable[string] `json:"domain,omitempty"` - DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` - DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` - DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` - SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` - IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` - SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` - SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` - Port badoption.Listable[uint16] `json:"port,omitempty"` - PortRange badoption.Listable[string] `json:"port_range,omitempty"` - ProcessName badoption.Listable[string] `json:"process_name,omitempty"` - ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` - ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` - PackageName badoption.Listable[string] `json:"package_name,omitempty"` - NetworkType badoption.Listable[string] `json:"network_type,omitempty"` - NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` - NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` - WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` - WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` - Invert bool `json:"invert,omitempty"` + QueryType badoption.Listable[DNSQueryType] `json:"query_type,omitempty"` + Network badoption.Listable[string] `json:"network,omitempty"` + Domain badoption.Listable[string] `json:"domain,omitempty"` + DomainSuffix badoption.Listable[string] `json:"domain_suffix,omitempty"` + DomainKeyword badoption.Listable[string] `json:"domain_keyword,omitempty"` + DomainRegex badoption.Listable[string] `json:"domain_regex,omitempty"` + SourceIPCIDR badoption.Listable[string] `json:"source_ip_cidr,omitempty"` + IPCIDR badoption.Listable[string] `json:"ip_cidr,omitempty"` + SourcePort badoption.Listable[uint16] `json:"source_port,omitempty"` + SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"` + Port badoption.Listable[uint16] `json:"port,omitempty"` + PortRange badoption.Listable[string] `json:"port_range,omitempty"` + ProcessName badoption.Listable[string] `json:"process_name,omitempty"` + ProcessPath badoption.Listable[string] `json:"process_path,omitempty"` + ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"` + PackageName badoption.Listable[string] `json:"package_name,omitempty"` + NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` + NetworkIsExpensive bool `json:"network_is_expensive,omitempty"` + NetworkIsConstrained bool `json:"network_is_constrained,omitempty"` + WIFISSID badoption.Listable[string] `json:"wifi_ssid,omitempty"` + WIFIBSSID badoption.Listable[string] `json:"wifi_bssid,omitempty"` + Invert bool `json:"invert,omitempty"` DomainMatcher *domain.Matcher `json:"-"` SourceIPSet *netipx.IPSet `json:"-"` diff --git a/option/types.go b/option/types.go index 8ed06250b5..66f58ef84c 100644 --- a/option/types.go +++ b/option/types.go @@ -171,3 +171,27 @@ func (n *NetworkStrategy) UnmarshalJSON(content []byte) error { *n = NetworkStrategy(strategy) return nil } + +type InterfaceType C.InterfaceType + +func (t InterfaceType) Build() C.InterfaceType { + return C.InterfaceType(t) +} + +func (t InterfaceType) MarshalJSON() ([]byte, error) { + return json.Marshal(C.InterfaceType(t).String()) +} + +func (t *InterfaceType) UnmarshalJSON(content []byte) error { + var value string + err := json.Unmarshal(content, &value) + if err != nil { + return err + } + interfaceType, loaded := C.StringToInterfaceType[value] + if !loaded { + return E.New("unknown interface type: ", value) + } + *t = InterfaceType(interfaceType) + return nil +} diff --git a/protocol/direct/outbound.go b/protocol/direct/outbound.go index 4251c36633..5ae0dac647 100644 --- a/protocol/direct/outbound.go +++ b/protocol/direct/outbound.go @@ -12,7 +12,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" - dns "github.com/sagernet/sing-dns" + "github.com/sagernet/sing-dns" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" @@ -37,6 +37,8 @@ type Outbound struct { domainStrategy dns.DomainStrategy fallbackDelay time.Duration networkStrategy C.NetworkStrategy + networkType []C.InterfaceType + fallbackNetworkType []C.InterfaceType networkFallbackDelay time.Duration overrideOption int overrideDestination M.Socksaddr @@ -55,6 +57,8 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL domainStrategy: dns.DomainStrategy(options.DomainStrategy), fallbackDelay: time.Duration(options.FallbackDelay), networkStrategy: C.NetworkStrategy(options.NetworkStrategy), + networkType: common.Map(options.NetworkType, option.InterfaceType.Build), + fallbackNetworkType: common.Map(options.FallbackNetworkType, option.InterfaceType.Build), networkFallbackDelay: time.Duration(options.NetworkFallbackDelay), dialer: outboundDialer, // loopBack: newLoopBackDetector(router), @@ -171,10 +175,10 @@ func (h *Outbound) DialParallel(ctx context.Context, network string, destination return nil, E.New("no IPv6 address available for ", destination) } } - return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == dns.DomainStrategyPreferIPv6, h.networkStrategy, h.fallbackDelay) + return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == dns.DomainStrategyPreferIPv6, h.networkStrategy, h.networkType, h.fallbackNetworkType, h.fallbackDelay) } -func (h *Outbound) DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy C.NetworkStrategy, fallbackDelay time.Duration) (net.Conn, error) { +func (h *Outbound) DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy C.NetworkStrategy, networkType []C.InterfaceType, fallbackNetworkType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination @@ -210,10 +214,10 @@ func (h *Outbound) DialParallelNetwork(ctx context.Context, network string, dest return nil, E.New("no IPv6 address available for ", destination) } } - return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == dns.DomainStrategyPreferIPv6, networkStrategy, fallbackDelay) + return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == dns.DomainStrategyPreferIPv6, networkStrategy, networkType, fallbackNetworkType, fallbackDelay) } -func (h *Outbound) ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy C.NetworkStrategy, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { +func (h *Outbound) ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy C.NetworkStrategy, networkType []C.InterfaceType, fallbackNetworkType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination @@ -232,7 +236,7 @@ func (h *Outbound) ListenSerialNetworkPacket(ctx context.Context, destination M. } else { h.logger.InfoContext(ctx, "outbound packet connection to ", destination) } - conn, newDestination, err := dialer.ListenSerialNetworkPacket(ctx, h.dialer, destination, destinationAddresses, networkStrategy, fallbackDelay) + conn, newDestination, err := dialer.ListenSerialNetworkPacket(ctx, h.dialer, destination, destinationAddresses, networkStrategy, networkType, fallbackNetworkType, fallbackDelay) if err != nil { return nil, netip.Addr{}, err } diff --git a/route/network.go b/route/network.go index eda1a054a9..e7c4df9ceb 100644 --- a/route/network.go +++ b/route/network.go @@ -59,10 +59,12 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOp interfaceFinder: control.NewDefaultInterfaceFinder(), autoDetectInterface: routeOptions.AutoDetectInterface, defaultOptions: adapter.NetworkOptions{ - DefaultInterface: routeOptions.DefaultInterface, - DefaultMark: routeOptions.DefaultMark, - DefaultNetworkStrategy: C.NetworkStrategy(routeOptions.DefaultNetworkStrategy), - DefaultFallbackDelay: time.Duration(routeOptions.DefaultFallbackDelay), + BindInterface: routeOptions.DefaultInterface, + RoutingMark: uint32(routeOptions.DefaultMark), + NetworkStrategy: C.NetworkStrategy(routeOptions.DefaultNetworkStrategy), + NetworkType: common.Map(routeOptions.DefaultNetworkType, option.InterfaceType.Build), + FallbackNetworkType: common.Map(routeOptions.DefaultFallbackNetworkType, option.InterfaceType.Build), + FallbackDelay: time.Duration(routeOptions.DefaultFallbackDelay), }, pauseManager: service.FromContext[pause.Manager](ctx), platformInterface: service.FromContext[platform.Interface](ctx), @@ -382,7 +384,7 @@ func (r *NetworkManager) notifyInterfaceUpdate(defaultInterface *control.Interfa networkInterface := common.Find(r.networkInterfaces.Load(), func(it adapter.NetworkInterface) bool { return it.Interface.Index == defaultInterface.Index }) - if networkInterface.Type == "" { + if networkInterface.Name == "" { // race return } diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index f9b2e64108..52e9520726 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -140,6 +140,8 @@ func (r *RuleActionRoute) String() string { type RuleActionRouteOptions struct { NetworkStrategy C.NetworkStrategy + NetworkType []C.InterfaceType + FallbackNetworkType []C.InterfaceType FallbackDelay time.Duration UDPDisableDomainUnmapping bool UDPConnect bool diff --git a/route/rule/rule_default.go b/route/rule/rule_default.go index a5d47b4445..2794c28751 100644 --- a/route/rule/rule_default.go +++ b/route/rule/rule_default.go @@ -8,6 +8,7 @@ import ( "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service" ) @@ -224,7 +225,7 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio rule.allItems = append(rule.allItems, item) } if len(options.NetworkType) > 0 { - item := NewNetworkTypeItem(networkManager, options.NetworkType) + item := NewNetworkTypeItem(networkManager, common.Map(options.NetworkType, option.InterfaceType.Build)) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } diff --git a/route/rule/rule_dns.go b/route/rule/rule_dns.go index 4ac08a5a44..fb8c6b786f 100644 --- a/route/rule/rule_dns.go +++ b/route/rule/rule_dns.go @@ -221,7 +221,7 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op rule.allItems = append(rule.allItems, item) } if len(options.NetworkType) > 0 { - item := NewNetworkTypeItem(networkManager, options.NetworkType) + item := NewNetworkTypeItem(networkManager, common.Map(options.NetworkType, option.InterfaceType.Build)) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } diff --git a/route/rule/rule_headless.go b/route/rule/rule_headless.go index 7f2dc5fca7..619856a5a5 100644 --- a/route/rule/rule_headless.go +++ b/route/rule/rule_headless.go @@ -6,6 +6,7 @@ import ( "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/service" ) @@ -142,7 +143,7 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR } if networkManager != nil { if len(options.NetworkType) > 0 { - item := NewNetworkTypeItem(networkManager, options.NetworkType) + item := NewNetworkTypeItem(networkManager, common.Map(options.NetworkType, option.InterfaceType.Build)) rule.items = append(rule.items, item) rule.allItems = append(rule.allItems, item) } diff --git a/route/rule/rule_item_network_type.go b/route/rule/rule_item_network_type.go index 8ebdb25e9d..31856e70df 100644 --- a/route/rule/rule_item_network_type.go +++ b/route/rule/rule_item_network_type.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" ) @@ -12,10 +13,10 @@ var _ RuleItem = (*NetworkTypeItem)(nil) type NetworkTypeItem struct { networkManager adapter.NetworkManager - networkType []string + networkType []C.InterfaceType } -func NewNetworkTypeItem(networkManager adapter.NetworkManager, networkType []string) *NetworkTypeItem { +func NewNetworkTypeItem(networkManager adapter.NetworkManager, networkType []C.InterfaceType) *NetworkTypeItem { return &NetworkTypeItem{ networkManager: networkManager, networkType: networkType, From c9f9d9ee1c901ef54ae0511d69ab18be6087403c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 14 Nov 2024 12:42:42 +0800 Subject: [PATCH 24/49] Add `dns.cache_capacity` --- docs/configuration/dns/index.md | 18 ++++++++++++++++++ docs/configuration/dns/index.zh.md | 19 ++++++++++++++++++- option/dns.go | 1 + route/router.go | 1 + 4 files changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/configuration/dns/index.md b/docs/configuration/dns/index.md index f37c93a4bb..fc81333443 100644 --- a/docs/configuration/dns/index.md +++ b/docs/configuration/dns/index.md @@ -1,3 +1,12 @@ +--- +icon: material/new +--- + + +!!! quote "Changes in sing-box 1.11.0" + + :material-plus: [cache_capacity](#cache_capacity) + !!! quote "Changes in sing-box 1.9.0" :material-plus: [client_subnet](#client_subnet) @@ -16,6 +25,7 @@ "disable_cache": false, "disable_expire": false, "independent_cache": false, + "cache_capacity": 0, "reverse_mapping": false, "client_subnet": "", "fakeip": {} @@ -58,6 +68,14 @@ Disable dns cache expire. Make each DNS server's cache independent for special purposes. If enabled, will slightly degrade performance. +#### cache_capacity + +!!! quote "Since sing-box 1.11.0" + +LRU cache capacity. + +Value less than 1024 will be ignored. + #### reverse_mapping Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing. diff --git a/docs/configuration/dns/index.zh.md b/docs/configuration/dns/index.zh.md index c845ae0aa7..20fbc00db5 100644 --- a/docs/configuration/dns/index.zh.md +++ b/docs/configuration/dns/index.zh.md @@ -1,4 +1,12 @@ -!!! quote "sing-box 1.9.0 中的更改" +--- +icon: material/new +--- + +!!! quote "自 sing-box 1.11.0 起" + + :material-plus: [cache_capacity](#cache_capacity) + +!!! quote "自 sing-box 1.9.0 起" :material-plus: [client_subnet](#client_subnet) @@ -16,6 +24,7 @@ "disable_cache": false, "disable_expire": false, "independent_cache": false, + "cache_capacity": 0, "reverse_mapping": false, "client_subnet": "", "fakeip": {} @@ -57,6 +66,14 @@ 使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。 +#### cache_capacity + +!!! quote "自 sing-box 1.11.0 起" + +LRU 缓存容量。 + +小于 1024 的值将被忽略。 + #### reverse_mapping 在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。 diff --git a/option/dns.go b/option/dns.go index 32c1ac2e1b..272c518054 100644 --- a/option/dns.go +++ b/option/dns.go @@ -31,6 +31,7 @@ type DNSClientOptions struct { DisableCache bool `json:"disable_cache,omitempty"` DisableExpire bool `json:"disable_expire,omitempty"` IndependentCache bool `json:"independent_cache,omitempty"` + CacheCapacity uint32 `json:"cache_capacity,omitempty"` ClientSubnet *badoption.Prefixable `json:"client_subnet,omitempty"` } diff --git a/route/router.go b/route/router.go index 1f760a86a0..f23604b306 100644 --- a/route/router.go +++ b/route/router.go @@ -96,6 +96,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route DisableCache: dnsOptions.DNSClientOptions.DisableCache, DisableExpire: dnsOptions.DNSClientOptions.DisableExpire, IndependentCache: dnsOptions.DNSClientOptions.IndependentCache, + CacheCapacity: dnsOptions.DNSClientOptions.CacheCapacity, RDRC: func() dns.RDRCStore { cacheFile := service.FromContext[adapter.CacheFile](ctx) if cacheFile == nil { From 6a5943f4ceaed212904bf5d18de7cea5a7b47a1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 14 Nov 2024 18:31:37 +0800 Subject: [PATCH 25/49] Add override destination to route options --- adapter/inbound.go | 7 +-- adapter/outbound/default.go | 24 ++++----- common/dialer/default_parallel_network.go | 44 +++++++++++----- docs/configuration/dns/index.md | 9 +--- docs/configuration/dns/index.zh.md | 10 ++-- docs/configuration/outbound/direct.md | 20 ++++++-- docs/configuration/outbound/direct.zh.md | 22 +++++--- docs/configuration/route/rule_action.md | 50 +++++++++++-------- docs/configuration/route/rule_action.zh.md | 58 +++++++++++++--------- docs/deprecated.md | 6 +++ docs/deprecated.zh.md | 7 +++ docs/migration.md | 38 ++++++++++++++ docs/migration.zh.md | 42 +++++++++++++++- experimental/deprecated/constants.go | 10 ++++ option/direct.go | 29 +++++++++-- option/rule_action.go | 25 +++++----- route/route.go | 37 +++++++++++--- route/rule/rule_action.go | 7 +++ 18 files changed, 324 insertions(+), 121 deletions(-) diff --git a/adapter/inbound.go b/adapter/inbound.go index 1093bac4e6..d3cc95b7d9 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -61,9 +61,10 @@ type InboundContext struct { // cache // Deprecated: implement in rule action - InboundDetour string - LastInbound string - OriginDestination M.Socksaddr + InboundDetour string + LastInbound string + OriginDestination M.Socksaddr + RouteOriginalDestination M.Socksaddr // Deprecated InboundOptions option.InboundOptions UDPDisableDomainUnmapping bool diff --git a/adapter/outbound/default.go b/adapter/outbound/default.go index 27cafb9d2f..573673f287 100644 --- a/adapter/outbound/default.go +++ b/adapter/outbound/default.go @@ -25,11 +25,7 @@ func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata a var outConn net.Conn var err error if len(metadata.DestinationAddresses) > 0 { - if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer { - outConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) - } else { - outConn, err = N.DialSerial(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses) - } + outConn, err = dialer.DialSerialNetwork(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) } else { outConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination) } @@ -73,11 +69,7 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, } } else { if len(metadata.DestinationAddresses) > 0 { - if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer { - outPacketConn, destinationAddress, err = dialer.ListenSerialNetworkPacket(ctx, parallelDialer, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) - } else { - outPacketConn, destinationAddress, err = N.ListenSerial(ctx, this, metadata.Destination, metadata.DestinationAddresses) - } + outPacketConn, destinationAddress, err = dialer.ListenSerialNetworkPacket(ctx, this, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) } else { outPacketConn, err = this.ListenPacket(ctx, metadata.Destination) } @@ -91,11 +83,17 @@ func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, return err } if destinationAddress.IsValid() { - if metadata.Destination.IsFqdn() { + var originDestination M.Socksaddr + if metadata.RouteOriginalDestination.IsValid() { + originDestination = metadata.RouteOriginalDestination + } else { + originDestination = metadata.Destination + } + if metadata.Destination != M.SocksaddrFrom(destinationAddress, metadata.Destination.Port) { if metadata.UDPDisableDomainUnmapping { - outPacketConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) + outPacketConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), originDestination) } else { - outPacketConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), metadata.Destination) + outPacketConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), originDestination) } } if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded { diff --git a/common/dialer/default_parallel_network.go b/common/dialer/default_parallel_network.go index ea043dfdb2..5145656ba8 100644 --- a/common/dialer/default_parallel_network.go +++ b/common/dialer/default_parallel_network.go @@ -13,17 +13,27 @@ import ( N "github.com/sagernet/sing/common/network" ) -func DialSerialNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { +func DialSerialNetwork(ctx context.Context, dialer N.Dialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel { return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } var errors []error - for _, address := range destinationAddresses { - conn, err := dialer.DialParallelInterface(ctx, network, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay) - if err == nil { - return conn, nil + if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel { + for _, address := range destinationAddresses { + conn, err := parallelDialer.DialParallelInterface(ctx, network, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay) + if err == nil { + return conn, nil + } + errors = append(errors, err) + } + } else { + for _, address := range destinationAddresses { + conn, err := dialer.DialContext(ctx, network, M.SocksaddrFrom(address, destination.Port)) + if err == nil { + return conn, nil + } + errors = append(errors, err) } - errors = append(errors, err) } return nil, E.Errors(errors...) } @@ -106,17 +116,27 @@ func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, ne } } -func ListenSerialNetworkPacket(ctx context.Context, dialer ParallelInterfaceDialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { +func ListenSerialNetworkPacket(ctx context.Context, dialer N.Dialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel { return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } var errors []error - for _, address := range destinationAddresses { - conn, err := dialer.ListenSerialInterfacePacket(ctx, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay) - if err == nil { - return conn, address, nil + if parallelDialer, isParallel := dialer.(ParallelInterfaceDialer); isParallel { + for _, address := range destinationAddresses { + conn, err := parallelDialer.ListenSerialInterfacePacket(ctx, M.SocksaddrFrom(address, destination.Port), strategy, interfaceType, fallbackInterfaceType, fallbackDelay) + if err == nil { + return conn, address, nil + } + errors = append(errors, err) + } + } else { + for _, address := range destinationAddresses { + conn, err := dialer.ListenPacket(ctx, M.SocksaddrFrom(address, destination.Port)) + if err == nil { + return conn, address, nil + } + errors = append(errors, err) } - errors = append(errors, err) } return nil, netip.Addr{}, E.Errors(errors...) } diff --git a/docs/configuration/dns/index.md b/docs/configuration/dns/index.md index fc81333443..0756281dbf 100644 --- a/docs/configuration/dns/index.md +++ b/docs/configuration/dns/index.md @@ -1,16 +1,11 @@ --- -icon: material/new +icon: material/new-box --- - !!! quote "Changes in sing-box 1.11.0" :material-plus: [cache_capacity](#cache_capacity) -!!! quote "Changes in sing-box 1.9.0" - - :material-plus: [client_subnet](#client_subnet) - # DNS ### Structure @@ -70,7 +65,7 @@ Make each DNS server's cache independent for special purposes. If enabled, will #### cache_capacity -!!! quote "Since sing-box 1.11.0" +!!! question "Since sing-box 1.11.0" LRU cache capacity. diff --git a/docs/configuration/dns/index.zh.md b/docs/configuration/dns/index.zh.md index 20fbc00db5..76c07b6a2c 100644 --- a/docs/configuration/dns/index.zh.md +++ b/docs/configuration/dns/index.zh.md @@ -1,15 +1,11 @@ --- -icon: material/new +icon: material/new-box --- -!!! quote "自 sing-box 1.11.0 起" +!!! quote "sing-box 1.11.0 中的更改" :material-plus: [cache_capacity](#cache_capacity) -!!! quote "自 sing-box 1.9.0 起" - - :material-plus: [client_subnet](#client_subnet) - # DNS ### 结构 @@ -68,7 +64,7 @@ icon: material/new #### cache_capacity -!!! quote "自 sing-box 1.11.0 起" +!!! question "自 sing-box 1.11.0 起" LRU 缓存容量。 diff --git a/docs/configuration/outbound/direct.md b/docs/configuration/outbound/direct.md index c2f5671a6b..71649243a5 100644 --- a/docs/configuration/outbound/direct.md +++ b/docs/configuration/outbound/direct.md @@ -1,3 +1,12 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.11.0" + + :material-alert-decagram: [override_address](#override_address) + :material-alert-decagram: [override_port](#override_port) + `direct` outbound send requests directly. ### Structure @@ -9,7 +18,6 @@ "override_address": "1.0.0.1", "override_port": 53, - "proxy_protocol": 0, ... // Dial Fields } @@ -19,15 +27,19 @@ #### override_address +!!! failure "Deprecated in sing-box 1.11.0" + + Destination override fields are deprecated in sing-box 1.11.0 and will be removed in sing-box 1.13.0, see [Migration](/migration/#migrate-destination-override-fields-to-route-options). + Override the connection destination address. #### override_port -Override the connection destination port. +!!! failure "Deprecated in sing-box 1.11.0" -#### proxy_protocol + Destination override fields are deprecated in sing-box 1.11.0 and will be removed in sing-box 1.13.0, see [Migration](/migration/#migrate-destination-override-fields-to-route-options). -Write [Proxy Protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) in the connection header. +Override the connection destination port. Protocol value can be `1` or `2`. diff --git a/docs/configuration/outbound/direct.zh.md b/docs/configuration/outbound/direct.zh.md index aee131758a..55d3bf8c2d 100644 --- a/docs/configuration/outbound/direct.zh.md +++ b/docs/configuration/outbound/direct.zh.md @@ -1,3 +1,12 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.11.0 中的更改" + + :material-alert-decagram: [override_address](#override_address) + :material-alert-decagram: [override_port](#override_port) + `direct` 出站直接发送请求。 ### 结构 @@ -9,7 +18,6 @@ "override_address": "1.0.0.1", "override_port": 53, - "proxy_protocol": 0, ... // 拨号字段 } @@ -19,17 +27,19 @@ #### override_address +!!! failure "已在 sing-box 1.11.0 废弃" + + 目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。 + 覆盖连接目标地址。 #### override_port -覆盖连接目标端口。 - -#### proxy_protocol +!!! failure "已在 sing-box 1.11.0 废弃" -写出 [代理协议](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) 到连接头。 + 目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。 -可用协议版本值:`1` 或 `2`。 +覆盖连接目标端口。 ### 拨号字段 diff --git a/docs/configuration/route/rule_action.md b/docs/configuration/route/rule_action.md index 6462c84836..63e2b00b75 100644 --- a/docs/configuration/route/rule_action.md +++ b/docs/configuration/route/rule_action.md @@ -10,12 +10,8 @@ icon: material/new-box { "action": "route", // default "outbound": "", - "network_strategy": "", - "network_type": [], - "fallback_network_type": [], - "fallback_delay": "", - "udp_disable_domain_unmapping": false, - "udp_connect": false + + ... // route-options Fields } ``` @@ -31,6 +27,34 @@ icon: material/new-box Tag of target outbound. +#### route-options Fields + +See `route-options` fields below. + +### route-options + +```json +{ + "action": "route-options", + "override_address": "", + "override_port": 0, + "network_strategy": "", + "fallback_delay": "", + "udp_disable_domain_unmapping": false, + "udp_connect": false +} +``` + +`route-options` set options for routing. + +#### override_address + +Override the connection destination address. + +#### override_port + +Override the connection destination port. + #### network_strategy See [Dial Fields](/configuration/shared/dial/#network_strategy) for details. @@ -62,20 +86,6 @@ do not support receiving UDP packets with domain addresses, such as Surge. If enabled, attempts to connect UDP connection to the destination instead of listen. -### route-options - -```json -{ - "action": "route-options", - "network_strategy": "", - "fallback_delay": "", - "udp_disable_domain_unmapping": false, - "udp_connect": false -} -``` - -`route-options` set options for routing. - ### reject ```json diff --git a/docs/configuration/route/rule_action.zh.md b/docs/configuration/route/rule_action.zh.md index acadb66d3c..7959fced45 100644 --- a/docs/configuration/route/rule_action.zh.md +++ b/docs/configuration/route/rule_action.zh.md @@ -10,12 +10,8 @@ icon: material/new-box { "action": "route", // 默认 "outbound": "", - "network_strategy": "", - "fallback_delay": "", - "network_type": [], - "fallback_network_type": [], - "udp_disable_domain_unmapping": false, - "udp_connect": false + + ... // route-options 字段 } ``` @@ -27,6 +23,38 @@ icon: material/new-box 目标出站的标签。 +#### route-options 字段 + +参阅下方的 `route-options` 字段。 + +### route-options + +```json +{ + "action": "route-options", + "override_address": "", + "override_port": 0, + "network_strategy": "", + "fallback_delay": "", + "udp_disable_domain_unmapping": false, + "udp_connect": false +} +``` + +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + +`route-options` 为路由设置选项。 + +#### override_address + +覆盖目标地址。 + +#### override_port + +覆盖目标端口。 + #### network_strategy 详情参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。 @@ -56,24 +84,6 @@ icon: material/new-box 如果启用,将尝试将 UDP 连接 connect 到目标而不是 listen。 -### route-options - -```json -{ - "action": "route-options", - "network_strategy": "", - "fallback_delay": "", - "udp_disable_domain_unmapping": false, - "udp_connect": false -} -``` - -!!! note "" - - 当内容只有一项时,可以忽略 JSON 数组 [] 标签 - -`route-options` 为路由设置选项。 - ### reject ```json diff --git a/docs/deprecated.md b/docs/deprecated.md index d57c93ade4..5dcec56241 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -22,6 +22,12 @@ check [Migration](../migration/#migrate-legacy-inbound-fields-to-rule-actions). Old fields will be removed in sing-box 1.13.0. +#### Destination override fields in direct outbound + +Destination override fields (`override_address` / `override_port`) in direct outbound are deprecated +and can be replaced by rule actions, +check [Migration](../migration/#migrate-destination-override-fields-to-route-options). + ## 1.10.0 #### TUN address fields are merged diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index 76a426d279..6f6c839fce 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -20,6 +20,13 @@ icon: material/delete-alert 旧字段将在 sing-box 1.13.0 中被移除。 +#### direct 出站中的目标地址覆盖字段 + +direct 出站中的目标地址覆盖字段(`override_address` / `override_port`)已废弃且可以通过规则动作替代, +参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。 + +旧字段将在 sing-box 1.13.0 中被移除。 + ## 1.10.0 #### Match source 规则项已重命名 diff --git a/docs/migration.md b/docs/migration.md index 8f01c4f031..ea1afa2dd2 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -156,6 +156,44 @@ Inbound fields are deprecated and can be replaced by rule actions. } ``` +### Migrate destination override fields to route options + +Destination override fields in direct outbound are deprecated and can be replaced by route options. + +!!! info "References" + + [Rule Action](/configuration/route/rule_action/) / + [Direct](/configuration/outbound/direct/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "outbounds": [ + { + "type": "direct", + "override_address": "1.1.1.1", + "override_port": 443 + } + ] + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "route": { + "rules": [ + { + "action": "route-options", // or route + "override_address": "1.1.1.1", + "override_port": 443 + } + ] + } + ``` + ## 1.10.0 ### TUN address fields are merged diff --git a/docs/migration.zh.md b/docs/migration.zh.md index dc62f37003..73afbb0519 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -104,6 +104,7 @@ icon: material/arrange-bring-forward ### 迁移旧的入站字段到规则动作 + 入站选项已被弃用,且可以被规则动作替代。 !!! info "参考" @@ -156,6 +157,45 @@ icon: material/arrange-bring-forward } ``` +### 迁移 direct 出站中的目标地址覆盖字段到路由字段 + +direct 出站中的目标地址覆盖字段已废弃,且可以被路由字段替代。 + +!!! info "参考" + + [Rule Action](/zh/configuration/route/rule_action/) / + [Direct](/zh/configuration/outbound/direct/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "outbounds": [ + { + "type": "direct", + "override_address": "1.1.1.1", + "override_port": 443 + } + ] + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "route": { + "rules": [ + { + "action": "route-options", // 或 route + "override_address": "1.1.1.1", + "override_port": 443 + } + ] + } + } + ``` + ## 1.10.0 ### TUN 地址字段已合并 @@ -164,8 +204,6 @@ icon: material/arrange-bring-forward `inet4_route_address` 和 `inet6_route_address` 已合并为 `route_address`, `inet4_route_exclude_address` 和 `inet6_route_exclude_address` 已合并为 `route_exclude_address`。 -旧字段已废弃,且将在 sing-box 1.11.0 中移除。 - !!! info "参考" [TUN](/zh/configuration/inbound/tun/) diff --git a/experimental/deprecated/constants.go b/experimental/deprecated/constants.go index 918b698df9..bf278f844e 100644 --- a/experimental/deprecated/constants.go +++ b/experimental/deprecated/constants.go @@ -104,6 +104,15 @@ var OptionInboundOptions = Note{ MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions", } +var OptionDestinationOverrideFields = Note{ + Name: "destination-override-fields", + Description: "destination override fields in direct outbound", + DeprecatedVersion: "1.11.0", + ScheduledVersion: "1.13.0", + EnvName: "DESTINATION_OVERRIDE_FIELDS", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-destination-override-fields-to-route-options", +} + var Options = []Note{ OptionBadMatchSource, OptionGEOIP, @@ -111,4 +120,5 @@ var Options = []Note{ OptionTUNAddressX, OptionSpecialOutbounds, OptionInboundOptions, + OptionDestinationOverrideFields, } diff --git a/option/direct.go b/option/direct.go index 8624846dbc..180ff0aa23 100644 --- a/option/direct.go +++ b/option/direct.go @@ -1,5 +1,12 @@ package option +import ( + "context" + + "github.com/sagernet/sing-box/experimental/deprecated" + "github.com/sagernet/sing/common/json" +) + type DirectInboundOptions struct { ListenOptions Network NetworkList `json:"network,omitempty"` @@ -7,9 +14,25 @@ type DirectInboundOptions struct { OverridePort uint16 `json:"override_port,omitempty"` } -type DirectOutboundOptions struct { +type _DirectOutboundOptions struct { DialerOptions + // Deprecated: Use Route Action instead OverrideAddress string `json:"override_address,omitempty"` - OverridePort uint16 `json:"override_port,omitempty"` - ProxyProtocol uint8 `json:"proxy_protocol,omitempty"` + // Deprecated: Use Route Action instead + OverridePort uint16 `json:"override_port,omitempty"` + // Deprecated: removed + ProxyProtocol uint8 `json:"proxy_protocol,omitempty"` +} + +type DirectOutboundOptions _DirectOutboundOptions + +func (d *DirectOutboundOptions) UnmarshalJSONContext(ctx context.Context, content []byte) error { + err := json.UnmarshalDisallowUnknownFields(content, (*_DirectOutboundOptions)(d)) + if err != nil { + return err + } + if d.OverrideAddress != "" || d.OverridePort != 0 { + deprecated.Report(ctx, deprecated.OptionDestinationOverrideFields) + } + return nil } diff --git a/option/rule_action.go b/option/rule_action.go index 7c31ea7a42..ce3b92d9e9 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -137,24 +137,25 @@ func (r *DNSRuleAction) UnmarshalJSONContext(ctx context.Context, data []byte) e } type RouteActionOptions struct { - Outbound string `json:"outbound,omitempty"` - NetworkStrategy NetworkStrategy `json:"network_strategy,omitempty"` - FallbackDelay uint32 `json:"fallback_delay,omitempty"` - UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` - UDPConnect bool `json:"udp_connect,omitempty"` + Outbound string `json:"outbound,omitempty"` + RawRouteOptionsActionOptions } -type _RouteOptionsActionOptions struct { - NetworkStrategy NetworkStrategy `json:"network_strategy,omitempty"` - FallbackDelay uint32 `json:"fallback_delay,omitempty"` - UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` - UDPConnect bool `json:"udp_connect,omitempty"` +type RawRouteOptionsActionOptions struct { + OverrideAddress string `json:"override_address,omitempty"` + OverridePort uint16 `json:"override_port,omitempty"` + + NetworkStrategy NetworkStrategy `json:"network_strategy,omitempty"` + FallbackDelay uint32 `json:"fallback_delay,omitempty"` + + UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` + UDPConnect bool `json:"udp_connect,omitempty"` } -type RouteOptionsActionOptions _RouteOptionsActionOptions +type RouteOptionsActionOptions RawRouteOptionsActionOptions func (r *RouteOptionsActionOptions) UnmarshalJSON(data []byte) error { - err := json.Unmarshal(data, (*_RouteOptionsActionOptions)(r)) + err := json.Unmarshal(data, (*RawRouteOptionsActionOptions)(r)) if err != nil { return err } diff --git a/route/route.go b/route/route.go index 051ee403ba..7d21d3154c 100644 --- a/route/route.go +++ b/route/route.go @@ -422,17 +422,38 @@ match: } } } + var routeOptions *rule.RuleActionRouteOptions switch action := currentRule.Action().(type) { case *rule.RuleActionRoute: - metadata.NetworkStrategy = action.NetworkStrategy - metadata.FallbackDelay = action.FallbackDelay - metadata.UDPDisableDomainUnmapping = action.UDPDisableDomainUnmapping - metadata.UDPConnect = action.UDPConnect + routeOptions = &action.RuleActionRouteOptions case *rule.RuleActionRouteOptions: - metadata.NetworkStrategy = action.NetworkStrategy - metadata.FallbackDelay = action.FallbackDelay - metadata.UDPDisableDomainUnmapping = action.UDPDisableDomainUnmapping - metadata.UDPConnect = action.UDPConnect + routeOptions = action + } + if routeOptions != nil { + // TODO: add nat + if (routeOptions.OverrideAddress.IsValid() || routeOptions.OverridePort > 0) && !metadata.RouteOriginalDestination.IsValid() { + metadata.RouteOriginalDestination = metadata.Destination + } + if routeOptions.OverrideAddress.IsValid() { + metadata.Destination = M.Socksaddr{ + Addr: routeOptions.OverrideAddress.Addr, + Port: metadata.Destination.Port, + Fqdn: routeOptions.OverrideAddress.Fqdn, + } + } + if routeOptions.OverridePort > 0 { + metadata.Destination = M.Socksaddr{ + Addr: metadata.Destination.Addr, + Port: routeOptions.OverridePort, + Fqdn: metadata.Destination.Fqdn, + } + } + metadata.NetworkStrategy = routeOptions.NetworkStrategy + metadata.FallbackDelay = routeOptions.FallbackDelay + metadata.UDPDisableDomainUnmapping = routeOptions.UDPDisableDomainUnmapping + metadata.UDPConnect = routeOptions.UDPConnect + } + switch action := currentRule.Action().(type) { case *rule.RuleActionSniff: if !preMatch { newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, action, inputConn, inputPacketConn) diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index 52e9520726..1b4099c903 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -19,6 +19,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" ) @@ -30,6 +31,8 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti return &RuleActionRoute{ Outbound: action.RouteOptions.Outbound, RuleActionRouteOptions: RuleActionRouteOptions{ + OverrideAddress: M.ParseSocksaddrHostPort(action.RouteOptions.OverrideAddress, 0), + OverridePort: action.RouteOptions.OverridePort, NetworkStrategy: C.NetworkStrategy(action.RouteOptions.NetworkStrategy), FallbackDelay: time.Duration(action.RouteOptions.FallbackDelay), UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping, @@ -38,6 +41,8 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti }, nil case C.RuleActionTypeRouteOptions: return &RuleActionRouteOptions{ + OverrideAddress: M.ParseSocksaddrHostPort(action.RouteOptionsOptions.OverrideAddress, 0), + OverridePort: action.RouteOptionsOptions.OverridePort, NetworkStrategy: C.NetworkStrategy(action.RouteOptionsOptions.NetworkStrategy), FallbackDelay: time.Duration(action.RouteOptionsOptions.FallbackDelay), UDPDisableDomainUnmapping: action.RouteOptionsOptions.UDPDisableDomainUnmapping, @@ -139,6 +144,8 @@ func (r *RuleActionRoute) String() string { } type RuleActionRouteOptions struct { + OverrideAddress M.Socksaddr + OverridePort uint16 NetworkStrategy C.NetworkStrategy NetworkType []C.InterfaceType FallbackNetworkType []C.InterfaceType From 48d102a0ab5a41360a7bdb861b356606a0ae2ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 16 Nov 2024 12:15:14 +0800 Subject: [PATCH 26/49] documentation: Fix typo --- docs/configuration/route/index.md | 2 +- docs/configuration/route/index.zh.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration/route/index.md b/docs/configuration/route/index.md index 58f733528e..1a1919e94b 100644 --- a/docs/configuration/route/index.md +++ b/docs/configuration/route/index.md @@ -9,7 +9,7 @@ icon: material/new-box :material-plus: [default_network_strategy](#default_network_strategy) :material-plus: [default_network_type](#default_network_type) :material-plus: [default_fallback_network_type](#default_fallback_network_type) - :material-alert: [default_fallback_delay](#default_fallback_delay) + :material-plus: [default_fallback_delay](#default_fallback_delay) !!! quote "Changes in sing-box 1.8.0" diff --git a/docs/configuration/route/index.zh.md b/docs/configuration/route/index.zh.md index 9550b4ac3b..a224ddc4c5 100644 --- a/docs/configuration/route/index.zh.md +++ b/docs/configuration/route/index.zh.md @@ -9,7 +9,7 @@ icon: material/new-box :material-plus: [network_strategy](#network_strategy) :material-plus: [default_network_type](#default_network_type) :material-plus: [default_fallback_network_type](#default_fallback_network_type) - :material-alert: [default_fallback_delay](#default_fallback_delay) + :material-plus: [default_fallback_delay](#default_fallback_delay) !!! quote "sing-box 1.8.0 中的更改" From b45cb0763ec64f0a1b01367c1f69204ecac3ac16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 20 Nov 2024 11:32:02 +0800 Subject: [PATCH 27/49] refactor: connection manager --- adapter/connections.go | 14 ++ box.go | 18 +- protocol/direct/inbound.go | 2 +- route/conn.go | 332 +++++++++++++++++++++++++++++++++++++ route/conn_monitor.go | 128 ++++++++++++++ route/conn_monitor_test.go | 43 +++++ route/dns.go | 6 +- route/geo_resources.go | 8 +- route/network.go | 8 + route/route.go | 42 ++--- route/router.go | 20 ++- 11 files changed, 569 insertions(+), 52 deletions(-) create mode 100644 adapter/connections.go create mode 100644 route/conn.go create mode 100644 route/conn_monitor.go create mode 100644 route/conn_monitor_test.go diff --git a/adapter/connections.go b/adapter/connections.go new file mode 100644 index 0000000000..0682d05a0f --- /dev/null +++ b/adapter/connections.go @@ -0,0 +1,14 @@ +package adapter + +import ( + "context" + "net" + + N "github.com/sagernet/sing/common/network" +) + +type ConnectionManager interface { + Lifecycle + NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata InboundContext, onClose N.CloseHandlerFunc) + NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) +} diff --git a/box.go b/box.go index 3b69617f64..44a29992e4 100644 --- a/box.go +++ b/box.go @@ -36,9 +36,10 @@ type Box struct { logFactory log.Factory logger log.ContextLogger network *route.NetworkManager - router *route.Router inbound *inbound.Manager outbound *outbound.Manager + connection *route.ConnectionManager + router *route.Router services []adapter.LifecycleService done chan struct{} } @@ -128,6 +129,8 @@ func New(options Options) (*Box, error) { return nil, E.Cause(err, "initialize network manager") } service.MustRegister[adapter.NetworkManager](ctx, networkManager) + connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection")) + service.MustRegister[adapter.ConnectionManager](ctx, connectionManager) router, err := route.NewRouter(ctx, logFactory, routeOptions, common.PtrValueOrDefault(options.DNS)) if err != nil { return nil, E.Cause(err, "initialize router") @@ -238,9 +241,10 @@ func New(options Options) (*Box, error) { } return &Box{ network: networkManager, - router: router, inbound: inboundManager, outbound: outboundManager, + connection: connectionManager, + router: router, createdAt: createdAt, logFactory: logFactory, logger: logFactory.Logger(), @@ -299,11 +303,11 @@ func (s *Box) preStart() error { if err != nil { return err } - err = adapter.Start(adapter.StartStateInitialize, s.network, s.router, s.outbound, s.inbound) + err = adapter.Start(adapter.StartStateInitialize, s.network, s.connection, s.router, s.outbound, s.inbound) if err != nil { return err } - err = adapter.Start(adapter.StartStateStart, s.outbound, s.network, s.router) + err = adapter.Start(adapter.StartStateStart, s.outbound, s.network, s.connection, s.router) if err != nil { return err } @@ -323,7 +327,7 @@ func (s *Box) start() error { if err != nil { return err } - err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.router, s.inbound) + err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.connection, s.router, s.inbound) if err != nil { return err } @@ -331,7 +335,7 @@ func (s *Box) start() error { if err != nil { return err } - err = adapter.Start(adapter.StartStateStarted, s.network, s.router, s.outbound, s.inbound) + err = adapter.Start(adapter.StartStateStarted, s.network, s.connection, s.router, s.outbound, s.inbound) if err != nil { return err } @@ -350,7 +354,7 @@ func (s *Box) Close() error { close(s.done) } err := common.Close( - s.inbound, s.outbound, s.router, s.network, + s.inbound, s.outbound, s.router, s.connection, s.network, ) for _, lifecycleService := range s.services { err = E.Append(err, lifecycleService.Close(), func(err error) error { diff --git a/protocol/direct/inbound.go b/protocol/direct/inbound.go index 8415e21fd8..6db60d7890 100644 --- a/protocol/direct/inbound.go +++ b/protocol/direct/inbound.go @@ -83,7 +83,7 @@ func (i *Inbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { destination = i.overrideDestination case 2: destination = i.overrideDestination - destination.Port = source.Port + destination.Port = i.listener.UDPAddr().Port case 3: destination = source destination.Port = i.overrideDestination.Port diff --git a/route/conn.go b/route/conn.go new file mode 100644 index 0000000000..594379cc93 --- /dev/null +++ b/route/conn.go @@ -0,0 +1,332 @@ +package route + +import ( + "context" + "io" + "net" + "net/netip" + "sync/atomic" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/dialer" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +var _ adapter.ConnectionManager = (*ConnectionManager)(nil) + +type ConnectionManager struct { + logger logger.ContextLogger + monitor *ConnectionMonitor +} + +func NewConnectionManager(logger logger.ContextLogger) *ConnectionManager { + return &ConnectionManager{ + logger: logger, + monitor: NewConnectionMonitor(), + } +} + +func (m *ConnectionManager) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateInitialize { + return nil + } + return m.monitor.Start() +} + +func (m *ConnectionManager) Close() error { + return m.monitor.Close() +} + +func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + ctx = adapter.WithContext(ctx, &metadata) + var ( + remoteConn net.Conn + err error + ) + if len(metadata.DestinationAddresses) > 0 { + remoteConn, err = dialer.DialSerialNetwork(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) + } else { + remoteConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination) + } + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + m.logger.ErrorContext(ctx, "open outbound connection: ", err) + return + } + err = N.ReportConnHandshakeSuccess(conn, remoteConn) + if err != nil { + remoteConn.Close() + N.CloseOnHandshakeFailure(conn, onClose, err) + m.logger.ErrorContext(ctx, "report handshake success: ", err) + return + } + var done atomic.Bool + if ctx.Done() != nil { + onClose = N.AppendClose(onClose, m.monitor.Add(ctx, conn)) + } + go m.connectionCopy(ctx, conn, remoteConn, false, &done, onClose) + go m.connectionCopy(ctx, remoteConn, conn, true, &done, onClose) +} + +func (m *ConnectionManager) connectionCopy(ctx context.Context, source io.Reader, destination io.Writer, direction bool, done *atomic.Bool, onClose N.CloseHandlerFunc) { + originSource := source + originDestination := destination + var readCounters, writeCounters []N.CountFunc + for { + source, readCounters = N.UnwrapCountReader(source, readCounters) + destination, writeCounters = N.UnwrapCountWriter(destination, writeCounters) + if cachedSrc, isCached := source.(N.CachedReader); isCached { + cachedBuffer := cachedSrc.ReadCached() + if cachedBuffer != nil { + dataLen := cachedBuffer.Len() + _, err := destination.Write(cachedBuffer.Bytes()) + cachedBuffer.Release() + if err != nil { + m.logger.ErrorContext(ctx, "connection upload payload: ", err) + if done.Swap(true) { + if onClose != nil { + onClose(err) + } + } + common.Close(originSource, originDestination) + return + } + for _, counter := range readCounters { + counter(int64(dataLen)) + } + for _, counter := range writeCounters { + counter(int64(dataLen)) + } + } + continue + } + break + } + _, err := bufio.CopyWithCounters(destination, source, originSource, readCounters, writeCounters) + if err != nil { + common.Close(originSource, originDestination) + } else if duplexDst, isDuplex := destination.(N.WriteCloser); isDuplex { + err = duplexDst.CloseWrite() + if err != nil { + common.Close(originSource, originDestination) + } + } else { + common.Close(originDestination) + } + if done.Swap(true) { + if onClose != nil { + onClose(err) + } + common.Close(originSource, originDestination) + } + if !direction { + if err == nil { + m.logger.DebugContext(ctx, "connection upload finished") + } else if !E.IsClosedOrCanceled(err) { + m.logger.ErrorContext(ctx, "connection upload closed: ", err) + } else { + m.logger.TraceContext(ctx, "connection upload closed") + } + } else { + if err == nil { + m.logger.DebugContext(ctx, "connection download finished") + } else if !E.IsClosedOrCanceled(err) { + m.logger.ErrorContext(ctx, "connection download closed: ", err) + } else { + m.logger.TraceContext(ctx, "connection download closed") + } + } +} + +func (m *ConnectionManager) NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + ctx = adapter.WithContext(ctx, &metadata) + var ( + remotePacketConn net.PacketConn + remoteConn net.Conn + destinationAddress netip.Addr + err error + ) + if metadata.UDPConnect { + if len(metadata.DestinationAddresses) > 0 { + if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer { + remoteConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) + } else { + remoteConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses) + } + } else { + remoteConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination) + } + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + m.logger.ErrorContext(ctx, "open outbound packet connection: ", err) + return + } + remotePacketConn = bufio.NewUnbindPacketConn(remoteConn) + connRemoteAddr := M.AddrFromNet(remoteConn.RemoteAddr()) + if connRemoteAddr != metadata.Destination.Addr { + destinationAddress = connRemoteAddr + } + } else { + if len(metadata.DestinationAddresses) > 0 { + remotePacketConn, destinationAddress, err = dialer.ListenSerialNetworkPacket(ctx, this, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) + } else { + remotePacketConn, err = this.ListenPacket(ctx, metadata.Destination) + } + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + m.logger.ErrorContext(ctx, "listen outbound packet connection: ", err) + return + } + } + err = N.ReportPacketConnHandshakeSuccess(conn, remotePacketConn) + if err != nil { + conn.Close() + remotePacketConn.Close() + m.logger.ErrorContext(ctx, "report handshake success: ", err) + return + } + if destinationAddress.IsValid() { + var originDestination M.Socksaddr + if metadata.RouteOriginalDestination.IsValid() { + originDestination = metadata.RouteOriginalDestination + } else { + originDestination = metadata.Destination + } + if metadata.Destination != M.SocksaddrFrom(destinationAddress, metadata.Destination.Port) { + if metadata.UDPDisableDomainUnmapping { + remotePacketConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(remotePacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), originDestination) + } else { + remotePacketConn = bufio.NewNATPacketConn(bufio.NewPacketConn(remotePacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), originDestination) + } + } + if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded { + natConn.UpdateDestination(destinationAddress) + } + } + destination := bufio.NewPacketConn(remotePacketConn) + var done atomic.Bool + if ctx.Done() != nil { + onClose = N.AppendClose(onClose, m.monitor.Add(ctx, conn)) + } + go m.packetConnectionCopy(ctx, conn, destination, false, &done, onClose) + go m.packetConnectionCopy(ctx, destination, conn, true, &done, onClose) +} + +func (m *ConnectionManager) packetConnectionCopy(ctx context.Context, source N.PacketReader, destination N.PacketWriter, direction bool, done *atomic.Bool, onClose N.CloseHandlerFunc) { + _, err := bufio.CopyPacket(destination, source) + /*var readCounters, writeCounters []N.CountFunc + var cachedPackets []*N.PacketBuffer + originSource := source + for { + source, readCounters = N.UnwrapCountPacketReader(source, readCounters) + destination, writeCounters = N.UnwrapCountPacketWriter(destination, writeCounters) + if cachedReader, isCached := source.(N.CachedPacketReader); isCached { + packet := cachedReader.ReadCachedPacket() + if packet != nil { + cachedPackets = append(cachedPackets, packet) + continue + } + } + break + } + var handled bool + if natConn, isNatConn := source.(udpnat.Conn); isNatConn { + natConn.SetHandler(&udpHijacker{ + ctx: ctx, + logger: m.logger, + source: natConn, + destination: destination, + direction: direction, + readCounters: readCounters, + writeCounters: writeCounters, + done: done, + onClose: onClose, + }) + handled = true + } + if cachedPackets != nil { + _, err := bufio.WritePacketWithPool(originSource, destination, cachedPackets, readCounters, writeCounters) + if err != nil { + common.Close(source, destination) + m.logger.ErrorContext(ctx, "packet upload payload: ", err) + return + } + } + if handled { + return + } + _, err := bufio.CopyPacketWithCounters(destination, source, originSource, readCounters, writeCounters)*/ + if !direction { + if E.IsClosedOrCanceled(err) { + m.logger.TraceContext(ctx, "packet upload closed") + } else { + m.logger.DebugContext(ctx, "packet upload closed: ", err) + } + } else { + if E.IsClosedOrCanceled(err) { + m.logger.TraceContext(ctx, "packet download closed") + } else { + m.logger.DebugContext(ctx, "packet download closed: ", err) + } + } + if !done.Swap(true) { + if onClose != nil { + onClose(err) + } + } + common.Close(source, destination) +} + +/*type udpHijacker struct { + ctx context.Context + logger logger.ContextLogger + source io.Closer + destination N.PacketWriter + direction bool + readCounters []N.CountFunc + writeCounters []N.CountFunc + done *atomic.Bool + onClose N.CloseHandlerFunc +} + +func (u *udpHijacker) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { + dataLen := buffer.Len() + for _, counter := range u.readCounters { + counter(int64(dataLen)) + } + err := u.destination.WritePacket(buffer, source) + if err != nil { + common.Close(u.source, u.destination) + u.logger.DebugContext(u.ctx, "packet upload closed: ", err) + return + } + for _, counter := range u.writeCounters { + counter(int64(dataLen)) + } +} + +func (u *udpHijacker) Close() error { + var err error + if !u.done.Swap(true) { + err = common.Close(u.source, u.destination) + if u.onClose != nil { + u.onClose(net.ErrClosed) + } + } + if u.direction { + u.logger.TraceContext(u.ctx, "packet download closed") + } else { + u.logger.TraceContext(u.ctx, "packet upload closed") + } + return err +} + +func (u *udpHijacker) Upstream() any { + return u.destination +} +*/ diff --git a/route/conn_monitor.go b/route/conn_monitor.go new file mode 100644 index 0000000000..9e271b82a0 --- /dev/null +++ b/route/conn_monitor.go @@ -0,0 +1,128 @@ +package route + +import ( + "context" + "io" + "reflect" + "sync" + "time" + + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" +) + +type ConnectionMonitor struct { + access sync.RWMutex + reloadChan chan struct{} + connections list.List[*monitorEntry] +} + +type monitorEntry struct { + ctx context.Context + closer io.Closer +} + +func NewConnectionMonitor() *ConnectionMonitor { + return &ConnectionMonitor{ + reloadChan: make(chan struct{}, 1), + } +} + +func (m *ConnectionMonitor) Add(ctx context.Context, closer io.Closer) N.CloseHandlerFunc { + m.access.Lock() + defer m.access.Unlock() + element := m.connections.PushBack(&monitorEntry{ + ctx: ctx, + closer: closer, + }) + select { + case <-m.reloadChan: + return nil + default: + select { + case m.reloadChan <- struct{}{}: + default: + } + } + return func(it error) { + m.access.Lock() + defer m.access.Unlock() + m.connections.Remove(element) + select { + case <-m.reloadChan: + default: + select { + case m.reloadChan <- struct{}{}: + default: + } + } + } +} + +func (m *ConnectionMonitor) Start() error { + go m.monitor() + return nil +} + +func (m *ConnectionMonitor) Close() error { + m.access.Lock() + defer m.access.Unlock() + close(m.reloadChan) + for element := m.connections.Front(); element != nil; element = element.Next() { + element.Value.closer.Close() + } + return nil +} + +func (m *ConnectionMonitor) monitor() { + var ( + selectCases []reflect.SelectCase + elements []*list.Element[*monitorEntry] + ) + rootCase := reflect.SelectCase{ + Dir: reflect.SelectRecv, + Chan: reflect.ValueOf(m.reloadChan), + } + for { + m.access.RLock() + if m.connections.Len() == 0 { + m.access.RUnlock() + if _, loaded := <-m.reloadChan; !loaded { + return + } else { + continue + } + } + if len(elements) < m.connections.Len() { + elements = make([]*list.Element[*monitorEntry], 0, m.connections.Len()) + } + if len(selectCases) < m.connections.Len()+1 { + selectCases = make([]reflect.SelectCase, 0, m.connections.Len()+1) + } + elements = elements[:0] + selectCases = selectCases[:1] + selectCases[0] = rootCase + for element := m.connections.Front(); element != nil; element = element.Next() { + elements = append(elements, element) + selectCases = append(selectCases, reflect.SelectCase{ + Dir: reflect.SelectRecv, + Chan: reflect.ValueOf(element.Value.ctx.Done()), + }) + } + m.access.RUnlock() + selected, _, loaded := reflect.Select(selectCases) + if selected == 0 { + if !loaded { + return + } else { + time.Sleep(time.Second) + continue + } + } + element := elements[selected-1] + m.access.Lock() + m.connections.Remove(element) + m.access.Unlock() + element.Value.closer.Close() // maybe go close + } +} diff --git a/route/conn_monitor_test.go b/route/conn_monitor_test.go new file mode 100644 index 0000000000..a712bddcdb --- /dev/null +++ b/route/conn_monitor_test.go @@ -0,0 +1,43 @@ +package route_test + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/sagernet/sing-box/route" + + "github.com/stretchr/testify/require" +) + +func TestMonitor(t *testing.T) { + t.Parallel() + var closer myCloser + closer.Add(1) + monitor := route.NewConnectionMonitor() + require.NoError(t, monitor.Start()) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + monitor.Add(ctx, &closer) + done := make(chan struct{}) + go func() { + closer.Wait() + close(done) + }() + select { + case <-done: + case <-time.After(time.Second + 100*time.Millisecond): + t.Fatal("timeout") + } + cancel() + require.NoError(t, monitor.Close()) +} + +type myCloser struct { + sync.WaitGroup +} + +func (c *myCloser) Close() error { + c.Done() + return nil +} diff --git a/route/dns.go b/route/dns.go index b69be5a279..2c6efefe40 100644 --- a/route/dns.go +++ b/route/dns.go @@ -32,15 +32,15 @@ func (r *Router) hijackDNSStream(ctx context.Context, conn net.Conn, metadata ad } func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetBuffers []*N.PacketBuffer, metadata adapter.InboundContext) { - if uConn, isUDPNAT2 := conn.(*udpnat.Conn); isUDPNAT2 { + if natConn, isNatConn := conn.(udpnat.Conn); isNatConn { metadata.Destination = M.Socksaddr{} for _, packet := range packetBuffers { buffer := packet.Buffer destination := packet.Destination N.PutPacketBuffer(packet) - go ExchangeDNSPacket(ctx, r, uConn, buffer, metadata, destination) + go ExchangeDNSPacket(ctx, r, natConn, buffer, metadata, destination) } - uConn.SetHandler(&dnsHijacker{ + natConn.SetHandler(&dnsHijacker{ router: r, conn: conn, ctx: ctx, diff --git a/route/geo_resources.go b/route/geo_resources.go index c5a45ffdee..8a8a3ef549 100644 --- a/route/geo_resources.go +++ b/route/geo_resources.go @@ -145,13 +145,13 @@ func (r *Router) downloadGeoIPDatabase(savePath string) error { r.logger.Info("downloading geoip database") var detour adapter.Outbound if r.geoIPOptions.DownloadDetour != "" { - outbound, loaded := r.outboundManager.Outbound(r.geoIPOptions.DownloadDetour) + outbound, loaded := r.outbound.Outbound(r.geoIPOptions.DownloadDetour) if !loaded { return E.New("detour outbound not found: ", r.geoIPOptions.DownloadDetour) } detour = outbound } else { - detour = r.outboundManager.Default() + detour = r.outbound.Default() } if parentDir := filepath.Dir(savePath); parentDir != "" { @@ -200,13 +200,13 @@ func (r *Router) downloadGeositeDatabase(savePath string) error { r.logger.Info("downloading geosite database") var detour adapter.Outbound if r.geositeOptions.DownloadDetour != "" { - outbound, loaded := r.outboundManager.Outbound(r.geositeOptions.DownloadDetour) + outbound, loaded := r.outbound.Outbound(r.geositeOptions.DownloadDetour) if !loaded { return E.New("detour outbound not found: ", r.geositeOptions.DownloadDetour) } detour = outbound } else { - detour = r.outboundManager.Default() + detour = r.outbound.Default() } if parentDir := filepath.Dir(savePath); parentDir != "" { diff --git a/route/network.go b/route/network.go index e7c4df9ceb..510686e521 100644 --- a/route/network.go +++ b/route/network.go @@ -48,6 +48,7 @@ type NetworkManager struct { powerListener winpowrprof.EventListener pauseManager pause.Manager platformInterface platform.Interface + inboundManager adapter.InboundManager outboundManager adapter.OutboundManager wifiState adapter.WIFIState started bool @@ -354,6 +355,13 @@ func (r *NetworkManager) WIFIState() adapter.WIFIState { func (r *NetworkManager) ResetNetwork() { conntrack.Close() + for _, inbound := range r.inboundManager.Inbounds() { + listener, isListener := inbound.(adapter.InterfaceUpdateListener) + if isListener { + listener.InterfaceUpdated() + } + } + for _, outbound := range r.outboundManager.Outbounds() { listener, isListener := outbound.(adapter.InterfaceUpdateListener) if isListener { diff --git a/route/route.go b/route/route.go index 7d21d3154c..a56016f93a 100644 --- a/route/route.go +++ b/route/route.go @@ -11,7 +11,6 @@ import ( "time" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/conntrack" "github.com/sagernet/sing-box/common/process" "github.com/sagernet/sing-box/common/sniff" @@ -58,7 +57,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad if metadata.LastInbound == metadata.InboundDetour { return E.New("routing loop on detour: ", metadata.InboundDetour) } - detour, loaded := r.inboundManager.Get(metadata.InboundDetour) + detour, loaded := r.inbound.Get(metadata.InboundDetour) if !loaded { return E.New("inbound detour not found: ", metadata.InboundDetour) } @@ -96,7 +95,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad switch action := selectedRule.Action().(type) { case *rule.RuleActionRoute: var loaded bool - selectedOutbound, loaded = r.outboundManager.Outbound(action.Outbound) + selectedOutbound, loaded = r.outbound.Outbound(action.Outbound) if !loaded { buf.ReleaseMulti(buffers) return E.New("outbound not found: ", action.Outbound) @@ -118,7 +117,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad } } if selectedRule == nil { - defaultOutbound := r.outboundManager.Default() + defaultOutbound := r.outbound.Default() if !common.Contains(defaultOutbound.Network(), N.NetworkTCP) { buf.ReleaseMulti(buffers) return E.New("TCP is not supported by default outbound: ", defaultOutbound.Tag()) @@ -148,19 +147,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad } return nil } - // TODO - err = outbound.NewConnection(ctx, selectedOutbound, conn, metadata) - if err != nil { - conn.Close() - if onClose != nil { - onClose(err) - } - return E.Cause(err, F.ToString("outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]")) - } else { - if onClose != nil { - onClose(nil) - } - } + r.connection.NewConnection(ctx, selectedOutbound, conn, metadata, onClose) return nil } @@ -199,7 +186,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m if metadata.LastInbound == metadata.InboundDetour { return E.New("routing loop on detour: ", metadata.InboundDetour) } - detour, loaded := r.inboundManager.Get(metadata.InboundDetour) + detour, loaded := r.inbound.Get(metadata.InboundDetour) if !loaded { return E.New("inbound detour not found: ", metadata.InboundDetour) } @@ -233,7 +220,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m switch action := selectedRule.Action().(type) { case *rule.RuleActionRoute: var loaded bool - selectedOutbound, loaded = r.outboundManager.Outbound(action.Outbound) + selectedOutbound, loaded = r.outbound.Outbound(action.Outbound) if !loaded { N.ReleaseMultiPacketBuffer(packetBuffers) return E.New("outbound not found: ", action.Outbound) @@ -252,7 +239,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m } } if selectedRule == nil || selectReturn { - defaultOutbound := r.outboundManager.Default() + defaultOutbound := r.outbound.Default() if !common.Contains(defaultOutbound.Network(), N.NetworkUDP) { N.ReleaseMultiPacketBuffer(packetBuffers) return E.New("UDP is not supported by outbound: ", defaultOutbound.Tag()) @@ -278,12 +265,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m } return nil } - // TODO - err = outbound.NewPacketConnection(ctx, selectedOutbound, conn, metadata) - N.CloseOnHandshakeFailure(conn, onClose, err) - if err != nil { - return E.Cause(err, F.ToString("outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]")) - } + r.connection.NewPacketConnection(ctx, selectedOutbound, conn, metadata, onClose) return nil } @@ -450,8 +432,12 @@ match: } metadata.NetworkStrategy = routeOptions.NetworkStrategy metadata.FallbackDelay = routeOptions.FallbackDelay - metadata.UDPDisableDomainUnmapping = routeOptions.UDPDisableDomainUnmapping - metadata.UDPConnect = routeOptions.UDPConnect + if routeOptions.UDPDisableDomainUnmapping { + metadata.UDPDisableDomainUnmapping = true + } + if routeOptions.UDPConnect { + metadata.UDPConnect = true + } } switch action := currentRule.Action().(type) { case *rule.RuleActionSniff: diff --git a/route/router.go b/route/router.go index f23604b306..792391e265 100644 --- a/route/router.go +++ b/route/router.go @@ -38,9 +38,10 @@ type Router struct { ctx context.Context logger log.ContextLogger dnsLogger log.ContextLogger - inboundManager adapter.InboundManager - outboundManager adapter.OutboundManager - networkManager adapter.NetworkManager + inbound adapter.InboundManager + outbound adapter.OutboundManager + connection adapter.ConnectionManager + network adapter.NetworkManager rules []adapter.Rule needGeoIPDatabase bool needGeositeDatabase bool @@ -74,9 +75,10 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route ctx: ctx, logger: logFactory.NewLogger("router"), dnsLogger: logFactory.NewLogger("dns"), - inboundManager: service.FromContext[adapter.InboundManager](ctx), - outboundManager: service.FromContext[adapter.OutboundManager](ctx), - networkManager: service.FromContext[adapter.NetworkManager](ctx), + inbound: service.FromContext[adapter.InboundManager](ctx), + outbound: service.FromContext[adapter.OutboundManager](ctx), + connection: service.FromContext[adapter.ConnectionManager](ctx), + network: service.FromContext[adapter.NetworkManager](ctx), rules: make([]adapter.Rule, 0, len(options.Rules)), dnsRules: make([]adapter.DNSRule, 0, len(dnsOptions.Rules)), ruleSetMap: make(map[string]adapter.RuleSet), @@ -260,7 +262,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route Context: ctx, Name: "local", Address: "local", - Dialer: common.Must1(dialer.NewDefault(router.networkManager, option.DialerOptions{})), + Dialer: common.Must1(dialer.NewDefault(router.network, option.DialerOptions{})), }))) } defaultTransport = transports[0] @@ -405,7 +407,7 @@ func (r *Router) Start(stage adapter.StartStage) error { monitor.Start("initialize process searcher") searcher, err := process.NewSearcher(process.Config{ Logger: r.logger, - PackageManager: r.networkManager.PackageManager(), + PackageManager: r.network.PackageManager(), }) monitor.Finish() if err != nil { @@ -507,7 +509,7 @@ func (r *Router) SetTracker(tracker adapter.ConnectionTracker) { } func (r *Router) ResetNetwork() { - r.networkManager.ResetNetwork() + r.network.ResetNetwork() for _, transport := range r.transports { transport.Reset() } From 8ce40f77b474ea8c9d4b8ea4960bd61b2de12d93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 21 Nov 2024 18:10:41 +0800 Subject: [PATCH 28/49] refactor: WireGuard endpoint --- adapter/endpoint.go | 28 ++ adapter/endpoint/adapter.go | 43 +++ adapter/endpoint/manager.go | 147 ++++++++++ adapter/endpoint/registry.go | 72 +++++ adapter/inbound.go | 2 +- adapter/inbound/manager.go | 11 +- adapter/lifecycle_legacy.go | 3 + adapter/outbound/adapter.go | 18 +- adapter/outbound/manager.go | 29 +- box.go | 51 +++- cmd/sing-box/cmd.go | 2 +- common/dialer/default.go | 2 +- docs/configuration/endpoint/index.md | 32 +++ docs/configuration/endpoint/index.zh.md | 32 +++ docs/configuration/endpoint/wireguard.md | 138 ++++++++++ docs/configuration/endpoint/wireguard.zh.md | 140 ++++++++++ docs/configuration/inbound/tun.md | 4 +- docs/configuration/inbound/tun.zh.md | 4 +- docs/configuration/index.md | 2 + docs/configuration/index.zh.md | 2 + docs/configuration/outbound/wireguard.md | 10 +- docs/configuration/outbound/wireguard.zh.md | 10 +- docs/configuration/shared/listen.md | 4 +- docs/configuration/shared/listen.zh.md | 2 +- docs/deprecated.md | 7 + docs/deprecated.zh.md | 7 + docs/migration.md | 72 +++++ docs/migration.zh.md | 73 ++++- experimental/deprecated/constants.go | 10 + experimental/libbox/config.go | 4 +- experimental/libbox/service.go | 2 +- include/registry.go | 9 + include/wireguard.go | 5 + include/wireguard_stub.go | 9 +- mkdocs.yml | 4 + option/endpoint.go | 47 ++++ option/inbound.go | 2 +- option/options.go | 1 + option/outbound.go | 2 +- option/wireguard.go | 38 ++- protocol/block/outbound.go | 2 +- protocol/direct/inbound.go | 5 +- protocol/direct/outbound.go | 2 +- protocol/dns/outbound.go | 2 +- protocol/group/selector.go | 2 +- protocol/group/urltest.go | 2 +- protocol/http/inbound.go | 5 +- protocol/http/outbound.go | 2 +- protocol/hysteria/inbound.go | 5 +- protocol/hysteria/outbound.go | 2 +- protocol/hysteria2/inbound.go | 5 +- protocol/hysteria2/outbound.go | 2 +- protocol/mixed/inbound.go | 5 +- protocol/naive/inbound.go | 5 +- protocol/redirect/redirect.go | 5 +- protocol/redirect/tproxy.go | 5 +- protocol/shadowsocks/inbound.go | 5 +- protocol/shadowsocks/inbound_multi.go | 5 +- protocol/shadowsocks/inbound_relay.go | 5 +- protocol/shadowsocks/outbound.go | 2 +- protocol/shadowtls/inbound.go | 5 +- protocol/shadowtls/outbound.go | 2 +- protocol/socks/inbound.go | 5 +- protocol/socks/outbound.go | 2 +- protocol/ssh/outbound.go | 2 +- protocol/tor/outbound.go | 2 +- protocol/trojan/inbound.go | 5 +- protocol/trojan/outbound.go | 2 +- protocol/tuic/inbound.go | 5 +- protocol/tuic/outbound.go | 2 +- protocol/tun/inbound.go | 182 ++++++------- protocol/vless/inbound.go | 5 +- protocol/vless/outbound.go | 2 +- protocol/vmess/inbound.go | 5 +- protocol/vmess/outbound.go | 2 +- protocol/wireguard/endpoint.go | 211 ++++++++++++++ protocol/wireguard/outbound.go | 257 +++++++----------- route/network.go | 37 ++- route/route_dns.go | 2 +- transport/wireguard/client_bind.go | 36 ++- transport/wireguard/device.go | 37 ++- transport/wireguard/device_stack.go | 123 ++++----- .../{gonet.go => device_stack_gonet.go} | 0 transport/wireguard/device_stack_stub.go | 10 +- transport/wireguard/device_system.go | 151 +++++----- transport/wireguard/device_system_stack.go | 182 +++++++++++++ transport/wireguard/endpoint.go | 253 ++++++++++++++++- transport/wireguard/endpoint_options.go | 40 +++ transport/wireguard/resolve.go | 148 ---------- 89 files changed, 2188 insertions(+), 680 deletions(-) create mode 100644 adapter/endpoint.go create mode 100644 adapter/endpoint/adapter.go create mode 100644 adapter/endpoint/manager.go create mode 100644 adapter/endpoint/registry.go create mode 100644 docs/configuration/endpoint/index.md create mode 100644 docs/configuration/endpoint/index.zh.md create mode 100644 docs/configuration/endpoint/wireguard.md create mode 100644 docs/configuration/endpoint/wireguard.zh.md create mode 100644 option/endpoint.go create mode 100644 protocol/wireguard/endpoint.go rename transport/wireguard/{gonet.go => device_stack_gonet.go} (100%) create mode 100644 transport/wireguard/device_system_stack.go create mode 100644 transport/wireguard/endpoint_options.go delete mode 100644 transport/wireguard/resolve.go diff --git a/adapter/endpoint.go b/adapter/endpoint.go new file mode 100644 index 0000000000..f09f08ce75 --- /dev/null +++ b/adapter/endpoint.go @@ -0,0 +1,28 @@ +package adapter + +import ( + "context" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" +) + +type Endpoint interface { + Lifecycle + Type() string + Tag() string + Outbound +} + +type EndpointRegistry interface { + option.EndpointOptionsRegistry + Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) (Endpoint, error) +} + +type EndpointManager interface { + Lifecycle + Endpoints() []Endpoint + Get(tag string) (Endpoint, bool) + Remove(tag string) error + Create(ctx context.Context, router Router, logger log.ContextLogger, tag string, endpointType string, options any) error +} diff --git a/adapter/endpoint/adapter.go b/adapter/endpoint/adapter.go new file mode 100644 index 0000000000..c75e4d839c --- /dev/null +++ b/adapter/endpoint/adapter.go @@ -0,0 +1,43 @@ +package endpoint + +import "github.com/sagernet/sing-box/option" + +type Adapter struct { + endpointType string + endpointTag string + network []string + dependencies []string +} + +func NewAdapter(endpointType string, endpointTag string, network []string, dependencies []string) Adapter { + return Adapter{ + endpointType: endpointType, + endpointTag: endpointTag, + network: network, + dependencies: dependencies, + } +} + +func NewAdapterWithDialerOptions(endpointType string, endpointTag string, network []string, dialOptions option.DialerOptions) Adapter { + var dependencies []string + if dialOptions.Detour != "" { + dependencies = []string{dialOptions.Detour} + } + return NewAdapter(endpointType, endpointTag, network, dependencies) +} + +func (a *Adapter) Type() string { + return a.endpointType +} + +func (a *Adapter) Tag() string { + return a.endpointTag +} + +func (a *Adapter) Network() []string { + return a.network +} + +func (a *Adapter) Dependencies() []string { + return a.dependencies +} diff --git a/adapter/endpoint/manager.go b/adapter/endpoint/manager.go new file mode 100644 index 0000000000..5a633beed3 --- /dev/null +++ b/adapter/endpoint/manager.go @@ -0,0 +1,147 @@ +package endpoint + +import ( + "context" + "os" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/common/taskmonitor" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" +) + +var _ adapter.EndpointManager = (*Manager)(nil) + +type Manager struct { + logger log.ContextLogger + registry adapter.EndpointRegistry + access sync.Mutex + started bool + stage adapter.StartStage + endpoints []adapter.Endpoint + endpointByTag map[string]adapter.Endpoint +} + +func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Manager { + return &Manager{ + logger: logger, + registry: registry, + endpointByTag: make(map[string]adapter.Endpoint), + } +} + +func (m *Manager) Start(stage adapter.StartStage) error { + m.access.Lock() + defer m.access.Unlock() + if m.started && m.stage >= stage { + panic("already started") + } + m.started = true + m.stage = stage + if stage == adapter.StartStateStart { + // started with outbound manager + return nil + } + for _, endpoint := range m.endpoints { + err := adapter.LegacyStart(endpoint, stage) + if err != nil { + return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]") + } + } + return nil +} + +func (m *Manager) Close() error { + m.access.Lock() + defer m.access.Unlock() + if !m.started { + return nil + } + m.started = false + endpoints := m.endpoints + m.endpoints = nil + monitor := taskmonitor.New(m.logger, C.StopTimeout) + var err error + for _, endpoint := range endpoints { + monitor.Start("close endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]") + err = E.Append(err, endpoint.Close(), func(err error) error { + return E.Cause(err, "close endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]") + }) + monitor.Finish() + } + return nil +} + +func (m *Manager) Endpoints() []adapter.Endpoint { + m.access.Lock() + defer m.access.Unlock() + return m.endpoints +} + +func (m *Manager) Get(tag string) (adapter.Endpoint, bool) { + m.access.Lock() + defer m.access.Unlock() + endpoint, found := m.endpointByTag[tag] + return endpoint, found +} + +func (m *Manager) Remove(tag string) error { + m.access.Lock() + endpoint, found := m.endpointByTag[tag] + if !found { + m.access.Unlock() + return os.ErrInvalid + } + delete(m.endpointByTag, tag) + index := common.Index(m.endpoints, func(it adapter.Endpoint) bool { + return it == endpoint + }) + if index == -1 { + panic("invalid endpoint index") + } + m.endpoints = append(m.endpoints[:index], m.endpoints[index+1:]...) + started := m.started + m.access.Unlock() + if started { + return endpoint.Close() + } + return nil +} + +func (m *Manager) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) error { + endpoint, err := m.registry.Create(ctx, router, logger, tag, outboundType, options) + if err != nil { + return err + } + m.access.Lock() + defer m.access.Unlock() + if m.started { + for _, stage := range adapter.ListStartStages { + err = adapter.LegacyStart(endpoint, stage) + if err != nil { + return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]") + } + } + } + if existsEndpoint, loaded := m.endpointByTag[tag]; loaded { + if m.started { + err = existsEndpoint.Close() + if err != nil { + return E.Cause(err, "close endpoint/", existsEndpoint.Type(), "[", existsEndpoint.Tag(), "]") + } + } + existsIndex := common.Index(m.endpoints, func(it adapter.Endpoint) bool { + return it == existsEndpoint + }) + if existsIndex == -1 { + panic("invalid endpoint index") + } + m.endpoints = append(m.endpoints[:existsIndex], m.endpoints[existsIndex+1:]...) + } + m.endpoints = append(m.endpoints, endpoint) + m.endpointByTag[tag] = endpoint + return nil +} diff --git a/adapter/endpoint/registry.go b/adapter/endpoint/registry.go new file mode 100644 index 0000000000..92cb9025de --- /dev/null +++ b/adapter/endpoint/registry.go @@ -0,0 +1,72 @@ +package endpoint + +import ( + "context" + "sync" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" +) + +type ConstructorFunc[T any] func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options T) (adapter.Endpoint, error) + +func Register[Options any](registry *Registry, outboundType string, constructor ConstructorFunc[Options]) { + registry.register(outboundType, func() any { + return new(Options) + }, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, rawOptions any) (adapter.Endpoint, error) { + var options *Options + if rawOptions != nil { + options = rawOptions.(*Options) + } + return constructor(ctx, router, logger, tag, common.PtrValueOrDefault(options)) + }) +} + +var _ adapter.EndpointRegistry = (*Registry)(nil) + +type ( + optionsConstructorFunc func() any + constructorFunc func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options any) (adapter.Endpoint, error) +) + +type Registry struct { + access sync.Mutex + optionsType map[string]optionsConstructorFunc + constructor map[string]constructorFunc +} + +func NewRegistry() *Registry { + return &Registry{ + optionsType: make(map[string]optionsConstructorFunc), + constructor: make(map[string]constructorFunc), + } +} + +func (m *Registry) CreateOptions(outboundType string) (any, bool) { + m.access.Lock() + defer m.access.Unlock() + optionsConstructor, loaded := m.optionsType[outboundType] + if !loaded { + return nil, false + } + return optionsConstructor(), true +} + +func (m *Registry) Create(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, outboundType string, options any) (adapter.Endpoint, error) { + m.access.Lock() + defer m.access.Unlock() + constructor, loaded := m.constructor[outboundType] + if !loaded { + return nil, E.New("outbound type not found: " + outboundType) + } + return constructor(ctx, router, logger, tag, options) +} + +func (m *Registry) register(outboundType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) { + m.access.Lock() + defer m.access.Unlock() + m.optionsType[outboundType] = optionsConstructor + m.constructor[outboundType] = constructor +} diff --git a/adapter/inbound.go b/adapter/inbound.go index d3cc95b7d9..b4eaca3d4d 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -13,7 +13,7 @@ import ( ) type Inbound interface { - Service + Lifecycle Type() string Tag() string } diff --git a/adapter/inbound/manager.go b/adapter/inbound/manager.go index d2be0f365e..c690b2c913 100644 --- a/adapter/inbound/manager.go +++ b/adapter/inbound/manager.go @@ -18,6 +18,7 @@ var _ adapter.InboundManager = (*Manager)(nil) type Manager struct { logger log.ContextLogger registry adapter.InboundRegistry + endpoint adapter.EndpointManager access sync.Mutex started bool stage adapter.StartStage @@ -25,10 +26,11 @@ type Manager struct { inboundByTag map[string]adapter.Inbound } -func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry) *Manager { +func NewManager(logger log.ContextLogger, registry adapter.InboundRegistry, endpoint adapter.EndpointManager) *Manager { return &Manager{ logger: logger, registry: registry, + endpoint: endpoint, inboundByTag: make(map[string]adapter.Inbound), } } @@ -79,9 +81,12 @@ func (m *Manager) Inbounds() []adapter.Inbound { func (m *Manager) Get(tag string) (adapter.Inbound, bool) { m.access.Lock() - defer m.access.Unlock() inbound, found := m.inboundByTag[tag] - return inbound, found + m.access.Unlock() + if found { + return inbound, true + } + return m.endpoint.Get(tag) } func (m *Manager) Remove(tag string) error { diff --git a/adapter/lifecycle_legacy.go b/adapter/lifecycle_legacy.go index 0c8c75daed..94a5cf8c8d 100644 --- a/adapter/lifecycle_legacy.go +++ b/adapter/lifecycle_legacy.go @@ -1,6 +1,9 @@ package adapter func LegacyStart(starter any, stage StartStage) error { + if lifecycle, isLifecycle := starter.(Lifecycle); isLifecycle { + return lifecycle.Start(stage) + } switch stage { case StartStateInitialize: if preStarter, isPreStarter := starter.(interface { diff --git a/adapter/outbound/adapter.go b/adapter/outbound/adapter.go index 481bb6197d..cd71527af5 100644 --- a/adapter/outbound/adapter.go +++ b/adapter/outbound/adapter.go @@ -5,35 +5,35 @@ import ( ) type Adapter struct { - protocol string + outboundType string + outboundTag string network []string - tag string dependencies []string } -func NewAdapter(protocol string, network []string, tag string, dependencies []string) Adapter { +func NewAdapter(outboundType string, outboundTag string, network []string, dependencies []string) Adapter { return Adapter{ - protocol: protocol, + outboundType: outboundType, + outboundTag: outboundTag, network: network, - tag: tag, dependencies: dependencies, } } -func NewAdapterWithDialerOptions(protocol string, network []string, tag string, dialOptions option.DialerOptions) Adapter { +func NewAdapterWithDialerOptions(outboundType string, outboundTag string, network []string, dialOptions option.DialerOptions) Adapter { var dependencies []string if dialOptions.Detour != "" { dependencies = []string{dialOptions.Detour} } - return NewAdapter(protocol, network, tag, dependencies) + return NewAdapter(outboundType, outboundTag, network, dependencies) } func (a *Adapter) Type() string { - return a.protocol + return a.outboundType } func (a *Adapter) Tag() string { - return a.tag + return a.outboundTag } func (a *Adapter) Network() []string { diff --git a/adapter/outbound/manager.go b/adapter/outbound/manager.go index 84a105c59e..c3941d02b5 100644 --- a/adapter/outbound/manager.go +++ b/adapter/outbound/manager.go @@ -21,6 +21,7 @@ var _ adapter.OutboundManager = (*Manager)(nil) type Manager struct { logger log.ContextLogger registry adapter.OutboundRegistry + endpoint adapter.EndpointManager defaultTag string access sync.Mutex started bool @@ -32,10 +33,11 @@ type Manager struct { defaultOutboundFallback adapter.Outbound } -func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, defaultTag string) *Manager { +func NewManager(logger logger.ContextLogger, registry adapter.OutboundRegistry, endpoint adapter.EndpointManager, defaultTag string) *Manager { return &Manager{ logger: logger, registry: registry, + endpoint: endpoint, defaultTag: defaultTag, outboundByTag: make(map[string]adapter.Outbound), dependByTag: make(map[string][]string), @@ -56,7 +58,14 @@ func (m *Manager) Start(stage adapter.StartStage) error { outbounds := m.outbounds m.access.Unlock() if stage == adapter.StartStateStart { - return m.startOutbounds(outbounds) + if m.defaultTag != "" && m.defaultOutbound == nil { + defaultEndpoint, loaded := m.endpoint.Get(m.defaultTag) + if !loaded { + return E.New("default outbound not found: ", m.defaultTag) + } + m.defaultOutbound = defaultEndpoint + } + return m.startOutbounds(append(outbounds, common.Map(m.endpoint.Endpoints(), func(it adapter.Endpoint) adapter.Outbound { return it })...)) } else { for _, outbound := range outbounds { err := adapter.LegacyStart(outbound, stage) @@ -87,7 +96,14 @@ func (m *Manager) startOutbounds(outbounds []adapter.Outbound) error { } started[outboundTag] = true canContinue = true - if starter, isStarter := outboundToStart.(interface { + if starter, isStarter := outboundToStart.(adapter.Lifecycle); isStarter { + monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]") + err := starter.Start(adapter.StartStateStart) + monitor.Finish() + if err != nil { + return E.Cause(err, "start outbound/", outboundToStart.Type(), "[", outboundTag, "]") + } + } else if starter, isStarter := outboundToStart.(interface { Start() error }); isStarter { monitor.Start("start outbound/", outboundToStart.Type(), "[", outboundTag, "]") @@ -160,9 +176,12 @@ func (m *Manager) Outbounds() []adapter.Outbound { func (m *Manager) Outbound(tag string) (adapter.Outbound, bool) { m.access.Lock() - defer m.access.Unlock() outbound, found := m.outboundByTag[tag] - return outbound, found + m.access.Unlock() + if found { + return outbound, true + } + return m.endpoint.Get(tag) } func (m *Manager) Default() adapter.Outbound { diff --git a/box.go b/box.go index 44a29992e4..5dc76ebc7b 100644 --- a/box.go +++ b/box.go @@ -9,6 +9,7 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" @@ -36,6 +37,7 @@ type Box struct { logFactory log.Factory logger log.ContextLogger network *route.NetworkManager + endpoint *endpoint.Manager inbound *inbound.Manager outbound *outbound.Manager connection *route.ConnectionManager @@ -54,6 +56,7 @@ func Context( ctx context.Context, inboundRegistry adapter.InboundRegistry, outboundRegistry adapter.OutboundRegistry, + endpointRegistry adapter.EndpointRegistry, ) context.Context { if service.FromContext[option.InboundOptionsRegistry](ctx) == nil || service.FromContext[adapter.InboundRegistry](ctx) == nil { @@ -65,6 +68,11 @@ func Context( ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry) ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry) } + if service.FromContext[option.EndpointOptionsRegistry](ctx) == nil || + service.FromContext[adapter.EndpointRegistry](ctx) == nil { + ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry) + ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry) + } return ctx } @@ -76,12 +84,16 @@ func New(options Options) (*Box, error) { } ctx = service.ContextWithDefaultRegistry(ctx) + endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx) inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx) + outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx) + + if endpointRegistry == nil { + return nil, E.New("missing endpoint registry in context") + } if inboundRegistry == nil { return nil, E.New("missing inbound registry in context") } - - outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx) if outboundRegistry == nil { return nil, E.New("missing outbound registry in context") } @@ -119,8 +131,10 @@ func New(options Options) (*Box, error) { } routeOptions := common.PtrValueOrDefault(options.Route) - inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry) - outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, routeOptions.Final) + endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry) + inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager) + outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final) + service.MustRegister[adapter.EndpointManager](ctx, endpointManager) service.MustRegister[adapter.InboundManager](ctx, inboundManager) service.MustRegister[adapter.OutboundManager](ctx, outboundManager) @@ -135,6 +149,24 @@ func New(options Options) (*Box, error) { if err != nil { return nil, E.Cause(err, "initialize router") } + for i, endpointOptions := range options.Endpoints { + var tag string + if endpointOptions.Tag != "" { + tag = endpointOptions.Tag + } else { + tag = F.ToString(i) + } + err = endpointManager.Create(ctx, + router, + logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")), + tag, + endpointOptions.Type, + endpointOptions.Options, + ) + if err != nil { + return nil, E.Cause(err, "initialize inbound[", i, "]") + } + } for i, inboundOptions := range options.Inbounds { var tag string if inboundOptions.Tag != "" { @@ -241,6 +273,7 @@ func New(options Options) (*Box, error) { } return &Box{ network: networkManager, + endpoint: endpointManager, inbound: inboundManager, outbound: outboundManager, connection: connectionManager, @@ -303,7 +336,7 @@ func (s *Box) preStart() error { if err != nil { return err } - err = adapter.Start(adapter.StartStateInitialize, s.network, s.connection, s.router, s.outbound, s.inbound) + err = adapter.Start(adapter.StartStateInitialize, s.network, s.connection, s.router, s.outbound, s.inbound, s.endpoint) if err != nil { return err } @@ -327,7 +360,11 @@ func (s *Box) start() error { if err != nil { return err } - err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.connection, s.router, s.inbound) + err = adapter.Start(adapter.StartStateStart, s.endpoint) + if err != nil { + return err + } + err = adapter.Start(adapter.StartStatePostStart, s.outbound, s.network, s.connection, s.router, s.inbound, s.endpoint) if err != nil { return err } @@ -335,7 +372,7 @@ func (s *Box) start() error { if err != nil { return err } - err = adapter.Start(adapter.StartStateStarted, s.network, s.connection, s.router, s.outbound, s.inbound) + err = adapter.Start(adapter.StartStateStarted, s.network, s.connection, s.router, s.outbound, s.inbound, s.endpoint) if err != nil { return err } diff --git a/cmd/sing-box/cmd.go b/cmd/sing-box/cmd.go index dc7a830965..d55235b855 100644 --- a/cmd/sing-box/cmd.go +++ b/cmd/sing-box/cmd.go @@ -69,5 +69,5 @@ func preRun(cmd *cobra.Command, args []string) { configPaths = append(configPaths, "config.json") } globalCtx = service.ContextWith(globalCtx, deprecated.NewStderrManager(log.StdLogger())) - globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry()) + globalCtx = box.Context(globalCtx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry()) } diff --git a/common/dialer/default.go b/common/dialer/default.go index 5871e64007..bf553618f7 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -285,7 +285,7 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina } func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) { - return d.listenSerialInterfacePacket(context.Background(), d.udpListener, network, address, d.networkStrategy, d.networkType, d.fallbackNetworkType, d.networkFallbackDelay) + return d.udpListener.ListenPacket(context.Background(), network, address) } func trackConn(conn net.Conn, err error) (net.Conn, error) { diff --git a/docs/configuration/endpoint/index.md b/docs/configuration/endpoint/index.md new file mode 100644 index 0000000000..e40333db35 --- /dev/null +++ b/docs/configuration/endpoint/index.md @@ -0,0 +1,32 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.11.0" + +# Endpoint + +Endpoint is protocols that has both inbound and outbound behavior. + +### Structure + +```json +{ + "endpoints": [ + { + "type": "", + "tag": "" + } + ] +} +``` + +### Fields + +| Type | Format | +|-------------|---------------------------| +| `wireguard` | [WireGuard](./wireguard/) | + +#### tag + +The tag of the endpoint. diff --git a/docs/configuration/endpoint/index.zh.md b/docs/configuration/endpoint/index.zh.md new file mode 100644 index 0000000000..69ba2d09ce --- /dev/null +++ b/docs/configuration/endpoint/index.zh.md @@ -0,0 +1,32 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.11.0 起" + +# 端点 + +端点是具有入站和出站行为的协议。 + +### 结构 + +```json +{ + "endpoints": [ + { + "type": "", + "tag": "" + } + ] +} +``` + +### 字段 + +| 类型 | 格式 | +|-------------|---------------------------| +| `wireguard` | [WireGuard](./wiregaurd/) | + +#### tag + +端点的标签。 diff --git a/docs/configuration/endpoint/wireguard.md b/docs/configuration/endpoint/wireguard.md new file mode 100644 index 0000000000..2d792e0279 --- /dev/null +++ b/docs/configuration/endpoint/wireguard.md @@ -0,0 +1,138 @@ +--- +icon: material/new-box +--- + +!!! question "Since sing-box 1.11.0" + +### Structure + +```json +{ + "type": "wireguard", + "tag": "wg-ep", + + "system": false, + "name": "", + "mtu": 1408, + "gso": false, + "address": [], + "private_key": "", + "listen_port": 10000, + "peers": [ + { + "address": "127.0.0.1", + "port": 10001, + "public_key": "", + "pre_shared_key": "", + "allowed_ips": [], + "persistent_keepalive_interval": 0, + "reserved": [0, 0, 0] + } + ], + "udp_timeout": "", + "workers": 0, + + ... // Dial Fields +} +``` + +### Fields + +#### system + +Use system interface. + +Requires privilege and cannot conflict with exists system interfaces. + +#### name + +Custom interface name for system interface. + +#### mtu + +WireGuard MTU. + +`1408` will be used by default. + +#### gso + +!!! quote "" + + Only supported on Linux. + +Try to enable generic segmentation offload. + +#### address + +==Required== + +List of IP (v4 or v6) address prefixes to be assigned to the interface. + +#### private_key + +==Required== + +WireGuard requires base64-encoded public and private keys. These can be generated using the wg(8) utility: + +```shell +wg genkey +echo "private key" || wg pubkey +``` + +or `sing-box generate wg-keypair`. + +#### peers + +==Required== + +List of WireGuard peers. + +#### peers.address + +WireGuard peer address. + +#### peers.port + +WireGuard peer port. + +#### peers.public_key + +==Required== + +WireGuard peer public key. + +#### peers.pre_shared_key + +WireGuard peer pre-shared key. + +#### peers.allowed_ips + +==Required== + +WireGuard allowed IPs. + +#### peers.persistent_keepalive_interval + +WireGuard persistent keepalive interval, in seconds. + +Disabled by default. + +#### peers.reserved + +WireGuard reserved field bytes. + +#### udp_timeout + +UDP NAT expiration time. + +`5m` will be used by default. + +#### workers + +WireGuard worker count. + +CPU count is used by default. + +### Dial Fields + +See [Dial Fields](/configuration/shared/dial/) for details. diff --git a/docs/configuration/endpoint/wireguard.zh.md b/docs/configuration/endpoint/wireguard.zh.md new file mode 100644 index 0000000000..8941b63095 --- /dev/null +++ b/docs/configuration/endpoint/wireguard.zh.md @@ -0,0 +1,140 @@ +--- +icon: material/new-box +--- + +!!! question "自 sing-box 1.11.0 起" + +### 结构 + +```json +{ + "type": "wireguard", + "tag": "wg-ep", + + "system": false, + "name": "", + "mtu": 1408, + "gso": false, + "address": [], + "private_key": "", + "listen_port": 10000, + "peers": [ + { + "address": "127.0.0.1", + "port": 10001, + "public_key": "", + "pre_shared_key": "", + "allowed_ips": [], + "persistent_keepalive_interval": 0, + "reserved": [0, 0, 0] + } + ], + "udp_timeout": "", + "workers": 0, + + ... // 拨号字段 +} +``` + +### 字段 + +#### system_interface + +使用系统设备。 + +需要特权且不能与已有系统接口冲突。 + +#### name + +为系统接口自定义设备名称。 + +#### mtu + +WireGuard MTU。 + +默认使用 1408。 + +#### gso + +!!! quote "" + + 仅支持 Linux。 + +尝试启用通用分段卸载。 + +#### address + +==必填== + +接口的 IPv4/IPv6 地址或地址段的列表您。 + +要分配给接口的 IP(v4 或 v6)地址段列表。 + +#### private_key + +==必填== + +WireGuard 需要 base64 编码的公钥和私钥。 这些可以使用 wg(8) 实用程序生成: + +```shell +wg genkey +echo "private key" || wg pubkey +``` + +或 `sing-box generate wg-keypair`. + +#### peers + +==必填== + +WireGuard 对等方的列表。 + +#### peers.address + +对等方的 IP 地址。 + +#### peers.port + +对等方的 WireGuard 端口。 + +#### peers.public_key + +==必填== + +对等方的 WireGuard 公钥。 + +#### peers.pre_shared_key + +对等方的预共享密钥。 + +#### peers.allowed_ips + +==必填== + +对等方的允许 IP 地址。 + +#### peers.persistent_keepalive_interval + +对等方的持久性保持活动间隔,以秒为单位。 + +默认禁用。 + +#### peers.reserved + +对等方的保留字段字节。 + +#### udp_timeout + +UDP NAT 过期时间。 + +默认使用 `5m`。 + +#### workers + +WireGuard worker 数量。 + +默认使用 CPU 数量。 + +### 拨号字段 + +参阅 [拨号字段](/zh/configuration/shared/dial/)。 diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index cc2f5603c8..51b8a10a3c 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -360,7 +360,9 @@ Performance may degrade slightly, so it is not recommended to enable on when it #### udp_timeout -UDP NAT expiration time in seconds, default is 300 (5 minutes). +UDP NAT expiration time. + +`5m` will be used by default. #### stack diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index 6bc1eb8793..0b8a6aa127 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -356,7 +356,9 @@ tun 接口的 IPv6 前缀。 #### udp_timeout -UDP NAT 过期时间,以秒为单位,默认为 300(5 分钟)。 +UDP NAT 过期时间。 + +默认使用 `5m`。 #### stack diff --git a/docs/configuration/index.md b/docs/configuration/index.md index 0c22fc25ab..05e6a87da8 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -9,6 +9,7 @@ sing-box uses JSON for configuration files. "log": {}, "dns": {}, "ntp": {}, + "endpoints": [], "inbounds": [], "outbounds": [], "route": {}, @@ -23,6 +24,7 @@ sing-box uses JSON for configuration files. | `log` | [Log](./log/) | | `dns` | [DNS](./dns/) | | `ntp` | [NTP](./ntp/) | +| `endpoints` | [Endpoint](./endpoint/) | | `inbounds` | [Inbound](./inbound/) | | `outbounds` | [Outbound](./outbound/) | | `route` | [Route](./route/) | diff --git a/docs/configuration/index.zh.md b/docs/configuration/index.zh.md index 0d24a7ca7a..6aeb4857f4 100644 --- a/docs/configuration/index.zh.md +++ b/docs/configuration/index.zh.md @@ -8,6 +8,7 @@ sing-box 使用 JSON 作为配置文件格式。 { "log": {}, "dns": {}, + "endpoints": [], "inbounds": [], "outbounds": [], "route": {}, @@ -21,6 +22,7 @@ sing-box 使用 JSON 作为配置文件格式。 |----------------|------------------------| | `log` | [日志](./log/) | | `dns` | [DNS](./dns/) | +| `endpoints` | [端点](./endpoint/) | | `inbounds` | [入站](./inbound/) | | `outbounds` | [出站](./outbound/) | | `route` | [路由](./route/) | diff --git a/docs/configuration/outbound/wireguard.md b/docs/configuration/outbound/wireguard.md index c3f51f1fb6..e3d2671a43 100644 --- a/docs/configuration/outbound/wireguard.md +++ b/docs/configuration/outbound/wireguard.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "Deprecated in sing-box 1.11.0" + + WireGuard outbound is deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-wireguard-outbound-to-endpoint). + !!! quote "Changes in sing-box 1.8.0" :material-plus: [gso](#gso) @@ -15,7 +23,7 @@ "gso": false, "interface_name": "wg0", "local_address": [ - "10.0.0.2/32" + "10.0.0.1/32" ], "private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=", "peers": [ diff --git a/docs/configuration/outbound/wireguard.zh.md b/docs/configuration/outbound/wireguard.zh.md index 5de2813225..63f2ddfd7d 100644 --- a/docs/configuration/outbound/wireguard.zh.md +++ b/docs/configuration/outbound/wireguard.zh.md @@ -1,3 +1,11 @@ +--- +icon: material/delete-clock +--- + +!!! failure "已在 sing-box 1.11.0 废弃" + + WireGuard 出站已被启用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-wireguard-outbound-to-endpoint)。 + !!! quote "sing-box 1.8.0 中的更改" :material-plus: [gso](#gso) @@ -15,7 +23,7 @@ "gso": false, "interface_name": "wg0", "local_address": [ - "10.0.0.2/32" + "10.0.0.1/32" ], "private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=", "peer_public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=", diff --git a/docs/configuration/shared/listen.md b/docs/configuration/shared/listen.md index fa6a05b97c..3e1b000f90 100644 --- a/docs/configuration/shared/listen.md +++ b/docs/configuration/shared/listen.md @@ -68,9 +68,9 @@ Enable UDP fragmentation. #### udp_timeout -UDP NAT expiration time in seconds. +UDP NAT expiration time. -`5m` is used by default. +`5m` will be used by default. #### detour diff --git a/docs/configuration/shared/listen.zh.md b/docs/configuration/shared/listen.zh.md index 3b472c4dff..4f8ca9d6a2 100644 --- a/docs/configuration/shared/listen.zh.md +++ b/docs/configuration/shared/listen.zh.md @@ -69,7 +69,7 @@ icon: material/delete-clock #### udp_timeout -UDP NAT 过期时间,以秒为单位。 +UDP NAT 过期时间。 默认使用 `5m`。 diff --git a/docs/deprecated.md b/docs/deprecated.md index 5dcec56241..b72ee11e7d 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -28,6 +28,13 @@ Destination override fields (`override_address` / `override_port`) in direct out and can be replaced by rule actions, check [Migration](../migration/#migrate-destination-override-fields-to-route-options). +#### WireGuard outbound + +WireGuard outbound is deprecated and can be replaced by endpoint, +check [Migration](../migration/#migrate-wireguard-outbound-to-endpoint). + +Old outbound will be removed in sing-box 1.13.0. + ## 1.10.0 #### TUN address fields are merged diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index 6f6c839fce..220725a9af 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -27,6 +27,13 @@ direct 出站中的目标地址覆盖字段(`override_address` / `override_por 旧字段将在 sing-box 1.13.0 中被移除。 +#### WireGuard 出站 + +WireGuard 出站已废弃且可以通过端点替代, +参阅 [迁移指南](/migration/#migrate-wireguard-outbound-to-endpoint)。 + +旧出站将在 sing-box 1.13.0 中被移除。 + ## 1.10.0 #### Match source 规则项已重命名 diff --git a/docs/migration.md b/docs/migration.md index ea1afa2dd2..480c0d50ce 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -194,6 +194,78 @@ Destination override fields in direct outbound are deprecated and can be replace } ``` +### Migrate WireGuard outbound to endpoint + +WireGuard outbound is deprecated and can be replaced by endpoint. + +!!! info "References" + + [Endpoint](/configuration/endpoint/) / + [WireGuard Endpoint](/configuration/endpoint/wireguard/) / + [WireGuard Outbound](/configuration/outbound/wireguard/) + +=== ":material-card-remove: Deprecated" + + ```json + { + "outbounds": [ + { + "type": "wireguard", + "tag": "wg-out", + + "server": "127.0.0.1", + "server_port": 10001, + "system_interface": true, + "gso": true, + "interface_name": "wg0", + "local_address": [ + "10.0.0.1/32" + ], + "private_key": "", + "peer_public_key": "", + "pre_shared_key": "", + "reserved": [0, 0, 0], + "mtu": 1408 + } + ] + } + ``` + +=== ":material-card-multiple: New" + + ```json + { + "endpoints": [ + { + "type": "wireguard", + "tag": "wg-ep", + "system": true, + "name": "wg0", + "mtu": 1408, + "gso": true, + "address": [ + "10.0.0.2/32" + ], + "private_key": "", + "listen_port": 10000, + "peers": [ + { + "address": "127.0.0.1", + "port": 10001, + "public_key": "", + "pre_shared_key": "", + "allowed_ips": [ + "0.0.0.0/0" + ], + "persistent_keepalive_interval": 30, + "reserved": [0, 0, 0] + } + ] + } + ] + } + ``` + ## 1.10.0 ### TUN address fields are merged diff --git a/docs/migration.zh.md b/docs/migration.zh.md index 73afbb0519..f03f63b095 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -104,7 +104,6 @@ icon: material/arrange-bring-forward ### 迁移旧的入站字段到规则动作 - 入站选项已被弃用,且可以被规则动作替代。 !!! info "参考" @@ -196,6 +195,78 @@ direct 出站中的目标地址覆盖字段已废弃,且可以被路由字段 } ``` +### 迁移 WireGuard 出站到端点 + +WireGuard 出站已被弃用,且可以被端点替代。 + +!!! info "参考" + + [端点](/zh/configuration/endpoint/) / + [WireGuard 端点](/zh/configuration/endpoint/wireguard/) / + [WireGuard 出站](/zh/configuration/outbound/wireguard/) + +=== ":material-card-remove: 弃用的" + + ```json + { + "outbounds": [ + { + "type": "wireguard", + "tag": "wg-out", + + "server": "127.0.0.1", + "server_port": 10001, + "system_interface": true, + "gso": true, + "interface_name": "wg0", + "local_address": [ + "10.0.0.1/32" + ], + "private_key": "", + "peer_public_key": "", + "pre_shared_key": "", + "reserved": [0, 0, 0], + "mtu": 1408 + } + ] + } + ``` + +=== ":material-card-multiple: 新的" + + ```json + { + "endpoints": [ + { + "type": "wireguard", + "tag": "wg-ep", + "system": true, + "name": "wg0", + "mtu": 1408, + "gso": true, + "address": [ + "10.0.0.2/32" + ], + "private_key": "", + "listen_port": 10000, + "peers": [ + { + "address": "127.0.0.1", + "port": 10001, + "public_key": "", + "pre_shared_key": "", + "allowed_ips": [ + "0.0.0.0/0" + ], + "persistent_keepalive_interval": 30, + "reserved": [0, 0, 0] + } + ] + } + ] + } + ``` + ## 1.10.0 ### TUN 地址字段已合并 diff --git a/experimental/deprecated/constants.go b/experimental/deprecated/constants.go index bf278f844e..6eadea6a44 100644 --- a/experimental/deprecated/constants.go +++ b/experimental/deprecated/constants.go @@ -113,6 +113,15 @@ var OptionDestinationOverrideFields = Note{ MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-destination-override-fields-to-route-options", } +var OptionWireGuardOutbound = Note{ + Name: "wireguard-outbound", + Description: "legacy wireguard outbound", + DeprecatedVersion: "1.11.0", + ScheduledVersion: "1.13.0", + EnvName: "WIREGUARD_OUTBOUND", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-wireguard-outbound-to-endpoint", +} + var Options = []Note{ OptionBadMatchSource, OptionGEOIP, @@ -121,4 +130,5 @@ var Options = []Note{ OptionSpecialOutbounds, OptionInboundOptions, OptionDestinationOverrideFields, + OptionWireGuardOutbound, } diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index 02d5b884b5..159fd8f6a0 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -30,7 +30,7 @@ func parseConfig(ctx context.Context, configContent string) (option.Options, err } func CheckConfig(configContent string) error { - ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry()) + ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry()) options, err := parseConfig(ctx, configContent) if err != nil { return err @@ -131,7 +131,7 @@ func (s *platformInterfaceStub) SendNotification(notification *platform.Notifica } func FormatConfig(configContent string) (*StringBox, error) { - options, err := parseConfig(box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry()), configContent) + options, err := parseConfig(box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry()), configContent) if err != nil { return nil, err } diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index e40cbe0367..8dd4cb22fc 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -44,7 +44,7 @@ type BoxService struct { } func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) { - ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry()) + ctx := box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry()) ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID) service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager)) options, err := parseConfig(ctx, configContent) diff --git a/include/registry.go b/include/registry.go index 03fb33f245..e71ffb0c82 100644 --- a/include/registry.go +++ b/include/registry.go @@ -4,6 +4,7 @@ import ( "context" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/inbound" "github.com/sagernet/sing-box/adapter/outbound" C "github.com/sagernet/sing-box/constant" @@ -82,6 +83,14 @@ func OutboundRegistry() *outbound.Registry { return registry } +func EndpointRegistry() *endpoint.Registry { + registry := endpoint.NewRegistry() + + registerWireGuardEndpoint(registry) + + return registry +} + func registerStubForRemovedInbounds(registry *inbound.Registry) { inbound.Register[option.ShadowsocksInboundOptions](registry, C.TypeShadowsocksR, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowsocksInboundOptions) (adapter.Inbound, error) { return nil, E.New("ShadowsocksR is deprecated and removed in sing-box 1.6.0") diff --git a/include/wireguard.go b/include/wireguard.go index dfc3a242a5..f2ce9e2341 100644 --- a/include/wireguard.go +++ b/include/wireguard.go @@ -3,6 +3,7 @@ package include import ( + "github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/protocol/wireguard" ) @@ -10,3 +11,7 @@ import ( func registerWireGuardOutbound(registry *outbound.Registry) { wireguard.RegisterOutbound(registry) } + +func registerWireGuardEndpoint(registry *endpoint.Registry) { + wireguard.RegisterEndpoint(registry) +} diff --git a/include/wireguard_stub.go b/include/wireguard_stub.go index a9e84522bb..247546e26e 100644 --- a/include/wireguard_stub.go +++ b/include/wireguard_stub.go @@ -6,6 +6,7 @@ import ( "context" "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/endpoint" "github.com/sagernet/sing-box/adapter/outbound" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" @@ -14,7 +15,13 @@ import ( ) func registerWireGuardOutbound(registry *outbound.Registry) { - outbound.Register[option.WireGuardOutboundOptions](registry, C.TypeWireGuard, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardOutboundOptions) (adapter.Outbound, error) { + outbound.Register[option.LegacyWireGuardOutboundOptions](registry, C.TypeWireGuard, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.LegacyWireGuardOutboundOptions) (adapter.Outbound, error) { + return nil, E.New(`WireGuard is not included in this build, rebuild with -tags with_wireguard`) + }) +} + +func registerWireGuardEndpoint(registry *endpoint.Registry) { + endpoint.Register[option.WireGuardEndpointOptions](registry, C.TypeWireGuard, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardEndpointOptions) (adapter.Endpoint, error) { return nil, E.New(`WireGuard is not included in this build, rebuild with -tags with_wireguard`) }) } diff --git a/mkdocs.yml b/mkdocs.yml index 66e8a2e99b..4854fa4a60 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -112,6 +112,9 @@ nav: - V2Ray Transport: configuration/shared/v2ray-transport.md - UDP over TCP: configuration/shared/udp-over-tcp.md - TCP Brutal: configuration/shared/tcp-brutal.md + - Endpoint: + - configuration/endpoint/index.md + - WireGuard: configuration/endpoint/wireguard.md - Inbound: - configuration/inbound/index.md - Direct: configuration/inbound/direct.md @@ -241,6 +244,7 @@ plugins: Multiplex: 多路复用 V2Ray Transport: V2Ray 传输层 + Endpoint: 端点 Inbound: 入站 Outbound: 出站 diff --git a/option/endpoint.go b/option/endpoint.go new file mode 100644 index 0000000000..909fb89618 --- /dev/null +++ b/option/endpoint.go @@ -0,0 +1,47 @@ +package option + +import ( + "context" + + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/service" +) + +type EndpointOptionsRegistry interface { + CreateOptions(endpointType string) (any, bool) +} + +type _Endpoint struct { + Type string `json:"type"` + Tag string `json:"tag,omitempty"` + Options any `json:"-"` +} + +type Endpoint _Endpoint + +func (h *Endpoint) MarshalJSONContext(ctx context.Context) ([]byte, error) { + return badjson.MarshallObjectsContext(ctx, (*_Endpoint)(h), h.Options) +} + +func (h *Endpoint) UnmarshalJSONContext(ctx context.Context, content []byte) error { + err := json.UnmarshalContext(ctx, content, (*_Endpoint)(h)) + if err != nil { + return err + } + registry := service.FromContext[EndpointOptionsRegistry](ctx) + if registry == nil { + return E.New("missing Endpoint fields registry in context") + } + options, loaded := registry.CreateOptions(h.Type) + if !loaded { + return E.New("unknown inbound type: ", h.Type) + } + err = badjson.UnmarshallExcludedContext(ctx, content, (*_Endpoint)(h), options) + if err != nil { + return err + } + h.Options = options + return nil +} diff --git a/option/inbound.go b/option/inbound.go index 2cc1598946..1cf16ff6ec 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -28,7 +28,7 @@ func (h *Inbound) MarshalJSONContext(ctx context.Context) ([]byte, error) { } func (h *Inbound) UnmarshalJSONContext(ctx context.Context, content []byte) error { - err := json.Unmarshal(content, (*_Inbound)(h)) + err := json.UnmarshalContext(ctx, content, (*_Inbound)(h)) if err != nil { return err } diff --git a/option/options.go b/option/options.go index 13a16c08b5..94c9771928 100644 --- a/option/options.go +++ b/option/options.go @@ -13,6 +13,7 @@ type _Options struct { Log *LogOptions `json:"log,omitempty"` DNS *DNSOptions `json:"dns,omitempty"` NTP *NTPOptions `json:"ntp,omitempty"` + Endpoints []Endpoint `json:"endpoints,omitempty"` Inbounds []Inbound `json:"inbounds,omitempty"` Outbounds []Outbound `json:"outbounds,omitempty"` Route *RouteOptions `json:"route,omitempty"` diff --git a/option/outbound.go b/option/outbound.go index 34ef904a80..833a2d2030 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -30,7 +30,7 @@ func (h *Outbound) MarshalJSONContext(ctx context.Context) ([]byte, error) { } func (h *Outbound) UnmarshalJSONContext(ctx context.Context, content []byte) error { - err := json.Unmarshal(content, (*_Outbound)(h)) + err := json.UnmarshalContext(ctx, content, (*_Outbound)(h)) if err != nil { return err } diff --git a/option/wireguard.go b/option/wireguard.go index ebdf159fde..62ef332ae7 100644 --- a/option/wireguard.go +++ b/option/wireguard.go @@ -6,14 +6,38 @@ import ( "github.com/sagernet/sing/common/json/badoption" ) -type WireGuardOutboundOptions struct { +type WireGuardEndpointOptions struct { + System bool `json:"system,omitempty"` + Name string `json:"name,omitempty"` + MTU uint32 `json:"mtu,omitempty"` + GSO bool `json:"gso,omitempty"` + Address badoption.Listable[netip.Prefix] `json:"address"` + PrivateKey string `json:"private_key"` + ListenPort uint16 `json:"listen_port,omitempty"` + Peers []WireGuardPeer `json:"peers,omitempty"` + UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` + Workers int `json:"workers,omitempty"` + DialerOptions +} + +type WireGuardPeer struct { + Address string `json:"address,omitempty"` + Port uint16 `json:"port,omitempty"` + PublicKey string `json:"public_key,omitempty"` + PreSharedKey string `json:"pre_shared_key,omitempty"` + AllowedIPs badoption.Listable[netip.Prefix] `json:"allowed_ips,omitempty"` + PersistentKeepaliveInterval uint16 `json:"persistent_keepalive_interval,omitempty"` + Reserved []uint8 `json:"reserved,omitempty"` +} + +type LegacyWireGuardOutboundOptions struct { DialerOptions SystemInterface bool `json:"system_interface,omitempty"` GSO bool `json:"gso,omitempty"` InterfaceName string `json:"interface_name,omitempty"` LocalAddress badoption.Listable[netip.Prefix] `json:"local_address"` PrivateKey string `json:"private_key"` - Peers []WireGuardPeer `json:"peers,omitempty"` + Peers []LegacyWireGuardPeer `json:"peers,omitempty"` ServerOptions PeerPublicKey string `json:"peer_public_key"` PreSharedKey string `json:"pre_shared_key,omitempty"` @@ -23,10 +47,10 @@ type WireGuardOutboundOptions struct { Network NetworkList `json:"network,omitempty"` } -type WireGuardPeer struct { +type LegacyWireGuardPeer struct { ServerOptions - PublicKey string `json:"public_key,omitempty"` - PreSharedKey string `json:"pre_shared_key,omitempty"` - AllowedIPs badoption.Listable[string] `json:"allowed_ips,omitempty"` - Reserved []uint8 `json:"reserved,omitempty"` + PublicKey string `json:"public_key,omitempty"` + PreSharedKey string `json:"pre_shared_key,omitempty"` + AllowedIPs badoption.Listable[netip.Prefix] `json:"allowed_ips,omitempty"` + Reserved []uint8 `json:"reserved,omitempty"` } diff --git a/protocol/block/outbound.go b/protocol/block/outbound.go index 75bc7797e0..fe1ccda760 100644 --- a/protocol/block/outbound.go +++ b/protocol/block/outbound.go @@ -26,7 +26,7 @@ type Outbound struct { func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, _ option.StubOptions) (adapter.Outbound, error) { return &Outbound{ - Adapter: outbound.NewAdapter(C.TypeBlock, []string{N.NetworkTCP, N.NetworkUDP}, tag, nil), + Adapter: outbound.NewAdapter(C.TypeBlock, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil), logger: logger, }, nil } diff --git a/protocol/direct/inbound.go b/protocol/direct/inbound.go index 6db60d7890..c9594f9a7c 100644 --- a/protocol/direct/inbound.go +++ b/protocol/direct/inbound.go @@ -68,7 +68,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return inbound, nil } -func (i *Inbound) Start() error { +func (i *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } return i.listener.Start() } diff --git a/protocol/direct/outbound.go b/protocol/direct/outbound.go index 5ae0dac647..4962ea7025 100644 --- a/protocol/direct/outbound.go +++ b/protocol/direct/outbound.go @@ -52,7 +52,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } outbound := &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions(C.TypeDirect, []string{N.NetworkTCP, N.NetworkUDP}, tag, options.DialerOptions), + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeDirect, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions), logger: logger, domainStrategy: dns.DomainStrategy(options.DomainStrategy), fallbackDelay: time.Duration(options.FallbackDelay), diff --git a/protocol/dns/outbound.go b/protocol/dns/outbound.go index 7ce9fde2f2..3c493f80e3 100644 --- a/protocol/dns/outbound.go +++ b/protocol/dns/outbound.go @@ -28,7 +28,7 @@ type Outbound struct { func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.StubOptions) (adapter.Outbound, error) { return &Outbound{ - Adapter: outbound.NewAdapter(C.TypeDNS, []string{N.NetworkTCP, N.NetworkUDP}, tag, nil), + Adapter: outbound.NewAdapter(C.TypeDNS, tag, []string{N.NetworkTCP, N.NetworkUDP}, nil), router: router, logger: logger, }, nil diff --git a/protocol/group/selector.go b/protocol/group/selector.go index 08db74c09e..0bb3cd6644 100644 --- a/protocol/group/selector.go +++ b/protocol/group/selector.go @@ -38,7 +38,7 @@ type Selector struct { func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.SelectorOutboundOptions) (adapter.Outbound, error) { outbound := &Selector{ - Adapter: outbound.NewAdapter(C.TypeSelector, nil, tag, options.Outbounds), + Adapter: outbound.NewAdapter(C.TypeSelector, tag, nil, options.Outbounds), ctx: ctx, outboundManager: service.FromContext[adapter.OutboundManager](ctx), logger: logger, diff --git a/protocol/group/urltest.go b/protocol/group/urltest.go index f1a84b5044..fcada7dc3b 100644 --- a/protocol/group/urltest.go +++ b/protocol/group/urltest.go @@ -49,7 +49,7 @@ type URLTest struct { func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (adapter.Outbound, error) { outbound := &URLTest{ - Adapter: outbound.NewAdapter(C.TypeURLTest, []string{N.NetworkTCP, N.NetworkUDP}, tag, options.Outbounds), + Adapter: outbound.NewAdapter(C.TypeURLTest, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.Outbounds), ctx: ctx, router: router, outboundManager: service.FromContext[adapter.OutboundManager](ctx), diff --git a/protocol/http/inbound.go b/protocol/http/inbound.go index bb8fe8c7dd..a7a463f76b 100644 --- a/protocol/http/inbound.go +++ b/protocol/http/inbound.go @@ -61,7 +61,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return inbound, nil } -func (h *Inbound) Start() error { +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { diff --git a/protocol/http/outbound.go b/protocol/http/outbound.go index 81fd024669..c58f307138 100644 --- a/protocol/http/outbound.go +++ b/protocol/http/outbound.go @@ -39,7 +39,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } return &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHTTP, []string{N.NetworkTCP}, tag, options.DialerOptions), + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHTTP, tag, []string{N.NetworkTCP}, options.DialerOptions), logger: logger, client: sHTTP.NewClient(sHTTP.Options{ Dialer: detour, diff --git a/protocol/hysteria/inbound.go b/protocol/hysteria/inbound.go index 00defa92ab..6fa03479a2 100644 --- a/protocol/hysteria/inbound.go +++ b/protocol/hysteria/inbound.go @@ -160,7 +160,10 @@ func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } -func (h *Inbound) Start() error { +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { diff --git a/protocol/hysteria/outbound.go b/protocol/hysteria/outbound.go index 89e6cc1089..e1d8716c45 100644 --- a/protocol/hysteria/outbound.go +++ b/protocol/hysteria/outbound.go @@ -95,7 +95,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } return &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHysteria, networkList, tag, options.DialerOptions), + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHysteria, tag, networkList, options.DialerOptions), logger: logger, client: client, }, nil diff --git a/protocol/hysteria2/inbound.go b/protocol/hysteria2/inbound.go index 03cd8d2d52..a3260e55ee 100644 --- a/protocol/hysteria2/inbound.go +++ b/protocol/hysteria2/inbound.go @@ -171,7 +171,10 @@ func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } -func (h *Inbound) Start() error { +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { diff --git a/protocol/hysteria2/outbound.go b/protocol/hysteria2/outbound.go index 4cabb4751c..068cc7f7fe 100644 --- a/protocol/hysteria2/outbound.go +++ b/protocol/hysteria2/outbound.go @@ -81,7 +81,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } return &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHysteria2, networkList, tag, options.DialerOptions), + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeHysteria2, tag, networkList, options.DialerOptions), logger: logger, client: client, }, nil diff --git a/protocol/mixed/inbound.go b/protocol/mixed/inbound.go index 4f48144086..ad7bed7de2 100644 --- a/protocol/mixed/inbound.go +++ b/protocol/mixed/inbound.go @@ -54,7 +54,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return inbound, nil } -func (h *Inbound) Start() error { +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } return h.listener.Start() } diff --git a/protocol/naive/inbound.go b/protocol/naive/inbound.go index 1a561aeaf2..18acd2accd 100644 --- a/protocol/naive/inbound.go +++ b/protocol/naive/inbound.go @@ -78,7 +78,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return inbound, nil } -func (n *Inbound) Start() error { +func (n *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } var tlsConfig *tls.STDConfig if n.tlsConfig != nil { err := n.tlsConfig.Start() diff --git a/protocol/redirect/redirect.go b/protocol/redirect/redirect.go index 23bfad3eb4..0950d102b9 100644 --- a/protocol/redirect/redirect.go +++ b/protocol/redirect/redirect.go @@ -42,7 +42,10 @@ func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextL return redirect, nil } -func (h *Redirect) Start() error { +func (h *Redirect) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } return h.listener.Start() } diff --git a/protocol/redirect/tproxy.go b/protocol/redirect/tproxy.go index 825b9f0085..78f0079fde 100644 --- a/protocol/redirect/tproxy.go +++ b/protocol/redirect/tproxy.go @@ -61,7 +61,10 @@ func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLog return tproxy, nil } -func (t *TProxy) Start() error { +func (t *TProxy) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } err := t.listener.Start() if err != nil { return err diff --git a/protocol/shadowsocks/inbound.go b/protocol/shadowsocks/inbound.go index 84ad43fcb0..8332a93cc0 100644 --- a/protocol/shadowsocks/inbound.go +++ b/protocol/shadowsocks/inbound.go @@ -93,7 +93,10 @@ func newInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return inbound, err } -func (h *Inbound) Start() error { +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } return h.listener.Start() } diff --git a/protocol/shadowsocks/inbound_multi.go b/protocol/shadowsocks/inbound_multi.go index a76075efe0..ec55713430 100644 --- a/protocol/shadowsocks/inbound_multi.go +++ b/protocol/shadowsocks/inbound_multi.go @@ -101,7 +101,10 @@ func newMultiInbound(ctx context.Context, router adapter.Router, logger log.Cont return inbound, err } -func (h *MultiInbound) Start() error { +func (h *MultiInbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } return h.listener.Start() } diff --git a/protocol/shadowsocks/inbound_relay.go b/protocol/shadowsocks/inbound_relay.go index f7ec2b7703..bb20de3f58 100644 --- a/protocol/shadowsocks/inbound_relay.go +++ b/protocol/shadowsocks/inbound_relay.go @@ -86,7 +86,10 @@ func newRelayInbound(ctx context.Context, router adapter.Router, logger log.Cont return inbound, err } -func (h *RelayInbound) Start() error { +func (h *RelayInbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } return h.listener.Start() } diff --git a/protocol/shadowsocks/outbound.go b/protocol/shadowsocks/outbound.go index 8771fa8e99..7e7277ef95 100644 --- a/protocol/shadowsocks/outbound.go +++ b/protocol/shadowsocks/outbound.go @@ -49,7 +49,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } outbound := &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions(C.TypeShadowsocks, options.Network.Build(), tag, options.DialerOptions), + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeShadowsocks, tag, options.Network.Build(), options.DialerOptions), logger: logger, dialer: outboundDialer, method: method, diff --git a/protocol/shadowtls/inbound.go b/protocol/shadowtls/inbound.go index 1be422decc..ce4238fd49 100644 --- a/protocol/shadowtls/inbound.go +++ b/protocol/shadowtls/inbound.go @@ -90,7 +90,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return inbound, nil } -func (h *Inbound) Start() error { +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } return h.listener.Start() } diff --git a/protocol/shadowtls/outbound.go b/protocol/shadowtls/outbound.go index e979dba270..2b480729e5 100644 --- a/protocol/shadowtls/outbound.go +++ b/protocol/shadowtls/outbound.go @@ -29,7 +29,7 @@ type Outbound struct { func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.ShadowTLSOutboundOptions) (adapter.Outbound, error) { outbound := &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions(C.TypeShadowTLS, []string{N.NetworkTCP}, tag, options.DialerOptions), + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeShadowTLS, tag, []string{N.NetworkTCP}, options.DialerOptions), } if options.TLS == nil || !options.TLS.Enabled { return nil, C.ErrTLSRequired diff --git a/protocol/socks/inbound.go b/protocol/socks/inbound.go index fddacb21fb..115db30308 100644 --- a/protocol/socks/inbound.go +++ b/protocol/socks/inbound.go @@ -50,7 +50,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return inbound, nil } -func (h *Inbound) Start() error { +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } return h.listener.Start() } diff --git a/protocol/socks/outbound.go b/protocol/socks/outbound.go index 70a5a5eda0..0632f0825f 100644 --- a/protocol/socks/outbound.go +++ b/protocol/socks/outbound.go @@ -50,7 +50,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } outbound := &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSOCKS, options.Network.Build(), tag, options.DialerOptions), + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSOCKS, tag, options.Network.Build(), options.DialerOptions), router: router, logger: logger, client: socks.NewClient(outboundDialer, options.ServerOptions.Build(), version, options.Username, options.Password), diff --git a/protocol/ssh/outbound.go b/protocol/ssh/outbound.go index 1dfc1f6d68..eb9970b5fb 100644 --- a/protocol/ssh/outbound.go +++ b/protocol/ssh/outbound.go @@ -54,7 +54,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } outbound := &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSSH, []string{N.NetworkTCP}, tag, options.DialerOptions), + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeSSH, tag, []string{N.NetworkTCP}, options.DialerOptions), ctx: ctx, logger: logger, dialer: outboundDialer, diff --git a/protocol/tor/outbound.go b/protocol/tor/outbound.go index 3d2170115d..58824b53fb 100644 --- a/protocol/tor/outbound.go +++ b/protocol/tor/outbound.go @@ -80,7 +80,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } return &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTor, []string{N.NetworkTCP}, tag, options.DialerOptions), + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTor, tag, []string{N.NetworkTCP}, options.DialerOptions), ctx: ctx, logger: logger, proxy: NewProxyListener(ctx, logger, outboundDialer), diff --git a/protocol/trojan/inbound.go b/protocol/trojan/inbound.go index 2dc1bb9421..107667c580 100644 --- a/protocol/trojan/inbound.go +++ b/protocol/trojan/inbound.go @@ -110,7 +110,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return inbound, nil } -func (h *Inbound) Start() error { +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { diff --git a/protocol/trojan/outbound.go b/protocol/trojan/outbound.go index 68b0069043..82889bc188 100644 --- a/protocol/trojan/outbound.go +++ b/protocol/trojan/outbound.go @@ -43,7 +43,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } outbound := &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTrojan, options.Network.Build(), tag, options.DialerOptions), + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTrojan, tag, options.Network.Build(), options.DialerOptions), logger: logger, dialer: outboundDialer, serverAddr: options.ServerOptions.Build(), diff --git a/protocol/tuic/inbound.go b/protocol/tuic/inbound.go index 496079c194..a21b72ea5b 100644 --- a/protocol/tuic/inbound.go +++ b/protocol/tuic/inbound.go @@ -142,7 +142,10 @@ func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } -func (h *Inbound) Start() error { +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { diff --git a/protocol/tuic/outbound.go b/protocol/tuic/outbound.go index 177f21fc95..49b01f96e9 100644 --- a/protocol/tuic/outbound.go +++ b/protocol/tuic/outbound.go @@ -80,7 +80,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } return &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTUIC, options.Network.Build(), tag, options.DialerOptions), + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeTUIC, tag, options.Network.Build(), options.DialerOptions), logger: logger, client: client, udpStream: options.UDPOverStream, diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index 302afb578d..b3ada90550 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -300,104 +300,104 @@ func (t *Inbound) Tag() string { return t.tag } -func (t *Inbound) Start() error { - if C.IsAndroid && t.platformInterface == nil { - t.tunOptions.BuildAndroidRules(t.networkManager.PackageManager()) - } - if t.tunOptions.Name == "" { - t.tunOptions.Name = tun.CalculateInterfaceName("") - } - var ( - tunInterface tun.Tun - err error - ) - monitor := taskmonitor.New(t.logger, C.StartTimeout) - monitor.Start("open tun interface") - if t.platformInterface != nil { - tunInterface, err = t.platformInterface.OpenTun(&t.tunOptions, t.platformOptions) - } else { - tunInterface, err = tun.New(t.tunOptions) - } - monitor.Finish() - if err != nil { - return E.Cause(err, "configure tun interface") - } - t.logger.Trace("creating stack") - t.tunIf = tunInterface - var ( - forwarderBindInterface bool - includeAllNetworks bool - ) - if t.platformInterface != nil { - forwarderBindInterface = true - includeAllNetworks = t.platformInterface.IncludeAllNetworks() - } - tunStack, err := tun.NewStack(t.stack, tun.StackOptions{ - Context: t.ctx, - Tun: tunInterface, - TunOptions: t.tunOptions, - UDPTimeout: t.udpTimeout, - Handler: t, - Logger: t.logger, - ForwarderBindInterface: forwarderBindInterface, - InterfaceFinder: t.networkManager.InterfaceFinder(), - IncludeAllNetworks: includeAllNetworks, - }) - if err != nil { - return err - } - t.tunStack = tunStack - t.logger.Info("started at ", t.tunOptions.Name) - return nil -} - -func (t *Inbound) PostStart() error { - monitor := taskmonitor.New(t.logger, C.StartTimeout) - monitor.Start("starting tun stack") - err := t.tunStack.Start() - monitor.Finish() - if err != nil { - return E.Cause(err, "starting tun stack") - } - monitor.Start("starting tun interface") - err = t.tunIf.Start() - monitor.Finish() - if err != nil { - return E.Cause(err, "starting TUN interface") - } - if t.autoRedirect != nil { - t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) - for _, routeRuleSet := range t.routeRuleSet { - ipSets := routeRuleSet.ExtractIPSet() - if len(ipSets) == 0 { - t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeRuleSet.Name()) - } - t.routeAddressSet = append(t.routeAddressSet, ipSets...) +func (t *Inbound) Start(stage adapter.StartStage) error { + switch stage { + case adapter.StartStateStart: + if C.IsAndroid && t.platformInterface == nil { + t.tunOptions.BuildAndroidRules(t.networkManager.PackageManager()) } - t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet) - for _, routeExcludeRuleSet := range t.routeExcludeRuleSet { - ipSets := routeExcludeRuleSet.ExtractIPSet() - if len(ipSets) == 0 { - t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeExcludeRuleSet.Name()) - } - t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ipSets...) + if t.tunOptions.Name == "" { + t.tunOptions.Name = tun.CalculateInterfaceName("") + } + var ( + tunInterface tun.Tun + err error + ) + monitor := taskmonitor.New(t.logger, C.StartTimeout) + monitor.Start("open tun interface") + if t.platformInterface != nil { + tunInterface, err = t.platformInterface.OpenTun(&t.tunOptions, t.platformOptions) + } else { + tunInterface, err = tun.New(t.tunOptions) } - monitor.Start("initialize auto-redirect") - err := t.autoRedirect.Start() monitor.Finish() if err != nil { - return E.Cause(err, "auto-redirect") + return E.Cause(err, "configure tun interface") } - for _, routeRuleSet := range t.routeRuleSet { - t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet)) - routeRuleSet.DecRef() + t.logger.Trace("creating stack") + t.tunIf = tunInterface + var ( + forwarderBindInterface bool + includeAllNetworks bool + ) + if t.platformInterface != nil { + forwarderBindInterface = true + includeAllNetworks = t.platformInterface.IncludeAllNetworks() } - for _, routeExcludeRuleSet := range t.routeExcludeRuleSet { - t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet)) - routeExcludeRuleSet.DecRef() + tunStack, err := tun.NewStack(t.stack, tun.StackOptions{ + Context: t.ctx, + Tun: tunInterface, + TunOptions: t.tunOptions, + UDPTimeout: t.udpTimeout, + Handler: t, + Logger: t.logger, + ForwarderBindInterface: forwarderBindInterface, + InterfaceFinder: t.networkManager.InterfaceFinder(), + IncludeAllNetworks: includeAllNetworks, + }) + if err != nil { + return err + } + t.tunStack = tunStack + t.logger.Info("started at ", t.tunOptions.Name) + case adapter.StartStatePostStart: + monitor := taskmonitor.New(t.logger, C.StartTimeout) + monitor.Start("starting tun stack") + err := t.tunStack.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "starting tun stack") + } + monitor.Start("starting tun interface") + err = t.tunIf.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "starting TUN interface") + } + if t.autoRedirect != nil { + t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) + for _, routeRuleSet := range t.routeRuleSet { + ipSets := routeRuleSet.ExtractIPSet() + if len(ipSets) == 0 { + t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeRuleSet.Name()) + } + t.routeAddressSet = append(t.routeAddressSet, ipSets...) + } + t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet) + for _, routeExcludeRuleSet := range t.routeExcludeRuleSet { + ipSets := routeExcludeRuleSet.ExtractIPSet() + if len(ipSets) == 0 { + t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeExcludeRuleSet.Name()) + } + t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ipSets...) + } + monitor.Start("initialize auto-redirect") + err := t.autoRedirect.Start() + monitor.Finish() + if err != nil { + return E.Cause(err, "auto-redirect") + } + for _, routeRuleSet := range t.routeRuleSet { + t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet)) + routeRuleSet.DecRef() + } + for _, routeExcludeRuleSet := range t.routeExcludeRuleSet { + t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet)) + routeExcludeRuleSet.DecRef() + } + t.routeAddressSet = nil + t.routeExcludeAddressSet = nil } - t.routeAddressSet = nil - t.routeExcludeAddressSet = nil } return nil } diff --git a/protocol/vless/inbound.go b/protocol/vless/inbound.go index f5dfeabb6b..b0aa959e06 100644 --- a/protocol/vless/inbound.go +++ b/protocol/vless/inbound.go @@ -89,7 +89,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return inbound, nil } -func (h *Inbound) Start() error { +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } if h.tlsConfig != nil { err := h.tlsConfig.Start() if err != nil { diff --git a/protocol/vless/outbound.go b/protocol/vless/outbound.go index de655230e0..1d832a654d 100644 --- a/protocol/vless/outbound.go +++ b/protocol/vless/outbound.go @@ -46,7 +46,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } outbound := &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions(C.TypeVLESS, options.Network.Build(), tag, options.DialerOptions), + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeVLESS, tag, options.Network.Build(), options.DialerOptions), logger: logger, dialer: outboundDialer, serverAddr: options.ServerOptions.Build(), diff --git a/protocol/vmess/inbound.go b/protocol/vmess/inbound.go index 88ad222764..9f1009a301 100644 --- a/protocol/vmess/inbound.go +++ b/protocol/vmess/inbound.go @@ -99,7 +99,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo return inbound, nil } -func (h *Inbound) Start() error { +func (h *Inbound) Start(stage adapter.StartStage) error { + if stage != adapter.StartStateStart { + return nil + } err := h.service.Start() if err != nil { return err diff --git a/protocol/vmess/outbound.go b/protocol/vmess/outbound.go index 1e84639f47..d41b30d964 100644 --- a/protocol/vmess/outbound.go +++ b/protocol/vmess/outbound.go @@ -46,7 +46,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } outbound := &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions(C.TypeVMess, options.Network.Build(), tag, options.DialerOptions), + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeVMess, tag, options.Network.Build(), options.DialerOptions), logger: logger, dialer: outboundDialer, serverAddr: options.ServerOptions.Build(), diff --git a/protocol/wireguard/endpoint.go b/protocol/wireguard/endpoint.go new file mode 100644 index 0000000000..5465099c02 --- /dev/null +++ b/protocol/wireguard/endpoint.go @@ -0,0 +1,211 @@ +package wireguard + +import ( + "context" + "net" + "net/netip" + "time" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/adapter/endpoint" + "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/wireguard" + "github.com/sagernet/sing-dns" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/bufio" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" +) + +func RegisterEndpoint(registry *endpoint.Registry) { + endpoint.Register[option.WireGuardEndpointOptions](registry, C.TypeWireGuard, NewEndpoint) +} + +var ( + _ adapter.Endpoint = (*Endpoint)(nil) + _ adapter.InterfaceUpdateListener = (*Endpoint)(nil) +) + +type Endpoint struct { + endpoint.Adapter + ctx context.Context + router adapter.Router + logger logger.ContextLogger + localAddresses []netip.Prefix + endpoint *wireguard.Endpoint +} + +func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardEndpointOptions) (adapter.Endpoint, error) { + ep := &Endpoint{ + Adapter: endpoint.NewAdapterWithDialerOptions(C.TypeWireGuard, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions), + ctx: ctx, + router: router, + logger: logger, + localAddresses: options.Address, + } + if options.Detour == "" { + options.IsWireGuardListener = true + } else if options.GSO { + return nil, E.New("gso is conflict with detour") + } + outboundDialer, err := dialer.New(ctx, options.DialerOptions) + if err != nil { + return nil, err + } + wgEndpoint, err := wireguard.NewEndpoint(wireguard.EndpointOptions{ + Context: ctx, + Logger: logger, + System: options.System, + Handler: ep, + UDPTimeout: time.Duration(options.UDPTimeout), + Dialer: outboundDialer, + CreateDialer: func(interfaceName string) N.Dialer { + return common.Must1(dialer.NewDefault(service.FromContext[adapter.NetworkManager](ctx), option.DialerOptions{ + BindInterface: interfaceName, + })) + }, + Name: options.Name, + MTU: options.MTU, + GSO: options.GSO, + Address: options.Address, + PrivateKey: options.PrivateKey, + ListenPort: options.ListenPort, + ResolvePeer: func(domain string) (netip.Addr, error) { + endpointAddresses, lookupErr := router.Lookup(ctx, domain, dns.DomainStrategy(options.DomainStrategy)) + if lookupErr != nil { + return netip.Addr{}, lookupErr + } + return endpointAddresses[0], nil + }, + Peers: common.Map(options.Peers, func(it option.WireGuardPeer) wireguard.PeerOptions { + return wireguard.PeerOptions{ + Endpoint: M.ParseSocksaddrHostPort(it.Address, it.Port), + PublicKey: it.PublicKey, + PreSharedKey: it.PreSharedKey, + AllowedIPs: it.AllowedIPs, + PersistentKeepaliveInterval: it.PersistentKeepaliveInterval, + Reserved: it.Reserved, + } + }), + Workers: options.Workers, + }) + if err != nil { + return nil, err + } + ep.endpoint = wgEndpoint + return ep, nil +} + +func (w *Endpoint) Start(stage adapter.StartStage) error { + switch stage { + case adapter.StartStateStart: + return w.endpoint.Start(false) + case adapter.StartStatePostStart: + return w.endpoint.Start(true) + } + return nil +} + +func (w *Endpoint) Close() error { + return w.endpoint.Close() +} + +func (w *Endpoint) InterfaceUpdated() { + w.endpoint.BindUpdate() + return +} + +func (w *Endpoint) PrepareConnection(network string, source M.Socksaddr, destination M.Socksaddr) error { + return w.router.PreMatch(adapter.InboundContext{ + Inbound: w.Tag(), + InboundType: w.Type(), + Network: network, + Source: source, + Destination: destination, + }) +} + +func (w *Endpoint) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Inbound = w.Tag() + metadata.InboundType = w.Type() + metadata.Source = source + for _, localPrefix := range w.localAddresses { + if localPrefix.Contains(destination.Addr) { + metadata.OriginDestination = destination + if destination.Addr.Is4() { + destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1}) + } else { + destination.Addr = netip.IPv6Loopback() + } + break + } + } + metadata.Destination = destination + w.logger.InfoContext(ctx, "inbound connection from ", source) + w.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + w.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (w *Endpoint) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { + var metadata adapter.InboundContext + metadata.Inbound = w.Tag() + metadata.InboundType = w.Type() + metadata.Source = source + metadata.Destination = destination + for _, localPrefix := range w.localAddresses { + if localPrefix.Contains(destination.Addr) { + metadata.OriginDestination = destination + if destination.Addr.Is4() { + metadata.Destination.Addr = netip.AddrFrom4([4]uint8{127, 0, 0, 1}) + } else { + metadata.Destination.Addr = netip.IPv6Loopback() + } + conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination) + } + } + w.logger.InfoContext(ctx, "inbound packet connection from ", source) + w.logger.InfoContext(ctx, "inbound packet connection to ", destination) + w.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) +} + +func (w *Endpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + switch network { + case N.NetworkTCP: + w.logger.InfoContext(ctx, "outbound connection to ", destination) + case N.NetworkUDP: + w.logger.InfoContext(ctx, "outbound packet connection to ", destination) + } + if destination.IsFqdn() { + destinationAddresses, err := w.router.LookupDefault(ctx, destination.Fqdn) + if err != nil { + return nil, err + } + return N.DialSerial(ctx, w.endpoint, network, destination, destinationAddresses) + } else if !destination.Addr.IsValid() { + return nil, E.New("invalid destination: ", destination) + } + return w.endpoint.DialContext(ctx, network, destination) +} + +func (w *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + w.logger.InfoContext(ctx, "outbound packet connection to ", destination) + if destination.IsFqdn() { + destinationAddresses, err := w.router.LookupDefault(ctx, destination.Fqdn) + if err != nil { + return nil, err + } + packetConn, _, err := N.ListenSerial(ctx, w.endpoint, destination, destinationAddresses) + if err != nil { + return nil, err + } + return packetConn, err + } + return w.endpoint.ListenPacket(ctx, destination) +} diff --git a/protocol/wireguard/outbound.go b/protocol/wireguard/outbound.go index 70d74140be..0831c2d7cf 100644 --- a/protocol/wireguard/outbound.go +++ b/protocol/wireguard/outbound.go @@ -2,238 +2,163 @@ package wireguard import ( "context" - "encoding/base64" - "encoding/hex" - "fmt" "net" "net/netip" - "strings" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/deprecated" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-box/transport/wireguard" - "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-dns" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/x/list" "github.com/sagernet/sing/service" - "github.com/sagernet/sing/service/pause" - "github.com/sagernet/wireguard-go/conn" - "github.com/sagernet/wireguard-go/device" ) func RegisterOutbound(registry *outbound.Registry) { - outbound.Register[option.WireGuardOutboundOptions](registry, C.TypeWireGuard, NewOutbound) + outbound.Register[option.LegacyWireGuardOutboundOptions](registry, C.TypeWireGuard, NewOutbound) } -var _ adapter.InterfaceUpdateListener = (*Outbound)(nil) +var ( + _ adapter.Endpoint = (*Endpoint)(nil) + _ adapter.InterfaceUpdateListener = (*Endpoint)(nil) +) type Outbound struct { outbound.Adapter - ctx context.Context - router adapter.Router - logger logger.ContextLogger - workers int - peers []wireguard.PeerConfig - useStdNetBind bool - listener N.Dialer - ipcConf string - - pauseManager pause.Manager - pauseCallback *list.Element[pause.Callback] - bind conn.Bind - device *device.Device - tunDevice wireguard.Device + ctx context.Context + router adapter.Router + logger logger.ContextLogger + localAddresses []netip.Prefix + endpoint *wireguard.Endpoint } -func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardOutboundOptions) (adapter.Outbound, error) { +func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.LegacyWireGuardOutboundOptions) (adapter.Outbound, error) { + deprecated.Report(ctx, deprecated.OptionWireGuardOutbound) outbound := &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions(C.TypeWireGuard, options.Network.Build(), tag, options.DialerOptions), - ctx: ctx, - router: router, - logger: logger, - workers: options.Workers, - pauseManager: service.FromContext[pause.Manager](ctx), - } - peers, err := wireguard.ParsePeers(options) - if err != nil { - return nil, err + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeWireGuard, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions), + ctx: ctx, + router: router, + logger: logger, + localAddresses: options.LocalAddress, } - outbound.peers = peers - if len(options.LocalAddress) == 0 { - return nil, E.New("missing local address") - } - if options.GSO { - if options.GSO && options.Detour != "" { - return nil, E.New("gso is conflict with detour") - } + if options.Detour == "" { options.IsWireGuardListener = true - outbound.useStdNetBind = true + } else if options.GSO { + return nil, E.New("gso is conflict with detour") } - listener, err := dialer.New(ctx, options.DialerOptions) + outboundDialer, err := dialer.New(ctx, options.DialerOptions) if err != nil { return nil, err } - outbound.listener = listener - var privateKey string - { - bytes, err := base64.StdEncoding.DecodeString(options.PrivateKey) - if err != nil { - return nil, E.Cause(err, "decode private key") - } - privateKey = hex.EncodeToString(bytes) - } - outbound.ipcConf = "private_key=" + privateKey - mtu := options.MTU - if mtu == 0 { - mtu = 1408 - } - var wireTunDevice wireguard.Device - if !options.SystemInterface && tun.WithGVisor { - wireTunDevice, err = wireguard.NewStackDevice(options.LocalAddress, mtu) - } else { - wireTunDevice, err = wireguard.NewSystemDevice(service.FromContext[adapter.NetworkManager](ctx), options.InterfaceName, options.LocalAddress, mtu, options.GSO) - } - if err != nil { - return nil, E.Cause(err, "create WireGuard device") - } - outbound.tunDevice = wireTunDevice - return outbound, nil -} - -func (w *Outbound) Start() error { - if common.Any(w.peers, func(peer wireguard.PeerConfig) bool { - return !peer.Endpoint.IsValid() - }) { - // wait for all outbounds to be started and continue in PortStart - return nil - } - return w.start() -} - -func (w *Outbound) PostStart() error { - if common.All(w.peers, func(peer wireguard.PeerConfig) bool { - return peer.Endpoint.IsValid() - }) { - return nil - } - return w.start() -} - -func (w *Outbound) start() error { - err := wireguard.ResolvePeers(w.ctx, w.router, w.peers) - if err != nil { - return err - } - var bind conn.Bind - if w.useStdNetBind { - bind = conn.NewStdNetBind(w.listener.(dialer.WireGuardListener)) - } else { - var ( - isConnect bool - connectAddr netip.AddrPort - reserved [3]uint8 - ) - peerLen := len(w.peers) - if peerLen == 1 { - isConnect = true - connectAddr = w.peers[0].Endpoint - reserved = w.peers[0].Reserved + peers := common.Map(options.Peers, func(it option.LegacyWireGuardPeer) wireguard.PeerOptions { + return wireguard.PeerOptions{ + Endpoint: it.ServerOptions.Build(), + PublicKey: it.PublicKey, + PreSharedKey: it.PreSharedKey, + AllowedIPs: it.AllowedIPs, + // PersistentKeepaliveInterval: time.Duration(it.PersistentKeepaliveInterval), + Reserved: it.Reserved, } - bind = wireguard.NewClientBind(w.ctx, w.logger, w.listener, isConnect, connectAddr, reserved) - } - if w.useStdNetBind || len(w.peers) > 1 { - for _, peer := range w.peers { - if peer.Reserved != [3]uint8{} { - bind.SetReservedForEndpoint(peer.Endpoint, peer.Reserved) - } - } - } - err = w.tunDevice.Start() - if err != nil { - return err - } - wgDevice := device.NewDevice(w.ctx, w.tunDevice, bind, &device.Logger{ - Verbosef: func(format string, args ...interface{}) { - w.logger.Debug(fmt.Sprintf(strings.ToLower(format), args...)) + }) + if len(peers) == 0 { + peers = []wireguard.PeerOptions{{ + Endpoint: options.ServerOptions.Build(), + PublicKey: options.PeerPublicKey, + PreSharedKey: options.PreSharedKey, + AllowedIPs: []netip.Prefix{netip.PrefixFrom(netip.IPv4Unspecified(), 0), netip.PrefixFrom(netip.IPv6Unspecified(), 0)}, + Reserved: options.Reserved, + }} + } + wgEndpoint, err := wireguard.NewEndpoint(wireguard.EndpointOptions{ + Context: ctx, + Logger: logger, + System: options.SystemInterface, + Dialer: outboundDialer, + CreateDialer: func(interfaceName string) N.Dialer { + return common.Must1(dialer.NewDefault(service.FromContext[adapter.NetworkManager](ctx), option.DialerOptions{ + BindInterface: interfaceName, + })) }, - Errorf: func(format string, args ...interface{}) { - w.logger.Error(fmt.Sprintf(strings.ToLower(format), args...)) + Name: options.InterfaceName, + MTU: options.MTU, + GSO: options.GSO, + Address: options.LocalAddress, + PrivateKey: options.PrivateKey, + ResolvePeer: func(domain string) (netip.Addr, error) { + endpointAddresses, lookupErr := router.Lookup(ctx, domain, dns.DomainStrategy(options.DomainStrategy)) + if lookupErr != nil { + return netip.Addr{}, lookupErr + } + return endpointAddresses[0], nil }, - }, w.workers) - ipcConf := w.ipcConf - for _, peer := range w.peers { - ipcConf += peer.GenerateIpcLines() - } - err = wgDevice.IpcSet(ipcConf) + Peers: peers, + Workers: options.Workers, + }) if err != nil { - return E.Cause(err, "setup wireguard: \n", ipcConf) + return nil, err } - w.device = wgDevice - w.pauseCallback = w.pauseManager.RegisterCallback(w.onPauseUpdated) - return nil + outbound.endpoint = wgEndpoint + return outbound, nil } -func (w *Outbound) Close() error { - if w.device != nil { - w.device.Close() - } - if w.pauseCallback != nil { - w.pauseManager.UnregisterCallback(w.pauseCallback) +func (o *Outbound) Start(stage adapter.StartStage) error { + switch stage { + case adapter.StartStateStart: + return o.endpoint.Start(false) + case adapter.StartStatePostStart: + return o.endpoint.Start(true) } return nil } -func (w *Outbound) InterfaceUpdated() { - w.device.BindUpdate() - return +func (o *Outbound) Close() error { + return o.endpoint.Close() } -func (w *Outbound) onPauseUpdated(event int) { - switch event { - case pause.EventDevicePaused: - w.device.Down() - case pause.EventDeviceWake: - w.device.Up() - } +func (o *Outbound) InterfaceUpdated() { + o.endpoint.BindUpdate() + return } -func (w *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (o *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { switch network { case N.NetworkTCP: - w.logger.InfoContext(ctx, "outbound connection to ", destination) + o.logger.InfoContext(ctx, "outbound connection to ", destination) case N.NetworkUDP: - w.logger.InfoContext(ctx, "outbound packet connection to ", destination) + o.logger.InfoContext(ctx, "outbound packet connection to ", destination) } if destination.IsFqdn() { - destinationAddresses, err := w.router.LookupDefault(ctx, destination.Fqdn) + destinationAddresses, err := o.router.LookupDefault(ctx, destination.Fqdn) if err != nil { return nil, err } - return N.DialSerial(ctx, w.tunDevice, network, destination, destinationAddresses) + return N.DialSerial(ctx, o.endpoint, network, destination, destinationAddresses) + } else if !destination.Addr.IsValid() { + return nil, E.New("invalid destination: ", destination) } - return w.tunDevice.DialContext(ctx, network, destination) + return o.endpoint.DialContext(ctx, network, destination) } -func (w *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - w.logger.InfoContext(ctx, "outbound packet connection to ", destination) +func (o *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + o.logger.InfoContext(ctx, "outbound packet connection to ", destination) if destination.IsFqdn() { - destinationAddresses, err := w.router.LookupDefault(ctx, destination.Fqdn) + destinationAddresses, err := o.router.LookupDefault(ctx, destination.Fqdn) if err != nil { return nil, err } - packetConn, _, err := N.ListenSerial(ctx, w.tunDevice, destination, destinationAddresses) + packetConn, _, err := N.ListenSerial(ctx, o.endpoint, destination, destinationAddresses) if err != nil { return nil, err } return packetConn, err } - return w.tunDevice.ListenPacket(ctx, destination) + return o.endpoint.ListenPacket(ctx, destination) } diff --git a/route/network.go b/route/network.go index 510686e521..7cb3df8d1c 100644 --- a/route/network.go +++ b/route/network.go @@ -41,17 +41,17 @@ type NetworkManager struct { autoDetectInterface bool defaultOptions adapter.NetworkOptions autoRedirectOutputMark uint32 - - networkMonitor tun.NetworkUpdateMonitor - interfaceMonitor tun.DefaultInterfaceMonitor - packageManager tun.PackageManager - powerListener winpowrprof.EventListener - pauseManager pause.Manager - platformInterface platform.Interface - inboundManager adapter.InboundManager - outboundManager adapter.OutboundManager - wifiState adapter.WIFIState - started bool + networkMonitor tun.NetworkUpdateMonitor + interfaceMonitor tun.DefaultInterfaceMonitor + packageManager tun.PackageManager + powerListener winpowrprof.EventListener + pauseManager pause.Manager + platformInterface platform.Interface + endpoint adapter.EndpointManager + inbound adapter.InboundManager + outbound adapter.OutboundManager + wifiState adapter.WIFIState + started bool } func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOptions option.RouteOptions) (*NetworkManager, error) { @@ -69,7 +69,9 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOp }, pauseManager: service.FromContext[pause.Manager](ctx), platformInterface: service.FromContext[platform.Interface](ctx), - outboundManager: service.FromContext[adapter.OutboundManager](ctx), + endpoint: service.FromContext[adapter.EndpointManager](ctx), + inbound: service.FromContext[adapter.InboundManager](ctx), + outbound: service.FromContext[adapter.OutboundManager](ctx), } if C.NetworkStrategy(routeOptions.DefaultNetworkStrategy) != C.NetworkStrategyDefault { if routeOptions.DefaultInterface != "" { @@ -355,14 +357,21 @@ func (r *NetworkManager) WIFIState() adapter.WIFIState { func (r *NetworkManager) ResetNetwork() { conntrack.Close() - for _, inbound := range r.inboundManager.Inbounds() { + for _, endpoint := range r.endpoint.Endpoints() { + listener, isListener := endpoint.(adapter.InterfaceUpdateListener) + if isListener { + listener.InterfaceUpdated() + } + } + + for _, inbound := range r.inbound.Inbounds() { listener, isListener := inbound.(adapter.InterfaceUpdateListener) if isListener { listener.InterfaceUpdated() } } - for _, outbound := range r.outboundManager.Outbounds() { + for _, outbound := range r.outbound.Outbounds() { listener, isListener := outbound.(adapter.InterfaceUpdateListener) if isListener { listener.InterfaceUpdated() diff --git a/route/route_dns.go b/route/route_dns.go index c11c07fe1b..696e167668 100644 --- a/route/route_dns.go +++ b/route/route_dns.go @@ -11,7 +11,7 @@ import ( C "github.com/sagernet/sing-box/constant" R "github.com/sagernet/sing-box/route/rule" "github.com/sagernet/sing-dns" - tun "github.com/sagernet/sing-tun" + "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common/cache" E "github.com/sagernet/sing/common/exceptions" F "github.com/sagernet/sing/common/format" diff --git a/transport/wireguard/client_bind.go b/transport/wireguard/client_bind.go index 20e7c0790c..e74e909d24 100644 --- a/transport/wireguard/client_bind.go +++ b/transport/wireguard/client_bind.go @@ -128,7 +128,7 @@ func (c *ClientBind) receive(packets [][]byte, sizes []int, eps []conn.Endpoint) select { case <-c.done: default: - c.logger.Error(context.Background(), E.Cause(err, "read packet")) + c.logger.Error(E.Cause(err, "read packet")) err = nil } return @@ -138,7 +138,7 @@ func (c *ClientBind) receive(packets [][]byte, sizes []int, eps []conn.Endpoint) b := packets[0] common.ClearArray(b[1:4]) } - eps[0] = Endpoint(M.AddrPortFromNet(addr)) + eps[0] = remoteEndpoint(M.AddrPortFromNet(addr)) count = 1 return } @@ -169,7 +169,7 @@ func (c *ClientBind) Send(bufs [][]byte, ep conn.Endpoint) error { time.Sleep(time.Second) return err } - destination := netip.AddrPort(ep.(Endpoint)) + destination := netip.AddrPort(ep.(remoteEndpoint)) for _, b := range bufs { if len(b) > 3 { reserved, loaded := c.reservedForEndpoint[destination] @@ -192,7 +192,7 @@ func (c *ClientBind) ParseEndpoint(s string) (conn.Endpoint, error) { if err != nil { return nil, err } - return Endpoint(ap), nil + return remoteEndpoint(ap), nil } func (c *ClientBind) BatchSize() int { @@ -229,3 +229,31 @@ func (w *wireConn) Close() error { close(w.done) return nil } + +var _ conn.Endpoint = (*remoteEndpoint)(nil) + +type remoteEndpoint netip.AddrPort + +func (e remoteEndpoint) ClearSrc() { +} + +func (e remoteEndpoint) SrcToString() string { + return "" +} + +func (e remoteEndpoint) DstToString() string { + return (netip.AddrPort)(e).String() +} + +func (e remoteEndpoint) DstToBytes() []byte { + b, _ := (netip.AddrPort)(e).MarshalBinary() + return b +} + +func (e remoteEndpoint) DstIP() netip.Addr { + return (netip.AddrPort)(e).Addr() +} + +func (e remoteEndpoint) SrcIP() netip.Addr { + return netip.Addr{} +} diff --git a/transport/wireguard/device.go b/transport/wireguard/device.go index 14e04bf56c..d5d3b78151 100644 --- a/transport/wireguard/device.go +++ b/transport/wireguard/device.go @@ -1,13 +1,44 @@ package wireguard import ( + "context" + "net/netip" + "time" + + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/logger" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/wireguard-go/tun" + "github.com/sagernet/wireguard-go/device" + wgTun "github.com/sagernet/wireguard-go/tun" ) type Device interface { - tun.Device + wgTun.Device N.Dialer Start() error - // NewEndpoint() (stack.LinkEndpoint, error) + SetDevice(device *device.Device) +} + +type DeviceOptions struct { + Context context.Context + Logger logger.ContextLogger + System bool + Handler tun.Handler + UDPTimeout time.Duration + CreateDialer func(interfaceName string) N.Dialer + Name string + MTU uint32 + GSO bool + Address []netip.Prefix + AllowedAddress []netip.Prefix +} + +func NewDevice(options DeviceOptions) (Device, error) { + if !options.System { + return newStackDevice(options) + } else if options.Handler == nil { + return newSystemDevice(options) + } else { + return newSystemStackDevice(options) + } } diff --git a/transport/wireguard/device_stack.go b/transport/wireguard/device_stack.go index 61286e6a9a..f9440f02fa 100644 --- a/transport/wireguard/device_stack.go +++ b/transport/wireguard/device_stack.go @@ -5,7 +5,6 @@ package wireguard import ( "context" "net" - "net/netip" "os" "github.com/sagernet/gvisor/pkg/buffer" @@ -15,52 +14,41 @@ import ( "github.com/sagernet/gvisor/pkg/tcpip/network/ipv4" "github.com/sagernet/gvisor/pkg/tcpip/network/ipv6" "github.com/sagernet/gvisor/pkg/tcpip/stack" - "github.com/sagernet/gvisor/pkg/tcpip/transport/icmp" "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" "github.com/sagernet/gvisor/pkg/tcpip/transport/udp" "github.com/sagernet/sing-tun" - "github.com/sagernet/sing/common/buf" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/wireguard-go/device" wgTun "github.com/sagernet/wireguard-go/tun" ) -var _ Device = (*StackDevice)(nil) - -const defaultNIC tcpip.NICID = 1 - -type StackDevice struct { - stack *stack.Stack - mtu uint32 - events chan wgTun.Event - outbound chan *stack.PacketBuffer - packetOutbound chan *buf.Buffer - done chan struct{} - dispatcher stack.NetworkDispatcher - addr4 tcpip.Address - addr6 tcpip.Address -} - -func NewStackDevice(localAddresses []netip.Prefix, mtu uint32) (*StackDevice, error) { - ipStack := stack.New(stack.Options{ - NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol}, - TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol4, icmp.NewProtocol6}, - HandleLocal: true, - }) - tunDevice := &StackDevice{ - stack: ipStack, - mtu: mtu, - events: make(chan wgTun.Event, 1), - outbound: make(chan *stack.PacketBuffer, 256), - packetOutbound: make(chan *buf.Buffer, 256), - done: make(chan struct{}), +var _ Device = (*stackDevice)(nil) + +type stackDevice struct { + stack *stack.Stack + mtu uint32 + events chan wgTun.Event + outbound chan *stack.PacketBuffer + done chan struct{} + dispatcher stack.NetworkDispatcher + addr4 tcpip.Address + addr6 tcpip.Address +} + +func newStackDevice(options DeviceOptions) (*stackDevice, error) { + tunDevice := &stackDevice{ + mtu: options.MTU, + events: make(chan wgTun.Event, 1), + outbound: make(chan *stack.PacketBuffer, 256), + done: make(chan struct{}), } - err := ipStack.CreateNIC(defaultNIC, (*wireEndpoint)(tunDevice)) + ipStack, err := tun.NewGVisorStack((*wireEndpoint)(tunDevice)) if err != nil { - return nil, E.New(err.String()) + return nil, err } - for _, prefix := range localAddresses { + for _, prefix := range options.Address { addr := tun.AddressFromAddr(prefix.Addr()) protoAddr := tcpip.ProtocolAddress{ AddressWithPrefix: tcpip.AddressWithPrefix{ @@ -75,32 +63,27 @@ func NewStackDevice(localAddresses []netip.Prefix, mtu uint32) (*StackDevice, er tunDevice.addr6 = addr protoAddr.Protocol = ipv6.ProtocolNumber } - err = ipStack.AddProtocolAddress(defaultNIC, protoAddr, stack.AddressProperties{}) - if err != nil { - return nil, E.New("parse local address ", protoAddr.AddressWithPrefix, ": ", err.String()) + gErr := ipStack.AddProtocolAddress(tun.DefaultNIC, protoAddr, stack.AddressProperties{}) + if gErr != nil { + return nil, E.New("parse local address ", protoAddr.AddressWithPrefix, ": ", gErr.String()) } } - sOpt := tcpip.TCPSACKEnabled(true) - ipStack.SetTransportProtocolOption(tcp.ProtocolNumber, &sOpt) - cOpt := tcpip.CongestionControlOption("cubic") - ipStack.SetTransportProtocolOption(tcp.ProtocolNumber, &cOpt) - ipStack.AddRoute(tcpip.Route{Destination: header.IPv4EmptySubnet, NIC: defaultNIC}) - ipStack.AddRoute(tcpip.Route{Destination: header.IPv6EmptySubnet, NIC: defaultNIC}) + tunDevice.stack = ipStack + if options.Handler != nil { + ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket) + ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket) + } return tunDevice, nil } -func (w *StackDevice) NewEndpoint() (stack.LinkEndpoint, error) { - return (*wireEndpoint)(w), nil -} - -func (w *StackDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (w *stackDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { addr := tcpip.FullAddress{ - NIC: defaultNIC, + NIC: tun.DefaultNIC, Port: destination.Port, Addr: tun.AddressFromAddr(destination.Addr), } bind := tcpip.FullAddress{ - NIC: defaultNIC, + NIC: tun.DefaultNIC, } var networkProtocol tcpip.NetworkProtocolNumber if destination.IsIPv4() { @@ -128,9 +111,9 @@ func (w *StackDevice) DialContext(ctx context.Context, network string, destinati } } -func (w *StackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (w *stackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { bind := tcpip.FullAddress{ - NIC: defaultNIC, + NIC: tun.DefaultNIC, } var networkProtocol tcpip.NetworkProtocolNumber if destination.IsIPv4() { @@ -147,24 +130,19 @@ func (w *StackDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) return udpConn, nil } -func (w *StackDevice) Inet4Address() netip.Addr { - return tun.AddrFromAddress(w.addr4) -} - -func (w *StackDevice) Inet6Address() netip.Addr { - return tun.AddrFromAddress(w.addr6) +func (w *stackDevice) SetDevice(device *device.Device) { } -func (w *StackDevice) Start() error { +func (w *stackDevice) Start() error { w.events <- wgTun.EventUp return nil } -func (w *StackDevice) File() *os.File { +func (w *stackDevice) File() *os.File { return nil } -func (w *StackDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) { +func (w *stackDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) { select { case packetBuffer, ok := <-w.outbound: if !ok { @@ -180,17 +158,12 @@ func (w *StackDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, e sizes[0] = n count = 1 return - case packet := <-w.packetOutbound: - defer packet.Release() - sizes[0] = copy(bufs[0][offset:], packet.Bytes()) - count = 1 - return case <-w.done: return 0, os.ErrClosed } } -func (w *StackDevice) Write(bufs [][]byte, offset int) (count int, err error) { +func (w *stackDevice) Write(bufs [][]byte, offset int) (count int, err error) { for _, b := range bufs { b = b[offset:] if len(b) == 0 { @@ -213,23 +186,23 @@ func (w *StackDevice) Write(bufs [][]byte, offset int) (count int, err error) { return } -func (w *StackDevice) Flush() error { +func (w *stackDevice) Flush() error { return nil } -func (w *StackDevice) MTU() (int, error) { +func (w *stackDevice) MTU() (int, error) { return int(w.mtu), nil } -func (w *StackDevice) Name() (string, error) { +func (w *stackDevice) Name() (string, error) { return "sing-box", nil } -func (w *StackDevice) Events() <-chan wgTun.Event { +func (w *stackDevice) Events() <-chan wgTun.Event { return w.events } -func (w *StackDevice) Close() error { +func (w *stackDevice) Close() error { close(w.done) close(w.events) w.stack.Close() @@ -240,13 +213,13 @@ func (w *StackDevice) Close() error { return nil } -func (w *StackDevice) BatchSize() int { +func (w *stackDevice) BatchSize() int { return 1 } var _ stack.LinkEndpoint = (*wireEndpoint)(nil) -type wireEndpoint StackDevice +type wireEndpoint stackDevice func (ep *wireEndpoint) MTU() uint32 { return ep.mtu diff --git a/transport/wireguard/gonet.go b/transport/wireguard/device_stack_gonet.go similarity index 100% rename from transport/wireguard/gonet.go rename to transport/wireguard/device_stack_gonet.go diff --git a/transport/wireguard/device_stack_stub.go b/transport/wireguard/device_stack_stub.go index b383ab3825..ea413559e6 100644 --- a/transport/wireguard/device_stack_stub.go +++ b/transport/wireguard/device_stack_stub.go @@ -2,12 +2,12 @@ package wireguard -import ( - "net/netip" +import "github.com/sagernet/sing-tun" - "github.com/sagernet/sing-tun" -) +func newStackDevice(options DeviceOptions) (Device, error) { + return nil, tun.ErrGVisorNotIncluded +} -func NewStackDevice(localAddresses []netip.Prefix, mtu uint32) (Device, error) { +func newSystemStackDevice(options DeviceOptions) (Device, error) { return nil, tun.ErrGVisorNotIncluded } diff --git a/transport/wireguard/device_system.go b/transport/wireguard/device_system.go index 8a54a75ef9..667443bab7 100644 --- a/transport/wireguard/device_system.go +++ b/transport/wireguard/device_system.go @@ -6,96 +6,89 @@ import ( "net" "net/netip" "os" + "runtime" "sync" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/dialer" - "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" + "github.com/sagernet/wireguard-go/device" wgTun "github.com/sagernet/wireguard-go/tun" ) -var _ Device = (*SystemDevice)(nil) - -type SystemDevice struct { - dialer N.Dialer - device tun.Tun - batchDevice tun.LinuxTUN - name string - mtu uint32 - inet4Addresses []netip.Prefix - inet6Addresses []netip.Prefix - gso bool - events chan wgTun.Event - closeOnce sync.Once +var _ Device = (*systemDevice)(nil) + +type systemDevice struct { + options DeviceOptions + dialer N.Dialer + device tun.Tun + batchDevice tun.LinuxTUN + events chan wgTun.Event + closeOnce sync.Once } -func NewSystemDevice(networkManager adapter.NetworkManager, interfaceName string, localPrefixes []netip.Prefix, mtu uint32, gso bool) (*SystemDevice, error) { - var inet4Addresses []netip.Prefix - var inet6Addresses []netip.Prefix - for _, prefixes := range localPrefixes { - if prefixes.Addr().Is4() { - inet4Addresses = append(inet4Addresses, prefixes) - } else { - inet6Addresses = append(inet6Addresses, prefixes) - } - } - if interfaceName == "" { - interfaceName = tun.CalculateInterfaceName("wg") +func newSystemDevice(options DeviceOptions) (*systemDevice, error) { + if options.Name == "" { + options.Name = tun.CalculateInterfaceName("wg") } - - return &SystemDevice{ - dialer: common.Must1(dialer.NewDefault(networkManager, option.DialerOptions{ - BindInterface: interfaceName, - })), - name: interfaceName, - mtu: mtu, - inet4Addresses: inet4Addresses, - inet6Addresses: inet6Addresses, - gso: gso, - events: make(chan wgTun.Event, 1), + return &systemDevice{ + options: options, + dialer: options.CreateDialer(options.Name), + events: make(chan wgTun.Event, 1), }, nil } -func (w *SystemDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { +func (w *systemDevice) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { return w.dialer.DialContext(ctx, network, destination) } -func (w *SystemDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { +func (w *systemDevice) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { return w.dialer.ListenPacket(ctx, destination) } -func (w *SystemDevice) Inet4Address() netip.Addr { - if len(w.inet4Addresses) == 0 { - return netip.Addr{} +func (w *systemDevice) SetDevice(device *device.Device) { +} + +func (w *systemDevice) Start() error { + networkManager := service.FromContext[adapter.NetworkManager](w.options.Context) + tunOptions := tun.Options{ + Name: w.options.Name, + Inet4Address: common.Filter(w.options.Address, func(it netip.Prefix) bool { + return it.Addr().Is4() + }), + Inet6Address: common.Filter(w.options.Address, func(it netip.Prefix) bool { + return it.Addr().Is6() + }), + MTU: w.options.MTU, + GSO: w.options.GSO, + InterfaceScope: true, + Inet4RouteAddress: common.Filter(w.options.AllowedAddress, func(it netip.Prefix) bool { + return it.Addr().Is4() + }), + Inet6RouteAddress: common.Filter(w.options.AllowedAddress, func(it netip.Prefix) bool { return it.Addr().Is6() }), + InterfaceMonitor: networkManager.InterfaceMonitor(), + InterfaceFinder: networkManager.InterfaceFinder(), + Logger: w.options.Logger, } - return w.inet4Addresses[0].Addr() -} - -func (w *SystemDevice) Inet6Address() netip.Addr { - if len(w.inet6Addresses) == 0 { - return netip.Addr{} + // works with Linux, macOS with IFSCOPE routes, not tested on Windows + if runtime.GOOS == "darwin" { + tunOptions.AutoRoute = true } - return w.inet6Addresses[0].Addr() -} - -func (w *SystemDevice) Start() error { - tunInterface, err := tun.New(tun.Options{ - Name: w.name, - Inet4Address: w.inet4Addresses, - Inet6Address: w.inet6Addresses, - MTU: w.mtu, - GSO: w.gso, - }) + tunInterface, err := tun.New(tunOptions) + if err != nil { + return err + } + err = tunInterface.Start() if err != nil { return err } + w.options.Logger.Info("started at ", w.options.Name) w.device = tunInterface - if w.gso { + if w.options.GSO { batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN) if !isBatchTUN { tunInterface.Close() @@ -107,15 +100,15 @@ func (w *SystemDevice) Start() error { return nil } -func (w *SystemDevice) File() *os.File { +func (w *systemDevice) File() *os.File { return nil } -func (w *SystemDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) { +func (w *systemDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, err error) { if w.batchDevice != nil { - count, err = w.batchDevice.BatchRead(bufs, offset, sizes) + count, err = w.batchDevice.BatchRead(bufs, offset-tun.PacketOffset, sizes) } else { - sizes[0], err = w.device.Read(bufs[0][offset:]) + sizes[0], err = w.device.Read(bufs[0][offset-tun.PacketOffset:]) if err == nil { count = 1 } else if errors.Is(err, tun.ErrTooManySegments) { @@ -125,12 +118,16 @@ func (w *SystemDevice) Read(bufs [][]byte, sizes []int, offset int) (count int, return } -func (w *SystemDevice) Write(bufs [][]byte, offset int) (count int, err error) { +func (w *systemDevice) Write(bufs [][]byte, offset int) (count int, err error) { if w.batchDevice != nil { - return 0, w.batchDevice.BatchWrite(bufs, offset) + return w.batchDevice.BatchWrite(bufs, offset) } else { - for _, b := range bufs { - _, err = w.device.Write(b[offset:]) + for _, packet := range bufs { + if tun.PacketOffset > 0 { + common.ClearArray(packet[offset-tun.PacketOffset : offset]) + tun.PacketFillHeader(packet[offset-tun.PacketOffset:], tun.PacketIPVersion(packet[offset:])) + } + _, err = w.device.Write(packet[offset-tun.PacketOffset:]) if err != nil { return } @@ -140,28 +137,28 @@ func (w *SystemDevice) Write(bufs [][]byte, offset int) (count int, err error) { return } -func (w *SystemDevice) Flush() error { +func (w *systemDevice) Flush() error { return nil } -func (w *SystemDevice) MTU() (int, error) { - return int(w.mtu), nil +func (w *systemDevice) MTU() (int, error) { + return int(w.options.MTU), nil } -func (w *SystemDevice) Name() (string, error) { - return w.name, nil +func (w *systemDevice) Name() (string, error) { + return w.options.Name, nil } -func (w *SystemDevice) Events() <-chan wgTun.Event { +func (w *systemDevice) Events() <-chan wgTun.Event { return w.events } -func (w *SystemDevice) Close() error { +func (w *systemDevice) Close() error { close(w.events) return w.device.Close() } -func (w *SystemDevice) BatchSize() int { +func (w *systemDevice) BatchSize() int { if w.batchDevice != nil { return w.batchDevice.BatchSize() } diff --git a/transport/wireguard/device_system_stack.go b/transport/wireguard/device_system_stack.go new file mode 100644 index 0000000000..4249e53eaa --- /dev/null +++ b/transport/wireguard/device_system_stack.go @@ -0,0 +1,182 @@ +//go:build with_gvisor + +package wireguard + +import ( + "net/netip" + + "github.com/sagernet/gvisor/pkg/buffer" + "github.com/sagernet/gvisor/pkg/tcpip" + "github.com/sagernet/gvisor/pkg/tcpip/header" + "github.com/sagernet/gvisor/pkg/tcpip/stack" + "github.com/sagernet/gvisor/pkg/tcpip/transport/tcp" + "github.com/sagernet/gvisor/pkg/tcpip/transport/udp" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + "github.com/sagernet/wireguard-go/device" +) + +var _ Device = (*systemStackDevice)(nil) + +type systemStackDevice struct { + *systemDevice + stack *stack.Stack + endpoint *deviceEndpoint + writeBufs [][]byte +} + +func newSystemStackDevice(options DeviceOptions) (*systemStackDevice, error) { + system, err := newSystemDevice(options) + if err != nil { + return nil, err + } + endpoint := &deviceEndpoint{ + mtu: options.MTU, + done: make(chan struct{}), + } + ipStack, err := tun.NewGVisorStack(endpoint) + if err != nil { + return nil, err + } + ipStack.SetTransportProtocolHandler(tcp.ProtocolNumber, tun.NewTCPForwarder(options.Context, ipStack, options.Handler).HandlePacket) + ipStack.SetTransportProtocolHandler(udp.ProtocolNumber, tun.NewUDPForwarder(options.Context, ipStack, options.Handler, options.UDPTimeout).HandlePacket) + return &systemStackDevice{ + systemDevice: system, + stack: ipStack, + endpoint: endpoint, + }, nil +} + +func (w *systemStackDevice) SetDevice(device *device.Device) { + w.endpoint.device = device +} + +func (w *systemStackDevice) Write(bufs [][]byte, offset int) (count int, err error) { + if w.batchDevice != nil { + w.writeBufs = w.writeBufs[:0] + for _, packet := range bufs { + if !w.writeStack(packet[offset:]) { + w.writeBufs = append(w.writeBufs, packet) + } + } + if len(w.writeBufs) > 0 { + return w.batchDevice.BatchWrite(bufs, offset) + } + } else { + for _, packet := range bufs { + if !w.writeStack(packet[offset:]) { + if tun.PacketOffset > 0 { + common.ClearArray(packet[offset-tun.PacketOffset : offset]) + tun.PacketFillHeader(packet[offset-tun.PacketOffset:], tun.PacketIPVersion(packet[offset:])) + } + _, err = w.device.Write(packet[offset-tun.PacketOffset:]) + } + if err != nil { + return + } + } + } + // WireGuard will not read count + return +} + +func (w *systemStackDevice) Close() error { + close(w.endpoint.done) + w.stack.Close() + for _, endpoint := range w.stack.CleanupEndpoints() { + endpoint.Abort() + } + w.stack.Wait() + return w.systemDevice.Close() +} + +func (w *systemStackDevice) writeStack(packet []byte) bool { + var ( + networkProtocol tcpip.NetworkProtocolNumber + destination netip.Addr + ) + switch header.IPVersion(packet) { + case header.IPv4Version: + networkProtocol = header.IPv4ProtocolNumber + destination = netip.AddrFrom4(header.IPv4(packet).DestinationAddress().As4()) + case header.IPv6Version: + networkProtocol = header.IPv6ProtocolNumber + destination = netip.AddrFrom16(header.IPv6(packet).DestinationAddress().As16()) + } + for _, prefix := range w.options.Address { + if prefix.Contains(destination) { + return false + } + } + packetBuffer := stack.NewPacketBuffer(stack.PacketBufferOptions{ + Payload: buffer.MakeWithData(packet), + }) + w.endpoint.dispatcher.DeliverNetworkPacket(networkProtocol, packetBuffer) + packetBuffer.DecRef() + return true +} + +type deviceEndpoint struct { + mtu uint32 + done chan struct{} + device *device.Device + dispatcher stack.NetworkDispatcher +} + +func (ep *deviceEndpoint) MTU() uint32 { + return ep.mtu +} + +func (ep *deviceEndpoint) SetMTU(mtu uint32) { +} + +func (ep *deviceEndpoint) MaxHeaderLength() uint16 { + return 0 +} + +func (ep *deviceEndpoint) LinkAddress() tcpip.LinkAddress { + return "" +} + +func (ep *deviceEndpoint) SetLinkAddress(addr tcpip.LinkAddress) { +} + +func (ep *deviceEndpoint) Capabilities() stack.LinkEndpointCapabilities { + return stack.CapabilityRXChecksumOffload +} + +func (ep *deviceEndpoint) Attach(dispatcher stack.NetworkDispatcher) { + ep.dispatcher = dispatcher +} + +func (ep *deviceEndpoint) IsAttached() bool { + return ep.dispatcher != nil +} + +func (ep *deviceEndpoint) Wait() { +} + +func (ep *deviceEndpoint) ARPHardwareType() header.ARPHardwareType { + return header.ARPHardwareNone +} + +func (ep *deviceEndpoint) AddHeader(buffer *stack.PacketBuffer) { +} + +func (ep *deviceEndpoint) ParseHeader(ptr *stack.PacketBuffer) bool { + return true +} + +func (ep *deviceEndpoint) WritePackets(list stack.PacketBufferList) (int, tcpip.Error) { + for _, packetBuffer := range list.AsSlice() { + destination := packetBuffer.Network().DestinationAddress() + ep.device.InputPacket(destination.AsSlice(), packetBuffer.AsSlices()) + } + return list.Len(), nil +} + +func (ep *deviceEndpoint) Close() { +} + +func (ep *deviceEndpoint) SetOnCloseAction(f func()) { +} diff --git a/transport/wireguard/endpoint.go b/transport/wireguard/endpoint.go index 3c3ec7db5c..bdac39e8ba 100644 --- a/transport/wireguard/endpoint.go +++ b/transport/wireguard/endpoint.go @@ -1,35 +1,260 @@ package wireguard import ( + "context" + "encoding/base64" + "encoding/hex" + "fmt" + "net" "net/netip" + "os" + "strings" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" + F "github.com/sagernet/sing/common/format" + M "github.com/sagernet/sing/common/metadata" + "github.com/sagernet/sing/common/x/list" + "github.com/sagernet/sing/service" + "github.com/sagernet/sing/service/pause" "github.com/sagernet/wireguard-go/conn" + "github.com/sagernet/wireguard-go/device" + + "go4.org/netipx" ) -var _ conn.Endpoint = (*Endpoint)(nil) +type Endpoint struct { + options EndpointOptions + peers []peerConfig + ipcConf string + allowedAddress []netip.Prefix + tunDevice Device + device *device.Device + pauseManager pause.Manager + pauseCallback *list.Element[pause.Callback] +} + +func NewEndpoint(options EndpointOptions) (*Endpoint, error) { + if options.PrivateKey == "" { + return nil, E.New("missing private key") + } + privateKeyBytes, err := base64.StdEncoding.DecodeString(options.PrivateKey) + if err != nil { + return nil, E.Cause(err, "decode private key") + } + privateKey := hex.EncodeToString(privateKeyBytes) + ipcConf := "private_key=" + privateKey + if options.ListenPort != 0 { + ipcConf += "\nlisten_port=" + F.ToString(options.ListenPort) + } + var peers []peerConfig + for peerIndex, rawPeer := range options.Peers { + peer := peerConfig{ + allowedIPs: rawPeer.AllowedIPs, + keepalive: rawPeer.PersistentKeepaliveInterval, + } + if rawPeer.Endpoint.Addr.IsValid() { + peer.endpoint = rawPeer.Endpoint.AddrPort() + } else if rawPeer.Endpoint.IsFqdn() { + peer.destination = rawPeer.Endpoint + } + publicKeyBytes, err := base64.StdEncoding.DecodeString(rawPeer.PublicKey) + if err != nil { + return nil, E.Cause(err, "decode public key for peer ", peerIndex) + } + peer.publicKeyHex = hex.EncodeToString(publicKeyBytes) + if rawPeer.PreSharedKey != "" { + preSharedKeyBytes, err := base64.StdEncoding.DecodeString(rawPeer.PreSharedKey) + if err != nil { + return nil, E.Cause(err, "decode pre shared key for peer ", peerIndex) + } + peer.preSharedKeyHex = hex.EncodeToString(preSharedKeyBytes) + } + if len(rawPeer.AllowedIPs) == 0 { + return nil, E.New("missing allowed ips for peer ", peerIndex) + } + if len(rawPeer.Reserved) > 0 { + if len(rawPeer.Reserved) != 3 { + return nil, E.New("invalid reserved value for peer ", peerIndex, ", required 3 bytes, got ", len(peer.reserved)) + } + copy(peer.reserved[:], rawPeer.Reserved[:]) + } + peers = append(peers, peer) + } + var allowedPrefixBuilder netipx.IPSetBuilder + for _, peer := range options.Peers { + for _, prefix := range peer.AllowedIPs { + allowedPrefixBuilder.AddPrefix(prefix) + } + } + allowedIPSet, err := allowedPrefixBuilder.IPSet() + if err != nil { + return nil, err + } + allowedAddresses := allowedIPSet.Prefixes() + if options.MTU == 0 { + options.MTU = 1408 + } + deviceOptions := DeviceOptions{ + Context: options.Context, + Logger: options.Logger, + System: options.System, + Handler: options.Handler, + UDPTimeout: options.UDPTimeout, + CreateDialer: options.CreateDialer, + Name: options.Name, + MTU: options.MTU, + GSO: options.GSO, + Address: options.Address, + AllowedAddress: allowedAddresses, + } + tunDevice, err := NewDevice(deviceOptions) + if err != nil { + return nil, E.Cause(err, "create WireGuard device") + } + return &Endpoint{ + options: options, + peers: peers, + ipcConf: ipcConf, + allowedAddress: allowedAddresses, + tunDevice: tunDevice, + }, nil +} -type Endpoint netip.AddrPort +func (e *Endpoint) Start(resolve bool) error { + if common.Any(e.peers, func(peer peerConfig) bool { + return !peer.endpoint.IsValid() && peer.destination.IsFqdn() + }) { + if !resolve { + return nil + } + for peerIndex, peer := range e.peers { + if peer.endpoint.IsValid() || !peer.destination.IsFqdn() { + continue + } + destinationAddress, err := e.options.ResolvePeer(peer.destination.Fqdn) + if err != nil { + return E.Cause(err, "resolve endpoint domain for peer[", peerIndex, "]: ", peer.destination) + } + e.peers[peerIndex].endpoint = netip.AddrPortFrom(destinationAddress, peer.destination.Port) + } + } else if resolve { + return nil + } + var bind conn.Bind + wgListener, isWgListener := e.options.Dialer.(conn.Listener) + if isWgListener { + bind = conn.NewStdNetBind(wgListener) + } else { + var ( + isConnect bool + connectAddr netip.AddrPort + reserved [3]uint8 + ) + if len(e.peers) == 1 { + isConnect = true + connectAddr = e.peers[0].endpoint + reserved = e.peers[0].reserved + } + bind = NewClientBind(e.options.Context, e.options.Logger, e.options.Dialer, isConnect, connectAddr, reserved) + } + if isWgListener || len(e.peers) > 1 { + for _, peer := range e.peers { + if peer.reserved != [3]uint8{} { + bind.SetReservedForEndpoint(peer.endpoint, peer.reserved) + } + } + } + err := e.tunDevice.Start() + if err != nil { + return err + } + logger := &device.Logger{ + Verbosef: func(format string, args ...interface{}) { + e.options.Logger.Debug(fmt.Sprintf(strings.ToLower(format), args...)) + }, + Errorf: func(format string, args ...interface{}) { + e.options.Logger.Error(fmt.Sprintf(strings.ToLower(format), args...)) + }, + } + wgDevice := device.NewDevice(e.options.Context, e.tunDevice, bind, logger, e.options.Workers) + e.tunDevice.SetDevice(wgDevice) + ipcConf := e.ipcConf + for _, peer := range e.peers { + ipcConf += peer.GenerateIpcLines() + } + err = wgDevice.IpcSet(ipcConf) + if err != nil { + return E.Cause(err, "setup wireguard: \n", ipcConf) + } + e.device = wgDevice + e.pauseManager = service.FromContext[pause.Manager](e.options.Context) + if e.pauseManager != nil { + e.pauseCallback = e.pauseManager.RegisterCallback(e.onPauseUpdated) + } + return nil +} + +func (e *Endpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { + if !destination.Addr.IsValid() { + return nil, E.Cause(os.ErrInvalid, "invalid non-IP destination") + } + return e.tunDevice.DialContext(ctx, network, destination) +} -func (e Endpoint) ClearSrc() { +func (e *Endpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { + if !destination.Addr.IsValid() { + return nil, E.Cause(os.ErrInvalid, "invalid non-IP destination") + } + return e.tunDevice.ListenPacket(ctx, destination) } -func (e Endpoint) SrcToString() string { - return "" +func (e *Endpoint) BindUpdate() error { + return e.device.BindUpdate() } -func (e Endpoint) DstToString() string { - return (netip.AddrPort)(e).String() +func (e *Endpoint) Close() error { + if e.device != nil { + e.device.Close() + } + if e.pauseCallback != nil { + e.pauseManager.UnregisterCallback(e.pauseCallback) + } + return nil } -func (e Endpoint) DstToBytes() []byte { - b, _ := (netip.AddrPort)(e).MarshalBinary() - return b +func (e *Endpoint) onPauseUpdated(event int) { + switch event { + case pause.EventDevicePaused: + e.device.Down() + case pause.EventDeviceWake: + e.device.Up() + } } -func (e Endpoint) DstIP() netip.Addr { - return (netip.AddrPort)(e).Addr() +type peerConfig struct { + destination M.Socksaddr + endpoint netip.AddrPort + publicKeyHex string + preSharedKeyHex string + allowedIPs []netip.Prefix + keepalive uint16 + reserved [3]uint8 } -func (e Endpoint) SrcIP() netip.Addr { - return netip.Addr{} +func (c peerConfig) GenerateIpcLines() string { + ipcLines := "\npublic_key=" + c.publicKeyHex + if c.endpoint.IsValid() { + ipcLines += "\nendpoint=" + c.endpoint.String() + } + if c.preSharedKeyHex != "" { + ipcLines += "\npreshared_key=" + c.preSharedKeyHex + } + for _, allowedIP := range c.allowedIPs { + ipcLines += "\nallowed_ip=" + allowedIP.String() + } + if c.keepalive > 0 { + ipcLines += "\npersistent_keepalive_interval=" + F.ToString(c.keepalive) + } + return ipcLines } diff --git a/transport/wireguard/endpoint_options.go b/transport/wireguard/endpoint_options.go new file mode 100644 index 0000000000..d44422e334 --- /dev/null +++ b/transport/wireguard/endpoint_options.go @@ -0,0 +1,40 @@ +package wireguard + +import ( + "context" + "net/netip" + "time" + + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/logger" + M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" +) + +type EndpointOptions struct { + Context context.Context + Logger logger.ContextLogger + System bool + Handler tun.Handler + UDPTimeout time.Duration + Dialer N.Dialer + CreateDialer func(interfaceName string) N.Dialer + Name string + MTU uint32 + GSO bool + Address []netip.Prefix + PrivateKey string + ListenPort uint16 + ResolvePeer func(domain string) (netip.Addr, error) + Peers []PeerOptions + Workers int +} + +type PeerOptions struct { + Endpoint M.Socksaddr + PublicKey string + PreSharedKey string + AllowedIPs []netip.Prefix + PersistentKeepaliveInterval uint16 + Reserved []uint8 +} diff --git a/transport/wireguard/resolve.go b/transport/wireguard/resolve.go deleted file mode 100644 index d7a1d19c03..0000000000 --- a/transport/wireguard/resolve.go +++ /dev/null @@ -1,148 +0,0 @@ -package wireguard - -import ( - "context" - "encoding/base64" - "encoding/hex" - "net/netip" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/option" - "github.com/sagernet/sing-dns" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" -) - -type PeerConfig struct { - destination M.Socksaddr - domainStrategy dns.DomainStrategy - Endpoint netip.AddrPort - PublicKey string - PreSharedKey string - AllowedIPs []string - Reserved [3]uint8 -} - -func (c PeerConfig) GenerateIpcLines() string { - ipcLines := "\npublic_key=" + c.PublicKey - ipcLines += "\nendpoint=" + c.Endpoint.String() - if c.PreSharedKey != "" { - ipcLines += "\npreshared_key=" + c.PreSharedKey - } - for _, allowedIP := range c.AllowedIPs { - ipcLines += "\nallowed_ip=" + allowedIP - } - return ipcLines -} - -func ParsePeers(options option.WireGuardOutboundOptions) ([]PeerConfig, error) { - var peers []PeerConfig - if len(options.Peers) > 0 { - for peerIndex, rawPeer := range options.Peers { - peer := PeerConfig{ - AllowedIPs: rawPeer.AllowedIPs, - } - destination := rawPeer.ServerOptions.Build() - if destination.IsFqdn() { - peer.destination = destination - peer.domainStrategy = dns.DomainStrategy(options.DomainStrategy) - } else { - peer.Endpoint = destination.AddrPort() - } - { - bytes, err := base64.StdEncoding.DecodeString(rawPeer.PublicKey) - if err != nil { - return nil, E.Cause(err, "decode public key for peer ", peerIndex) - } - peer.PublicKey = hex.EncodeToString(bytes) - } - if rawPeer.PreSharedKey != "" { - bytes, err := base64.StdEncoding.DecodeString(rawPeer.PreSharedKey) - if err != nil { - return nil, E.Cause(err, "decode pre shared key for peer ", peerIndex) - } - peer.PreSharedKey = hex.EncodeToString(bytes) - } - if len(rawPeer.AllowedIPs) == 0 { - return nil, E.New("missing allowed_ips for peer ", peerIndex) - } - if len(rawPeer.Reserved) > 0 { - if len(rawPeer.Reserved) != 3 { - return nil, E.New("invalid reserved value for peer ", peerIndex, ", required 3 bytes, got ", len(peer.Reserved)) - } - copy(peer.Reserved[:], options.Reserved) - } - peers = append(peers, peer) - } - } else { - peer := PeerConfig{} - var ( - addressHas4 bool - addressHas6 bool - ) - for _, localAddress := range options.LocalAddress { - if localAddress.Addr().Is4() { - addressHas4 = true - } else { - addressHas6 = true - } - } - if addressHas4 { - peer.AllowedIPs = append(peer.AllowedIPs, netip.PrefixFrom(netip.IPv4Unspecified(), 0).String()) - } - if addressHas6 { - peer.AllowedIPs = append(peer.AllowedIPs, netip.PrefixFrom(netip.IPv6Unspecified(), 0).String()) - } - destination := options.ServerOptions.Build() - if destination.IsFqdn() { - peer.destination = destination - peer.domainStrategy = dns.DomainStrategy(options.DomainStrategy) - } else { - peer.Endpoint = destination.AddrPort() - } - { - bytes, err := base64.StdEncoding.DecodeString(options.PeerPublicKey) - if err != nil { - return nil, E.Cause(err, "decode peer public key") - } - peer.PublicKey = hex.EncodeToString(bytes) - } - if options.PreSharedKey != "" { - bytes, err := base64.StdEncoding.DecodeString(options.PreSharedKey) - if err != nil { - return nil, E.Cause(err, "decode pre shared key") - } - peer.PreSharedKey = hex.EncodeToString(bytes) - } - if len(options.Reserved) > 0 { - if len(options.Reserved) != 3 { - return nil, E.New("invalid reserved value, required 3 bytes, got ", len(peer.Reserved)) - } - copy(peer.Reserved[:], options.Reserved) - } - peers = append(peers, peer) - } - return peers, nil -} - -func ResolvePeers(ctx context.Context, router adapter.Router, peers []PeerConfig) error { - for peerIndex, peer := range peers { - if peer.Endpoint.IsValid() { - continue - } - destinationAddresses, err := router.Lookup(ctx, peer.destination.Fqdn, peer.domainStrategy) - if err != nil { - if len(peers) == 1 { - return E.Cause(err, "resolve endpoint domain") - } else { - return E.Cause(err, "resolve endpoint domain for peer ", peerIndex) - } - } - if len(destinationAddresses) == 0 { - return E.New("no addresses found for endpoint domain: ", peer.destination.Fqdn) - } - peers[peerIndex].Endpoint = netip.AddrPortFrom(destinationAddresses[0], peer.destination.Port) - - } - return nil -} From 5821b974bd44b0145ed840d1b687759bd60c19a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 23 Nov 2024 22:34:02 +0800 Subject: [PATCH 29/49] Fix lint --- adapter/handler.go | 3 + adapter/inbound.go | 3 +- adapter/upstream_legacy.go | 26 +++++- common/dialer/default_parallel_interface.go | 3 - common/mux/router.go | 10 ++- go.mod | 2 +- go.sum | 4 +- option/tun.go | 3 +- protocol/direct/inbound.go | 13 +-- protocol/direct/outbound.go | 4 + protocol/http/inbound.go | 30 +++---- protocol/hysteria/inbound.go | 4 + protocol/hysteria2/inbound.go | 4 + protocol/mixed/inbound.go | 4 - protocol/naive/inbound.go | 2 + protocol/redirect/redirect.go | 2 - protocol/redirect/tproxy.go | 2 - protocol/shadowsocks/inbound.go | 6 +- protocol/shadowsocks/inbound_multi.go | 7 ++ protocol/shadowsocks/inbound_relay.go | 7 ++ protocol/shadowtls/inbound.go | 2 + protocol/socks/inbound.go | 4 - protocol/tor/proxy.go | 24 +++--- protocol/trojan/inbound.go | 96 ++++++++++----------- protocol/tuic/inbound.go | 4 + protocol/tun/inbound.go | 20 ++--- protocol/vless/inbound.go | 58 ++++--------- protocol/vmess/inbound.go | 58 ++++--------- route/network.go | 2 +- route/route.go | 4 +- transport/trojan/mux.go | 22 +++-- transport/trojan/service.go | 29 +++---- 32 files changed, 230 insertions(+), 232 deletions(-) diff --git a/adapter/handler.go b/adapter/handler.go index 6da158ab20..f8912110f9 100644 --- a/adapter/handler.go +++ b/adapter/handler.go @@ -46,6 +46,9 @@ type PacketConnectionHandlerEx interface { NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata InboundContext, onClose N.CloseHandlerFunc) } +// Deprecated: use TCPConnectionHandlerEx instead +// +//nolint:staticcheck type UpstreamHandlerAdapter interface { N.TCPConnectionHandler N.UDPConnectionHandler diff --git a/adapter/inbound.go b/adapter/inbound.go index b4eaca3d4d..fd9c5405d4 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -65,7 +65,8 @@ type InboundContext struct { LastInbound string OriginDestination M.Socksaddr RouteOriginalDestination M.Socksaddr - // Deprecated + // Deprecated: to be removed + //nolint:staticcheck InboundOptions option.InboundOptions UDPDisableDomainUnmapping bool UDPConnect bool diff --git a/adapter/upstream_legacy.go b/adapter/upstream_legacy.go index 1c6c15c017..60f5fdb466 100644 --- a/adapter/upstream_legacy.go +++ b/adapter/upstream_legacy.go @@ -18,6 +18,8 @@ type ( ) // Deprecated +// +//nolint:staticcheck func NewUpstreamHandler( metadata InboundContext, connectionHandler ConnectionHandlerFunc, @@ -34,7 +36,9 @@ func NewUpstreamHandler( var _ UpstreamHandlerAdapter = (*myUpstreamHandlerWrapper)(nil) -// Deprecated +// Deprecated: use myUpstreamHandlerWrapperEx instead. +// +//nolint:staticcheck type myUpstreamHandlerWrapper struct { metadata InboundContext connectionHandler ConnectionHandlerFunc @@ -42,6 +46,7 @@ type myUpstreamHandlerWrapper struct { errorHandler E.Handler } +// Deprecated: use myUpstreamHandlerWrapperEx instead. func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { myMetadata := w.metadata if metadata.Source.IsValid() { @@ -53,6 +58,7 @@ func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.C return w.connectionHandler(ctx, conn, myMetadata) } +// Deprecated: use myUpstreamHandlerWrapperEx instead. func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { myMetadata := w.metadata if metadata.Source.IsValid() { @@ -64,11 +70,12 @@ func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn return w.packetHandler(ctx, conn, myMetadata) } +// Deprecated: use myUpstreamHandlerWrapperEx instead. func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) { w.errorHandler.NewError(ctx, err) } -// Deprecated +// Deprecated: removed func UpstreamMetadata(metadata InboundContext) M.Metadata { return M.Metadata{ Source: metadata.Source, @@ -76,14 +83,14 @@ func UpstreamMetadata(metadata InboundContext) M.Metadata { } } -// Deprecated +// Deprecated: Use NewUpstreamContextHandlerEx instead. type myUpstreamContextHandlerWrapper struct { connectionHandler ConnectionHandlerFunc packetHandler PacketConnectionHandlerFunc errorHandler E.Handler } -// Deprecated +// Deprecated: Use NewUpstreamContextHandlerEx instead. func NewUpstreamContextHandler( connectionHandler ConnectionHandlerFunc, packetHandler PacketConnectionHandlerFunc, @@ -96,6 +103,7 @@ func NewUpstreamContextHandler( } } +// Deprecated: Use NewUpstreamContextHandlerEx instead. func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { myMetadata := ContextFrom(ctx) if metadata.Source.IsValid() { @@ -107,6 +115,7 @@ func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, con return w.connectionHandler(ctx, conn, *myMetadata) } +// Deprecated: Use NewUpstreamContextHandlerEx instead. func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { myMetadata := ContextFrom(ctx) if metadata.Source.IsValid() { @@ -118,6 +127,7 @@ func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Contex return w.packetHandler(ctx, conn, *myMetadata) } +// Deprecated: Use NewUpstreamContextHandlerEx instead. func (w *myUpstreamContextHandlerWrapper) NewError(ctx context.Context, err error) { w.errorHandler.NewError(ctx, err) } @@ -149,12 +159,15 @@ func NewRouteContextHandler( var _ UpstreamHandlerAdapter = (*routeHandlerWrapper)(nil) // Deprecated: Use ConnectionRouterEx instead. +// +//nolint:staticcheck type routeHandlerWrapper struct { metadata InboundContext router ConnectionRouter logger logger.ContextLogger } +// Deprecated: Use ConnectionRouterEx instead. func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { myMetadata := w.metadata if metadata.Source.IsValid() { @@ -166,6 +179,7 @@ func (w *routeHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, return w.router.RouteConnection(ctx, conn, myMetadata) } +// Deprecated: Use ConnectionRouterEx instead. func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { myMetadata := w.metadata if metadata.Source.IsValid() { @@ -177,6 +191,7 @@ func (w *routeHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.Pa return w.router.RoutePacketConnection(ctx, conn, myMetadata) } +// Deprecated: Use ConnectionRouterEx instead. func (w *routeHandlerWrapper) NewError(ctx context.Context, err error) { w.logger.ErrorContext(ctx, err) } @@ -189,6 +204,7 @@ type routeContextHandlerWrapper struct { logger logger.ContextLogger } +// Deprecated: Use ConnectionRouterEx instead. func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { myMetadata := ContextFrom(ctx) if metadata.Source.IsValid() { @@ -200,6 +216,7 @@ func (w *routeContextHandlerWrapper) NewConnection(ctx context.Context, conn net return w.router.RouteConnection(ctx, conn, *myMetadata) } +// Deprecated: Use ConnectionRouterEx instead. func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error { myMetadata := ContextFrom(ctx) if metadata.Source.IsValid() { @@ -211,6 +228,7 @@ func (w *routeContextHandlerWrapper) NewPacketConnection(ctx context.Context, co return w.router.RoutePacketConnection(ctx, conn, *myMetadata) } +// Deprecated: Use ConnectionRouterEx instead. func (w *routeContextHandlerWrapper) NewError(ctx context.Context, err error) { w.logger.ErrorContext(ctx, err) } diff --git a/common/dialer/default_parallel_interface.go b/common/dialer/default_parallel_interface.go index 71d9814bd2..37a1f79cab 100644 --- a/common/dialer/default_parallel_interface.go +++ b/common/dialer/default_parallel_interface.go @@ -149,9 +149,6 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene if len(primaryInterfaces)+len(fallbackInterfaces) == 0 { return nil, E.New("no available network interface") } - if fallbackDelay == 0 { - fallbackDelay = N.DefaultFallbackDelay - } var errors []error for _, primaryInterface := range primaryInterfaces { perNetListener := listener diff --git a/common/mux/router.go b/common/mux/router.go index 2d4e7705d9..ec78808600 100644 --- a/common/mux/router.go +++ b/common/mux/router.go @@ -41,10 +41,10 @@ func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.Conte NewStreamContext: func(ctx context.Context, conn net.Conn) context.Context { return log.ContextWithNewID(ctx) }, - Logger: logger, - Handler: adapter.NewRouteContextHandler(router, logger), - Padding: options.Padding, - Brutal: brutalOptions, + Logger: logger, + HandlerEx: adapter.NewRouteContextHandlerEx(router), + Padding: options.Padding, + Brutal: brutalOptions, }) if err != nil { return nil, err @@ -52,6 +52,7 @@ func NewRouterWithOptions(router adapter.ConnectionRouterEx, logger logger.Conte return &Router{router, service}, nil } +// Deprecated: Use RouteConnectionEx instead. func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { if metadata.Destination == mux.Destination { // TODO: check if WithContext is necessary @@ -61,6 +62,7 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad } } +// Deprecated: Use RoutePacketConnectionEx instead. func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { return r.router.RoutePacketConnection(ctx, conn, metadata) } diff --git a/go.mod b/go.mod index 3123b18d6c..79d467ce7d 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/sagernet/sing-shadowsocks2 v0.2.0 github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 github.com/sagernet/sing-tun v0.6.0-beta.7 - github.com/sagernet/sing-vmess v0.1.13 + github.com/sagernet/sing-vmess v0.2.0-beta.2 github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 github.com/sagernet/utls v1.6.7 github.com/sagernet/wireguard-go v0.0.1-beta.5 diff --git a/go.sum b/go.sum index 0c5c79aa71..b84a97f2b3 100644 --- a/go.sum +++ b/go.sum @@ -135,8 +135,8 @@ github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 h1:RPrpgAdkP5td0vLfS5ldvYosFjS github.com/sagernet/sing-shadowtls v0.2.0-alpha.2/go.mod h1:0j5XlzKxaWRIEjc1uiSKmVoWb0k+L9QgZVb876+thZA= github.com/sagernet/sing-tun v0.6.0-beta.7 h1:FCSX8oGBqb0H57AAvfGeeH/jMGYWCOg6XWkN/oeES+0= github.com/sagernet/sing-tun v0.6.0-beta.7/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE= -github.com/sagernet/sing-vmess v0.1.13 h1:/GSfD1Rt6/mVfE80WFHNBykNT7KJNWWmvcMP9DoElEs= -github.com/sagernet/sing-vmess v0.1.13/go.mod h1:D+g+lhv4iOk1Pn08pd3MtUd72khL5+wgEE3xVH8J+ow= +github.com/sagernet/sing-vmess v0.2.0-beta.2 h1:obAkAL35X7ql4RnGzDg4dBYIRpGXRKqcN4LyLZpZGSs= +github.com/sagernet/sing-vmess v0.2.0-beta.2/go.mod h1:HGhf9XUdeE2iOWrX0hQNFgXPbKyGlzpeYFyX0c/pykk= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8= diff --git a/option/tun.go b/option/tun.go index a7a0f6bc07..d729e341a1 100644 --- a/option/tun.go +++ b/option/tun.go @@ -35,7 +35,6 @@ type TunInboundOptions struct { IncludeAndroidUser badoption.Listable[int] `json:"include_android_user,omitempty"` IncludePackage badoption.Listable[string] `json:"include_package,omitempty"` ExcludePackage badoption.Listable[string] `json:"exclude_package,omitempty"` - EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` Stack string `json:"stack,omitempty"` Platform *TunPlatformOptions `json:"platform,omitempty"` @@ -53,6 +52,8 @@ type TunInboundOptions struct { Inet4RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"inet4_route_exclude_address,omitempty"` // Deprecated: merged to RouteExcludeAddress Inet6RouteExcludeAddress badoption.Listable[netip.Prefix] `json:"inet6_route_exclude_address,omitempty"` + // Deprecated: removed + EndpointIndependentNat bool `json:"endpoint_independent_nat,omitempty"` } type FwMark uint32 diff --git a/protocol/direct/inbound.go b/protocol/direct/inbound.go index c9594f9a7c..4b34e37c2e 100644 --- a/protocol/direct/inbound.go +++ b/protocol/direct/inbound.go @@ -95,6 +95,9 @@ func (i *Inbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { } func (i *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = i.Tag() + metadata.InboundType = i.Type() + metadata.Destination = M.SocksaddrFromNet(conn.LocalAddr()) switch i.overrideOption { case 1: metadata.Destination = i.overrideDestination @@ -105,11 +108,9 @@ func (i *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a case 3: metadata.Destination.Port = i.overrideDestination.Port } - i.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) - metadata.Inbound = i.Tag() - metadata.InboundType = i.Type() - metadata.InboundDetour = i.listener.ListenOptions().Detour - metadata.InboundOptions = i.listener.ListenOptions().InboundOptions + if i.overrideOption != 0 { + i.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) + } i.router.RouteConnectionEx(ctx, conn, metadata, onClose) } @@ -119,7 +120,9 @@ func (i *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, var metadata adapter.InboundContext metadata.Inbound = i.Tag() metadata.InboundType = i.Type() + //nolint:staticcheck metadata.InboundDetour = i.listener.ListenOptions().Detour + //nolint:staticcheck metadata.InboundOptions = i.listener.ListenOptions().InboundOptions metadata.Source = source metadata.Destination = destination diff --git a/protocol/direct/outbound.go b/protocol/direct/outbound.go index 4962ea7025..42fd284de5 100644 --- a/protocol/direct/outbound.go +++ b/protocol/direct/outbound.go @@ -63,9 +63,11 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL dialer: outboundDialer, // loopBack: newLoopBackDetector(router), } + //nolint:staticcheck if options.ProxyProtocol != 0 { return nil, E.New("Proxy Protocol is deprecated and removed in sing-box 1.6.0") } + //nolint:staticcheck if options.OverrideAddress != "" && options.OverridePort != 0 { outbound.overrideOption = 1 outbound.overrideDestination = M.ParseSocksaddrHostPort(options.OverrideAddress, options.OverridePort) @@ -161,6 +163,7 @@ func (h *Outbound) DialParallel(ctx context.Context, network string, destination if h.domainStrategy != dns.DomainStrategyAsIS { domainStrategy = h.domainStrategy } else { + //nolint:staticcheck domainStrategy = dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) } switch domainStrategy { @@ -200,6 +203,7 @@ func (h *Outbound) DialParallelNetwork(ctx context.Context, network string, dest if h.domainStrategy != dns.DomainStrategyAsIS { domainStrategy = h.domainStrategy } else { + //nolint:staticcheck domainStrategy = dns.DomainStrategy(metadata.InboundOptions.DomainStrategy) } switch domainStrategy { diff --git a/protocol/http/inbound.go b/protocol/http/inbound.go index a7a463f76b..8aa1d4159a 100644 --- a/protocol/http/inbound.go +++ b/protocol/http/inbound.go @@ -82,33 +82,25 @@ func (h *Inbound) Close() error { } func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - err := h.newConnection(ctx, conn, metadata, onClose) - N.CloseOnHandshakeFailure(conn, onClose, err) - if err != nil { - if E.IsClosedOrCanceled(err) { - h.logger.DebugContext(ctx, "connection closed: ", err) - } else { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) - } - } -} - -func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error { - var err error if h.tlsConfig != nil { - conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig) + tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig) if err != nil { - return err + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake")) + return } + conn = tlsConn + } + err := http.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) + if err != nil { + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) } - return http.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) } func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions user, loaded := auth.UserFromContext[string](ctx) if !loaded { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) @@ -123,8 +115,6 @@ func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions user, loaded := auth.UserFromContext[string](ctx) if !loaded { h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) diff --git a/protocol/hysteria/inbound.go b/protocol/hysteria/inbound.go index 6fa03479a2..872e4dd770 100644 --- a/protocol/hysteria/inbound.go +++ b/protocol/hysteria/inbound.go @@ -123,7 +123,9 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.S var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() + //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.OriginDestination = h.listener.UDPAddr() metadata.Source = source @@ -144,7 +146,9 @@ func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() + //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.OriginDestination = h.listener.UDPAddr() metadata.Source = source diff --git a/protocol/hysteria2/inbound.go b/protocol/hysteria2/inbound.go index a3260e55ee..8d00072cc4 100644 --- a/protocol/hysteria2/inbound.go +++ b/protocol/hysteria2/inbound.go @@ -134,7 +134,9 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.S var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() + //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.OriginDestination = h.listener.UDPAddr() metadata.Source = source @@ -155,7 +157,9 @@ func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() + //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.OriginDestination = h.listener.UDPAddr() metadata.Source = source diff --git a/protocol/mixed/inbound.go b/protocol/mixed/inbound.go index ad7bed7de2..d1d343a991 100644 --- a/protocol/mixed/inbound.go +++ b/protocol/mixed/inbound.go @@ -94,8 +94,6 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions user, loaded := auth.UserFromContext[string](ctx) if !loaded { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) @@ -110,8 +108,6 @@ func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions user, loaded := auth.UserFromContext[string](ctx) if !loaded { h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) diff --git a/protocol/naive/inbound.go b/protocol/naive/inbound.go index 18acd2accd..5e77af435c 100644 --- a/protocol/naive/inbound.go +++ b/protocol/naive/inbound.go @@ -195,7 +195,9 @@ func (n *Inbound) newConnection(ctx context.Context, waitForClose bool, conn net var metadata adapter.InboundContext metadata.Inbound = n.Tag() metadata.InboundType = n.Type() + //nolint:staticcheck metadata.InboundDetour = n.listener.ListenOptions().Detour + //nolint:staticcheck metadata.InboundOptions = n.listener.ListenOptions().InboundOptions metadata.Source = source metadata.Destination = destination diff --git a/protocol/redirect/redirect.go b/protocol/redirect/redirect.go index 0950d102b9..e04db8c4df 100644 --- a/protocol/redirect/redirect.go +++ b/protocol/redirect/redirect.go @@ -62,8 +62,6 @@ func (h *Redirect) NewConnectionEx(ctx context.Context, conn net.Conn, metadata } metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.Destination = M.SocksaddrFromNetIP(destination) h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) h.router.RouteConnectionEx(ctx, conn, metadata, onClose) diff --git a/protocol/redirect/tproxy.go b/protocol/redirect/tproxy.go index 78f0079fde..7860e173d6 100644 --- a/protocol/redirect/tproxy.go +++ b/protocol/redirect/tproxy.go @@ -106,8 +106,6 @@ func (t *TProxy) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, s var metadata adapter.InboundContext metadata.Inbound = t.Tag() metadata.InboundType = t.Type() - metadata.InboundDetour = t.listener.ListenOptions().Detour - metadata.InboundOptions = t.listener.ListenOptions().InboundOptions metadata.Source = source metadata.Destination = destination metadata.OriginDestination = t.listener.UDPAddr() diff --git a/protocol/shadowsocks/inbound.go b/protocol/shadowsocks/inbound.go index 8332a93cc0..89af24ea43 100644 --- a/protocol/shadowsocks/inbound.go +++ b/protocol/shadowsocks/inbound.go @@ -104,6 +104,7 @@ func (h *Inbound) Close() error { return h.listener.Close() } +//nolint:staticcheck func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) N.CloseOnHandshakeFailure(conn, onClose, err) @@ -116,6 +117,7 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a } } +//nolint:staticcheck func (h *Inbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { err := h.service.NewPacket(h.ctx, &stubPacketConn{h.listener.PacketWriter()}, buffer, M.Metadata{Source: source}) if err != nil { @@ -127,8 +129,6 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions return h.router.RouteConnection(ctx, conn, metadata) } @@ -138,8 +138,6 @@ func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, me h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions return h.router.RoutePacketConnection(ctx, conn, metadata) } diff --git a/protocol/shadowsocks/inbound_multi.go b/protocol/shadowsocks/inbound_multi.go index ec55713430..5b6043657b 100644 --- a/protocol/shadowsocks/inbound_multi.go +++ b/protocol/shadowsocks/inbound_multi.go @@ -112,6 +112,7 @@ func (h *MultiInbound) Close() error { return h.listener.Close() } +//nolint:staticcheck func (h *MultiInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) N.CloseOnHandshakeFailure(conn, onClose, err) @@ -124,6 +125,7 @@ func (h *MultiInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metad } } +//nolint:staticcheck func (h *MultiInbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { err := h.service.NewPacket(h.ctx, &stubPacketConn{h.listener.PacketWriter()}, buffer, M.Metadata{Source: source}) if err != nil { @@ -145,7 +147,9 @@ func (h *MultiInbound) newConnection(ctx context.Context, conn net.Conn, metadat h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) metadata.Inbound = h.Tag() metadata.InboundType = h.Type() + //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck metadata.InboundOptions = h.listener.ListenOptions().InboundOptions return h.router.RouteConnection(ctx, conn, metadata) } @@ -166,11 +170,14 @@ func (h *MultiInbound) newPacketConnection(ctx context.Context, conn N.PacketCon h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) metadata.Inbound = h.Tag() metadata.InboundType = h.Type() + //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck metadata.InboundOptions = h.listener.ListenOptions().InboundOptions return h.router.RoutePacketConnection(ctx, conn, metadata) } +//nolint:staticcheck func (h *MultiInbound) NewError(ctx context.Context, err error) { NewError(h.logger, ctx, err) } diff --git a/protocol/shadowsocks/inbound_relay.go b/protocol/shadowsocks/inbound_relay.go index bb20de3f58..9760b2f0ec 100644 --- a/protocol/shadowsocks/inbound_relay.go +++ b/protocol/shadowsocks/inbound_relay.go @@ -97,6 +97,7 @@ func (h *RelayInbound) Close() error { return h.listener.Close() } +//nolint:staticcheck func (h *RelayInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { err := h.service.NewConnection(ctx, conn, adapter.UpstreamMetadata(metadata)) N.CloseOnHandshakeFailure(conn, onClose, err) @@ -109,6 +110,7 @@ func (h *RelayInbound) NewConnectionEx(ctx context.Context, conn net.Conn, metad } } +//nolint:staticcheck func (h *RelayInbound) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { err := h.service.NewPacket(h.ctx, &stubPacketConn{h.listener.PacketWriter()}, buffer, M.Metadata{Source: source}) if err != nil { @@ -130,7 +132,9 @@ func (h *RelayInbound) newConnection(ctx context.Context, conn net.Conn, metadat h.logger.InfoContext(ctx, "[", destination, "] inbound connection to ", metadata.Destination) metadata.Inbound = h.Tag() metadata.InboundType = h.Type() + //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck metadata.InboundOptions = h.listener.ListenOptions().InboundOptions return h.router.RouteConnection(ctx, conn, metadata) } @@ -151,11 +155,14 @@ func (h *RelayInbound) newPacketConnection(ctx context.Context, conn N.PacketCon h.logger.InfoContext(ctx, "[", destination, "] inbound packet connection to ", metadata.Destination) metadata.Inbound = h.Tag() metadata.InboundType = h.Type() + //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck metadata.InboundOptions = h.listener.ListenOptions().InboundOptions return h.router.RoutePacketConnection(ctx, conn, metadata) } +//nolint:staticcheck func (h *RelayInbound) NewError(ctx context.Context, err error) { NewError(h.logger, ctx, err) } diff --git a/protocol/shadowtls/inbound.go b/protocol/shadowtls/inbound.go index ce4238fd49..5ae5656f48 100644 --- a/protocol/shadowtls/inbound.go +++ b/protocol/shadowtls/inbound.go @@ -119,7 +119,9 @@ func (h *inboundHandler) NewConnectionEx(ctx context.Context, conn net.Conn, sou var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() + //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.Source = source metadata.Destination = destination diff --git a/protocol/socks/inbound.go b/protocol/socks/inbound.go index 115db30308..1490cee5cf 100644 --- a/protocol/socks/inbound.go +++ b/protocol/socks/inbound.go @@ -76,8 +76,6 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions user, loaded := auth.UserFromContext[string](ctx) if !loaded { h.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) @@ -92,8 +90,6 @@ func (h *Inbound) newUserConnection(ctx context.Context, conn net.Conn, metadata func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions user, loaded := auth.UserFromContext[string](ctx) if !loaded { h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) diff --git a/protocol/tor/proxy.go b/protocol/tor/proxy.go index ef60bd1f7e..1ed303350b 100644 --- a/protocol/tor/proxy.go +++ b/protocol/tor/proxy.go @@ -1,13 +1,13 @@ package tor import ( + std_bufio "bufio" "context" "crypto/rand" "encoding/hex" "net" "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" @@ -15,12 +15,14 @@ import ( M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/protocol/socks" + "github.com/sagernet/sing/service" ) type ProxyListener struct { ctx context.Context logger log.ContextLogger dialer N.Dialer + connection adapter.ConnectionManager tcpListener *net.TCPListener username string password string @@ -38,6 +40,7 @@ func NewProxyListener(ctx context.Context, logger log.ContextLogger, dialer N.Di ctx: ctx, logger: logger, dialer: dialer, + connection: service.FromContext[adapter.ConnectionManager](ctx), authenticator: auth.NewAuthenticator([]auth.User{{Username: username, Password: password}}), username: username, password: password, @@ -95,25 +98,24 @@ func (l *ProxyListener) acceptLoop() { } } -// TODO: migrate to new api -// -//nolint:staticcheck func (l *ProxyListener) accept(ctx context.Context, conn *net.TCPConn) error { - return socks.HandleConnection(ctx, conn, l.authenticator, l, M.Metadata{}) + return socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), l.authenticator, nil, l, M.SocksaddrFromNet(conn.RemoteAddr()), M.Socksaddr{}, nil) } -func (l *ProxyListener) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata M.Metadata) error { +func (l *ProxyListener) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { var metadata adapter.InboundContext + metadata.Source = source + metadata.Destination = destination metadata.Network = N.NetworkTCP - metadata.Destination = upstreamMetadata.Destination l.logger.InfoContext(ctx, "proxy connection to ", metadata.Destination) - return outbound.NewConnection(ctx, l.dialer, conn, metadata) + l.connection.NewConnection(ctx, l.dialer, conn, metadata, onClose) } -func (l *ProxyListener) NewPacketConnection(ctx context.Context, conn N.PacketConn, upstreamMetadata M.Metadata) error { +func (l *ProxyListener) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { var metadata adapter.InboundContext + metadata.Source = source + metadata.Destination = destination metadata.Network = N.NetworkUDP - metadata.Destination = upstreamMetadata.Destination l.logger.InfoContext(ctx, "proxy packet connection to ", metadata.Destination) - return outbound.NewPacketConnection(ctx, l.dialer, conn, metadata) + l.connection.NewPacketConnection(ctx, l.dialer, conn, metadata, onClose) } diff --git a/protocol/trojan/inbound.go b/protocol/trojan/inbound.go index 107667c580..ec95a81eab 100644 --- a/protocol/trojan/inbound.go +++ b/protocol/trojan/inbound.go @@ -56,7 +56,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } inbound.tlsConfig = tlsConfig } - var fallbackHandler N.TCPConnectionHandler + var fallbackHandler N.TCPConnectionHandlerEx if options.Fallback != nil && options.Fallback.Server != "" || len(options.FallbackForALPN) > 0 { if options.Fallback != nil && options.Fallback.Server != "" { inbound.fallbackAddr = options.Fallback.Build() @@ -78,9 +78,9 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } inbound.fallbackAddrTLSNextProto = fallbackAddrNextProto } - fallbackHandler = adapter.NewUpstreamContextHandler(inbound.fallbackConnection, nil, nil) + fallbackHandler = adapter.NewUpstreamContextHandlerEx(inbound.fallbackConnection, nil) } - service := trojan.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, nil), fallbackHandler, logger) + service := trojan.NewService[int](adapter.NewUpstreamContextHandlerEx(inbound.newConnection, inbound.newPacketConnection), fallbackHandler, logger) err := service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.TrojanUser) int { return index }), common.Map(options.Users, func(it option.TrojanUser) string { @@ -158,37 +158,30 @@ func (h *Inbound) Close() error { ) } -func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - var err error +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { if h.tlsConfig != nil && h.transport == nil { - conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig) + tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig) if err != nil { - return err + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake")) + return } + conn = tlsConn } - return h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, adapter.UpstreamMetadata(metadata)) -} - -func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - err := h.NewConnection(ctx, conn, metadata) - N.CloseOnHandshakeFailure(conn, onClose, err) + err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose) if err != nil { - if E.IsClosedOrCanceled(err) { - h.logger.DebugContext(ctx, "connection closed: ", err) - } else { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) - } + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) } } -func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { - return os.ErrInvalid + N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) + return } user := h.users[userIndex].Name if user == "" { @@ -197,53 +190,54 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada metadata.User = user } h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) - return h.router.RouteConnection(ctx, conn, metadata) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) +} + +func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + metadata.Inbound = h.Tag() + metadata.InboundType = h.Type() + userIndex, loaded := auth.UserFromContext[int](ctx) + if !loaded { + N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) + return + } + user := h.users[userIndex].Name + if user == "" { + user = F.ToString(userIndex) + } else { + metadata.User = user + } + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } -func (h *Inbound) fallbackConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) fallbackConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { var fallbackAddr M.Socksaddr if len(h.fallbackAddrTLSNextProto) > 0 { if tlsConn, loaded := common.Cast[tls.Conn](conn); loaded { connectionState := tlsConn.ConnectionState() if connectionState.NegotiatedProtocol != "" { if fallbackAddr, loaded = h.fallbackAddrTLSNextProto[connectionState.NegotiatedProtocol]; !loaded { - return E.New("fallback disabled for ALPN: ", connectionState.NegotiatedProtocol) + h.logger.DebugContext(ctx, "process connection from ", metadata.Source, ": fallback disabled for ALPN: ", connectionState.NegotiatedProtocol) + N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) + return } } } } if !fallbackAddr.IsValid() { if !h.fallbackAddr.IsValid() { - return E.New("fallback disabled by default") + h.logger.DebugContext(ctx, "process connection from ", metadata.Source, ": fallback disabled by default") + N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) + return } fallbackAddr = h.fallbackAddr } metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions - h.logger.InfoContext(ctx, "fallback connection to ", fallbackAddr) metadata.Destination = fallbackAddr - return h.router.RouteConnection(ctx, conn, metadata) -} - -func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - metadata.Inbound = h.Tag() - metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions - userIndex, loaded := auth.UserFromContext[int](ctx) - if !loaded { - return os.ErrInvalid - } - user := h.users[userIndex].Name - if user == "" { - user = F.ToString(userIndex) - } else { - metadata.User = user - } - h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) - return h.router.RoutePacketConnection(ctx, conn, metadata) + h.logger.InfoContext(ctx, "fallback connection to ", fallbackAddr) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil) @@ -254,6 +248,10 @@ func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net. var metadata adapter.InboundContext metadata.Source = source metadata.Destination = destination + //nolint:staticcheck + metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck + metadata.InboundOptions = h.listener.ListenOptions().InboundOptions h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) (*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose) } diff --git a/protocol/tuic/inbound.go b/protocol/tuic/inbound.go index a21b72ea5b..c4c632363f 100644 --- a/protocol/tuic/inbound.go +++ b/protocol/tuic/inbound.go @@ -105,7 +105,9 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.S var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() + //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.OriginDestination = h.listener.UDPAddr() metadata.Source = source @@ -126,7 +128,9 @@ func (h *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, var metadata adapter.InboundContext metadata.Inbound = h.Tag() metadata.InboundType = h.Type() + //nolint:staticcheck metadata.InboundDetour = h.listener.ListenOptions().Detour + //nolint:staticcheck metadata.InboundOptions = h.listener.ListenOptions().InboundOptions metadata.OriginDestination = h.listener.UDPAddr() metadata.Source = source diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index b3ada90550..6463f215ca 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -41,11 +41,9 @@ type Inbound struct { router adapter.Router networkManager adapter.NetworkManager logger log.ContextLogger - // Deprecated - inboundOptions option.InboundOptions - tunOptions tun.Options - // Deprecated - endpointIndependentNat bool + //nolint:staticcheck + inboundOptions option.InboundOptions + tunOptions tun.Options udpTimeout time.Duration stack string tunIf tun.Tun @@ -202,11 +200,10 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo ExcludePackage: options.ExcludePackage, InterfaceMonitor: networkManager.InterfaceMonitor(), }, - endpointIndependentNat: options.EndpointIndependentNat, - udpTimeout: udpTimeout, - stack: options.Stack, - platformInterface: service.FromContext[platform.Interface](ctx), - platformOptions: common.PtrValueOrDefault(options.Platform), + udpTimeout: udpTimeout, + stack: options.Stack, + platformInterface: service.FromContext[platform.Interface](ctx), + platformOptions: common.PtrValueOrDefault(options.Platform), } if options.AutoRedirect { if !options.AutoRoute { @@ -436,6 +433,7 @@ func (t *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, source M.S metadata.InboundType = C.TypeTun metadata.Source = source metadata.Destination = destination + //nolint:staticcheck metadata.InboundOptions = t.inboundOptions t.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) @@ -449,6 +447,7 @@ func (t *Inbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata.InboundType = C.TypeTun metadata.Source = source metadata.Destination = destination + //nolint:staticcheck metadata.InboundOptions = t.inboundOptions t.logger.InfoContext(ctx, "inbound packet connection from ", metadata.Source) t.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) @@ -464,6 +463,7 @@ func (t *autoRedirectHandler) NewConnectionEx(ctx context.Context, conn net.Conn metadata.InboundType = C.TypeTun metadata.Source = source metadata.Destination = destination + //nolint:staticcheck metadata.InboundOptions = t.inboundOptions t.logger.InfoContext(ctx, "inbound redirect connection from ", metadata.Source) t.logger.InfoContext(ctx, "inbound connection to ", metadata.Destination) diff --git a/protocol/vless/inbound.go b/protocol/vless/inbound.go index b0aa959e06..a977f73952 100644 --- a/protocol/vless/inbound.go +++ b/protocol/vless/inbound.go @@ -58,7 +58,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if err != nil { return nil, err } - service := vless.NewService[int](logger, adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound)) + service := vless.NewService[int](logger, adapter.NewUpstreamContextHandlerEx(inbound.newConnectionEx, inbound.newPacketConnectionEx)) service.UpdateUsers(common.MapIndexed(inbound.users, func(index int, _ option.VLESSUser) int { return index }), common.Map(inbound.users, func(it option.VLESSUser) string { @@ -138,37 +138,30 @@ func (h *Inbound) Close() error { ) } -func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - var err error +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { if h.tlsConfig != nil && h.transport == nil { - conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig) + tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig) if err != nil { - return err + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake")) + return } + conn = tlsConn } - return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata)) -} - -func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - err := h.NewConnection(ctx, conn, metadata) - N.CloseOnHandshakeFailure(conn, onClose, err) + err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose) if err != nil { - if E.IsClosedOrCanceled(err) { - h.logger.DebugContext(ctx, "connection closed: ", err) - } else { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) - } + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) } } -func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) newConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { - return os.ErrInvalid + N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) + return } user := h.users[userIndex].Name if user == "" { @@ -177,17 +170,16 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada metadata.User = user } h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) - return h.router.RouteConnection(ctx, conn, metadata) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } -func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { - return os.ErrInvalid + N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) + return } user := h.users[userIndex].Name if user == "" { @@ -202,7 +194,7 @@ func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, me } else { h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) } - return h.router.RoutePacketConnection(ctx, conn, metadata) + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil) @@ -216,17 +208,3 @@ func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net. h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) (*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose) } - -func (h *Inbound) NewError(ctx context.Context, err error) { - NewError(h.logger, ctx, err) -} - -// Deprecated: remove -func NewError(logger logger.ContextLogger, ctx context.Context, err error) { - common.Close(err) - if E.IsClosedOrCanceled(err) { - logger.DebugContext(ctx, "connection closed: ", err) - return - } - logger.ErrorContext(ctx, err) -} diff --git a/protocol/vmess/inbound.go b/protocol/vmess/inbound.go index 9f1009a301..0b8c96c697 100644 --- a/protocol/vmess/inbound.go +++ b/protocol/vmess/inbound.go @@ -65,7 +65,7 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if options.Transport != nil && options.Transport.Type != "" { serviceOptions = append(serviceOptions, vmess.ServiceWithDisableHeaderProtection()) } - service := vmess.NewService[int](adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound), serviceOptions...) + service := vmess.NewService[int](adapter.NewUpstreamContextHandlerEx(inbound.newConnectionEx, inbound.newPacketConnectionEx), serviceOptions...) inbound.service = service err = service.UpdateUsers(common.MapIndexed(options.Users, func(index int, it option.VMessUser) int { return index @@ -152,37 +152,30 @@ func (h *Inbound) Close() error { ) } -func (h *Inbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { - var err error +func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { if h.tlsConfig != nil && h.transport == nil { - conn, err = tls.ServerHandshake(ctx, conn, h.tlsConfig) + tlsConn, err := tls.ServerHandshake(ctx, conn, h.tlsConfig) if err != nil { - return err + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source, ": TLS handshake")) + return } + conn = tlsConn } - return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata)) -} - -func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - err := h.NewConnection(ctx, conn, metadata) - N.CloseOnHandshakeFailure(conn, onClose, err) + err := h.service.NewConnection(adapter.WithContext(ctx, &metadata), conn, metadata.Source, onClose) if err != nil { - if E.IsClosedOrCanceled(err) { - h.logger.DebugContext(ctx, "connection closed: ", err) - } else { - h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) - } + N.CloseOnHandshakeFailure(conn, onClose, err) + h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) } } -func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (h *Inbound) newConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { - return os.ErrInvalid + N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) + return } user := h.users[userIndex].Name if user == "" { @@ -191,17 +184,16 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada metadata.User = user } h.logger.InfoContext(ctx, "[", user, "] inbound connection to ", metadata.Destination) - return h.router.RouteConnection(ctx, conn, metadata) + h.router.RouteConnectionEx(ctx, conn, metadata, onClose) } -func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (h *Inbound) newPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Inbound = h.Tag() metadata.InboundType = h.Type() - metadata.InboundDetour = h.listener.ListenOptions().Detour - metadata.InboundOptions = h.listener.ListenOptions().InboundOptions userIndex, loaded := auth.UserFromContext[int](ctx) if !loaded { - return os.ErrInvalid + N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid) + return } user := h.users[userIndex].Name if user == "" { @@ -216,7 +208,7 @@ func (h *Inbound) newPacketConnection(ctx context.Context, conn N.PacketConn, me } else { h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) } - return h.router.RoutePacketConnection(ctx, conn, metadata) + h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } var _ adapter.V2RayServerTransportHandler = (*inboundTransportHandler)(nil) @@ -230,17 +222,3 @@ func (h *inboundTransportHandler) NewConnectionEx(ctx context.Context, conn net. h.logger.InfoContext(ctx, "inbound connection from ", metadata.Source) (*Inbound)(h).NewConnectionEx(ctx, conn, metadata, onClose) } - -func (h *Inbound) NewError(ctx context.Context, err error) { - NewError(h.logger, ctx, err) -} - -// Deprecated: remove -func NewError(logger logger.ContextLogger, ctx context.Context, err error) { - common.Close(err) - if E.IsClosedOrCanceled(err) { - logger.DebugContext(ctx, "connection closed: ", err) - return - } - logger.ErrorContext(ctx, err) -} diff --git a/route/network.go b/route/network.go index 7cb3df8d1c..d11508b125 100644 --- a/route/network.go +++ b/route/network.go @@ -202,7 +202,7 @@ func (r *NetworkManager) Close() error { }) monitor.Finish() } - return nil + return err } func (r *NetworkManager) InterfaceFinder() control.InterfaceFinder { diff --git a/route/route.go b/route/route.go index a56016f93a..67eb2c691b 100644 --- a/route/route.go +++ b/route/route.go @@ -53,6 +53,7 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad return E.New("reject connection to ", metadata.Destination, " while device paused") } + //nolint:staticcheck if metadata.InboundDetour != "" { if metadata.LastInbound == metadata.InboundDetour { return E.New("routing loop on detour: ", metadata.InboundDetour) @@ -182,6 +183,7 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m if r.pauseManager.IsDevicePaused() { return E.New("reject packet connection to ", metadata.Destination, " while device paused") } + //nolint:staticcheck if metadata.InboundDetour != "" { if metadata.LastInbound == metadata.InboundDetour { return E.New("routing loop on detour: ", metadata.InboundDetour) @@ -281,7 +283,7 @@ func (r *Router) PreMatch(metadata adapter.InboundContext) error { if !isReject { return nil } - return rejectAction.Error(nil) + return rejectAction.Error(context.Background()) } func (r *Router) matchRule( diff --git a/transport/trojan/mux.go b/transport/trojan/mux.go index 0329bd40fe..72d5a77621 100644 --- a/transport/trojan/mux.go +++ b/transport/trojan/mux.go @@ -4,17 +4,19 @@ import ( std_bufio "bufio" "context" "net" + "os" "github.com/sagernet/sing/common/buf" "github.com/sagernet/sing/common/bufio" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" + N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/common/task" "github.com/sagernet/smux" ) -func HandleMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler, logger logger.ContextLogger) error { +func HandleMuxConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, handler Handler, logger logger.ContextLogger, onClose N.CloseHandlerFunc) error { session, err := smux.Server(conn, smuxConfig()) if err != nil { return err @@ -27,29 +29,32 @@ func HandleMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata if err != nil { return err } - go newMuxConnection(ctx, stream, metadata, handler, logger) + go newMuxConnection(ctx, stream, source, handler, logger) } }) group.Cleanup(func() { session.Close() + if onClose != nil { + onClose(os.ErrClosed) + } }) return group.Run(ctx) } -func newMuxConnection(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler, logger logger.ContextLogger) { - err := newMuxConnection0(ctx, conn, metadata, handler) +func newMuxConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, handler Handler, logger logger.ContextLogger) { + err := newMuxConnection0(ctx, conn, source, handler) if err != nil { logger.ErrorContext(ctx, E.Cause(err, "process trojan-go multiplex connection")) } } -func newMuxConnection0(ctx context.Context, conn net.Conn, metadata M.Metadata, handler Handler) error { +func newMuxConnection0(ctx context.Context, conn net.Conn, source M.Socksaddr, handler Handler) error { reader := std_bufio.NewReader(conn) command, err := reader.ReadByte() if err != nil { return E.Cause(err, "read command") } - metadata.Destination, err = M.SocksaddrSerializer.ReadAddrPort(reader) + destination, err := M.SocksaddrSerializer.ReadAddrPort(reader) if err != nil { return E.Cause(err, "read destination") } @@ -63,12 +68,13 @@ func newMuxConnection0(ctx context.Context, conn net.Conn, metadata M.Metadata, } switch command { case CommandTCP: - return handler.NewConnection(ctx, conn, metadata) + handler.NewConnectionEx(ctx, conn, source, destination, nil) case CommandUDP: - return handler.NewPacketConnection(ctx, &PacketConn{Conn: conn}, metadata) + handler.NewPacketConnectionEx(ctx, &PacketConn{Conn: conn}, source, destination, nil) default: return E.New("unknown command ", command) } + return nil } func smuxConfig() *smux.Config { diff --git a/transport/trojan/service.go b/transport/trojan/service.go index 978d737fe5..7f1803bb38 100644 --- a/transport/trojan/service.go +++ b/transport/trojan/service.go @@ -16,19 +16,19 @@ import ( ) type Handler interface { - N.TCPConnectionHandler - N.UDPConnectionHandler + N.TCPConnectionHandlerEx + N.UDPConnectionHandlerEx } type Service[K comparable] struct { users map[K][56]byte keys map[[56]byte]K handler Handler - fallbackHandler N.TCPConnectionHandler + fallbackHandler N.TCPConnectionHandlerEx logger logger.ContextLogger } -func NewService[K comparable](handler Handler, fallbackHandler N.TCPConnectionHandler, logger logger.ContextLogger) *Service[K] { +func NewService[K comparable](handler Handler, fallbackHandler N.TCPConnectionHandlerEx, logger logger.ContextLogger) *Service[K] { return &Service[K]{ users: make(map[K][56]byte), keys: make(map[[56]byte]K), @@ -59,19 +59,19 @@ func (s *Service[K]) UpdateUsers(userList []K, passwordList []string) error { return nil } -func (s *Service[K]) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error { +func (s *Service[K]) NewConnection(ctx context.Context, conn net.Conn, source M.Socksaddr, onClose N.CloseHandlerFunc) error { var key [KeyLength]byte n, err := conn.Read(key[:]) if err != nil { return err } else if n != KeyLength { - return s.fallback(ctx, conn, metadata, key[:n], E.New("bad request size")) + return s.fallback(ctx, conn, source, key[:n], E.New("bad request size"), onClose) } if user, loaded := s.keys[key]; loaded { ctx = auth.ContextWithUser(ctx, user) } else { - return s.fallback(ctx, conn, metadata, key[:], E.New("bad request")) + return s.fallback(ctx, conn, source, key[:], E.New("bad request"), onClose) } err = rw.SkipN(conn, 2) @@ -102,26 +102,25 @@ func (s *Service[K]) NewConnection(ctx context.Context, conn net.Conn, metadata return E.Cause(err, "skip crlf") } - metadata.Protocol = "trojan" - metadata.Destination = destination - switch command { case CommandTCP: - return s.handler.NewConnection(ctx, conn, metadata) + s.handler.NewConnectionEx(ctx, conn, source, destination, onClose) case CommandUDP: - return s.handler.NewPacketConnection(ctx, &PacketConn{Conn: conn}, metadata) + s.handler.NewPacketConnectionEx(ctx, &PacketConn{Conn: conn}, source, destination, onClose) // case CommandMux: default: - return HandleMuxConnection(ctx, conn, metadata, s.handler, s.logger) + return HandleMuxConnection(ctx, conn, source, s.handler, s.logger, onClose) } + return nil } -func (s *Service[K]) fallback(ctx context.Context, conn net.Conn, metadata M.Metadata, header []byte, err error) error { +func (s *Service[K]) fallback(ctx context.Context, conn net.Conn, source M.Socksaddr, header []byte, err error, onClose N.CloseHandlerFunc) error { if s.fallbackHandler == nil { return E.Extend(err, "fallback disabled") } conn = bufio.NewCachedConn(conn, buf.As(header).ToOwned()) - return s.fallbackHandler.NewConnection(ctx, conn, metadata) + s.fallbackHandler.NewConnectionEx(ctx, conn, source, M.Socksaddr{}, onClose) + return nil } type PacketConn struct { From 093e539d3d216e3c5ac5ddcfbe4e131d6890953f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 22 Nov 2024 17:17:01 +0800 Subject: [PATCH 30/49] Make GSO adaptive --- docs/configuration/dns/rule.zh.md | 2 +- docs/configuration/endpoint/wireguard.md | 13 ++-- docs/configuration/endpoint/wireguard.zh.md | 13 ++-- docs/configuration/inbound/tun.md | 77 +++++++++++---------- docs/configuration/inbound/tun.zh.md | 75 ++++++++++---------- docs/configuration/outbound/wireguard.md | 13 +++- docs/configuration/outbound/wireguard.zh.md | 13 +++- docs/configuration/route/rule.zh.md | 2 +- docs/deprecated.md | 6 ++ docs/deprecated.zh.md | 6 ++ docs/migration.md | 1 - docs/migration.zh.md | 1 - experimental/deprecated/constants.go | 34 +++++++-- experimental/deprecated/stderr.go | 2 +- option/tun.go | 3 +- option/wireguard.go | 1 - protocol/tun/inbound.go | 18 +++-- protocol/wireguard/endpoint.go | 3 - protocol/wireguard/outbound.go | 4 +- transport/wireguard/device.go | 1 - transport/wireguard/device_system.go | 11 +-- transport/wireguard/endpoint.go | 1 - transport/wireguard/endpoint_options.go | 1 - 23 files changed, 177 insertions(+), 124 deletions(-) diff --git a/docs/configuration/dns/rule.zh.md b/docs/configuration/dns/rule.zh.md index e904f8cd02..bf0a03e23a 100644 --- a/docs/configuration/dns/rule.zh.md +++ b/docs/configuration/dns/rule.zh.md @@ -379,7 +379,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. !!! failure "已在 sing-box 1.10.0 废弃" - `rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 移除。 + `rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 中被移除。 使规则集中的 `ip_cidr` 规则匹配源 IP。 diff --git a/docs/configuration/endpoint/wireguard.md b/docs/configuration/endpoint/wireguard.md index 2d792e0279..65bb692910 100644 --- a/docs/configuration/endpoint/wireguard.md +++ b/docs/configuration/endpoint/wireguard.md @@ -14,7 +14,6 @@ icon: material/new-box "system": false, "name": "", "mtu": 1408, - "gso": false, "address": [], "private_key": "", "listen_port": 10000, @@ -36,6 +35,10 @@ icon: material/new-box } ``` +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + ### Fields #### system @@ -54,14 +57,6 @@ WireGuard MTU. `1408` will be used by default. -#### gso - -!!! quote "" - - Only supported on Linux. - -Try to enable generic segmentation offload. - #### address ==Required== diff --git a/docs/configuration/endpoint/wireguard.zh.md b/docs/configuration/endpoint/wireguard.zh.md index 8941b63095..918e7cbfe3 100644 --- a/docs/configuration/endpoint/wireguard.zh.md +++ b/docs/configuration/endpoint/wireguard.zh.md @@ -14,7 +14,6 @@ icon: material/new-box "system": false, "name": "", "mtu": 1408, - "gso": false, "address": [], "private_key": "", "listen_port": 10000, @@ -36,6 +35,10 @@ icon: material/new-box } ``` +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + ### 字段 #### system_interface @@ -54,14 +57,6 @@ WireGuard MTU。 默认使用 1408。 -#### gso - -!!! quote "" - - 仅支持 Linux。 - -尝试启用通用分段卸载。 - #### address ==必填== diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index 51b8a10a3c..d82d451b2e 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -1,7 +1,11 @@ --- -icon: material/new-box +icon: material/alert-decagram --- +!!! quote "Changes in sing-box 1.11.0" + + :material-delete-alert: [gso](#gso) + !!! quote "Changes in sing-box 1.10.0" :material-plus: [address](#address) @@ -46,16 +50,7 @@ icon: material/new-box "172.18.0.1/30", "fdfe:dcba:9876::1/126" ], - // deprecated - "inet4_address": [ - "172.19.0.1/30" - ], - // deprecated - "inet6_address": [ - "fdfe:dcba:9876::1/126" - ], "mtu": 9000, - "gso": false, "auto_route": true, "iproute2_table_index": 2022, "iproute2_rule_index": 9000, @@ -69,28 +64,11 @@ icon: material/new-box "::/1", "8000::/1" ], - // deprecated - "inet4_route_address": [ - "0.0.0.0/1", - "128.0.0.0/1" - ], - // deprecated - "inet6_route_address": [ - "::/1", - "8000::/1" - ], + "route_exclude_address": [ "192.168.0.0/16", "fc00::/7" ], - // deprecated - "inet4_route_exclude_address": [ - "192.168.0.0/16" - ], - // deprecated - "inet6_route_exclude_address": [ - "fc00::/7" - ], "route_address_set": [ "geoip-cloudflare" ], @@ -137,8 +115,31 @@ icon: material/new-box "match_domain": [] } }, - ... - // Listen Fields + + // Deprecated + "gso": false, + "inet4_address": [ + "172.19.0.1/30" + ], + "inet6_address": [ + "fdfe:dcba:9876::1/126" + ], + "inet4_route_address": [ + "0.0.0.0/1", + "128.0.0.0/1" + ], + "inet6_route_address": [ + "::/1", + "8000::/1" + ], + "inet4_route_exclude_address": [ + "192.168.0.0/16" + ], + "inet6_route_exclude_address": [ + "fc00::/7" + ], + + ... // Listen Fields } ``` @@ -166,7 +167,7 @@ IPv4 and IPv6 prefix for the tun interface. !!! failure "Deprecated in sing-box 1.10.0" - `inet4_address` is merged to `address` and will be removed in sing-box 1.11.0. + `inet4_address` is merged to `address` and will be removed in sing-box 1.12.0. IPv4 prefix for the tun interface. @@ -174,7 +175,7 @@ IPv4 prefix for the tun interface. !!! failure "Deprecated in sing-box 1.10.0" - `inet6_address` is merged to `address` and will be removed in sing-box 1.11.0. + `inet6_address` is merged to `address` and will be removed in sing-box 1.12.0. IPv6 prefix for the tun interface. @@ -184,6 +185,10 @@ The maximum transmission unit. #### gso +!!! failure "Deprecated in sing-box 1.11.0" + + GSO has no advantages for transparent proxy scenarios, is deprecated and no longer works, and will be removed in sing-box 1.12.0. + !!! question "Since sing-box 1.8.0" !!! quote "" @@ -284,7 +289,7 @@ Use custom routes instead of default when `auto_route` is enabled. !!! failure "Deprecated in sing-box 1.10.0" -`inet4_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) +`inet4_route_address` is deprecated and will be removed in sing-box 1.12.0, please use [route_address](#route_address) instead. Use custom routes instead of default when `auto_route` is enabled. @@ -293,7 +298,7 @@ Use custom routes instead of default when `auto_route` is enabled. !!! failure "Deprecated in sing-box 1.10.0" -`inet6_route_address` is deprecated and will be removed in sing-box 1.11.0, please use [route_address](#route_address) +`inet6_route_address` is deprecated and will be removed in sing-box 1.12.0, please use [route_address](#route_address) instead. Use custom routes instead of default when `auto_route` is enabled. @@ -308,7 +313,7 @@ Exclude custom routes when `auto_route` is enabled. !!! failure "Deprecated in sing-box 1.10.0" -`inet4_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please +`inet4_route_exclude_address` is deprecated and will be removed in sing-box 1.12.0, please use [route_exclude_address](#route_exclude_address) instead. Exclude custom routes when `auto_route` is enabled. @@ -317,7 +322,7 @@ Exclude custom routes when `auto_route` is enabled. !!! failure "Deprecated in sing-box 1.10.0" -`inet6_route_exclude_address` is deprecated and will be removed in sing-box 1.11.0, please +`inet6_route_exclude_address` is deprecated and will be removed in sing-box 1.12.0, please use [route_exclude_address](#route_exclude_address) instead. Exclude custom routes when `auto_route` is enabled. diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index 0b8a6aa127..2b5752ba44 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -1,8 +1,12 @@ --- -icon: material/new-box +icon: material/alert-decagram --- -!!! quote "Changes in sing-box 1.10.0" +!!! quote "sing-box 1.11.0 中的更改" + + :material-delete-alert: [gso](#gso) + +!!! quote "sing-box 1.10.0 中的更改" :material-plus: [address](#address) :material-delete-clock: [inet4_address](#inet4_address) @@ -46,16 +50,7 @@ icon: material/new-box "172.18.0.1/30", "fdfe:dcba:9876::1/126" ], - // 已弃用 - "inet4_address": [ - "172.19.0.1/30" - ], - // 已弃用 - "inet6_address": [ - "fdfe:dcba:9876::1/126" - ], "mtu": 9000, - "gso": false, "auto_route": true, "iproute2_table_index": 2022, "iproute2_rule_index": 9000, @@ -69,28 +64,11 @@ icon: material/new-box "::/1", "8000::/1" ], - // 已弃用 - "inet4_route_address": [ - "0.0.0.0/1", - "128.0.0.0/1" - ], - // 已弃用 - "inet6_route_address": [ - "::/1", - "8000::/1" - ], + "route_exclude_address": [ "192.168.0.0/16", "fc00::/7" ], - // 已弃用 - "inet4_route_exclude_address": [ - "192.168.0.0/16" - ], - // 已弃用 - "inet6_route_exclude_address": [ - "fc00::/7" - ], "route_address_set": [ "geoip-cloudflare" ], @@ -137,6 +115,29 @@ icon: material/new-box "match_domain": [] } }, + + // 已弃用 + "gso": false, + "inet4_address": [ + "172.19.0.1/30" + ], + "inet6_address": [ + "fdfe:dcba:9876::1/126" + ], + "inet4_route_address": [ + "0.0.0.0/1", + "128.0.0.0/1" + ], + "inet6_route_address": [ + "::/1", + "8000::/1" + ], + "inet4_route_exclude_address": [ + "192.168.0.0/16" + ], + "inet6_route_exclude_address": [ + "fc00::/7" + ], ... // 监听字段 } @@ -168,7 +169,7 @@ tun 接口的 IPv4 和 IPv6 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet4_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除。 + `inet4_address` 已合并到 `address` 且将在 sing-box 1.12.0 中被移除。 ==必填== @@ -178,7 +179,7 @@ tun 接口的 IPv4 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet6_address` 已合并到 `address` 且将在 sing-box 1.11.0 移除。 + `inet6_address` 已合并到 `address` 且将在 sing-box 1.12.0 中被移除。 tun 接口的 IPv6 前缀。 @@ -188,6 +189,10 @@ tun 接口的 IPv6 前缀。 #### gso +!!! failure "已在 sing-box 1.11.0 废弃" + + GSO 对于透明代理场景没有优势,已废弃和不再生效,且将在 sing-box 1.12.0 中被移除。 + !!! question "自 sing-box 1.8.0 起" !!! quote "" @@ -288,7 +293,7 @@ tun 接口的 IPv6 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet4_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除。 + `inet4_route_address` 已合并到 `route_address` 且将在 sing-box 1.12.0 中被移除。 启用 `auto_route` 时使用自定义路由而不是默认路由。 @@ -296,7 +301,7 @@ tun 接口的 IPv6 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet6_route_address` 已合并到 `route_address` 且将在 sing-box 1.11.0 移除。 + `inet6_route_address` 已合并到 `route_address` 且将在 sing-box 1.12.0 中被移除。 启用 `auto_route` 时使用自定义路由而不是默认路由。 @@ -310,7 +315,7 @@ tun 接口的 IPv6 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet4_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除。 + `inet4_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.12.0 中被移除。 启用 `auto_route` 时排除自定义路由。 @@ -318,7 +323,7 @@ tun 接口的 IPv6 前缀。 !!! failure "已在 sing-box 1.10.0 废弃" - `inet6_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.11.0 移除。 + `inet6_route_exclude_address` 已合并到 `route_exclude_address` 且将在 sing-box 1.12.0 中被移除。 启用 `auto_route` 时排除自定义路由。 diff --git a/docs/configuration/outbound/wireguard.md b/docs/configuration/outbound/wireguard.md index e3d2671a43..96c5dc7588 100644 --- a/docs/configuration/outbound/wireguard.md +++ b/docs/configuration/outbound/wireguard.md @@ -6,6 +6,10 @@ icon: material/delete-clock WireGuard outbound is deprecated and will be removed in sing-box 1.13.0, check [Migration](/migration/#migrate-wireguard-outbound-to-endpoint). +!!! quote "Changes in sing-box 1.11.0" + + :material-delete-alert: [gso](#gso) + !!! quote "Changes in sing-box 1.8.0" :material-plus: [gso](#gso) @@ -20,7 +24,6 @@ icon: material/delete-clock "server": "127.0.0.1", "server_port": 1080, "system_interface": false, - "gso": false, "interface_name": "wg0", "local_address": [ "10.0.0.1/32" @@ -45,6 +48,10 @@ icon: material/delete-clock "mtu": 1408, "network": "tcp", + // Deprecated + + "gso": false, + ... // Dial Fields } ``` @@ -77,6 +84,10 @@ Custom interface name for system interface. #### gso +!!! failure "Deprecated in sing-box 1.11.0" + + GSO will be automatically enabled when available since sing-box 1.11.0. + !!! question "Since sing-box 1.8.0" !!! quote "" diff --git a/docs/configuration/outbound/wireguard.zh.md b/docs/configuration/outbound/wireguard.zh.md index 63f2ddfd7d..c4e77c24f8 100644 --- a/docs/configuration/outbound/wireguard.zh.md +++ b/docs/configuration/outbound/wireguard.zh.md @@ -6,6 +6,10 @@ icon: material/delete-clock WireGuard 出站已被启用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-wireguard-outbound-to-endpoint)。 +!!! quote "sing-box 1.11.0 中的更改" + + :material-delete-alert: [gso](#gso) + !!! quote "sing-box 1.8.0 中的更改" :material-plus: [gso](#gso) @@ -20,7 +24,6 @@ icon: material/delete-clock "server": "127.0.0.1", "server_port": 1080, "system_interface": false, - "gso": false, "interface_name": "wg0", "local_address": [ "10.0.0.1/32" @@ -32,6 +35,10 @@ icon: material/delete-clock "workers": 4, "mtu": 1408, "network": "tcp", + + // 废弃的 + + "gso": false, ... // 拨号字段 } @@ -65,6 +72,10 @@ icon: material/delete-clock #### gso +!!! failure "已在 sing-box 1.11.0 废弃" + + 自 sing-box 1.11.0 起,GSO 将可用时自动启用。 + !!! question "自 sing-box 1.8.0 起" !!! quote "" diff --git a/docs/configuration/route/rule.zh.md b/docs/configuration/route/rule.zh.md index e5c5f01776..8deab2f332 100644 --- a/docs/configuration/route/rule.zh.md +++ b/docs/configuration/route/rule.zh.md @@ -388,7 +388,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`. !!! failure "已在 sing-box 1.10.0 废弃" - `rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 移除。 + `rule_set_ipcidr_match_source` 已重命名为 `rule_set_ip_cidr_match_source` 且将在 sing-box 1.11.0 中被移除。 使规则集中的 `ip_cidr` 规则匹配源 IP。 diff --git a/docs/deprecated.md b/docs/deprecated.md index b72ee11e7d..1868d95205 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -35,6 +35,12 @@ check [Migration](../migration/#migrate-wireguard-outbound-to-endpoint). Old outbound will be removed in sing-box 1.13.0. +#### GSO option in TUN + +GSO has no advantages for transparent proxy scenarios, is deprecated and no longer works in TUN. + +Old fields will be removed in sing-box 1.13.0. + ## 1.10.0 #### TUN address fields are merged diff --git a/docs/deprecated.zh.md b/docs/deprecated.zh.md index 220725a9af..2cda6386c6 100644 --- a/docs/deprecated.zh.md +++ b/docs/deprecated.zh.md @@ -34,6 +34,12 @@ WireGuard 出站已废弃且可以通过端点替代, 旧出站将在 sing-box 1.13.0 中被移除。 +#### TUN 的 GSO 字段 + +GSO 对透明代理场景没有优势,已废弃且在 TUN 中不再起作用。 + +旧字段将在 sing-box 1.13.0 中被移除。 + ## 1.10.0 #### Match source 规则项已重命名 diff --git a/docs/migration.md b/docs/migration.md index 480c0d50ce..c8b876f7cf 100644 --- a/docs/migration.md +++ b/docs/migration.md @@ -242,7 +242,6 @@ WireGuard outbound is deprecated and can be replaced by endpoint. "system": true, "name": "wg0", "mtu": 1408, - "gso": true, "address": [ "10.0.0.2/32" ], diff --git a/docs/migration.zh.md b/docs/migration.zh.md index f03f63b095..32be560427 100644 --- a/docs/migration.zh.md +++ b/docs/migration.zh.md @@ -243,7 +243,6 @@ WireGuard 出站已被弃用,且可以被端点替代。 "system": true, "name": "wg0", "mtu": 1408, - "gso": true, "address": [ "10.0.0.2/32" ], diff --git a/experimental/deprecated/constants.go b/experimental/deprecated/constants.go index 6eadea6a44..68aa9aca05 100644 --- a/experimental/deprecated/constants.go +++ b/experimental/deprecated/constants.go @@ -44,10 +44,17 @@ func (n Note) Message() string { } func (n Note) MessageWithLink() string { - return F.ToString( - n.Description, " is deprecated in sing-box ", n.DeprecatedVersion, - " and will be removed in sing-box ", n.ScheduledVersion, ", checkout documentation for migration: ", n.MigrationLink, - ) + if n.MigrationLink != "" { + return F.ToString( + n.Description, " is deprecated in sing-box ", n.DeprecatedVersion, + " and will be removed in sing-box ", n.ScheduledVersion, ", checkout documentation for migration: ", n.MigrationLink, + ) + } else { + return F.ToString( + n.Description, " is deprecated in sing-box ", n.DeprecatedVersion, + " and will be removed in sing-box ", n.ScheduledVersion, ".", + ) + } } var OptionBadMatchSource = Note{ @@ -122,6 +129,23 @@ var OptionWireGuardOutbound = Note{ MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-wireguard-outbound-to-endpoint", } +var OptionWireGuardGSO = Note{ + Name: "wireguard-gso", + Description: "GSO option in wireguard outbound", + DeprecatedVersion: "1.11.0", + ScheduledVersion: "1.13.0", + EnvName: "WIREGUARD_GSO", + MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-wireguard-outbound-to-endpoint", +} + +var OptionTUNGSO = Note{ + Name: "tun-gso", + Description: "GSO option in tun", + DeprecatedVersion: "1.11.0", + ScheduledVersion: "1.12.0", + EnvName: "TUN_GSO", +} + var Options = []Note{ OptionBadMatchSource, OptionGEOIP, @@ -131,4 +155,6 @@ var Options = []Note{ OptionInboundOptions, OptionDestinationOverrideFields, OptionWireGuardOutbound, + OptionWireGuardGSO, + OptionTUNGSO, } diff --git a/experimental/deprecated/stderr.go b/experimental/deprecated/stderr.go index 0826baf94b..f29b4754f4 100644 --- a/experimental/deprecated/stderr.go +++ b/experimental/deprecated/stderr.go @@ -34,5 +34,5 @@ func (f *stderrManager) ReportDeprecated(feature Note) { return } f.logger.Error(feature.MessageWithLink()) - f.logger.Fatal("to continuing using this feature, set ENABLE_DEPRECATED_" + feature.EnvName + "=true") + f.logger.Fatal("to continuing using this feature, set environment variable ENABLE_DEPRECATED_" + feature.EnvName + "=true") } diff --git a/option/tun.go b/option/tun.go index d729e341a1..9263dd1671 100644 --- a/option/tun.go +++ b/option/tun.go @@ -13,7 +13,6 @@ import ( type TunInboundOptions struct { InterfaceName string `json:"interface_name,omitempty"` MTU uint32 `json:"mtu,omitempty"` - GSO bool `json:"gso,omitempty"` Address badoption.Listable[netip.Prefix] `json:"address,omitempty"` AutoRoute bool `json:"auto_route,omitempty"` IPRoute2TableIndex int `json:"iproute2_table_index,omitempty"` @@ -40,6 +39,8 @@ type TunInboundOptions struct { Platform *TunPlatformOptions `json:"platform,omitempty"` InboundOptions + // Deprecated: removed + GSO bool `json:"gso,omitempty"` // Deprecated: merged to Address Inet4Address badoption.Listable[netip.Prefix] `json:"inet4_address,omitempty"` // Deprecated: merged to Address diff --git a/option/wireguard.go b/option/wireguard.go index 62ef332ae7..b9860d1199 100644 --- a/option/wireguard.go +++ b/option/wireguard.go @@ -10,7 +10,6 @@ type WireGuardEndpointOptions struct { System bool `json:"system,omitempty"` Name string `json:"name,omitempty"` MTU uint32 `json:"mtu,omitempty"` - GSO bool `json:"gso,omitempty"` Address badoption.Listable[netip.Prefix] `json:"address"` PrivateKey string `json:"private_key"` ListenPort uint16 `json:"listen_port,omitempty"` diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index 6463f215ca..26158d7a56 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -62,14 +62,14 @@ type Inbound struct { func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions) (adapter.Inbound, error) { address := options.Address var deprecatedAddressUsed bool + //nolint:staticcheck - //goland:noinspection GoDeprecation if len(options.Inet4Address) > 0 { address = append(address, options.Inet4Address...) deprecatedAddressUsed = true } + //nolint:staticcheck - //goland:noinspection GoDeprecation if len(options.Inet6Address) > 0 { address = append(address, options.Inet6Address...) deprecatedAddressUsed = true @@ -82,14 +82,14 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo }) routeAddress := options.RouteAddress + //nolint:staticcheck - //goland:noinspection GoDeprecation if len(options.Inet4RouteAddress) > 0 { routeAddress = append(routeAddress, options.Inet4RouteAddress...) deprecatedAddressUsed = true } + //nolint:staticcheck - //goland:noinspection GoDeprecation if len(options.Inet6RouteAddress) > 0 { routeAddress = append(routeAddress, options.Inet6RouteAddress...) deprecatedAddressUsed = true @@ -102,14 +102,14 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo }) routeExcludeAddress := options.RouteExcludeAddress + //nolint:staticcheck - //goland:noinspection GoDeprecation if len(options.Inet4RouteExcludeAddress) > 0 { routeExcludeAddress = append(routeExcludeAddress, options.Inet4RouteExcludeAddress...) deprecatedAddressUsed = true } + //nolint:staticcheck - //goland:noinspection GoDeprecation if len(options.Inet6RouteExcludeAddress) > 0 { routeExcludeAddress = append(routeExcludeAddress, options.Inet6RouteExcludeAddress...) deprecatedAddressUsed = true @@ -125,6 +125,11 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo deprecated.Report(ctx, deprecated.OptionTUNAddressX) } + //nolint:staticcheck + if options.GSO { + deprecated.Report(ctx, deprecated.OptionTUNGSO) + } + tunMTU := options.MTU if tunMTU == 0 { tunMTU = 9000 @@ -178,7 +183,6 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo tunOptions: tun.Options{ Name: options.InterfaceName, MTU: tunMTU, - GSO: options.GSO, Inet4Address: inet4Address, Inet6Address: inet6Address, AutoRoute: options.AutoRoute, diff --git a/protocol/wireguard/endpoint.go b/protocol/wireguard/endpoint.go index 5465099c02..dc40b61330 100644 --- a/protocol/wireguard/endpoint.go +++ b/protocol/wireguard/endpoint.go @@ -51,8 +51,6 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL } if options.Detour == "" { options.IsWireGuardListener = true - } else if options.GSO { - return nil, E.New("gso is conflict with detour") } outboundDialer, err := dialer.New(ctx, options.DialerOptions) if err != nil { @@ -72,7 +70,6 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL }, Name: options.Name, MTU: options.MTU, - GSO: options.GSO, Address: options.Address, PrivateKey: options.PrivateKey, ListenPort: options.ListenPort, diff --git a/protocol/wireguard/outbound.go b/protocol/wireguard/outbound.go index 0831c2d7cf..a1fce79610 100644 --- a/protocol/wireguard/outbound.go +++ b/protocol/wireguard/outbound.go @@ -42,6 +42,9 @@ type Outbound struct { func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.LegacyWireGuardOutboundOptions) (adapter.Outbound, error) { deprecated.Report(ctx, deprecated.OptionWireGuardOutbound) + if options.GSO { + deprecated.Report(ctx, deprecated.OptionWireGuardGSO) + } outbound := &Outbound{ Adapter: outbound.NewAdapterWithDialerOptions(C.TypeWireGuard, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions), ctx: ctx, @@ -89,7 +92,6 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL }, Name: options.InterfaceName, MTU: options.MTU, - GSO: options.GSO, Address: options.LocalAddress, PrivateKey: options.PrivateKey, ResolvePeer: func(domain string) (netip.Addr, error) { diff --git a/transport/wireguard/device.go b/transport/wireguard/device.go index d5d3b78151..7a17b8f35c 100644 --- a/transport/wireguard/device.go +++ b/transport/wireguard/device.go @@ -28,7 +28,6 @@ type DeviceOptions struct { CreateDialer func(interfaceName string) N.Dialer Name string MTU uint32 - GSO bool Address []netip.Prefix AllowedAddress []netip.Prefix } diff --git a/transport/wireguard/device_system.go b/transport/wireguard/device_system.go index 667443bab7..fa54f33293 100644 --- a/transport/wireguard/device_system.go +++ b/transport/wireguard/device_system.go @@ -12,7 +12,6 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-tun" "github.com/sagernet/sing/common" - E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" "github.com/sagernet/sing/service" @@ -64,7 +63,7 @@ func (w *systemDevice) Start() error { return it.Addr().Is6() }), MTU: w.options.MTU, - GSO: w.options.GSO, + GSO: true, InterfaceScope: true, Inet4RouteAddress: common.Filter(w.options.AllowedAddress, func(it netip.Prefix) bool { return it.Addr().Is4() @@ -88,12 +87,8 @@ func (w *systemDevice) Start() error { } w.options.Logger.Info("started at ", w.options.Name) w.device = tunInterface - if w.options.GSO { - batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN) - if !isBatchTUN { - tunInterface.Close() - return E.New("GSO is not supported on current platform") - } + batchTUN, isBatchTUN := tunInterface.(tun.LinuxTUN) + if isBatchTUN { w.batchDevice = batchTUN } w.events <- wgTun.EventUp diff --git a/transport/wireguard/endpoint.go b/transport/wireguard/endpoint.go index bdac39e8ba..69ce917020 100644 --- a/transport/wireguard/endpoint.go +++ b/transport/wireguard/endpoint.go @@ -104,7 +104,6 @@ func NewEndpoint(options EndpointOptions) (*Endpoint, error) { CreateDialer: options.CreateDialer, Name: options.Name, MTU: options.MTU, - GSO: options.GSO, Address: options.Address, AllowedAddress: allowedAddresses, } diff --git a/transport/wireguard/endpoint_options.go b/transport/wireguard/endpoint_options.go index d44422e334..bb9a46e69f 100644 --- a/transport/wireguard/endpoint_options.go +++ b/transport/wireguard/endpoint_options.go @@ -21,7 +21,6 @@ type EndpointOptions struct { CreateDialer func(interfaceName string) N.Dialer Name string MTU uint32 - GSO bool Address []netip.Prefix PrivateKey string ListenPort uint16 From 04cc343b2e9596329cee8bb2df1cf61d120b3267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 24 Nov 2024 14:45:40 +0800 Subject: [PATCH 31/49] Add UDP timeout route option --- adapter/inbound.go | 10 +- adapter/outbound/default.go | 157 --------------------- constant/protocol.go | 1 + constant/timeout.go | 17 ++- docs/configuration/route/rule_action.md | 25 +++- docs/configuration/route/rule_action.zh.md | 25 +++- option/rule_action.go | 5 +- option/wireguard.go | 2 +- protocol/dns/outbound.go | 15 +- protocol/group/selector.go | 59 ++++---- protocol/group/urltest.go | 22 ++- route/conn.go | 18 +++ route/route.go | 36 ++--- route/rule/rule_action.go | 2 + 14 files changed, 155 insertions(+), 239 deletions(-) delete mode 100644 adapter/outbound/default.go diff --git a/adapter/inbound.go b/adapter/inbound.go index fd9c5405d4..f5d5c95b8f 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -70,10 +70,12 @@ type InboundContext struct { InboundOptions option.InboundOptions UDPDisableDomainUnmapping bool UDPConnect bool - NetworkStrategy C.NetworkStrategy - NetworkType []C.InterfaceType - FallbackNetworkType []C.InterfaceType - FallbackDelay time.Duration + UDPTimeout time.Duration + + NetworkStrategy C.NetworkStrategy + NetworkType []C.InterfaceType + FallbackNetworkType []C.InterfaceType + FallbackDelay time.Duration DNSServer string diff --git a/adapter/outbound/default.go b/adapter/outbound/default.go deleted file mode 100644 index 573673f287..0000000000 --- a/adapter/outbound/default.go +++ /dev/null @@ -1,157 +0,0 @@ -package outbound - -import ( - "context" - "net" - "net/netip" - "os" - "time" - - "github.com/sagernet/sing-box/adapter" - "github.com/sagernet/sing-box/common/dialer" - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing/common" - "github.com/sagernet/sing/common/buf" - "github.com/sagernet/sing/common/bufio" - "github.com/sagernet/sing/common/canceler" - E "github.com/sagernet/sing/common/exceptions" - M "github.com/sagernet/sing/common/metadata" - N "github.com/sagernet/sing/common/network" -) - -func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext) error { - defer conn.Close() - ctx = adapter.WithContext(ctx, &metadata) - var outConn net.Conn - var err error - if len(metadata.DestinationAddresses) > 0 { - outConn, err = dialer.DialSerialNetwork(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) - } else { - outConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination) - } - if err != nil { - return N.ReportHandshakeFailure(conn, err) - } - err = N.ReportConnHandshakeSuccess(conn, outConn) - if err != nil { - outConn.Close() - return err - } - return CopyEarlyConn(ctx, conn, outConn) -} - -func NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata adapter.InboundContext) error { - defer conn.Close() - ctx = adapter.WithContext(ctx, &metadata) - var ( - outPacketConn net.PacketConn - outConn net.Conn - destinationAddress netip.Addr - err error - ) - if metadata.UDPConnect { - if len(metadata.DestinationAddresses) > 0 { - if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer { - outConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) - } else { - outConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses) - } - } else { - outConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination) - } - if err != nil { - return N.ReportHandshakeFailure(conn, err) - } - outPacketConn = bufio.NewUnbindPacketConn(outConn) - connRemoteAddr := M.AddrFromNet(outConn.RemoteAddr()) - if connRemoteAddr != metadata.Destination.Addr { - destinationAddress = connRemoteAddr - } - } else { - if len(metadata.DestinationAddresses) > 0 { - outPacketConn, destinationAddress, err = dialer.ListenSerialNetworkPacket(ctx, this, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) - } else { - outPacketConn, err = this.ListenPacket(ctx, metadata.Destination) - } - if err != nil { - return N.ReportHandshakeFailure(conn, err) - } - } - err = N.ReportPacketConnHandshakeSuccess(conn, outPacketConn) - if err != nil { - outPacketConn.Close() - return err - } - if destinationAddress.IsValid() { - var originDestination M.Socksaddr - if metadata.RouteOriginalDestination.IsValid() { - originDestination = metadata.RouteOriginalDestination - } else { - originDestination = metadata.Destination - } - if metadata.Destination != M.SocksaddrFrom(destinationAddress, metadata.Destination.Port) { - if metadata.UDPDisableDomainUnmapping { - outPacketConn = bufio.NewUnidirectionalNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), originDestination) - } else { - outPacketConn = bufio.NewNATPacketConn(bufio.NewPacketConn(outPacketConn), M.SocksaddrFrom(destinationAddress, metadata.Destination.Port), originDestination) - } - } - if natConn, loaded := common.Cast[bufio.NATPacketConn](conn); loaded { - natConn.UpdateDestination(destinationAddress) - } - } - switch metadata.Protocol { - case C.ProtocolSTUN: - ctx, conn = canceler.NewPacketConn(ctx, conn, C.STUNTimeout) - case C.ProtocolQUIC: - ctx, conn = canceler.NewPacketConn(ctx, conn, C.QUICTimeout) - case C.ProtocolDNS: - ctx, conn = canceler.NewPacketConn(ctx, conn, C.DNSTimeout) - } - return bufio.CopyPacketConn(ctx, conn, bufio.NewPacketConn(outPacketConn)) -} - -func CopyEarlyConn(ctx context.Context, conn net.Conn, serverConn net.Conn) error { - if cachedReader, isCached := conn.(N.CachedReader); isCached { - payload := cachedReader.ReadCached() - if payload != nil && !payload.IsEmpty() { - _, err := serverConn.Write(payload.Bytes()) - payload.Release() - if err != nil { - serverConn.Close() - return err - } - return bufio.CopyConn(ctx, conn, serverConn) - } - } - if earlyConn, isEarlyConn := common.Cast[N.EarlyConn](serverConn); isEarlyConn && earlyConn.NeedHandshake() { - payload := buf.NewPacket() - err := conn.SetReadDeadline(time.Now().Add(C.ReadPayloadTimeout)) - if err != os.ErrInvalid { - if err != nil { - payload.Release() - serverConn.Close() - return err - } - _, err = payload.ReadOnceFrom(conn) - if err != nil && !E.IsTimeout(err) { - payload.Release() - serverConn.Close() - return E.Cause(err, "read payload") - } - err = conn.SetReadDeadline(time.Time{}) - if err != nil { - payload.Release() - serverConn.Close() - return err - } - } - _, err = serverConn.Write(payload.Bytes()) - payload.Release() - if err != nil { - serverConn.Close() - return N.ReportHandshakeFailure(conn, err) - } - } - return bufio.CopyConn(ctx, conn, serverConn) -} diff --git a/constant/protocol.go b/constant/protocol.go index 1485408945..dbe16e51c4 100644 --- a/constant/protocol.go +++ b/constant/protocol.go @@ -10,6 +10,7 @@ const ( ProtocolDTLS = "dtls" ProtocolSSH = "ssh" ProtocolRDP = "rdp" + ProtocolNTP = "ntp" ) const ( diff --git a/constant/timeout.go b/constant/timeout.go index 67ae6f6644..3b5a452bed 100644 --- a/constant/timeout.go +++ b/constant/timeout.go @@ -9,8 +9,6 @@ const ( TCPTimeout = 15 * time.Second ReadPayloadTimeout = 300 * time.Millisecond DNSTimeout = 10 * time.Second - QUICTimeout = 30 * time.Second - STUNTimeout = 15 * time.Second UDPTimeout = 5 * time.Minute DefaultURLTestInterval = 3 * time.Minute DefaultURLTestIdleTimeout = 30 * time.Minute @@ -19,3 +17,18 @@ const ( FatalStopTimeout = 10 * time.Second FakeIPMetadataSaveInterval = 10 * time.Second ) + +var PortProtocols = map[uint16]string{ + 53: ProtocolDNS, + 123: ProtocolNTP, + 3478: ProtocolSTUN, + 443: ProtocolQUIC, +} + +var ProtocolTimeouts = map[string]time.Duration{ + ProtocolDNS: 10 * time.Second, + ProtocolNTP: 10 * time.Second, + ProtocolSTUN: 10 * time.Second, + ProtocolQUIC: 30 * time.Second, + ProtocolDTLS: 30 * time.Second, +} diff --git a/docs/configuration/route/rule_action.md b/docs/configuration/route/rule_action.md index 63e2b00b75..fae52e8540 100644 --- a/docs/configuration/route/rule_action.md +++ b/docs/configuration/route/rule_action.md @@ -41,7 +41,8 @@ See `route-options` fields below. "network_strategy": "", "fallback_delay": "", "udp_disable_domain_unmapping": false, - "udp_connect": false + "udp_connect": false, + "udp_timeout": "" } ``` @@ -86,6 +87,28 @@ do not support receiving UDP packets with domain addresses, such as Surge. If enabled, attempts to connect UDP connection to the destination instead of listen. +#### udp_timeout + +Timeout for UDP connections. + +Setting a larger value than the UDP timeout in inbounds will have no effect. + +Default value for protocol sniffed connections: + +| Timeout | Protocol | +|---------|----------------------| +| `10s` | `dns`, `ntp`, `stun` | +| `30s` | `quic`, `dtls` | + +If no protocol is sniffed, the following ports will be recognized as protocols by default: + +| Port | Protocol | +|------|----------| +| 53 | `dns` | +| 123 | `ntp` | +| 443 | `quic` | +| 3478 | `stun` | + ### reject ```json diff --git a/docs/configuration/route/rule_action.zh.md b/docs/configuration/route/rule_action.zh.md index 7959fced45..2f558f4e46 100644 --- a/docs/configuration/route/rule_action.zh.md +++ b/docs/configuration/route/rule_action.zh.md @@ -37,7 +37,8 @@ icon: material/new-box "network_strategy": "", "fallback_delay": "", "udp_disable_domain_unmapping": false, - "udp_connect": false + "udp_connect": false, + "udp_timeout": "" } ``` @@ -84,6 +85,28 @@ icon: material/new-box 如果启用,将尝试将 UDP 连接 connect 到目标而不是 listen。 +#### udp_timeout + +UDP 连接超时时间。 + +设置比入站 UDP 超时更大的值将无效。 + +已探测协议连接的默认值: + +| 超时 | 协议 | +|-------|----------------------| +| `10s` | `dns`, `ntp`, `stun` | +| `30s` | `quic`, `dtls` | + +如果没有探测到协议,以下端口将默认识别为协议: + +| 端口 | 协议 | +|------|--------| +| 53 | `dns` | +| 123 | `ntp` | +| 443 | `quic` | +| 3478 | `stun` | + ### reject ```json diff --git a/option/rule_action.go b/option/rule_action.go index ce3b92d9e9..29c5a0c30e 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -148,8 +148,9 @@ type RawRouteOptionsActionOptions struct { NetworkStrategy NetworkStrategy `json:"network_strategy,omitempty"` FallbackDelay uint32 `json:"fallback_delay,omitempty"` - UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` - UDPConnect bool `json:"udp_connect,omitempty"` + UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` + UDPConnect bool `json:"udp_connect,omitempty"` + UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"` } type RouteOptionsActionOptions RawRouteOptionsActionOptions diff --git a/option/wireguard.go b/option/wireguard.go index b9860d1199..43d3139c5e 100644 --- a/option/wireguard.go +++ b/option/wireguard.go @@ -14,7 +14,7 @@ type WireGuardEndpointOptions struct { PrivateKey string `json:"private_key"` ListenPort uint16 `json:"listen_port,omitempty"` Peers []WireGuardPeer `json:"peers,omitempty"` - UDPTimeout UDPTimeoutCompat `json:"udp_timeout,omitempty"` + UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"` Workers int `json:"workers,omitempty"` DialerOptions } diff --git a/protocol/dns/outbound.go b/protocol/dns/outbound.go index 3c493f80e3..5f06557ba4 100644 --- a/protocol/dns/outbound.go +++ b/protocol/dns/outbound.go @@ -42,20 +42,21 @@ func (d *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (n return nil, os.ErrInvalid } -// Deprecated -func (d *Outbound) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (d *Outbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { metadata.Destination = M.Socksaddr{} - defer conn.Close() for { conn.SetReadDeadline(time.Now().Add(C.DNSTimeout)) err := HandleStreamDNSRequest(ctx, d.router, conn, metadata) if err != nil { - return err + conn.Close() + if onClose != nil { + onClose(err) + } + return } } } -// Deprecated -func (d *Outbound) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { - return NewDNSPacketConnection(ctx, d.router, conn, nil, metadata) +func (d *Outbound) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { + NewDNSPacketConnection(ctx, d.router, conn, nil, metadata) } diff --git a/protocol/group/selector.go b/protocol/group/selector.go index 0bb3cd6644..9806e03354 100644 --- a/protocol/group/selector.go +++ b/protocol/group/selector.go @@ -10,6 +10,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common/atomic" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" @@ -21,17 +22,22 @@ func RegisterSelector(registry *outbound.Registry) { outbound.Register[option.SelectorOutboundOptions](registry, C.TypeSelector, NewSelector) } -var _ adapter.OutboundGroup = (*Selector)(nil) +var ( + _ adapter.OutboundGroup = (*Selector)(nil) + _ adapter.ConnectionHandlerEx = (*Selector)(nil) + _ adapter.PacketConnectionHandlerEx = (*Selector)(nil) +) type Selector struct { outbound.Adapter ctx context.Context - outboundManager adapter.OutboundManager + outbound adapter.OutboundManager + connection adapter.ConnectionManager logger logger.ContextLogger tags []string defaultTag string outbounds map[string]adapter.Outbound - selected adapter.Outbound + selected atomic.TypedValue[adapter.Outbound] interruptGroup *interrupt.Group interruptExternalConnections bool } @@ -40,7 +46,8 @@ func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextL outbound := &Selector{ Adapter: outbound.NewAdapter(C.TypeSelector, tag, nil, options.Outbounds), ctx: ctx, - outboundManager: service.FromContext[adapter.OutboundManager](ctx), + outbound: service.FromContext[adapter.OutboundManager](ctx), + connection: service.FromContext[adapter.ConnectionManager](ctx), logger: logger, tags: options.Outbounds, defaultTag: options.Default, @@ -55,15 +62,16 @@ func NewSelector(ctx context.Context, router adapter.Router, logger log.ContextL } func (s *Selector) Network() []string { - if s.selected == nil { + selected := s.selected.Load() + if selected == nil { return []string{N.NetworkTCP, N.NetworkUDP} } - return s.selected.Network() + return selected.Network() } func (s *Selector) Start() error { for i, tag := range s.tags { - detour, loaded := s.outboundManager.Outbound(tag) + detour, loaded := s.outbound.Outbound(tag) if !loaded { return E.New("outbound ", i, " not found: ", tag) } @@ -77,7 +85,7 @@ func (s *Selector) Start() error { if selected != "" { detour, loaded := s.outbounds[selected] if loaded { - s.selected = detour + s.selected.Store(detour) return nil } } @@ -89,16 +97,16 @@ func (s *Selector) Start() error { if !loaded { return E.New("default outbound not found: ", s.defaultTag) } - s.selected = detour + s.selected.Store(detour) return nil } - s.selected = s.outbounds[s.tags[0]] + s.selected.Store(s.outbounds[s.tags[0]]) return nil } func (s *Selector) Now() string { - selected := s.selected + selected := s.selected.Load() if selected == nil { return s.tags[0] } @@ -114,10 +122,9 @@ func (s *Selector) SelectOutbound(tag string) bool { if !loaded { return false } - if s.selected == detour { + if s.selected.Swap(detour) == detour { return true } - s.selected = detour if s.Tag() != "" { cacheFile := service.FromContext[adapter.CacheFile](s.ctx) if cacheFile != nil { @@ -132,7 +139,7 @@ func (s *Selector) SelectOutbound(tag string) bool { } func (s *Selector) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) { - conn, err := s.selected.DialContext(ctx, network, destination) + conn, err := s.selected.Load().DialContext(ctx, network, destination) if err != nil { return nil, err } @@ -140,32 +147,30 @@ func (s *Selector) DialContext(ctx context.Context, network string, destination } func (s *Selector) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - conn, err := s.selected.ListenPacket(ctx, destination) + conn, err := s.selected.Load().ListenPacket(ctx, destination) if err != nil { return nil, err } return s.interruptGroup.NewPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx)), nil } -// TODO -// Deprecated -func (s *Selector) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (s *Selector) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { ctx = interrupt.ContextWithIsExternalConnection(ctx) - if legacyHandler, ok := s.selected.(adapter.ConnectionHandler); ok { - return legacyHandler.NewConnection(ctx, conn, metadata) + selected := s.selected.Load() + if outboundHandler, isHandler := selected.(adapter.ConnectionHandlerEx); isHandler { + outboundHandler.NewConnectionEx(ctx, conn, metadata, onClose) } else { - return outbound.NewConnection(ctx, s.selected, conn, metadata) + s.connection.NewConnection(ctx, selected, conn, metadata, onClose) } } -// TODO -// Deprecated -func (s *Selector) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (s *Selector) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { ctx = interrupt.ContextWithIsExternalConnection(ctx) - if legacyHandler, ok := s.selected.(adapter.PacketConnectionHandler); ok { - return legacyHandler.NewPacketConnection(ctx, conn, metadata) + selected := s.selected.Load() + if outboundHandler, isHandler := selected.(adapter.PacketConnectionHandlerEx); isHandler { + outboundHandler.NewPacketConnectionEx(ctx, conn, metadata, onClose) } else { - return outbound.NewPacketConnection(ctx, s.selected, conn, metadata) + s.connection.NewPacketConnection(ctx, selected, conn, metadata, onClose) } } diff --git a/protocol/group/urltest.go b/protocol/group/urltest.go index fcada7dc3b..564c2373ea 100644 --- a/protocol/group/urltest.go +++ b/protocol/group/urltest.go @@ -36,7 +36,8 @@ type URLTest struct { outbound.Adapter ctx context.Context router adapter.Router - outboundManager adapter.OutboundManager + outbound adapter.OutboundManager + connection adapter.ConnectionManager logger log.ContextLogger tags []string link string @@ -52,7 +53,8 @@ func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLo Adapter: outbound.NewAdapter(C.TypeURLTest, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.Outbounds), ctx: ctx, router: router, - outboundManager: service.FromContext[adapter.OutboundManager](ctx), + outbound: service.FromContext[adapter.OutboundManager](ctx), + connection: service.FromContext[adapter.ConnectionManager](ctx), logger: logger, tags: options.Outbounds, link: options.URL, @@ -70,13 +72,13 @@ func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLo func (s *URLTest) Start() error { outbounds := make([]adapter.Outbound, 0, len(s.tags)) for i, tag := range s.tags { - detour, loaded := s.outboundManager.Outbound(tag) + detour, loaded := s.outbound.Outbound(tag) if !loaded { return E.New("outbound ", i, " not found: ", tag) } outbounds = append(outbounds, detour) } - group, err := NewURLTestGroup(s.ctx, s.outboundManager, s.logger, outbounds, s.link, s.interval, s.tolerance, s.idleTimeout, s.interruptExternalConnections) + group, err := NewURLTestGroup(s.ctx, s.outbound, s.logger, outbounds, s.link, s.interval, s.tolerance, s.idleTimeout, s.interruptExternalConnections) if err != nil { return err } @@ -160,18 +162,14 @@ func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (ne return nil, err } -// TODO -// Deprecated -func (s *URLTest) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { +func (s *URLTest) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { ctx = interrupt.ContextWithIsExternalConnection(ctx) - return outbound.NewConnection(ctx, s, conn, metadata) + s.connection.NewConnection(ctx, s, conn, metadata, onClose) } -// TODO -// Deprecated -func (s *URLTest) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { +func (s *URLTest) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { ctx = interrupt.ContextWithIsExternalConnection(ctx) - return outbound.NewPacketConnection(ctx, s, conn, metadata) + s.connection.NewPacketConnection(ctx, s, conn, metadata, onClose) } func (s *URLTest) InterfaceUpdated() { diff --git a/route/conn.go b/route/conn.go index 594379cc93..4a2192e0da 100644 --- a/route/conn.go +++ b/route/conn.go @@ -6,11 +6,14 @@ import ( "net" "net/netip" "sync/atomic" + "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/dialer" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/bufio" + "github.com/sagernet/sing/common/canceler" E "github.com/sagernet/sing/common/exceptions" "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" @@ -208,6 +211,21 @@ func (m *ConnectionManager) NewPacketConnection(ctx context.Context, this N.Dial natConn.UpdateDestination(destinationAddress) } } + var udpTimeout time.Duration + if metadata.UDPTimeout > 0 { + udpTimeout = metadata.UDPTimeout + } else { + protocol := metadata.Protocol + if protocol == "" { + protocol = C.PortProtocols[metadata.Destination.Port] + } + if protocol != "" { + udpTimeout = C.ProtocolTimeouts[protocol] + } + } + if udpTimeout > 0 { + ctx, conn = canceler.NewPacketConn(ctx, conn, udpTimeout) + } destination := bufio.NewPacketConn(remotePacketConn) var done atomic.Bool if ctx.Done() != nil { diff --git a/route/route.go b/route/route.go index 67eb2c691b..05e22c2514 100644 --- a/route/route.go +++ b/route/route.go @@ -132,23 +132,11 @@ func (r *Router) routeConnection(ctx context.Context, conn net.Conn, metadata ad if r.tracker != nil { conn = r.tracker.RoutedConnection(ctx, conn, metadata, selectedRule, selectedOutbound) } - legacyOutbound, isLegacy := selectedOutbound.(adapter.ConnectionHandler) - if isLegacy { - err = legacyOutbound.NewConnection(ctx, conn, metadata) - if err != nil { - conn.Close() - if onClose != nil { - onClose(err) - } - return E.Cause(err, F.ToString("outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]")) - } else { - if onClose != nil { - onClose(nil) - } - } - return nil + if outboundHandler, isHandler := selectedOutbound.(adapter.ConnectionHandlerEx); isHandler { + outboundHandler.NewConnectionEx(ctx, conn, metadata, onClose) + } else { + r.connection.NewConnection(ctx, selectedOutbound, conn, metadata, onClose) } - r.connection.NewConnection(ctx, selectedOutbound, conn, metadata, onClose) return nil } @@ -258,16 +246,11 @@ func (r *Router) routePacketConnection(ctx context.Context, conn N.PacketConn, m if metadata.FakeIP { conn = bufio.NewNATPacketConn(bufio.NewNetPacketConn(conn), metadata.OriginDestination, metadata.Destination) } - legacyOutbound, isLegacy := selectedOutbound.(adapter.PacketConnectionHandler) - if isLegacy { - err = legacyOutbound.NewPacketConnection(ctx, conn, metadata) - N.CloseOnHandshakeFailure(conn, onClose, err) - if err != nil { - return E.Cause(err, F.ToString("outbound/", selectedOutbound.Type(), "[", selectedOutbound.Tag(), "]")) - } - return nil + if outboundHandler, isHandler := selectedOutbound.(adapter.PacketConnectionHandlerEx); isHandler { + outboundHandler.NewPacketConnectionEx(ctx, conn, metadata, onClose) + } else { + r.connection.NewPacketConnection(ctx, selectedOutbound, conn, metadata, onClose) } - r.connection.NewPacketConnection(ctx, selectedOutbound, conn, metadata, onClose) return nil } @@ -440,6 +423,9 @@ match: if routeOptions.UDPConnect { metadata.UDPConnect = true } + if routeOptions.UDPTimeout > 0 { + metadata.UDPTimeout = routeOptions.UDPTimeout + } } switch action := currentRule.Action().(type) { case *rule.RuleActionSniff: diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index 1b4099c903..34354cc0d3 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -47,6 +47,7 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti FallbackDelay: time.Duration(action.RouteOptionsOptions.FallbackDelay), UDPDisableDomainUnmapping: action.RouteOptionsOptions.UDPDisableDomainUnmapping, UDPConnect: action.RouteOptionsOptions.UDPConnect, + UDPTimeout: time.Duration(action.RouteOptionsOptions.UDPTimeout), }, nil case C.RuleActionTypeDirect: directDialer, err := dialer.New(ctx, option.DialerOptions(action.DirectOptions)) @@ -152,6 +153,7 @@ type RuleActionRouteOptions struct { FallbackDelay time.Duration UDPDisableDomainUnmapping bool UDPConnect bool + UDPTimeout time.Duration } func (r *RuleActionRouteOptions) Type() string { From 22bda86bbf0be264da6f24db03cac55da97e3ca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Wed, 27 Nov 2024 18:08:19 +0800 Subject: [PATCH 32/49] Improve timeouts --- protocol/wireguard/endpoint.go | 8 +- route/conn.go | 270 ++++++++++++--------------------- route/conn_monitor.go | 128 ---------------- route/conn_monitor_test.go | 43 ------ 4 files changed, 104 insertions(+), 345 deletions(-) delete mode 100644 route/conn_monitor.go delete mode 100644 route/conn_monitor_test.go diff --git a/protocol/wireguard/endpoint.go b/protocol/wireguard/endpoint.go index dc40b61330..937f84dd9a 100644 --- a/protocol/wireguard/endpoint.go +++ b/protocol/wireguard/endpoint.go @@ -56,12 +56,18 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL if err != nil { return nil, err } + var udpTimeout time.Duration + if options.UDPTimeout != 0 { + udpTimeout = time.Duration(options.UDPTimeout) + } else { + udpTimeout = C.UDPTimeout + } wgEndpoint, err := wireguard.NewEndpoint(wireguard.EndpointOptions{ Context: ctx, Logger: logger, System: options.System, Handler: ep, - UDPTimeout: time.Duration(options.UDPTimeout), + UDPTimeout: udpTimeout, Dialer: outboundDialer, CreateDialer: func(interfaceName string) N.Dialer { return common.Must1(dialer.NewDefault(service.FromContext[adapter.NetworkManager](ctx), option.DialerOptions{ diff --git a/route/conn.go b/route/conn.go index 4a2192e0da..93ac33e359 100644 --- a/route/conn.go +++ b/route/conn.go @@ -5,6 +5,7 @@ import ( "io" "net" "net/netip" + "sync" "sync/atomic" "time" @@ -18,31 +19,35 @@ import ( "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/x/list" ) var _ adapter.ConnectionManager = (*ConnectionManager)(nil) type ConnectionManager struct { - logger logger.ContextLogger - monitor *ConnectionMonitor + logger logger.ContextLogger + access sync.Mutex + connections list.List[io.Closer] } func NewConnectionManager(logger logger.ContextLogger) *ConnectionManager { return &ConnectionManager{ - logger: logger, - monitor: NewConnectionMonitor(), + logger: logger, } } func (m *ConnectionManager) Start(stage adapter.StartStage) error { - if stage != adapter.StartStateInitialize { - return nil - } - return m.monitor.Start() + return nil } func (m *ConnectionManager) Close() error { - return m.monitor.Close() + m.access.Lock() + defer m.access.Unlock() + for element := m.connections.Front(); element != nil; element = element.Next() { + common.Close(element.Value) + } + m.connections.Init() + return nil } func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { @@ -57,95 +62,32 @@ func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, co remoteConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination) } if err != nil { + err = E.Cause(err, "open outbound connection") N.CloseOnHandshakeFailure(conn, onClose, err) - m.logger.ErrorContext(ctx, "open outbound connection: ", err) + m.logger.ErrorContext(ctx, err) return } err = N.ReportConnHandshakeSuccess(conn, remoteConn) if err != nil { + err = E.Cause(err, "report handshake success") remoteConn.Close() N.CloseOnHandshakeFailure(conn, onClose, err) - m.logger.ErrorContext(ctx, "report handshake success: ", err) + m.logger.ErrorContext(ctx, err) return } + m.access.Lock() + element := m.connections.PushBack(conn) + m.access.Unlock() + onClose = N.AppendClose(onClose, func(it error) { + m.access.Lock() + defer m.access.Unlock() + m.connections.Remove(element) + }) var done atomic.Bool - if ctx.Done() != nil { - onClose = N.AppendClose(onClose, m.monitor.Add(ctx, conn)) - } go m.connectionCopy(ctx, conn, remoteConn, false, &done, onClose) go m.connectionCopy(ctx, remoteConn, conn, true, &done, onClose) } -func (m *ConnectionManager) connectionCopy(ctx context.Context, source io.Reader, destination io.Writer, direction bool, done *atomic.Bool, onClose N.CloseHandlerFunc) { - originSource := source - originDestination := destination - var readCounters, writeCounters []N.CountFunc - for { - source, readCounters = N.UnwrapCountReader(source, readCounters) - destination, writeCounters = N.UnwrapCountWriter(destination, writeCounters) - if cachedSrc, isCached := source.(N.CachedReader); isCached { - cachedBuffer := cachedSrc.ReadCached() - if cachedBuffer != nil { - dataLen := cachedBuffer.Len() - _, err := destination.Write(cachedBuffer.Bytes()) - cachedBuffer.Release() - if err != nil { - m.logger.ErrorContext(ctx, "connection upload payload: ", err) - if done.Swap(true) { - if onClose != nil { - onClose(err) - } - } - common.Close(originSource, originDestination) - return - } - for _, counter := range readCounters { - counter(int64(dataLen)) - } - for _, counter := range writeCounters { - counter(int64(dataLen)) - } - } - continue - } - break - } - _, err := bufio.CopyWithCounters(destination, source, originSource, readCounters, writeCounters) - if err != nil { - common.Close(originSource, originDestination) - } else if duplexDst, isDuplex := destination.(N.WriteCloser); isDuplex { - err = duplexDst.CloseWrite() - if err != nil { - common.Close(originSource, originDestination) - } - } else { - common.Close(originDestination) - } - if done.Swap(true) { - if onClose != nil { - onClose(err) - } - common.Close(originSource, originDestination) - } - if !direction { - if err == nil { - m.logger.DebugContext(ctx, "connection upload finished") - } else if !E.IsClosedOrCanceled(err) { - m.logger.ErrorContext(ctx, "connection upload closed: ", err) - } else { - m.logger.TraceContext(ctx, "connection upload closed") - } - } else { - if err == nil { - m.logger.DebugContext(ctx, "connection download finished") - } else if !E.IsClosedOrCanceled(err) { - m.logger.ErrorContext(ctx, "connection download closed: ", err) - } else { - m.logger.TraceContext(ctx, "connection download closed") - } - } -} - func (m *ConnectionManager) NewPacketConnection(ctx context.Context, this N.Dialer, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { ctx = adapter.WithContext(ctx, &metadata) var ( @@ -227,58 +169,91 @@ func (m *ConnectionManager) NewPacketConnection(ctx context.Context, this N.Dial ctx, conn = canceler.NewPacketConn(ctx, conn, udpTimeout) } destination := bufio.NewPacketConn(remotePacketConn) + m.access.Lock() + element := m.connections.PushBack(conn) + m.access.Unlock() + onClose = N.AppendClose(onClose, func(it error) { + m.access.Lock() + defer m.access.Unlock() + m.connections.Remove(element) + }) var done atomic.Bool - if ctx.Done() != nil { - onClose = N.AppendClose(onClose, m.monitor.Add(ctx, conn)) - } go m.packetConnectionCopy(ctx, conn, destination, false, &done, onClose) go m.packetConnectionCopy(ctx, destination, conn, true, &done, onClose) } -func (m *ConnectionManager) packetConnectionCopy(ctx context.Context, source N.PacketReader, destination N.PacketWriter, direction bool, done *atomic.Bool, onClose N.CloseHandlerFunc) { - _, err := bufio.CopyPacket(destination, source) - /*var readCounters, writeCounters []N.CountFunc - var cachedPackets []*N.PacketBuffer +func (m *ConnectionManager) connectionCopy(ctx context.Context, source io.Reader, destination io.Writer, direction bool, done *atomic.Bool, onClose N.CloseHandlerFunc) { originSource := source + originDestination := destination + var readCounters, writeCounters []N.CountFunc for { - source, readCounters = N.UnwrapCountPacketReader(source, readCounters) - destination, writeCounters = N.UnwrapCountPacketWriter(destination, writeCounters) - if cachedReader, isCached := source.(N.CachedPacketReader); isCached { - packet := cachedReader.ReadCachedPacket() - if packet != nil { - cachedPackets = append(cachedPackets, packet) - continue + source, readCounters = N.UnwrapCountReader(source, readCounters) + destination, writeCounters = N.UnwrapCountWriter(destination, writeCounters) + if cachedSrc, isCached := source.(N.CachedReader); isCached { + cachedBuffer := cachedSrc.ReadCached() + if cachedBuffer != nil { + dataLen := cachedBuffer.Len() + _, err := destination.Write(cachedBuffer.Bytes()) + cachedBuffer.Release() + if err != nil { + if done.Swap(true) { + onClose(err) + } + common.Close(originSource, originDestination) + if !direction { + m.logger.ErrorContext(ctx, "connection upload payload: ", err) + } else { + m.logger.ErrorContext(ctx, "connection download payload: ", err) + } + return + } + for _, counter := range readCounters { + counter(int64(dataLen)) + } + for _, counter := range writeCounters { + counter(int64(dataLen)) + } } + continue } break } - var handled bool - if natConn, isNatConn := source.(udpnat.Conn); isNatConn { - natConn.SetHandler(&udpHijacker{ - ctx: ctx, - logger: m.logger, - source: natConn, - destination: destination, - direction: direction, - readCounters: readCounters, - writeCounters: writeCounters, - done: done, - onClose: onClose, - }) - handled = true - } - if cachedPackets != nil { - _, err := bufio.WritePacketWithPool(originSource, destination, cachedPackets, readCounters, writeCounters) + _, err := bufio.CopyWithCounters(destination, source, originSource, readCounters, writeCounters) + if err != nil { + common.Close(originDestination) + } else if duplexDst, isDuplex := destination.(N.WriteCloser); isDuplex { + err = duplexDst.CloseWrite() if err != nil { - common.Close(source, destination) - m.logger.ErrorContext(ctx, "packet upload payload: ", err) - return + common.Close(originSource, originDestination) } + } else { + common.Close(originDestination) } - if handled { - return + if done.Swap(true) { + onClose(err) + common.Close(originSource, originDestination) + } + if !direction { + if err == nil { + m.logger.DebugContext(ctx, "connection upload finished") + } else if !E.IsClosedOrCanceled(err) { + m.logger.ErrorContext(ctx, "connection upload closed: ", err) + } else { + m.logger.TraceContext(ctx, "connection upload closed") + } + } else { + if err == nil { + m.logger.DebugContext(ctx, "connection download finished") + } else if !E.IsClosedOrCanceled(err) { + m.logger.ErrorContext(ctx, "connection download closed: ", err) + } else { + m.logger.TraceContext(ctx, "connection download closed") + } } - _, err := bufio.CopyPacketWithCounters(destination, source, originSource, readCounters, writeCounters)*/ +} + +func (m *ConnectionManager) packetConnectionCopy(ctx context.Context, source N.PacketReader, destination N.PacketWriter, direction bool, done *atomic.Bool, onClose N.CloseHandlerFunc) { + _, err := bufio.CopyPacket(destination, source) if !direction { if E.IsClosedOrCanceled(err) { m.logger.TraceContext(ctx, "packet upload closed") @@ -293,58 +268,7 @@ func (m *ConnectionManager) packetConnectionCopy(ctx context.Context, source N.P } } if !done.Swap(true) { - if onClose != nil { - onClose(err) - } + onClose(err) } common.Close(source, destination) } - -/*type udpHijacker struct { - ctx context.Context - logger logger.ContextLogger - source io.Closer - destination N.PacketWriter - direction bool - readCounters []N.CountFunc - writeCounters []N.CountFunc - done *atomic.Bool - onClose N.CloseHandlerFunc -} - -func (u *udpHijacker) NewPacketEx(buffer *buf.Buffer, source M.Socksaddr) { - dataLen := buffer.Len() - for _, counter := range u.readCounters { - counter(int64(dataLen)) - } - err := u.destination.WritePacket(buffer, source) - if err != nil { - common.Close(u.source, u.destination) - u.logger.DebugContext(u.ctx, "packet upload closed: ", err) - return - } - for _, counter := range u.writeCounters { - counter(int64(dataLen)) - } -} - -func (u *udpHijacker) Close() error { - var err error - if !u.done.Swap(true) { - err = common.Close(u.source, u.destination) - if u.onClose != nil { - u.onClose(net.ErrClosed) - } - } - if u.direction { - u.logger.TraceContext(u.ctx, "packet download closed") - } else { - u.logger.TraceContext(u.ctx, "packet upload closed") - } - return err -} - -func (u *udpHijacker) Upstream() any { - return u.destination -} -*/ diff --git a/route/conn_monitor.go b/route/conn_monitor.go deleted file mode 100644 index 9e271b82a0..0000000000 --- a/route/conn_monitor.go +++ /dev/null @@ -1,128 +0,0 @@ -package route - -import ( - "context" - "io" - "reflect" - "sync" - "time" - - N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/common/x/list" -) - -type ConnectionMonitor struct { - access sync.RWMutex - reloadChan chan struct{} - connections list.List[*monitorEntry] -} - -type monitorEntry struct { - ctx context.Context - closer io.Closer -} - -func NewConnectionMonitor() *ConnectionMonitor { - return &ConnectionMonitor{ - reloadChan: make(chan struct{}, 1), - } -} - -func (m *ConnectionMonitor) Add(ctx context.Context, closer io.Closer) N.CloseHandlerFunc { - m.access.Lock() - defer m.access.Unlock() - element := m.connections.PushBack(&monitorEntry{ - ctx: ctx, - closer: closer, - }) - select { - case <-m.reloadChan: - return nil - default: - select { - case m.reloadChan <- struct{}{}: - default: - } - } - return func(it error) { - m.access.Lock() - defer m.access.Unlock() - m.connections.Remove(element) - select { - case <-m.reloadChan: - default: - select { - case m.reloadChan <- struct{}{}: - default: - } - } - } -} - -func (m *ConnectionMonitor) Start() error { - go m.monitor() - return nil -} - -func (m *ConnectionMonitor) Close() error { - m.access.Lock() - defer m.access.Unlock() - close(m.reloadChan) - for element := m.connections.Front(); element != nil; element = element.Next() { - element.Value.closer.Close() - } - return nil -} - -func (m *ConnectionMonitor) monitor() { - var ( - selectCases []reflect.SelectCase - elements []*list.Element[*monitorEntry] - ) - rootCase := reflect.SelectCase{ - Dir: reflect.SelectRecv, - Chan: reflect.ValueOf(m.reloadChan), - } - for { - m.access.RLock() - if m.connections.Len() == 0 { - m.access.RUnlock() - if _, loaded := <-m.reloadChan; !loaded { - return - } else { - continue - } - } - if len(elements) < m.connections.Len() { - elements = make([]*list.Element[*monitorEntry], 0, m.connections.Len()) - } - if len(selectCases) < m.connections.Len()+1 { - selectCases = make([]reflect.SelectCase, 0, m.connections.Len()+1) - } - elements = elements[:0] - selectCases = selectCases[:1] - selectCases[0] = rootCase - for element := m.connections.Front(); element != nil; element = element.Next() { - elements = append(elements, element) - selectCases = append(selectCases, reflect.SelectCase{ - Dir: reflect.SelectRecv, - Chan: reflect.ValueOf(element.Value.ctx.Done()), - }) - } - m.access.RUnlock() - selected, _, loaded := reflect.Select(selectCases) - if selected == 0 { - if !loaded { - return - } else { - time.Sleep(time.Second) - continue - } - } - element := elements[selected-1] - m.access.Lock() - m.connections.Remove(element) - m.access.Unlock() - element.Value.closer.Close() // maybe go close - } -} diff --git a/route/conn_monitor_test.go b/route/conn_monitor_test.go deleted file mode 100644 index a712bddcdb..0000000000 --- a/route/conn_monitor_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package route_test - -import ( - "context" - "sync" - "testing" - "time" - - "github.com/sagernet/sing-box/route" - - "github.com/stretchr/testify/require" -) - -func TestMonitor(t *testing.T) { - t.Parallel() - var closer myCloser - closer.Add(1) - monitor := route.NewConnectionMonitor() - require.NoError(t, monitor.Start()) - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - monitor.Add(ctx, &closer) - done := make(chan struct{}) - go func() { - closer.Wait() - close(done) - }() - select { - case <-done: - case <-time.After(time.Second + 100*time.Millisecond): - t.Fatal("timeout") - } - cancel() - require.NoError(t, monitor.Close()) -} - -type myCloser struct { - sync.WaitGroup -} - -func (c *myCloser) Close() error { - c.Done() - return nil -} From 11a448b52dbbec281b46ca7cb8eea58c09fa1643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 28 Nov 2024 22:39:19 +0800 Subject: [PATCH 33/49] hysteria2: Add more masquerade options --- constant/hysteria2.go | 7 ++ docs/configuration/inbound/hysteria2.md | 58 ++++++++++++-- docs/configuration/inbound/hysteria2.zh.md | 58 ++++++++++++-- docs/configuration/outbound/direct.md | 4 +- option/hysteria2.go | 90 +++++++++++++++++++++- protocol/hysteria2/inbound.go | 36 ++++++--- 6 files changed, 228 insertions(+), 25 deletions(-) create mode 100644 constant/hysteria2.go diff --git a/constant/hysteria2.go b/constant/hysteria2.go new file mode 100644 index 0000000000..35c0b14f20 --- /dev/null +++ b/constant/hysteria2.go @@ -0,0 +1,7 @@ +package constant + +const ( + Hysterai2MasqueradeTypeFile = "file" + Hysterai2MasqueradeTypeProxy = "proxy" + Hysterai2MasqueradeTypeString = "string" +) diff --git a/docs/configuration/inbound/hysteria2.md b/docs/configuration/inbound/hysteria2.md index 7c611e6491..6230d3154b 100644 --- a/docs/configuration/inbound/hysteria2.md +++ b/docs/configuration/inbound/hysteria2.md @@ -1,11 +1,19 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "Changes in sing-box 1.11.0" + + :material-alert: [masquerade](#masquerade) + ### Structure ```json { "type": "hysteria2", "tag": "hy2-in", - ... - // Listen Fields + + ... // Listen Fields "up_mbps": 100, "down_mbps": 100, @@ -21,7 +29,7 @@ ], "ignore_client_bandwidth": false, "tls": {}, - "masquerade": "", + "masquerade": "", // or {} "brutal_debug": false } ``` @@ -79,14 +87,54 @@ TLS configuration, see [TLS](/configuration/shared/tls/#inbound). #### masquerade -HTTP3 server behavior when authentication fails. +HTTP3 server behavior (URL string configuration) when authentication fails. | Scheme | Example | Description | |--------------|-------------------------|--------------------| | `file` | `file:///var/www` | As a file server | | `http/https` | `http://127.0.0.1:8080` | As a reverse proxy | -A 404 page will be returned if empty. +Conflict with `masquerade.type`. + +A 404 page will be returned if masquerade is not configured. + +#### masquerade.type + +HTTP3 server behavior (Object configuration) when authentication fails. + +| Type | Description | Fields | +|----------|-----------------------------|-------------------------------------| +| `file` | As a file server | `directory` | +| `proxy` | As a reverse proxy | `url`, `rewrite_host` | +| `string` | Reply with a fixed response | `status_code`, `headers`, `content` | + +Conflict with `masquerade`. + +A 404 page will be returned if masquerade is not configured. + +#### masquerade.directory + +File server root directory. + +#### masquerade.url + +Reverse proxy target URL. + +#### masquerade.rewrite_host + +Rewrite the `Host` header to the target URL. + +#### masquerade.status_code + +Fixed response status code. + +#### masquerade.headers + +Fixed response headers. + +#### masquerade.content + +Fixed response content. #### brutal_debug diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md index c936aae8f8..fceacf9325 100644 --- a/docs/configuration/inbound/hysteria2.zh.md +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -1,11 +1,19 @@ +--- +icon: material/alert-decagram +--- + +!!! quote "sing-box 1.11.0 中的更改" + + :material-alert: [masquerade](#masquerade) + ### 结构 ```json { "type": "hysteria2", "tag": "hy2-in", - ... - // 监听字段 + + ... // 监听字段 "up_mbps": 100, "down_mbps": 100, @@ -21,7 +29,7 @@ ], "ignore_client_bandwidth": false, "tls": {}, - "masquerade": "", + "masquerade": "", // 或 {} "brutal_debug": false } ``` @@ -76,14 +84,54 @@ TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。 #### masquerade -HTTP3 服务器认证失败时的行为。 +HTTP3 服务器认证失败时的行为 (URL 字符串配置)。 | Scheme | 示例 | 描述 | |--------------|-------------------------|---------| | `file` | `file:///var/www` | 作为文件服务器 | | `http/https` | `http://127.0.0.1:8080` | 作为反向代理 | -如果为空,则返回 404 页。 +如果 masquerade 未配置,则返回 404 页。 + +与 `masquerade.type` 冲突。 + +#### masquerade.type + +HTTP3 服务器认证失败时的行为 (对象配置)。 + +| Type | 描述 | 字段 | +|----------|---------|-------------------------------------| +| `file` | 作为文件服务器 | `directory` | +| `proxy` | 作为反向代理 | `url`, `rewrite_host` | +| `string` | 返回固定响应 | `status_code`, `headers`, `content` | + +如果 masquerade 未配置,则返回 404 页。 + +与 `masquerade` 冲突。 + +#### masquerade.directory + +文件服务器根目录。 + +#### masquerade.url + +反向代理目标 URL。 + +#### masquerade.rewrite_host + +重写请求头中的 Host 字段到目标 URL。 + +#### masquerade.status_code + +固定响应状态码。 + +#### masquerade.headers + +固定响应头。 + +#### masquerade.content + +固定响应内容。 #### brutal_debug diff --git a/docs/configuration/outbound/direct.md b/docs/configuration/outbound/direct.md index 71649243a5..3e28db8fc6 100644 --- a/docs/configuration/outbound/direct.md +++ b/docs/configuration/outbound/direct.md @@ -4,8 +4,8 @@ icon: material/alert-decagram !!! quote "Changes in sing-box 1.11.0" - :material-alert-decagram: [override_address](#override_address) - :material-alert-decagram: [override_port](#override_port) + :material-delete-clock: [override_address](#override_address) + :material-delete-clock: [override_port](#override_port) `direct` outbound send requests directly. diff --git a/option/hysteria2.go b/option/hysteria2.go index 5032c734a3..9e55ec40d3 100644 --- a/option/hysteria2.go +++ b/option/hysteria2.go @@ -1,5 +1,15 @@ package option +import ( + "net/url" + + C "github.com/sagernet/sing-box/constant" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/json/badoption" +) + type Hysteria2InboundOptions struct { ListenOptions UpMbps int `json:"up_mbps,omitempty"` @@ -8,8 +18,8 @@ type Hysteria2InboundOptions struct { Users []Hysteria2User `json:"users,omitempty"` IgnoreClientBandwidth bool `json:"ignore_client_bandwidth,omitempty"` InboundTLSOptionsContainer - Masquerade string `json:"masquerade,omitempty"` - BrutalDebug bool `json:"brutal_debug,omitempty"` + Masquerade *Hysteria2Masquerade `json:"masquerade,omitempty"` + BrutalDebug bool `json:"brutal_debug,omitempty"` } type Hysteria2Obfs struct { @@ -22,6 +32,82 @@ type Hysteria2User struct { Password string `json:"password,omitempty"` } +type _Hysteria2Masquerade struct { + Type string `json:"type,omitempty"` + FileOptions Hysteria2MasqueradeFile `json:"-"` + ProxyOptions Hysteria2MasqueradeProxy `json:"-"` + StringOptions Hysteria2MasqueradeString `json:"-"` +} + +type Hysteria2Masquerade _Hysteria2Masquerade + +func (m Hysteria2Masquerade) MarshalJSON() ([]byte, error) { + var v any + switch m.Type { + case C.Hysterai2MasqueradeTypeFile: + v = m.FileOptions + case C.Hysterai2MasqueradeTypeProxy: + v = m.ProxyOptions + case C.Hysterai2MasqueradeTypeString: + v = m.StringOptions + default: + return nil, E.New("unknown masquerade type: ", m.Type) + } + return badjson.MarshallObjects((_Hysteria2Masquerade)(m), v) +} + +func (m *Hysteria2Masquerade) UnmarshalJSON(bytes []byte) error { + var urlString string + err := json.Unmarshal(bytes, &urlString) + if err == nil { + masqueradeURL, err := url.Parse(urlString) + if err != nil { + return E.Cause(err, "invalid masquerade URL") + } + switch masqueradeURL.Scheme { + case "file": + m.Type = C.Hysterai2MasqueradeTypeFile + m.FileOptions.Directory = masqueradeURL.Path + case "http", "https": + m.Type = C.Hysterai2MasqueradeTypeProxy + m.ProxyOptions.URL = urlString + default: + return E.New("unknown masquerade URL scheme: ", masqueradeURL.Scheme) + } + } + err = json.Unmarshal(bytes, (*_Hysteria2Masquerade)(m)) + if err != nil { + return err + } + var v any + switch m.Type { + case C.Hysterai2MasqueradeTypeFile: + v = &m.FileOptions + case C.Hysterai2MasqueradeTypeProxy: + v = &m.ProxyOptions + case C.Hysterai2MasqueradeTypeString: + v = &m.StringOptions + default: + return E.New("unknown masquerade type: ", m.Type) + } + return badjson.UnmarshallExcluded(bytes, (*_Hysteria2Masquerade)(m), v) +} + +type Hysteria2MasqueradeFile struct { + Directory string `json:"directory"` +} + +type Hysteria2MasqueradeProxy struct { + URL string `json:"url"` + RewriteHost bool `json:"rewrite_host,omitempty"` +} + +type Hysteria2MasqueradeString struct { + StatusCode int `json:"status_code,omitempty"` + Headers badoption.HTTPHeader `json:"headers,omitempty"` + Content string `json:"content"` +} + type Hysteria2OutboundOptions struct { DialerOptions ServerOptions diff --git a/protocol/hysteria2/inbound.go b/protocol/hysteria2/inbound.go index 8d00072cc4..f55b6ae877 100644 --- a/protocol/hysteria2/inbound.go +++ b/protocol/hysteria2/inbound.go @@ -60,26 +60,40 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo } } var masqueradeHandler http.Handler - if options.Masquerade != "" { - masqueradeURL, err := url.Parse(options.Masquerade) - if err != nil { - return nil, E.Cause(err, "parse masquerade URL") - } - switch masqueradeURL.Scheme { - case "file": - masqueradeHandler = http.FileServer(http.Dir(masqueradeURL.Path)) - case "http", "https": + if options.Masquerade != nil && options.Masquerade.Type != "" { + switch options.Masquerade.Type { + case C.Hysterai2MasqueradeTypeFile: + masqueradeHandler = http.FileServer(http.Dir(options.Masquerade.FileOptions.Directory)) + case C.Hysterai2MasqueradeTypeProxy: + masqueradeURL, err := url.Parse(options.Masquerade.ProxyOptions.URL) + if err != nil { + return nil, E.Cause(err, "parse masquerade URL") + } masqueradeHandler = &httputil.ReverseProxy{ Rewrite: func(r *httputil.ProxyRequest) { r.SetURL(masqueradeURL) - r.Out.Host = r.In.Host + if !options.Masquerade.ProxyOptions.RewriteHost { + r.Out.Host = r.In.Host + } }, ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { w.WriteHeader(http.StatusBadGateway) }, } + case C.Hysterai2MasqueradeTypeString: + masqueradeHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if options.Masquerade.StringOptions.StatusCode != 0 { + w.WriteHeader(options.Masquerade.StringOptions.StatusCode) + } + for key, values := range options.Masquerade.StringOptions.Headers { + for _, value := range values { + w.Header().Add(key, value) + } + } + w.Write([]byte(options.Masquerade.StringOptions.Content)) + }) default: - return nil, E.New("unknown masquerade URL scheme: ", masqueradeURL.Scheme) + return nil, E.New("unknown masquerade type: ", options.Masquerade.Type) } } inbound := &Inbound{ From 01291d16e0aab7a20a93f8c4f2ec30273db8ce85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Fri, 6 Dec 2024 16:47:30 +0800 Subject: [PATCH 34/49] clash-api: Fix missing endpoints --- experimental/clashapi/api_meta_group.go | 6 ++-- experimental/clashapi/proxies.go | 11 ++++++-- experimental/clashapi/server.go | 34 ++++++++++++----------- experimental/clashapi/server_resources.go | 4 +-- 4 files changed, 31 insertions(+), 24 deletions(-) diff --git a/experimental/clashapi/api_meta_group.go b/experimental/clashapi/api_meta_group.go index c5c07ba6a3..8f09ced9e3 100644 --- a/experimental/clashapi/api_meta_group.go +++ b/experimental/clashapi/api_meta_group.go @@ -32,7 +32,7 @@ func groupRouter(server *Server) http.Handler { func getGroups(server *Server) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { - groups := common.Map(common.Filter(server.outboundManager.Outbounds(), func(it adapter.Outbound) bool { + groups := common.Map(common.Filter(server.outbound.Outbounds(), func(it adapter.Outbound) bool { _, isGroup := it.(adapter.OutboundGroup) return isGroup }), func(it adapter.Outbound) *badjson.JSONObject { @@ -86,7 +86,7 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) result, err = urlTestGroup.URLTest(ctx) } else { outbounds := common.FilterNotNil(common.Map(outboundGroup.All(), func(it string) adapter.Outbound { - itOutbound, _ := server.outboundManager.Outbound(it) + itOutbound, _ := server.outbound.Outbound(it) return itOutbound })) b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10)) @@ -100,7 +100,7 @@ func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) continue } checked[realTag] = true - p, loaded := server.outboundManager.Outbound(realTag) + p, loaded := server.outbound.Outbound(realTag) if !loaded { continue } diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go index 8d8ecb3845..6246b9da0d 100644 --- a/experimental/clashapi/proxies.go +++ b/experimental/clashapi/proxies.go @@ -46,7 +46,7 @@ func findProxyByName(server *Server) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { name := r.Context().Value(CtxKeyProxyName).(string) - proxy, exist := server.outboundManager.Outbound(name) + proxy, exist := server.outbound.Outbound(name) if !exist { render.Status(r, http.StatusNotFound) render.JSON(w, r, ErrNotFound) @@ -86,9 +86,14 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject { func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var proxyMap badjson.JSONObject - outbounds := common.Filter(server.outboundManager.Outbounds(), func(detour adapter.Outbound) bool { + outbounds := common.Filter(server.outbound.Outbounds(), func(detour adapter.Outbound) bool { return detour.Tag() != "" }) + outbounds = append(outbounds, common.Map(common.Filter(server.endpoint.Endpoints(), func(detour adapter.Endpoint) bool { + return detour.Tag() != "" + }), func(it adapter.Endpoint) adapter.Outbound { + return it + })...) allProxies := make([]string, 0, len(outbounds)) @@ -100,7 +105,7 @@ func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) { allProxies = append(allProxies, detour.Tag()) } - defaultTag := server.outboundManager.Default().Tag() + defaultTag := server.outbound.Default().Tag() sort.SliceStable(allProxies, func(i, j int) bool { return allProxies[i] == defaultTag diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index 106bc73ff7..2b4da4a4a1 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -40,16 +40,17 @@ func init() { var _ adapter.ClashServer = (*Server)(nil) type Server struct { - ctx context.Context - router adapter.Router - outboundManager adapter.OutboundManager - logger log.Logger - httpServer *http.Server - trafficManager *trafficontrol.Manager - urlTestHistory *urltest.HistoryStorage - mode string - modeList []string - modeUpdateHook chan<- struct{} + ctx context.Context + router adapter.Router + outbound adapter.OutboundManager + endpoint adapter.EndpointManager + logger log.Logger + httpServer *http.Server + trafficManager *trafficontrol.Manager + urlTestHistory *urltest.HistoryStorage + mode string + modeList []string + modeUpdateHook chan<- struct{} externalController bool externalUI string @@ -61,10 +62,11 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op trafficManager := trafficontrol.NewManager() chiRouter := chi.NewRouter() s := &Server{ - ctx: ctx, - router: service.FromContext[adapter.Router](ctx), - outboundManager: service.FromContext[adapter.OutboundManager](ctx), - logger: logFactory.NewLogger("clash-api"), + ctx: ctx, + router: service.FromContext[adapter.Router](ctx), + outbound: service.FromContext[adapter.OutboundManager](ctx), + endpoint: service.FromContext[adapter.EndpointManager](ctx), + logger: logFactory.NewLogger("clash-api"), httpServer: &http.Server{ Addr: options.ExternalController, Handler: chiRouter, @@ -239,11 +241,11 @@ func (s *Server) TrafficManager() *trafficontrol.Manager { } func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn { - return trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.outboundManager, matchedRule, matchOutbound) + return trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.outbound, matchedRule, matchOutbound) } func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) N.PacketConn { - return trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.outboundManager, matchedRule, matchOutbound) + return trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.outbound, matchedRule, matchOutbound) } func authentication(serverSecret string) func(next http.Handler) http.Handler { diff --git a/experimental/clashapi/server_resources.go b/experimental/clashapi/server_resources.go index e5b28e300c..2e73121f1a 100644 --- a/experimental/clashapi/server_resources.go +++ b/experimental/clashapi/server_resources.go @@ -44,13 +44,13 @@ func (s *Server) downloadExternalUI() error { s.logger.Info("downloading external ui") var detour adapter.Outbound if s.externalUIDownloadDetour != "" { - outbound, loaded := s.outboundManager.Outbound(s.externalUIDownloadDetour) + outbound, loaded := s.outbound.Outbound(s.externalUIDownloadDetour) if !loaded { return E.New("detour outbound not found: ", s.externalUIDownloadDetour) } detour = outbound } else { - outbound := s.outboundManager.Default() + outbound := s.outbound.Default() detour = outbound } httpClient := &http.Client{ From c385e7c137400248509a7f3294b26cb2d5b12b93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Tue, 10 Dec 2024 20:36:09 +0800 Subject: [PATCH 35/49] Fix socks5 UDP implementation --- protocol/http/inbound.go | 2 +- protocol/mixed/inbound.go | 16 ++++++++++++---- protocol/socks/inbound.go | 14 +++++++++++--- protocol/tor/proxy.go | 2 +- route/route.go | 11 +++++++---- 5 files changed, 32 insertions(+), 13 deletions(-) diff --git a/protocol/http/inbound.go b/protocol/http/inbound.go index 8aa1d4159a..68150fa741 100644 --- a/protocol/http/inbound.go +++ b/protocol/http/inbound.go @@ -91,7 +91,7 @@ func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata a } conn = tlsConn } - err := http.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) + err := http.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) if err != nil { N.CloseOnHandshakeFailure(conn, onClose, err) h.logger.ErrorContext(ctx, E.Cause(err, "process connection from ", metadata.Source)) diff --git a/protocol/mixed/inbound.go b/protocol/mixed/inbound.go index d1d343a991..b675a5362e 100644 --- a/protocol/mixed/inbound.go +++ b/protocol/mixed/inbound.go @@ -85,9 +85,9 @@ func (h *Inbound) newConnection(ctx context.Context, conn net.Conn, metadata ada } switch headerBytes[0] { case socks4.Version, socks5.Version: - return socks.HandleConnectionEx(ctx, conn, reader, h.authenticator, nil, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, metadata.Destination, onClose) + return socks.HandleConnectionEx(ctx, conn, reader, h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) default: - return http.HandleConnectionEx(ctx, conn, reader, h.authenticator, nil, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) + return http.HandleConnectionEx(ctx, conn, reader, h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) } } @@ -110,11 +110,19 @@ func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketC metadata.InboundType = h.Type() user, loaded := auth.UserFromContext[string](ctx) if !loaded { - h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + if !metadata.Destination.IsValid() { + h.logger.InfoContext(ctx, "inbound packet connection") + } else { + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + } h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) return } metadata.User = user - h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + if !metadata.Destination.IsValid() { + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection") + } else { + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + } h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } diff --git a/protocol/socks/inbound.go b/protocol/socks/inbound.go index 1490cee5cf..820c7bbb5d 100644 --- a/protocol/socks/inbound.go +++ b/protocol/socks/inbound.go @@ -62,7 +62,7 @@ func (h *Inbound) Close() error { } func (h *Inbound) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) { - err := socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, nil, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, metadata.Destination, onClose) + err := socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), h.authenticator, adapter.NewUpstreamHandlerEx(metadata, h.newUserConnection, h.streamUserPacketConnection), metadata.Source, onClose) N.CloseOnHandshakeFailure(conn, onClose, err) if err != nil { if E.IsClosedOrCanceled(err) { @@ -92,11 +92,19 @@ func (h *Inbound) streamUserPacketConnection(ctx context.Context, conn N.PacketC metadata.InboundType = h.Type() user, loaded := auth.UserFromContext[string](ctx) if !loaded { - h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + if !metadata.Destination.IsValid() { + h.logger.InfoContext(ctx, "inbound packet connection") + } else { + h.logger.InfoContext(ctx, "inbound packet connection to ", metadata.Destination) + } h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) return } metadata.User = user - h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + if !metadata.Destination.IsValid() { + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection") + } else { + h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination) + } h.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose) } diff --git a/protocol/tor/proxy.go b/protocol/tor/proxy.go index 1ed303350b..feab7971c0 100644 --- a/protocol/tor/proxy.go +++ b/protocol/tor/proxy.go @@ -99,7 +99,7 @@ func (l *ProxyListener) acceptLoop() { } func (l *ProxyListener) accept(ctx context.Context, conn *net.TCPConn) error { - return socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), l.authenticator, nil, l, M.SocksaddrFromNet(conn.RemoteAddr()), M.Socksaddr{}, nil) + return socks.HandleConnectionEx(ctx, conn, std_bufio.NewReader(conn), l.authenticator, l, M.SocksaddrFromNet(conn.RemoteAddr()), nil) } func (l *ProxyListener) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) { diff --git a/route/route.go b/route/route.go index 05e22c2514..fb2de85d51 100644 --- a/route/route.go +++ b/route/route.go @@ -461,8 +461,12 @@ match: break match } } - if !preMatch && metadata.Destination.Addr.IsUnspecified() { - newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{}, inputConn, inputPacketConn) + if !preMatch && inputPacketConn != nil && !metadata.Destination.IsFqdn() && !metadata.Destination.Addr.IsGlobalUnicast() { + var timeout time.Duration + if metadata.InboundType == C.TypeSOCKS { + timeout = C.TCPTimeout + } + newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{Timeout: timeout}, inputConn, inputPacketConn) if newErr != nil { fatalErr = newErr return @@ -558,8 +562,7 @@ func (r *Router) actionSniff( return } } else { - // TODO: maybe always override destination - if metadata.Destination.Addr.IsUnspecified() { + if !metadata.Destination.Addr.IsGlobalUnicast() { metadata.Destination = destination } if len(packetBuffers) > 0 { From 74de437bfb764fc9a81dd2cd4decc6761cf0fccd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 14 Dec 2024 22:41:58 +0800 Subject: [PATCH 36/49] Fix time service --- box.go | 16 ++++++++++++---- common/tls/time_wrapper.go | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 common/tls/time_wrapper.go diff --git a/box.go b/box.go index 5dc76ebc7b..1908f6dbfe 100644 --- a/box.go +++ b/box.go @@ -14,6 +14,7 @@ import ( "github.com/sagernet/sing-box/adapter/outbound" "github.com/sagernet/sing-box/common/dialer" "github.com/sagernet/sing-box/common/taskmonitor" + "github.com/sagernet/sing-box/common/tls" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/experimental/cachefile" @@ -149,6 +150,14 @@ func New(options Options) (*Box, error) { if err != nil { return nil, E.Cause(err, "initialize router") } + + ntpOptions := common.PtrValueOrDefault(options.NTP) + var timeService *tls.TimeServiceWrapper + if ntpOptions.Enabled { + timeService = new(tls.TimeServiceWrapper) + service.MustRegister[ntp.TimeService](ctx, timeService) + } + for i, endpointOptions := range options.Endpoints { var tag string if endpointOptions.Tag != "" { @@ -254,13 +263,12 @@ func New(options Options) (*Box, error) { service.MustRegister[adapter.V2RayServer](ctx, v2rayServer) } } - ntpOptions := common.PtrValueOrDefault(options.NTP) if ntpOptions.Enabled { ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions) if err != nil { return nil, E.Cause(err, "create NTP service") } - timeService := ntp.NewService(ntp.Options{ + ntpService := ntp.NewService(ntp.Options{ Context: ctx, Dialer: ntpDialer, Logger: logFactory.NewLogger("ntp"), @@ -268,8 +276,8 @@ func New(options Options) (*Box, error) { Interval: time.Duration(ntpOptions.Interval), WriteToSystem: ntpOptions.WriteToSystem, }) - service.MustRegister[ntp.TimeService](ctx, timeService) - services = append(services, adapter.NewLifecycleService(timeService, "ntp service")) + timeService.TimeService = ntpService + services = append(services, adapter.NewLifecycleService(ntpService, "ntp service")) } return &Box{ network: networkManager, diff --git a/common/tls/time_wrapper.go b/common/tls/time_wrapper.go new file mode 100644 index 0000000000..491fca9890 --- /dev/null +++ b/common/tls/time_wrapper.go @@ -0,0 +1,22 @@ +package tls + +import ( + "time" + + "github.com/sagernet/sing/common/ntp" +) + +type TimeServiceWrapper struct { + ntp.TimeService +} + +func (w *TimeServiceWrapper) TimeFunc() func() time.Time { + if w.TimeService == nil { + return nil + } + return w.TimeService.TimeFunc() +} + +func (w *TimeServiceWrapper) Upstream() any { + return w.TimeService +} From 0a9bf97438282ec636e3c697c2ad285b012abcd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 15 Dec 2024 00:45:41 +0800 Subject: [PATCH 37/49] Fix domain strategy --- adapter/inbound.go | 2 +- adapter/network.go | 2 +- common/dialer/default.go | 98 +++++++++++++-------- common/dialer/default_parallel_interface.go | 55 +++++++----- common/dialer/default_parallel_network.go | 25 +++++- common/dialer/dialer.go | 21 ++--- common/dialer/resolve.go | 4 +- option/outbound.go | 37 ++++---- option/route.go | 2 +- option/rule_action.go | 4 +- protocol/direct/outbound.go | 36 +++----- protocol/wireguard/endpoint.go | 3 +- protocol/wireguard/outbound.go | 3 +- route/conn.go | 11 ++- route/network.go | 4 +- route/route.go | 14 ++- route/router.go | 2 +- route/rule/rule_action.go | 6 +- transport/dhcp/server.go | 2 +- 19 files changed, 195 insertions(+), 136 deletions(-) diff --git a/adapter/inbound.go b/adapter/inbound.go index f5d5c95b8f..93d2ec60c5 100644 --- a/adapter/inbound.go +++ b/adapter/inbound.go @@ -72,7 +72,7 @@ type InboundContext struct { UDPConnect bool UDPTimeout time.Duration - NetworkStrategy C.NetworkStrategy + NetworkStrategy *C.NetworkStrategy NetworkType []C.InterfaceType FallbackNetworkType []C.InterfaceType FallbackDelay time.Duration diff --git a/adapter/network.go b/adapter/network.go index 08fc00fac0..00ef54b8c0 100644 --- a/adapter/network.go +++ b/adapter/network.go @@ -28,7 +28,7 @@ type NetworkManager interface { } type NetworkOptions struct { - NetworkStrategy C.NetworkStrategy + NetworkStrategy *C.NetworkStrategy NetworkType []C.InterfaceType FallbackNetworkType []C.InterfaceType FallbackDelay time.Duration diff --git a/common/dialer/default.go b/common/dialer/default.go index bf553618f7..49bd145cd6 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -9,6 +9,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/conntrack" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/atomic" @@ -16,6 +17,7 @@ import ( E "github.com/sagernet/sing/common/exceptions" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/service" ) var ( @@ -33,19 +35,22 @@ type DefaultDialer struct { udpAddr6 string isWireGuardListener bool networkManager adapter.NetworkManager - networkStrategy C.NetworkStrategy + networkStrategy *C.NetworkStrategy networkType []C.InterfaceType fallbackNetworkType []C.InterfaceType networkFallbackDelay time.Duration networkLastFallback atomic.TypedValue[time.Time] } -func NewDefault(networkManager adapter.NetworkManager, options option.DialerOptions) (*DefaultDialer, error) { +func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) { + networkManager := service.FromContext[adapter.NetworkManager](ctx) + platformInterface := service.FromContext[platform.Interface](ctx) + var ( dialer net.Dialer listener net.ListenConfig interfaceFinder control.InterfaceFinder - networkStrategy C.NetworkStrategy + networkStrategy *C.NetworkStrategy networkType []C.InterfaceType fallbackNetworkType []C.InterfaceType networkFallbackDelay time.Duration @@ -74,31 +79,37 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti listener.Control = control.Append(listener.Control, control.RoutingMark(autoRedirectOutputMark)) } } - if C.NetworkStrategy(options.NetworkStrategy) != C.NetworkStrategyDefault { - if options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil { - return nil, E.New("`network_strategy` is conflict with `bind_interface`, `inet4_bind_address` and `inet6_bind_address`") - } - networkStrategy = C.NetworkStrategy(options.NetworkStrategy) - networkType = common.Map(options.NetworkType, option.InterfaceType.Build) - fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build) - networkFallbackDelay = time.Duration(options.NetworkFallbackDelay) - if networkManager == nil || !networkManager.AutoDetectInterface() { - return nil, E.New("`route.auto_detect_interface` is require by `network_strategy`") + disableDefaultBind := options.BindInterface != "" || options.Inet4BindAddress != nil || options.Inet6BindAddress != nil + if disableDefaultBind || options.TCPFastOpen { + if options.NetworkStrategy != nil || len(options.NetworkType) > 0 && options.FallbackNetworkType == nil && options.FallbackDelay == 0 { + return nil, E.New("`network_strategy` is conflict with `bind_interface`, `inet4_bind_address`, `inet6_bind_address` and `tcp_fast_open`") } } - if networkManager != nil && options.BindInterface == "" && options.Inet4BindAddress == nil && options.Inet6BindAddress == nil { + + if networkManager != nil { defaultOptions := networkManager.DefaultOptions() - if options.BindInterface == "" { + if !disableDefaultBind { if defaultOptions.BindInterface != "" { bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1) dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) } else if networkManager.AutoDetectInterface() { - if defaultOptions.NetworkStrategy != C.NetworkStrategyDefault && C.NetworkStrategy(options.NetworkStrategy) == C.NetworkStrategyDefault { - networkStrategy = defaultOptions.NetworkStrategy - networkType = defaultOptions.NetworkType - fallbackNetworkType = defaultOptions.FallbackNetworkType - networkFallbackDelay = defaultOptions.FallbackDelay + if platformInterface != nil { + networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy) + if networkStrategy == nil { + networkStrategy = common.Ptr(C.NetworkStrategyDefault) + } + networkType = common.Map(options.NetworkType, option.InterfaceType.Build) + fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build) + if networkStrategy == nil && len(networkType) == 0 && len(fallbackNetworkType) == 0 { + networkStrategy = defaultOptions.NetworkStrategy + networkType = defaultOptions.NetworkType + fallbackNetworkType = defaultOptions.FallbackNetworkType + } + networkFallbackDelay = time.Duration(options.FallbackDelay) + if networkFallbackDelay == 0 && defaultOptions.FallbackDelay != 0 { + networkFallbackDelay = defaultOptions.FallbackDelay + } bindFunc := networkManager.ProtectFunc() dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) @@ -172,9 +183,6 @@ func NewDefault(networkManager adapter.NetworkManager, options option.DialerOpti listener.Control = control.Append(listener.Control, controlFn) } } - if networkStrategy != C.NetworkStrategyDefault && options.TCPFastOpen { - return nil, E.New("`tcp_fast_open` is conflict with `network_strategy` or `route.default_network_strategy`") - } tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen) if err != nil { return nil, err @@ -204,7 +212,7 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address if !address.IsValid() { return nil, E.New("invalid address") } - if d.networkStrategy == C.NetworkStrategyDefault { + if d.networkStrategy == nil { switch N.NetworkName(network) { case N.NetworkUDP: if !address.IsIPv6() { @@ -223,12 +231,21 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address } } -func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network string, address M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { - if strategy == C.NetworkStrategyDefault { +func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network string, address M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { + if strategy == nil { + strategy = d.networkStrategy + } + if strategy == nil { return d.DialContext(ctx, network, address) } - if !d.networkManager.AutoDetectInterface() { - return nil, E.New("`route.auto_detect_interface` is require by `network_strategy`") + if len(interfaceType) == 0 { + interfaceType = d.networkType + } + if len(fallbackInterfaceType) == 0 { + fallbackInterfaceType = d.fallbackNetworkType + } + if fallbackDelay == 0 { + fallbackDelay = d.networkFallbackDelay } var dialer net.Dialer if N.NetworkName(network) == N.NetworkTCP { @@ -243,9 +260,9 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin err error ) if !fastFallback { - conn, isPrimary, err = d.dialParallelInterface(ctx, dialer, network, address.String(), strategy, interfaceType, fallbackInterfaceType, fallbackDelay) + conn, isPrimary, err = d.dialParallelInterface(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } else { - conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), strategy, interfaceType, fallbackInterfaceType, fallbackDelay, d.networkLastFallback.Store) + conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay, d.networkLastFallback.Store) } if err != nil { return nil, err @@ -257,7 +274,7 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin } func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) { - if d.networkStrategy == C.NetworkStrategyDefault { + if d.networkStrategy == nil { if destination.IsIPv6() { return trackPacketConn(d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)) } else if destination.IsIPv4() && !destination.Addr.IsUnspecified() { @@ -270,18 +287,27 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd } } -func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) { - if strategy == C.NetworkStrategyDefault { +func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) { + if strategy == nil { + strategy = d.networkStrategy + } + if strategy == nil { return d.ListenPacket(ctx, destination) } - if !d.networkManager.AutoDetectInterface() { - return nil, E.New("`route.auto_detect_interface` is require by `network_strategy`") + if len(interfaceType) == 0 { + interfaceType = d.networkType + } + if len(fallbackInterfaceType) == 0 { + fallbackInterfaceType = d.fallbackNetworkType + } + if fallbackDelay == 0 { + fallbackDelay = d.networkFallbackDelay } network := N.NetworkUDP if destination.IsIPv4() && !destination.Addr.IsUnspecified() { network += "4" } - return trackPacketConn(d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", strategy, interfaceType, fallbackInterfaceType, fallbackDelay)) + return trackPacketConn(d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", *strategy, interfaceType, fallbackInterfaceType, fallbackDelay)) } func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) { diff --git a/common/dialer/default_parallel_interface.go b/common/dialer/default_parallel_interface.go index 37a1f79cab..269546a413 100644 --- a/common/dialer/default_parallel_interface.go +++ b/common/dialer/default_parallel_interface.go @@ -40,7 +40,7 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di } } else { select { - case results <- dialResult{Conn: conn}: + case results <- dialResult{Conn: conn, primary: primary}: case <-returned: conn.Close() } @@ -112,7 +112,7 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d } } else { select { - case results <- dialResult{Conn: conn}: + case results <- dialResult{Conn: conn, primary: primary}: case <-returned: if primary && time.Since(startAt) <= fallbackDelay { resetFastFallback(time.Time{}) @@ -177,44 +177,57 @@ func selectInterfaces(networkManager adapter.NetworkManager, strategy C.NetworkS case C.NetworkStrategyDefault: if len(interfaceType) == 0 { defaultIf := networkManager.InterfaceMonitor().DefaultInterface() - for _, iif := range interfaces { - if iif.Index == defaultIf.Index { - primaryInterfaces = append(primaryInterfaces, iif) - } else { - fallbackInterfaces = append(fallbackInterfaces, iif) + if defaultIf != nil { + for _, iif := range interfaces { + if iif.Index == defaultIf.Index { + primaryInterfaces = append(primaryInterfaces, iif) + } } + } else { + primaryInterfaces = interfaces } } else { - primaryInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool { - return common.Contains(interfaceType, iif.Type) + primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool { + return common.Contains(interfaceType, it.Type) }) } case C.NetworkStrategyHybrid: if len(interfaceType) == 0 { primaryInterfaces = interfaces } else { - primaryInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool { - return common.Contains(interfaceType, iif.Type) + primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool { + return common.Contains(interfaceType, it.Type) }) } case C.NetworkStrategyFallback: if len(interfaceType) == 0 { defaultIf := networkManager.InterfaceMonitor().DefaultInterface() - for _, iif := range interfaces { - if iif.Index == defaultIf.Index { - primaryInterfaces = append(primaryInterfaces, iif) - } else { - fallbackInterfaces = append(fallbackInterfaces, iif) + if defaultIf != nil { + for _, iif := range interfaces { + if iif.Index == defaultIf.Index { + primaryInterfaces = append(primaryInterfaces, iif) + break + } } + } else { + primaryInterfaces = interfaces } } else { - primaryInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool { - return common.Contains(interfaceType, iif.Type) + primaryInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool { + return common.Contains(interfaceType, it.Type) + }) + } + if len(fallbackInterfaceType) == 0 { + fallbackInterfaces = common.Filter(interfaces, func(it adapter.NetworkInterface) bool { + return !common.Any(primaryInterfaces, func(iif adapter.NetworkInterface) bool { + return it.Index == iif.Index + }) + }) + } else { + fallbackInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool { + return common.Contains(fallbackInterfaceType, iif.Type) }) } - fallbackInterfaces = common.Filter(interfaces, func(iif adapter.NetworkInterface) bool { - return common.Contains(fallbackInterfaceType, iif.Type) - }) } return primaryInterfaces, fallbackInterfaces } diff --git a/common/dialer/default_parallel_network.go b/common/dialer/default_parallel_network.go index 5145656ba8..006e574759 100644 --- a/common/dialer/default_parallel_network.go +++ b/common/dialer/default_parallel_network.go @@ -13,7 +13,13 @@ import ( N "github.com/sagernet/sing/common/network" ) -func DialSerialNetwork(ctx context.Context, dialer N.Dialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { +func DialSerialNetwork(ctx context.Context, dialer N.Dialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { + if len(destinationAddresses) == 0 { + if !destination.IsIP() { + panic("invalid usage") + } + destinationAddresses = []netip.Addr{destination.Addr} + } if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel { return parallelDialer.DialParallelNetwork(ctx, network, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } @@ -38,7 +44,14 @@ func DialSerialNetwork(ctx context.Context, dialer N.Dialer, network string, des return nil, E.Errors(errors...) } -func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, preferIPv6 bool, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { +func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, preferIPv6 bool, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { + if len(destinationAddresses) == 0 { + if !destination.IsIP() { + panic("invalid usage") + } + destinationAddresses = []netip.Addr{destination.Addr} + } + if fallbackDelay == 0 { fallbackDelay = N.DefaultFallbackDelay } @@ -116,7 +129,13 @@ func DialParallelNetwork(ctx context.Context, dialer ParallelInterfaceDialer, ne } } -func ListenSerialNetworkPacket(ctx context.Context, dialer N.Dialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { +func ListenSerialNetworkPacket(ctx context.Context, dialer N.Dialer, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { + if len(destinationAddresses) == 0 { + if !destination.IsIP() { + panic("invalid usage") + } + destinationAddresses = []netip.Addr{destination.Addr} + } if parallelDialer, isParallel := dialer.(ParallelNetworkDialer); isParallel { return parallelDialer.ListenSerialNetworkPacket(ctx, destination, destinationAddresses, strategy, interfaceType, fallbackInterfaceType, fallbackDelay) } diff --git a/common/dialer/dialer.go b/common/dialer/dialer.go index b307a3303b..89d1eeaba9 100644 --- a/common/dialer/dialer.go +++ b/common/dialer/dialer.go @@ -17,16 +17,15 @@ import ( ) func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) { - networkManager := service.FromContext[adapter.NetworkManager](ctx) if options.IsWireGuardListener { - return NewDefault(networkManager, options) + return NewDefault(ctx, options) } var ( dialer N.Dialer err error ) if options.Detour == "" { - dialer, err = NewDefault(networkManager, options) + dialer, err = NewDefault(ctx, options) if err != nil { return nil, err } @@ -37,9 +36,6 @@ func New(ctx context.Context, options option.DialerOptions) (N.Dialer, error) { } dialer = NewDetour(outboundManager, options.Detour) } - if networkManager == nil { - return NewDefault(networkManager, options) - } if options.Detour == "" { router := service.FromContext[adapter.Router](ctx) if router != nil { @@ -58,11 +54,10 @@ func NewDirect(ctx context.Context, options option.DialerOptions) (ParallelInter if options.Detour != "" { return nil, E.New("`detour` is not supported in direct context") } - networkManager := service.FromContext[adapter.NetworkManager](ctx) if options.IsWireGuardListener { - return NewDefault(networkManager, options) + return NewDefault(ctx, options) } - dialer, err := NewDefault(networkManager, options) + dialer, err := NewDefault(ctx, options) if err != nil { return nil, err } @@ -77,11 +72,11 @@ func NewDirect(ctx context.Context, options option.DialerOptions) (ParallelInter type ParallelInterfaceDialer interface { N.Dialer - DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) - ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) + DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) + ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) } type ParallelNetworkDialer interface { - DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) - ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) + DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) + ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) } diff --git a/common/dialer/resolve.go b/common/dialer/resolve.go index b5d922b375..ede1afd614 100644 --- a/common/dialer/resolve.go +++ b/common/dialer/resolve.go @@ -106,7 +106,7 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil } -func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { +func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context, network string, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { if !destination.IsFqdn() { return d.dialer.DialContext(ctx, network, destination) } @@ -134,7 +134,7 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context } } -func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) { +func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) { if !destination.IsFqdn() { return d.dialer.ListenPacket(ctx, destination) } diff --git a/option/outbound.go b/option/outbound.go index 833a2d2030..5cadd3e2ae 100644 --- a/option/outbound.go +++ b/option/outbound.go @@ -65,25 +65,24 @@ type DialerOptionsWrapper interface { } type DialerOptions struct { - Detour string `json:"detour,omitempty"` - BindInterface string `json:"bind_interface,omitempty"` - Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"` - Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"` - ProtectPath string `json:"protect_path,omitempty"` - RoutingMark FwMark `json:"routing_mark,omitempty"` - ReuseAddr bool `json:"reuse_addr,omitempty"` - ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"` - TCPFastOpen bool `json:"tcp_fast_open,omitempty"` - TCPMultiPath bool `json:"tcp_multi_path,omitempty"` - UDPFragment *bool `json:"udp_fragment,omitempty"` - UDPFragmentDefault bool `json:"-"` - DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` - NetworkStrategy NetworkStrategy `json:"network_strategy,omitempty"` - NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` - FallbackNetworkType badoption.Listable[InterfaceType] `json:"fallback_network_type,omitempty"` - FallbackDelay badoption.Duration `json:"fallback_delay,omitempty"` - NetworkFallbackDelay badoption.Duration `json:"network_fallback_delay,omitempty"` - IsWireGuardListener bool `json:"-"` + Detour string `json:"detour,omitempty"` + BindInterface string `json:"bind_interface,omitempty"` + Inet4BindAddress *badoption.Addr `json:"inet4_bind_address,omitempty"` + Inet6BindAddress *badoption.Addr `json:"inet6_bind_address,omitempty"` + ProtectPath string `json:"protect_path,omitempty"` + RoutingMark FwMark `json:"routing_mark,omitempty"` + ReuseAddr bool `json:"reuse_addr,omitempty"` + ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"` + TCPFastOpen bool `json:"tcp_fast_open,omitempty"` + TCPMultiPath bool `json:"tcp_multi_path,omitempty"` + UDPFragment *bool `json:"udp_fragment,omitempty"` + UDPFragmentDefault bool `json:"-"` + DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"` + NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"` + NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"` + FallbackNetworkType badoption.Listable[InterfaceType] `json:"fallback_network_type,omitempty"` + FallbackDelay badoption.Duration `json:"fallback_delay,omitempty"` + IsWireGuardListener bool `json:"-"` } func (o *DialerOptions) TakeDialerOptions() DialerOptions { diff --git a/option/route.go b/option/route.go index 0eb1cbf1b2..1eb2294bfe 100644 --- a/option/route.go +++ b/option/route.go @@ -13,7 +13,7 @@ type RouteOptions struct { OverrideAndroidVPN bool `json:"override_android_vpn,omitempty"` DefaultInterface string `json:"default_interface,omitempty"` DefaultMark FwMark `json:"default_mark,omitempty"` - DefaultNetworkStrategy NetworkStrategy `json:"default_network_strategy,omitempty"` + DefaultNetworkStrategy *NetworkStrategy `json:"default_network_strategy,omitempty"` DefaultNetworkType badoption.Listable[InterfaceType] `json:"default_network_type,omitempty"` DefaultFallbackNetworkType badoption.Listable[InterfaceType] `json:"default_fallback_network_type,omitempty"` DefaultFallbackDelay badoption.Duration `json:"default_fallback_delay,omitempty"` diff --git a/option/rule_action.go b/option/rule_action.go index 29c5a0c30e..b700362824 100644 --- a/option/rule_action.go +++ b/option/rule_action.go @@ -145,8 +145,8 @@ type RawRouteOptionsActionOptions struct { OverrideAddress string `json:"override_address,omitempty"` OverridePort uint16 `json:"override_port,omitempty"` - NetworkStrategy NetworkStrategy `json:"network_strategy,omitempty"` - FallbackDelay uint32 `json:"fallback_delay,omitempty"` + NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"` + FallbackDelay uint32 `json:"fallback_delay,omitempty"` UDPDisableDomainUnmapping bool `json:"udp_disable_domain_unmapping,omitempty"` UDPConnect bool `json:"udp_connect,omitempty"` diff --git a/protocol/direct/outbound.go b/protocol/direct/outbound.go index 42fd284de5..aba5633621 100644 --- a/protocol/direct/outbound.go +++ b/protocol/direct/outbound.go @@ -32,16 +32,12 @@ var ( type Outbound struct { outbound.Adapter - logger logger.ContextLogger - dialer dialer.ParallelInterfaceDialer - domainStrategy dns.DomainStrategy - fallbackDelay time.Duration - networkStrategy C.NetworkStrategy - networkType []C.InterfaceType - fallbackNetworkType []C.InterfaceType - networkFallbackDelay time.Duration - overrideOption int - overrideDestination M.Socksaddr + logger logger.ContextLogger + dialer dialer.ParallelInterfaceDialer + domainStrategy dns.DomainStrategy + fallbackDelay time.Duration + overrideOption int + overrideDestination M.Socksaddr // loopBack *loopBackDetector } @@ -52,15 +48,11 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL return nil, err } outbound := &Outbound{ - Adapter: outbound.NewAdapterWithDialerOptions(C.TypeDirect, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions), - logger: logger, - domainStrategy: dns.DomainStrategy(options.DomainStrategy), - fallbackDelay: time.Duration(options.FallbackDelay), - networkStrategy: C.NetworkStrategy(options.NetworkStrategy), - networkType: common.Map(options.NetworkType, option.InterfaceType.Build), - fallbackNetworkType: common.Map(options.FallbackNetworkType, option.InterfaceType.Build), - networkFallbackDelay: time.Duration(options.NetworkFallbackDelay), - dialer: outboundDialer, + Adapter: outbound.NewAdapterWithDialerOptions(C.TypeDirect, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions), + logger: logger, + domainStrategy: dns.DomainStrategy(options.DomainStrategy), + fallbackDelay: time.Duration(options.FallbackDelay), + dialer: outboundDialer, // loopBack: newLoopBackDetector(router), } //nolint:staticcheck @@ -178,10 +170,10 @@ func (h *Outbound) DialParallel(ctx context.Context, network string, destination return nil, E.New("no IPv6 address available for ", destination) } } - return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == dns.DomainStrategyPreferIPv6, h.networkStrategy, h.networkType, h.fallbackNetworkType, h.fallbackDelay) + return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == dns.DomainStrategyPreferIPv6, nil, nil, nil, h.fallbackDelay) } -func (h *Outbound) DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy C.NetworkStrategy, networkType []C.InterfaceType, fallbackNetworkType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { +func (h *Outbound) DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy *C.NetworkStrategy, networkType []C.InterfaceType, fallbackNetworkType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination @@ -221,7 +213,7 @@ func (h *Outbound) DialParallelNetwork(ctx context.Context, network string, dest return dialer.DialParallelNetwork(ctx, h.dialer, network, destination, destinationAddresses, domainStrategy == dns.DomainStrategyPreferIPv6, networkStrategy, networkType, fallbackNetworkType, fallbackDelay) } -func (h *Outbound) ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy C.NetworkStrategy, networkType []C.InterfaceType, fallbackNetworkType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { +func (h *Outbound) ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, networkStrategy *C.NetworkStrategy, networkType []C.InterfaceType, fallbackNetworkType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error) { ctx, metadata := adapter.ExtendContext(ctx) metadata.Outbound = h.Tag() metadata.Destination = destination diff --git a/protocol/wireguard/endpoint.go b/protocol/wireguard/endpoint.go index 937f84dd9a..21d72bd982 100644 --- a/protocol/wireguard/endpoint.go +++ b/protocol/wireguard/endpoint.go @@ -20,7 +20,6 @@ import ( "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/service" ) func RegisterEndpoint(registry *endpoint.Registry) { @@ -70,7 +69,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL UDPTimeout: udpTimeout, Dialer: outboundDialer, CreateDialer: func(interfaceName string) N.Dialer { - return common.Must1(dialer.NewDefault(service.FromContext[adapter.NetworkManager](ctx), option.DialerOptions{ + return common.Must1(dialer.NewDefault(ctx, option.DialerOptions{ BindInterface: interfaceName, })) }, diff --git a/protocol/wireguard/outbound.go b/protocol/wireguard/outbound.go index a1fce79610..3e29970535 100644 --- a/protocol/wireguard/outbound.go +++ b/protocol/wireguard/outbound.go @@ -19,7 +19,6 @@ import ( "github.com/sagernet/sing/common/logger" M "github.com/sagernet/sing/common/metadata" N "github.com/sagernet/sing/common/network" - "github.com/sagernet/sing/service" ) func RegisterOutbound(registry *outbound.Registry) { @@ -86,7 +85,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL System: options.SystemInterface, Dialer: outboundDialer, CreateDialer: func(interfaceName string) N.Dialer { - return common.Must1(dialer.NewDefault(service.FromContext[adapter.NetworkManager](ctx), option.DialerOptions{ + return common.Must1(dialer.NewDefault(ctx, option.DialerOptions{ BindInterface: interfaceName, })) }, diff --git a/route/conn.go b/route/conn.go index 93ac33e359..e010c2cdb6 100644 --- a/route/conn.go +++ b/route/conn.go @@ -56,7 +56,7 @@ func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, co remoteConn net.Conn err error ) - if len(metadata.DestinationAddresses) > 0 { + if len(metadata.DestinationAddresses) > 0 || metadata.Destination.IsIP() { remoteConn, err = dialer.DialSerialNetwork(ctx, this, N.NetworkTCP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) } else { remoteConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination) @@ -97,12 +97,19 @@ func (m *ConnectionManager) NewPacketConnection(ctx context.Context, this N.Dial err error ) if metadata.UDPConnect { + parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer) if len(metadata.DestinationAddresses) > 0 { - if parallelDialer, isParallelDialer := this.(dialer.ParallelInterfaceDialer); isParallelDialer { + if isParallelDialer { remoteConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) } else { remoteConn, err = N.DialSerial(ctx, this, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses) } + } else if metadata.Destination.IsIP() { + if isParallelDialer { + remoteConn, err = dialer.DialSerialNetwork(ctx, parallelDialer, N.NetworkUDP, metadata.Destination, metadata.DestinationAddresses, metadata.NetworkStrategy, metadata.NetworkType, metadata.FallbackNetworkType, metadata.FallbackDelay) + } else { + remoteConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination) + } } else { remoteConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination) } diff --git a/route/network.go b/route/network.go index d11508b125..97d165f119 100644 --- a/route/network.go +++ b/route/network.go @@ -62,7 +62,7 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOp defaultOptions: adapter.NetworkOptions{ BindInterface: routeOptions.DefaultInterface, RoutingMark: uint32(routeOptions.DefaultMark), - NetworkStrategy: C.NetworkStrategy(routeOptions.DefaultNetworkStrategy), + NetworkStrategy: (*C.NetworkStrategy)(routeOptions.DefaultNetworkStrategy), NetworkType: common.Map(routeOptions.DefaultNetworkType, option.InterfaceType.Build), FallbackNetworkType: common.Map(routeOptions.DefaultFallbackNetworkType, option.InterfaceType.Build), FallbackDelay: time.Duration(routeOptions.DefaultFallbackDelay), @@ -73,7 +73,7 @@ func NewNetworkManager(ctx context.Context, logger logger.ContextLogger, routeOp inbound: service.FromContext[adapter.InboundManager](ctx), outbound: service.FromContext[adapter.OutboundManager](ctx), } - if C.NetworkStrategy(routeOptions.DefaultNetworkStrategy) != C.NetworkStrategyDefault { + if routeOptions.DefaultNetworkStrategy != nil { if routeOptions.DefaultInterface != "" { return nil, E.New("`default_network_strategy` is conflict with `default_interface`") } diff --git a/route/route.go b/route/route.go index fb2de85d51..ca7474623b 100644 --- a/route/route.go +++ b/route/route.go @@ -415,8 +415,18 @@ match: Fqdn: metadata.Destination.Fqdn, } } - metadata.NetworkStrategy = routeOptions.NetworkStrategy - metadata.FallbackDelay = routeOptions.FallbackDelay + if routeOptions.NetworkStrategy != nil { + metadata.NetworkStrategy = routeOptions.NetworkStrategy + } + if len(routeOptions.NetworkType) > 0 { + metadata.NetworkType = routeOptions.NetworkType + } + if len(routeOptions.FallbackNetworkType) > 0 { + metadata.FallbackNetworkType = routeOptions.FallbackNetworkType + } + if routeOptions.FallbackDelay != 0 { + metadata.FallbackDelay = routeOptions.FallbackDelay + } if routeOptions.UDPDisableDomainUnmapping { metadata.UDPDisableDomainUnmapping = true } diff --git a/route/router.go b/route/router.go index 792391e265..6526778bea 100644 --- a/route/router.go +++ b/route/router.go @@ -262,7 +262,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route Context: ctx, Name: "local", Address: "local", - Dialer: common.Must1(dialer.NewDefault(router.network, option.DialerOptions{})), + Dialer: common.Must1(dialer.NewDefault(ctx, option.DialerOptions{})), }))) } defaultTransport = transports[0] diff --git a/route/rule/rule_action.go b/route/rule/rule_action.go index 34354cc0d3..f4f2299ae7 100644 --- a/route/rule/rule_action.go +++ b/route/rule/rule_action.go @@ -33,7 +33,7 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti RuleActionRouteOptions: RuleActionRouteOptions{ OverrideAddress: M.ParseSocksaddrHostPort(action.RouteOptions.OverrideAddress, 0), OverridePort: action.RouteOptions.OverridePort, - NetworkStrategy: C.NetworkStrategy(action.RouteOptions.NetworkStrategy), + NetworkStrategy: (*C.NetworkStrategy)(action.RouteOptions.NetworkStrategy), FallbackDelay: time.Duration(action.RouteOptions.FallbackDelay), UDPDisableDomainUnmapping: action.RouteOptions.UDPDisableDomainUnmapping, UDPConnect: action.RouteOptions.UDPConnect, @@ -43,7 +43,7 @@ func NewRuleAction(ctx context.Context, logger logger.ContextLogger, action opti return &RuleActionRouteOptions{ OverrideAddress: M.ParseSocksaddrHostPort(action.RouteOptionsOptions.OverrideAddress, 0), OverridePort: action.RouteOptionsOptions.OverridePort, - NetworkStrategy: C.NetworkStrategy(action.RouteOptionsOptions.NetworkStrategy), + NetworkStrategy: (*C.NetworkStrategy)(action.RouteOptionsOptions.NetworkStrategy), FallbackDelay: time.Duration(action.RouteOptionsOptions.FallbackDelay), UDPDisableDomainUnmapping: action.RouteOptionsOptions.UDPDisableDomainUnmapping, UDPConnect: action.RouteOptionsOptions.UDPConnect, @@ -147,7 +147,7 @@ func (r *RuleActionRoute) String() string { type RuleActionRouteOptions struct { OverrideAddress M.Socksaddr OverridePort uint16 - NetworkStrategy C.NetworkStrategy + NetworkStrategy *C.NetworkStrategy NetworkType []C.InterfaceType FallbackNetworkType []C.InterfaceType FallbackDelay time.Duration diff --git a/transport/dhcp/server.go b/transport/dhcp/server.go index 29c6bbe022..8b9187f0fb 100644 --- a/transport/dhcp/server.go +++ b/transport/dhcp/server.go @@ -253,7 +253,7 @@ func (t *Transport) recreateServers(iface *control.Interface, serverAddrs []neti return it.String() }), ","), "]") } - serverDialer := common.Must1(dialer.NewDefault(t.networkManager, option.DialerOptions{ + serverDialer := common.Must1(dialer.NewDefault(t.options.Context, option.DialerOptions{ BindInterface: iface.Name, UDPFragmentDefault: true, })) From be8ee370acde6d6dc6ce4eeba64addcc4e6fb878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 15 Dec 2024 21:27:39 +0800 Subject: [PATCH 38/49] Fix DNS match log --- route/route_dns.go | 115 +++++++++++++++++++++++---------------------- 1 file changed, 58 insertions(+), 57 deletions(-) diff --git a/route/route_dns.go b/route/route_dns.go index 696e167668..4f850b5336 100644 --- a/route/route_dns.go +++ b/route/route_dns.go @@ -45,69 +45,70 @@ func (r *Router) matchDNS(ctx context.Context, allowFakeIP bool, ruleIndex int, panic("no context") } var options dns.QueryOptions - if ruleIndex < len(r.dnsRules) { - dnsRules := r.dnsRules - if ruleIndex != -1 { - dnsRules = dnsRules[ruleIndex+1:] + var ( + currentRuleIndex int + currentRule adapter.DNSRule + ) + if ruleIndex != -1 { + currentRuleIndex = ruleIndex + 1 + } + for currentRuleIndex, currentRule = range r.dnsRules[currentRuleIndex:] { + if currentRule.WithAddressLimit() && !isAddressQuery { + continue } - for currentRuleIndex, currentRule := range dnsRules { - if currentRule.WithAddressLimit() && !isAddressQuery { - continue + metadata.ResetRuleCache() + if currentRule.Match(metadata) { + displayRuleIndex := currentRuleIndex + if ruleIndex != -1 { + displayRuleIndex += ruleIndex + 1 + } + ruleDescription := currentRule.String() + if ruleDescription != "" { + r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] ", currentRule, " => ", currentRule.Action()) + } else { + r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action()) } - metadata.ResetRuleCache() - if currentRule.Match(metadata) { - displayRuleIndex := currentRuleIndex - if displayRuleIndex != -1 { - displayRuleIndex += displayRuleIndex + 1 + switch action := currentRule.Action().(type) { + case *R.RuleActionDNSRoute: + transport, loaded := r.transportMap[action.Server] + if !loaded { + r.dnsLogger.ErrorContext(ctx, "transport not found: ", action.Server) + continue } - ruleDescription := currentRule.String() - if ruleDescription != "" { - r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] ", currentRule, " => ", currentRule.Action()) + _, isFakeIP := transport.(adapter.FakeIPTransport) + if isFakeIP && !allowFakeIP { + continue + } + if isFakeIP || action.DisableCache { + options.DisableCache = true + } + if action.RewriteTTL != nil { + options.RewriteTTL = action.RewriteTTL + } + if action.ClientSubnet.IsValid() { + options.ClientSubnet = action.ClientSubnet + } + if domainStrategy, dsLoaded := r.transportDomainStrategy[transport]; dsLoaded { + options.Strategy = domainStrategy } else { - r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action()) + options.Strategy = r.defaultDomainStrategy } - switch action := currentRule.Action().(type) { - case *R.RuleActionDNSRoute: - transport, loaded := r.transportMap[action.Server] - if !loaded { - r.dnsLogger.ErrorContext(ctx, "transport not found: ", action.Server) - continue - } - _, isFakeIP := transport.(adapter.FakeIPTransport) - if isFakeIP && !allowFakeIP { - continue - } - if isFakeIP || action.DisableCache { - options.DisableCache = true - } - if action.RewriteTTL != nil { - options.RewriteTTL = action.RewriteTTL - } - if action.ClientSubnet.IsValid() { - options.ClientSubnet = action.ClientSubnet - } - if domainStrategy, dsLoaded := r.transportDomainStrategy[transport]; dsLoaded { - options.Strategy = domainStrategy - } else { - options.Strategy = r.defaultDomainStrategy - } - r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action()) - return transport, options, currentRule, currentRuleIndex - case *R.RuleActionDNSRouteOptions: - if action.DisableCache { - options.DisableCache = true - } - if action.RewriteTTL != nil { - options.RewriteTTL = action.RewriteTTL - } - if action.ClientSubnet.IsValid() { - options.ClientSubnet = action.ClientSubnet - } - r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action()) - case *R.RuleActionReject: - r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action()) - return nil, options, currentRule, currentRuleIndex + r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action()) + return transport, options, currentRule, currentRuleIndex + case *R.RuleActionDNSRouteOptions: + if action.DisableCache { + options.DisableCache = true + } + if action.RewriteTTL != nil { + options.RewriteTTL = action.RewriteTTL + } + if action.ClientSubnet.IsValid() { + options.ClientSubnet = action.ClientSubnet } + r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action()) + case *R.RuleActionReject: + r.logger.DebugContext(ctx, "match[", displayRuleIndex, "] => ", currentRule.Action()) + return nil, options, currentRule, currentRuleIndex } } } From 60fc913dc31cafe5c5eb63cc5690d81dcfff2269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 16 Dec 2024 12:01:42 +0800 Subject: [PATCH 39/49] Add `rule-set merge` command --- Makefile | 2 +- cmd/sing-box/cmd_merge.go | 2 +- cmd/sing-box/cmd_rule_set_merge.go | 162 +++++++++++++++++++++++++++++ option/rule_set.go | 6 +- release/completions/sing-box.bash | 31 ++++++ 5 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 cmd/sing-box/cmd_rule_set_merge.go diff --git a/Makefile b/Makefile index 11cecd17f6..461233271f 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ ci_build: go build $(MAIN_PARAMS) $(MAIN) generate_completions: - go run -v --tags generate,generate_completions $(MAIN) + go run -v --tags $(TAGS),generate,generate_completions $(MAIN) install: go build -o $(PREFIX)/bin/$(NAME) $(MAIN_PARAMS) $(MAIN) diff --git a/cmd/sing-box/cmd_merge.go b/cmd/sing-box/cmd_merge.go index fa194ed342..6ca9a15a10 100644 --- a/cmd/sing-box/cmd_merge.go +++ b/cmd/sing-box/cmd_merge.go @@ -18,7 +18,7 @@ import ( ) var commandMerge = &cobra.Command{ - Use: "merge ", + Use: "merge ", Short: "Merge configurations", Run: func(cmd *cobra.Command, args []string) { err := merge(args[0]) diff --git a/cmd/sing-box/cmd_rule_set_merge.go b/cmd/sing-box/cmd_rule_set_merge.go new file mode 100644 index 0000000000..7c8f7a5353 --- /dev/null +++ b/cmd/sing-box/cmd_rule_set_merge.go @@ -0,0 +1,162 @@ +package main + +import ( + "bytes" + "io" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/json" + "github.com/sagernet/sing/common/json/badjson" + "github.com/sagernet/sing/common/rw" + + "github.com/spf13/cobra" +) + +var ( + ruleSetPaths []string + ruleSetDirectories []string +) + +var commandRuleSetMerge = &cobra.Command{ + Use: "merge ", + Short: "Merge rule-set source files", + Run: func(cmd *cobra.Command, args []string) { + err := mergeRuleSet(args[0]) + if err != nil { + log.Fatal(err) + } + }, + Args: cobra.ExactArgs(1), +} + +func init() { + commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetPaths, "config", "c", nil, "set input rule-set file path") + commandRuleSetMerge.Flags().StringArrayVarP(&ruleSetDirectories, "config-directory", "C", nil, "set input rule-set directory path") + commandRuleSet.AddCommand(commandRuleSetMerge) +} + +type RuleSetEntry struct { + content []byte + path string + options option.PlainRuleSetCompat +} + +func readRuleSetAt(path string) (*RuleSetEntry, error) { + var ( + configContent []byte + err error + ) + if path == "stdin" { + configContent, err = io.ReadAll(os.Stdin) + } else { + configContent, err = os.ReadFile(path) + } + if err != nil { + return nil, E.Cause(err, "read config at ", path) + } + options, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, configContent) + if err != nil { + return nil, E.Cause(err, "decode config at ", path) + } + return &RuleSetEntry{ + content: configContent, + path: path, + options: options, + }, nil +} + +func readRuleSet() ([]*RuleSetEntry, error) { + var optionsList []*RuleSetEntry + for _, path := range ruleSetPaths { + optionsEntry, err := readRuleSetAt(path) + if err != nil { + return nil, err + } + optionsList = append(optionsList, optionsEntry) + } + for _, directory := range ruleSetDirectories { + entries, err := os.ReadDir(directory) + if err != nil { + return nil, E.Cause(err, "read rule-set directory at ", directory) + } + for _, entry := range entries { + if !strings.HasSuffix(entry.Name(), ".json") || entry.IsDir() { + continue + } + optionsEntry, err := readRuleSetAt(filepath.Join(directory, entry.Name())) + if err != nil { + return nil, err + } + optionsList = append(optionsList, optionsEntry) + } + } + sort.Slice(optionsList, func(i, j int) bool { + return optionsList[i].path < optionsList[j].path + }) + return optionsList, nil +} + +func readRuleSetAndMerge() (option.PlainRuleSetCompat, error) { + optionsList, err := readRuleSet() + if err != nil { + return option.PlainRuleSetCompat{}, err + } + if len(optionsList) == 1 { + return optionsList[0].options, nil + } + var optionVersion uint8 + for _, options := range optionsList { + if optionVersion < options.options.Version { + optionVersion = options.options.Version + } + } + var mergedMessage json.RawMessage + for _, options := range optionsList { + mergedMessage, err = badjson.MergeJSON(globalCtx, options.options.RawMessage, mergedMessage, false) + if err != nil { + return option.PlainRuleSetCompat{}, E.Cause(err, "merge config at ", options.path) + } + } + mergedOptions, err := json.UnmarshalExtendedContext[option.PlainRuleSetCompat](globalCtx, mergedMessage) + if err != nil { + return option.PlainRuleSetCompat{}, E.Cause(err, "unmarshal merged config") + } + mergedOptions.Version = optionVersion + return mergedOptions, nil +} + +func mergeRuleSet(outputPath string) error { + mergedOptions, err := readRuleSetAndMerge() + if err != nil { + return err + } + buffer := new(bytes.Buffer) + encoder := json.NewEncoder(buffer) + encoder.SetIndent("", " ") + err = encoder.Encode(mergedOptions) + if err != nil { + return E.Cause(err, "encode config") + } + if existsContent, err := os.ReadFile(outputPath); err != nil { + if string(existsContent) == buffer.String() { + return nil + } + } + err = rw.MkdirParent(outputPath) + if err != nil { + return err + } + err = os.WriteFile(outputPath, buffer.Bytes(), 0o644) + if err != nil { + return err + } + outputPath, _ = filepath.Abs(outputPath) + os.Stderr.WriteString(outputPath + "\n") + return nil +} diff --git a/option/rule_set.go b/option/rule_set.go index f7a5f3344f..bf644764ad 100644 --- a/option/rule_set.go +++ b/option/rule_set.go @@ -194,8 +194,9 @@ func (r LogicalHeadlessRule) IsValid() bool { } type _PlainRuleSetCompat struct { - Version uint8 `json:"version"` - Options PlainRuleSet `json:"-"` + Version uint8 `json:"version"` + Options PlainRuleSet `json:"-"` + RawMessage json.RawMessage `json:"-"` } type PlainRuleSetCompat _PlainRuleSetCompat @@ -229,6 +230,7 @@ func (r *PlainRuleSetCompat) UnmarshalJSON(bytes []byte) error { if err != nil { return err } + r.RawMessage = bytes return nil } diff --git a/release/completions/sing-box.bash b/release/completions/sing-box.bash index ab1029770b..22df7763cf 100644 --- a/release/completions/sing-box.bash +++ b/release/completions/sing-box.bash @@ -1179,6 +1179,36 @@ _sing-box_rule-set_match() noun_aliases=() } +_sing-box_rule-set_merge() +{ + last_command="sing-box_rule-set_merge" + + command_aliases=() + + commands=() + + flags=() + two_word_flags=() + local_nonpersistent_flags=() + flags_with_completion=() + flags_completion=() + + flags+=("--config=") + two_word_flags+=("--config") + two_word_flags+=("-c") + flags+=("--config-directory=") + two_word_flags+=("--config-directory") + two_word_flags+=("-C") + flags+=("--directory=") + two_word_flags+=("--directory") + two_word_flags+=("-D") + flags+=("--disable-color") + + must_have_one_flag=() + must_have_one_noun=() + noun_aliases=() +} + _sing-box_rule-set_upgrade() { last_command="sing-box_rule-set_upgrade" @@ -1225,6 +1255,7 @@ _sing-box_rule-set() commands+=("decompile") commands+=("format") commands+=("match") + commands+=("merge") commands+=("upgrade") flags=() From 1ed8f3a8d38683689f8ef07cfbc938106576c1f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 21 Dec 2024 21:26:42 +0800 Subject: [PATCH 40/49] Fix default dialer on legacy xiaomi systems --- common/dialer/default.go | 94 +++++++++++++-------- common/dialer/default_parallel_interface.go | 8 +- 2 files changed, 62 insertions(+), 40 deletions(-) diff --git a/common/dialer/default.go b/common/dialer/default.go index 49bd145cd6..77536c43db 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -2,8 +2,10 @@ package dialer import ( "context" + "errors" "net" "net/netip" + "syscall" "time" "github.com/sagernet/sing-box/adapter" @@ -26,20 +28,21 @@ var ( ) type DefaultDialer struct { - dialer4 tcpDialer - dialer6 tcpDialer - udpDialer4 net.Dialer - udpDialer6 net.Dialer - udpListener net.ListenConfig - udpAddr4 string - udpAddr6 string - isWireGuardListener bool - networkManager adapter.NetworkManager - networkStrategy *C.NetworkStrategy - networkType []C.InterfaceType - fallbackNetworkType []C.InterfaceType - networkFallbackDelay time.Duration - networkLastFallback atomic.TypedValue[time.Time] + dialer4 tcpDialer + dialer6 tcpDialer + udpDialer4 net.Dialer + udpDialer6 net.Dialer + udpListener net.ListenConfig + udpAddr4 string + udpAddr6 string + isWireGuardListener bool + networkManager adapter.NetworkManager + networkStrategy *C.NetworkStrategy + defaultNetworkStrategy bool + networkType []C.InterfaceType + fallbackNetworkType []C.InterfaceType + networkFallbackDelay time.Duration + networkLastFallback atomic.TypedValue[time.Time] } func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) { @@ -47,13 +50,14 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial platformInterface := service.FromContext[platform.Interface](ctx) var ( - dialer net.Dialer - listener net.ListenConfig - interfaceFinder control.InterfaceFinder - networkStrategy *C.NetworkStrategy - networkType []C.InterfaceType - fallbackNetworkType []C.InterfaceType - networkFallbackDelay time.Duration + dialer net.Dialer + listener net.ListenConfig + interfaceFinder control.InterfaceFinder + networkStrategy *C.NetworkStrategy + defaultNetworkStrategy bool + networkType []C.InterfaceType + fallbackNetworkType []C.InterfaceType + networkFallbackDelay time.Duration ) if networkManager != nil { interfaceFinder = networkManager.InterfaceFinder() @@ -98,6 +102,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial networkStrategy = (*C.NetworkStrategy)(options.NetworkStrategy) if networkStrategy == nil { networkStrategy = common.Ptr(C.NetworkStrategyDefault) + defaultNetworkStrategy = true } networkType = common.Map(options.NetworkType, option.InterfaceType.Build) fallbackNetworkType = common.Map(options.FallbackNetworkType, option.InterfaceType.Build) @@ -192,19 +197,20 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial return nil, err } return &DefaultDialer{ - dialer4: tcpDialer4, - dialer6: tcpDialer6, - udpDialer4: udpDialer4, - udpDialer6: udpDialer6, - udpListener: listener, - udpAddr4: udpAddr4, - udpAddr6: udpAddr6, - isWireGuardListener: options.IsWireGuardListener, - networkManager: networkManager, - networkStrategy: networkStrategy, - networkType: networkType, - fallbackNetworkType: fallbackNetworkType, - networkFallbackDelay: networkFallbackDelay, + dialer4: tcpDialer4, + dialer6: tcpDialer6, + udpDialer4: udpDialer4, + udpDialer6: udpDialer6, + udpListener: listener, + udpAddr4: udpAddr4, + udpAddr6: udpAddr6, + isWireGuardListener: options.IsWireGuardListener, + networkManager: networkManager, + networkStrategy: networkStrategy, + defaultNetworkStrategy: defaultNetworkStrategy, + networkType: networkType, + fallbackNetworkType: fallbackNetworkType, + networkFallbackDelay: networkFallbackDelay, }, nil } @@ -265,7 +271,13 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin conn, isPrimary, err = d.dialParallelInterfaceFastFallback(ctx, dialer, network, address.String(), *strategy, interfaceType, fallbackInterfaceType, fallbackDelay, d.networkLastFallback.Store) } if err != nil { - return nil, err + // bind interface failed on legacy xiaomi systems + if d.defaultNetworkStrategy && errors.Is(err, syscall.EPERM) { + d.networkStrategy = nil + return d.DialContext(ctx, network, address) + } else { + return nil, err + } } if !fastFallback && !isPrimary { d.networkLastFallback.Store(time.Now()) @@ -307,7 +319,17 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina if destination.IsIPv4() && !destination.Addr.IsUnspecified() { network += "4" } - return trackPacketConn(d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", *strategy, interfaceType, fallbackInterfaceType, fallbackDelay)) + packetConn, err := d.listenSerialInterfacePacket(ctx, d.udpListener, network, "", *strategy, interfaceType, fallbackInterfaceType, fallbackDelay) + if err != nil { + // bind interface failed on legacy xiaomi systems + if d.defaultNetworkStrategy && errors.Is(err, syscall.EPERM) { + d.networkStrategy = nil + return d.ListenPacket(ctx, destination) + } else { + return nil, err + } + } + return trackPacketConn(packetConn, nil) } func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) { diff --git a/common/dialer/default_parallel_interface.go b/common/dialer/default_parallel_interface.go index 269546a413..92e3535e81 100644 --- a/common/dialer/default_parallel_interface.go +++ b/common/dialer/default_parallel_interface.go @@ -35,7 +35,7 @@ func (d *DefaultDialer) dialParallelInterface(ctx context.Context, dialer net.Di conn, err := perNetDialer.DialContext(ctx, network, addr) if err != nil { select { - case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Name, ")"), primary: primary}: + case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")"), primary: primary}: case <-returned: } } else { @@ -107,7 +107,7 @@ func (d *DefaultDialer) dialParallelInterfaceFastFallback(ctx context.Context, d conn, err := perNetDialer.DialContext(ctx, network, addr) if err != nil { select { - case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Name, ")"), primary: primary}: + case results <- dialResult{error: E.Cause(err, "dial ", iif.Name, " (", iif.Index, ")"), primary: primary}: case <-returned: } } else { @@ -157,7 +157,7 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene if err == nil { return conn, nil } - errors = append(errors, E.Cause(err, "listen ", primaryInterface.Name, " (", primaryInterface.Name, ")")) + errors = append(errors, E.Cause(err, "listen ", primaryInterface.Name, " (", primaryInterface.Index, ")")) } for _, fallbackInterface := range fallbackInterfaces { perNetListener := listener @@ -166,7 +166,7 @@ func (d *DefaultDialer) listenSerialInterfacePacket(ctx context.Context, listene if err == nil { return conn, nil } - errors = append(errors, E.Cause(err, "listen ", fallbackInterface.Name, " (", fallbackInterface.Name, ")")) + errors = append(errors, E.Cause(err, "listen ", fallbackInterface.Name, " (", fallbackInterface.Index, ")")) } return nil, E.Errors(errors...) } From f43703801b87c59eb100f459ccb6c78f85fdbb7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 22 Dec 2024 01:14:41 +0800 Subject: [PATCH 41/49] release: Upload debug apks to action --- .github/workflows/build.yml | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b813a66c11..357428f7c6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -256,7 +256,8 @@ jobs: with: path: ~/.gradle key: gradle-${{ hashFiles('**/*.gradle') }} - - name: Build + - name: Build release + if: github.event_name == 'workflow_dispatch' run: |- go run -v ./cmd/internal/update_android_version --ci mkdir clients/android/app/libs @@ -267,18 +268,47 @@ jobs: JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} - - name: Prepare upload + - name: Build debug + if: github.event_name != 'workflow_dispatch' + run: |- + go run -v ./cmd/internal/update_android_version --ci + mkdir clients/android/app/libs + cp libbox.aar clients/android/app/libs + cd clients/android + ./gradlew :app:assemblePlayRelease + env: + JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + LOCAL_PROPERTIES: ${{ secrets.LOCAL_PROPERTIES }} + - name: Prepare release upload if: github.event_name == 'workflow_dispatch' run: |- mkdir -p dist/release cp clients/android/app/build/outputs/apk/play/release/*.apk dist/release cp clients/android/app/build/outputs/apk/other/release/*-universal.apk dist/release + - name: Prepare debug upload + if: github.event_name != 'workflow_dispatch' + run: |- + mkdir -p dist/release + cp clients/android/app/build/outputs/apk/play/release/*.apk dist/release - name: Upload artifact if: github.event_name == 'workflow_dispatch' uses: actions/upload-artifact@v4 with: name: binary-android-apks path: 'dist' + - name: Upload debug apk (arm64-v8a) + if: github.event_name != 'workflow_dispatch' + uses: actions/upload-artifact@v4 + with: + name: "SFA-${{ needs.calculate_version.outputs.version }}-arm64-v8a.apk" + path: 'dist/release/*-arm64-v8a.apk' + - name: Upload debug apk (universal) + if: github.event_name != 'workflow_dispatch' + uses: actions/upload-artifact@v4 + with: + name: "SFA-${{ needs.calculate_version.outputs.version }}-universal.apk" + path: 'dist/release/*-universal.apk' publish_android: name: Publish Android if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android' From 606ff668da4a3b2e0fe85103399695bf99b37563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 22 Dec 2024 01:00:19 +0800 Subject: [PATCH 42/49] platform: Implement set underlying networks for android --- adapter/network.go | 1 + experimental/libbox/config.go | 4 ++++ experimental/libbox/platform.go | 8 ++++++++ experimental/libbox/platform/interface.go | 1 + experimental/libbox/raw_network_android.go | 3 +++ experimental/libbox/raw_network_stub.go | 7 +++++++ experimental/libbox/service.go | 7 +++++++ route/network.go | 12 ++++++++++++ 8 files changed, 43 insertions(+) create mode 100644 experimental/libbox/raw_network_android.go create mode 100644 experimental/libbox/raw_network_stub.go diff --git a/adapter/network.go b/adapter/network.go index 00ef54b8c0..22816f07ac 100644 --- a/adapter/network.go +++ b/adapter/network.go @@ -51,4 +51,5 @@ type NetworkInterface struct { DNSServers []string Expensive bool Constrained bool + RawNetwork any } diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index 159fd8f6a0..b27d34abfd 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -78,6 +78,10 @@ func (s *platformInterfaceStub) Interfaces() ([]adapter.NetworkInterface, error) return nil, os.ErrInvalid } +func (s *platformInterfaceStub) SetUnderlyingNetworks(networks []adapter.NetworkInterface) error { + return os.ErrInvalid +} + func (s *platformInterfaceStub) UnderNetworkExtension() bool { return false } diff --git a/experimental/libbox/platform.go b/experimental/libbox/platform.go index d5951cd38f..9b7423ad1d 100644 --- a/experimental/libbox/platform.go +++ b/experimental/libbox/platform.go @@ -17,6 +17,7 @@ type PlatformInterface interface { StartDefaultInterfaceMonitor(listener InterfaceUpdateListener) error CloseDefaultInterfaceMonitor(listener InterfaceUpdateListener) error GetInterfaces() (NetworkInterfaceIterator, error) + SetUnderlyingNetworks(networks RawNetworkIterator) error UnderNetworkExtension() bool IncludeAllNetworks() bool ReadWIFIState() *WIFIState @@ -50,6 +51,8 @@ type NetworkInterface struct { Type int32 DNSServer StringIterator Metered bool + + RawNetwork RawNetwork } type WIFIState struct { @@ -66,6 +69,11 @@ type NetworkInterfaceIterator interface { HasNext() bool } +type RawNetworkIterator interface { + Next() RawNetwork + HasNext() bool +} + type Notification struct { Identifier string TypeName string diff --git a/experimental/libbox/platform/interface.go b/experimental/libbox/platform/interface.go index ef37daea22..23849a3b83 100644 --- a/experimental/libbox/platform/interface.go +++ b/experimental/libbox/platform/interface.go @@ -15,6 +15,7 @@ type Interface interface { OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor Interfaces() ([]adapter.NetworkInterface, error) + SetUnderlyingNetworks(networks []adapter.NetworkInterface) error UnderNetworkExtension() bool IncludeAllNetworks() bool ClearDNSCache() diff --git a/experimental/libbox/raw_network_android.go b/experimental/libbox/raw_network_android.go new file mode 100644 index 0000000000..b8d38aaa9a --- /dev/null +++ b/experimental/libbox/raw_network_android.go @@ -0,0 +1,3 @@ +package libbox + +type RawNetwork interface{} diff --git a/experimental/libbox/raw_network_stub.go b/experimental/libbox/raw_network_stub.go new file mode 100644 index 0000000000..6e23135c04 --- /dev/null +++ b/experimental/libbox/raw_network_stub.go @@ -0,0 +1,7 @@ +//go:build !android + +package libbox + +type RawNetwork interface { + stub() +} diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 8dd4cb22fc..473ba4f231 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -206,11 +206,18 @@ func (w *platformInterfaceWrapper) Interfaces() ([]adapter.NetworkInterface, err DNSServers: iteratorToArray[string](netInterface.DNSServer), Expensive: netInterface.Metered || isDefault && w.isExpensive, Constrained: isDefault && w.isConstrained, + RawNetwork: netInterface.RawNetwork, }) } return interfaces, nil } +func (w *platformInterfaceWrapper) SetUnderlyingNetworks(networks []adapter.NetworkInterface) error { + return w.iif.SetUnderlyingNetworks(newIterator(common.Map(networks, func(it adapter.NetworkInterface) RawNetwork { + return it.RawNetwork.(RawNetwork) + }))) +} + func (w *platformInterfaceWrapper) UnderNetworkExtension() bool { return w.iif.UnderNetworkExtension() } diff --git a/route/network.go b/route/network.go index 97d165f119..05dffb019b 100644 --- a/route/network.go +++ b/route/network.go @@ -237,6 +237,9 @@ func (r *NetworkManager) UpdateInterfaces() error { newInterfaces := common.Filter(interfaces, func(it adapter.NetworkInterface) bool { return it.Flags&net.FlagUp != 0 }) + for _, networkInterface := range newInterfaces { + networkInterface.RawNetwork = nil + } r.networkInterfaces.Store(newInterfaces) if len(newInterfaces) > 0 && !slices.EqualFunc(oldInterfaces, newInterfaces, func(oldInterface adapter.NetworkInterface, newInterface adapter.NetworkInterface) bool { return oldInterface.Interface.Index == newInterface.Interface.Index && @@ -257,6 +260,15 @@ func (r *NetworkManager) UpdateInterfaces() error { } return F.ToString(it.Name, " (", strings.Join(options, ", "), ")") }), ", ")) + if C.IsAndroid { + err = r.platformInterface.SetUnderlyingNetworks(newInterfaces) + if err != nil { + r.logger.Error("set underlying networks: ", err) + } + } + } + for _, networkInterface := range interfaces { + networkInterface.RawNetwork = nil } return nil } From 0c754505f759b35ac91ecf3dc78fae2336fa7045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 23 Dec 2024 22:24:10 +0800 Subject: [PATCH 43/49] tun: Set address sets to routes --- docs/configuration/inbound/tun.md | 56 ++++++-- docs/configuration/inbound/tun.zh.md | 52 +++++-- experimental/libbox/config.go | 4 + experimental/libbox/platform.go | 1 + experimental/libbox/platform/interface.go | 1 + experimental/libbox/service.go | 18 ++- protocol/tun/inbound.go | 161 ++++++++++++++-------- route/router.go | 2 +- 8 files changed, 209 insertions(+), 86 deletions(-) diff --git a/docs/configuration/inbound/tun.md b/docs/configuration/inbound/tun.md index d82d451b2e..b6bf5c75f3 100644 --- a/docs/configuration/inbound/tun.md +++ b/docs/configuration/inbound/tun.md @@ -4,7 +4,9 @@ icon: material/alert-decagram !!! quote "Changes in sing-box 1.11.0" - :material-delete-alert: [gso](#gso) + :material-delete-alert: [gso](#gso) + :material-alert-decagram: [route_address_set](#stack) + :material-alert-decagram: [route_exclude_address_set](#stack) !!! quote "Changes in sing-box 1.10.0" @@ -248,7 +250,7 @@ use [VPNHotspot](https://github.com/Mygod/VPNHotspot). !!! question "Since sing-box 1.10.0" -Connection input mark used by `route_address_set` and `route_exclude_address_set`. +Connection input mark used by `route[_exclude]_address_set` with `auto_redirect`. `0x2023` is used by default. @@ -256,7 +258,7 @@ Connection input mark used by `route_address_set` and `route_exclude_address_set !!! question "Since sing-box 1.10.0" -Connection output mark used by `route_address_set` and `route_exclude_address_set`. +Connection input mark used by `route[_exclude]_address_set` with `auto_redirect`. `0x2024` is used by default. @@ -329,29 +331,55 @@ Exclude custom routes when `auto_route` is enabled. #### route_address_set -!!! question "Since sing-box 1.10.0" +=== "With `auto_redirect` enabled" -!!! quote "" + !!! question "Since sing-box 1.10.0" - Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled. + !!! quote "" + + Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled. + + Add the destination IP CIDR rules in the specified rule-sets to the firewall. + Unmatched traffic will bypass the sing-box routes. + + Conflict with `route.default_mark` and `[dialOptions].routing_mark`. -Add the destination IP CIDR rules in the specified rule-sets to the firewall. -Unmatched traffic will bypass the sing-box routes. +=== "Without `auto_redirect` enabled" -Conflict with `route.default_mark` and `[dialOptions].routing_mark`. + !!! question "Since sing-box 1.11.0" + + Add the destination IP CIDR rules in the specified rule-sets to routes, equivalent to adding to `route_address`. + Unmatched traffic will bypass the sing-box routes. + + Note that it **doesn't work on the Android graphical client** due to + the Android VpnService not being able to handle a large number of routes (DeadSystemException), + but otherwise it works fine on all command line clients and Apple platforms. #### route_exclude_address_set -!!! question "Since sing-box 1.10.0" +=== "With `auto_redirect` enabled" -!!! quote "" + !!! question "Since sing-box 1.10.0" + + !!! quote "" Only supported on Linux with nftables and requires `auto_route` and `auto_redirect` enabled. -Add the destination IP CIDR rules in the specified rule-sets to the firewall. -Matched traffic will bypass the sing-box routes. + Add the destination IP CIDR rules in the specified rule-sets to the firewall. + Matched traffic will bypass the sing-box routes. + + Conflict with `route.default_mark` and `[dialOptions].routing_mark`. + +=== "Without `auto_redirect` enabled" + + !!! question "Since sing-box 1.11.0" + + Add the destination IP CIDR rules in the specified rule-sets to routes, equivalent to adding to `route_exclude_address`. + Matched traffic will bypass the sing-box routes. -Conflict with `route.default_mark` and `[dialOptions].routing_mark`. + Note that it **doesn't work on the Android graphical client** due to + the Android VpnService not being able to handle a large number of routes (DeadSystemException), + but otherwise it works fine on all command line clients and Apple platforms. #### endpoint_independent_nat diff --git a/docs/configuration/inbound/tun.zh.md b/docs/configuration/inbound/tun.zh.md index 2b5752ba44..c9bd844d41 100644 --- a/docs/configuration/inbound/tun.zh.md +++ b/docs/configuration/inbound/tun.zh.md @@ -4,7 +4,9 @@ icon: material/alert-decagram !!! quote "sing-box 1.11.0 中的更改" - :material-delete-alert: [gso](#gso) + :material-delete-alert: [gso](#gso) + :material-alert-decagram: [route_address_set](#stack) + :material-alert-decagram: [route_exclude_address_set](#stack) !!! quote "sing-box 1.10.0 中的更改" @@ -329,29 +331,53 @@ tun 接口的 IPv6 前缀。 #### route_address_set -!!! question "自 sing-box 1.10.0 起" +=== "`auto_redirect` 已启用" -!!! quote "" + !!! question "自 sing-box 1.10.0 起" + + !!! quote "" + + 仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。 + + 将指定规则集中的目标 IP CIDR 规则添加到防火墙。 + 不匹配的流量将绕过 sing-box 路由。 + + 与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。 - 仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。 +=== "`auto_redirect` 未启用" -将指定规则集中的目标 IP CIDR 规则添加到防火墙。 -不匹配的流量将绕过 sing-box 路由。 + !!! question "自 sing-box 1.11.0 起" -与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。 + 将指定规则集中的目标 IP CIDR 规则添加到路由,相当于添加到 `route_address`。 + 不匹配的流量将绕过 sing-box 路由。 + + 请注意,由于 Android VpnService 无法处理大量路由(DeadSystemException), + 因此它**在 Android 图形客户端上不起作用**,但除此之外,它在所有命令行客户端和 Apple 平台上都可以正常工作。 #### route_exclude_address_set -!!! question "自 sing-box 1.10.0 起" +=== "`auto_redirect` 已启用" -!!! quote "" + !!! question "自 sing-box 1.10.0 起" + + !!! quote "" + + 仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。 + + 将指定规则集中的目标 IP CIDR 规则添加到防火墙。 + 匹配的流量将绕过 sing-box 路由。 + + 与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。 + +=== "`auto_redirect` 未启用" - 仅支持 Linux,且需要 nftables,`auto_route` 和 `auto_redirect` 已启用。 + !!! question "自 sing-box 1.11.0 起" -将指定规则集中的目标 IP CIDR 规则添加到防火墙。 -匹配的流量将绕过 sing-box 路由。 + 将指定规则集中的目标 IP CIDR 规则添加到路由,相当于添加到 `route_exclude_address`。 + 匹配的流量将绕过 sing-box 路由。 -与 `route.default_mark` 和 `[dialOptions].routing_mark` 冲突。 + 请注意,由于 Android VpnService 无法处理大量路由(DeadSystemException), + 因此它**在 Android 图形客户端上不起作用**,但除此之外,它在所有命令行客户端和 Apple 平台上都可以正常工作。 #### endpoint_independent_nat diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index b27d34abfd..7d0627fe17 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -66,6 +66,10 @@ func (s *platformInterfaceStub) OpenTun(options *tun.Options, platformOptions op return nil, os.ErrInvalid } +func (s *platformInterfaceStub) UpdateRouteOptions(options *tun.Options, platformInterface option.TunPlatformOptions) error { + return os.ErrInvalid +} + func (s *platformInterfaceStub) UsePlatformDefaultInterfaceMonitor() bool { return true } diff --git a/experimental/libbox/platform.go b/experimental/libbox/platform.go index 9b7423ad1d..f4bc7ea6bf 100644 --- a/experimental/libbox/platform.go +++ b/experimental/libbox/platform.go @@ -9,6 +9,7 @@ type PlatformInterface interface { UsePlatformAutoDetectInterfaceControl() bool AutoDetectInterfaceControl(fd int32) error OpenTun(options TunOptions) (int32, error) + UpdateRouteOptions(options TunOptions) error WriteLog(message string) UseProcFS() bool FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (int32, error) diff --git a/experimental/libbox/platform/interface.go b/experimental/libbox/platform/interface.go index 23849a3b83..1684d88cc5 100644 --- a/experimental/libbox/platform/interface.go +++ b/experimental/libbox/platform/interface.go @@ -13,6 +13,7 @@ type Interface interface { UsePlatformAutoDetectInterfaceControl() bool AutoDetectInterfaceControl(fd int) error OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) + UpdateRouteOptions(options *tun.Options, platformOptions option.TunPlatformOptions) error CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor Interfaces() ([]adapter.NetworkInterface, error) SetUnderlyingNetworks(networks []adapter.NetworkInterface) error diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 473ba4f231..613b2e66fe 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -148,10 +148,10 @@ func (w *platformInterfaceWrapper) AutoDetectInterfaceControl(fd int) error { func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) { if len(options.IncludeUID) > 0 || len(options.ExcludeUID) > 0 { - return nil, E.New("android: unsupported uid options") + return nil, E.New("platform: unsupported uid options") } if len(options.IncludeAndroidUser) > 0 { - return nil, E.New("android: unsupported android_user option") + return nil, E.New("platform: unsupported android_user option") } routeRanges, err := options.BuildAutoRouteRanges(true) if err != nil { @@ -174,6 +174,20 @@ func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions return tun.New(*options) } +func (w *platformInterfaceWrapper) UpdateRouteOptions(options *tun.Options, platformOptions option.TunPlatformOptions) error { + if len(options.IncludeUID) > 0 || len(options.ExcludeUID) > 0 { + return E.New("android: unsupported uid options") + } + if len(options.IncludeAndroidUser) > 0 { + return E.New("android: unsupported android_user option") + } + routeRanges, err := options.BuildAutoRouteRanges(true) + if err != nil { + return err + } + return w.iif.UpdateRouteOptions(&tunOptions{options, routeRanges, platformOptions}) +} + func (w *platformInterfaceWrapper) CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor { return &platformDefaultInterfaceMonitor{ platformInterfaceWrapper: w, diff --git a/protocol/tun/inbound.go b/protocol/tun/inbound.go index 26158d7a56..51e7ca78fe 100644 --- a/protocol/tun/inbound.go +++ b/protocol/tun/inbound.go @@ -209,6 +209,22 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo platformInterface: service.FromContext[platform.Interface](ctx), platformOptions: common.PtrValueOrDefault(options.Platform), } + for _, routeAddressSet := range options.RouteAddressSet { + ruleSet, loaded := router.RuleSet(routeAddressSet) + if !loaded { + return nil, E.New("parse route_address_set: rule-set not found: ", routeAddressSet) + } + ruleSet.IncRef() + inbound.routeRuleSet = append(inbound.routeRuleSet, ruleSet) + } + for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet { + ruleSet, loaded := router.RuleSet(routeExcludeAddressSet) + if !loaded { + return nil, E.New("parse route_exclude_address_set: rule-set not found: ", routeExcludeAddressSet) + } + ruleSet.IncRef() + inbound.routeExcludeRuleSet = append(inbound.routeExcludeRuleSet, ruleSet) + } if options.AutoRedirect { if !options.AutoRoute { return nil, E.New("`auto_route` is required by `auto_redirect`") @@ -229,32 +245,11 @@ func NewInbound(ctx context.Context, router adapter.Router, logger log.ContextLo if err != nil { return nil, E.Cause(err, "initialize auto-redirect") } - if runtime.GOOS != "android" { - var markMode bool - for _, routeAddressSet := range options.RouteAddressSet { - ruleSet, loaded := router.RuleSet(routeAddressSet) - if !loaded { - return nil, E.New("parse route_address_set: rule-set not found: ", routeAddressSet) - } - ruleSet.IncRef() - inbound.routeRuleSet = append(inbound.routeRuleSet, ruleSet) - markMode = true - } - for _, routeExcludeAddressSet := range options.RouteExcludeAddressSet { - ruleSet, loaded := router.RuleSet(routeExcludeAddressSet) - if !loaded { - return nil, E.New("parse route_exclude_address_set: rule-set not found: ", routeExcludeAddressSet) - } - ruleSet.IncRef() - inbound.routeExcludeRuleSet = append(inbound.routeExcludeRuleSet, ruleSet) - markMode = true - } - if markMode { - inbound.tunOptions.AutoRedirectMarkMode = true - err = networkManager.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark) - if err != nil { - return nil, err - } + if runtime.GOOS != "android" && len(inbound.routeAddressSet) > 0 || len(inbound.routeExcludeAddressSet) > 0 { + inbound.tunOptions.AutoRedirectMarkMode = true + err = networkManager.RegisterAutoRedirectOutputMark(inbound.tunOptions.AutoRedirectOutputMark) + if err != nil { + return nil, err } } } @@ -310,18 +305,62 @@ func (t *Inbound) Start(stage adapter.StartStage) error { if t.tunOptions.Name == "" { t.tunOptions.Name = tun.CalculateInterfaceName("") } + if t.platformInterface == nil || runtime.GOOS != "android" { + t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) + for _, routeRuleSet := range t.routeRuleSet { + ipSets := routeRuleSet.ExtractIPSet() + if len(ipSets) == 0 { + t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeRuleSet.Name()) + } + t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet)) + routeRuleSet.DecRef() + t.routeAddressSet = append(t.routeAddressSet, ipSets...) + } + t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet) + for _, routeExcludeRuleSet := range t.routeExcludeRuleSet { + ipSets := routeExcludeRuleSet.ExtractIPSet() + if len(ipSets) == 0 { + t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeExcludeRuleSet.Name()) + } + t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet)) + routeExcludeRuleSet.DecRef() + t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ipSets...) + } + } var ( tunInterface tun.Tun err error ) monitor := taskmonitor.New(t.logger, C.StartTimeout) - monitor.Start("open tun interface") + tunOptions := t.tunOptions + if t.autoRedirect == nil && !(runtime.GOOS == "android" && t.platformInterface != nil) { + for _, ipSet := range t.routeAddressSet { + for _, prefix := range ipSet.Prefixes() { + if prefix.Addr().Is4() { + tunOptions.Inet4RouteAddress = append(tunOptions.Inet4RouteAddress, prefix) + } else { + tunOptions.Inet6RouteAddress = append(tunOptions.Inet6RouteAddress, prefix) + } + } + } + for _, ipSet := range t.routeExcludeAddressSet { + for _, prefix := range ipSet.Prefixes() { + if prefix.Addr().Is4() { + tunOptions.Inet4RouteExcludeAddress = append(tunOptions.Inet4RouteExcludeAddress, prefix) + } else { + tunOptions.Inet6RouteExcludeAddress = append(tunOptions.Inet6RouteExcludeAddress, prefix) + } + } + } + } + monitor.Start("open interface") if t.platformInterface != nil { - tunInterface, err = t.platformInterface.OpenTun(&t.tunOptions, t.platformOptions) + tunInterface, err = t.platformInterface.OpenTun(&tunOptions, t.platformOptions) } else { - tunInterface, err = tun.New(t.tunOptions) + tunInterface, err = tun.New(tunOptions) } monitor.Finish() + t.tunOptions.Name = tunOptions.Name if err != nil { return E.Cause(err, "configure tun interface") } @@ -366,39 +405,15 @@ func (t *Inbound) Start(stage adapter.StartStage) error { return E.Cause(err, "starting TUN interface") } if t.autoRedirect != nil { - t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) - for _, routeRuleSet := range t.routeRuleSet { - ipSets := routeRuleSet.ExtractIPSet() - if len(ipSets) == 0 { - t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeRuleSet.Name()) - } - t.routeAddressSet = append(t.routeAddressSet, ipSets...) - } - t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet) - for _, routeExcludeRuleSet := range t.routeExcludeRuleSet { - ipSets := routeExcludeRuleSet.ExtractIPSet() - if len(ipSets) == 0 { - t.logger.Warn("route_address_set: no destination IP CIDR rules found in rule-set: ", routeExcludeRuleSet.Name()) - } - t.routeExcludeAddressSet = append(t.routeExcludeAddressSet, ipSets...) - } monitor.Start("initialize auto-redirect") err := t.autoRedirect.Start() monitor.Finish() if err != nil { return E.Cause(err, "auto-redirect") } - for _, routeRuleSet := range t.routeRuleSet { - t.routeRuleSetCallback = append(t.routeRuleSetCallback, routeRuleSet.RegisterCallback(t.updateRouteAddressSet)) - routeRuleSet.DecRef() - } - for _, routeExcludeRuleSet := range t.routeExcludeRuleSet { - t.routeExcludeRuleSetCallback = append(t.routeExcludeRuleSetCallback, routeExcludeRuleSet.RegisterCallback(t.updateRouteAddressSet)) - routeExcludeRuleSet.DecRef() - } - t.routeAddressSet = nil - t.routeExcludeAddressSet = nil } + t.routeAddressSet = nil + t.routeExcludeAddressSet = nil } return nil } @@ -406,7 +421,41 @@ func (t *Inbound) Start(stage adapter.StartStage) error { func (t *Inbound) updateRouteAddressSet(it adapter.RuleSet) { t.routeAddressSet = common.FlatMap(t.routeRuleSet, adapter.RuleSet.ExtractIPSet) t.routeExcludeAddressSet = common.FlatMap(t.routeExcludeRuleSet, adapter.RuleSet.ExtractIPSet) - t.autoRedirect.UpdateRouteAddressSet() + if t.autoRedirect != nil { + t.autoRedirect.UpdateRouteAddressSet() + } else { + tunOptions := t.tunOptions + for _, ipSet := range t.routeAddressSet { + for _, prefix := range ipSet.Prefixes() { + if prefix.Addr().Is4() { + tunOptions.Inet4RouteAddress = append(tunOptions.Inet4RouteAddress, prefix) + } else { + tunOptions.Inet6RouteAddress = append(tunOptions.Inet6RouteAddress, prefix) + } + } + } + for _, ipSet := range t.routeExcludeAddressSet { + for _, prefix := range ipSet.Prefixes() { + if prefix.Addr().Is4() { + tunOptions.Inet4RouteExcludeAddress = append(tunOptions.Inet4RouteExcludeAddress, prefix) + } else { + tunOptions.Inet6RouteExcludeAddress = append(tunOptions.Inet6RouteExcludeAddress, prefix) + } + } + } + if t.platformInterface != nil { + err := t.platformInterface.UpdateRouteOptions(&tunOptions, t.platformOptions) + if err != nil { + t.logger.Error("update route addresses: ", err) + } + } else { + err := t.tunIf.UpdateRouteOptions(tunOptions) + if err != nil { + t.logger.Error("update route addresses: ", err) + } + } + t.logger.Info("updated route addresses") + } t.routeAddressSet = nil t.routeExcludeAddressSet = nil } diff --git a/route/router.go b/route/router.go index 6526778bea..642340d4ab 100644 --- a/route/router.go +++ b/route/router.go @@ -363,7 +363,6 @@ func (r *Router) Start(stage adapter.StartStage) error { return E.Cause(err, "initialize DNS server[", i, "]") } } - case adapter.StartStatePostStart: var cacheContext *adapter.HTTPStartContext if len(r.ruleSets) > 0 { monitor.Start("initialize rule-set") @@ -419,6 +418,7 @@ func (r *Router) Start(stage adapter.StartStage) error { } } } + case adapter.StartStatePostStart: for i, rule := range r.rules { monitor.Start("initialize rule[", i, "]") err := rule.Start() From d9d0a2373a1919e5def033e29358f436926de5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Thu, 26 Dec 2024 14:39:57 +0800 Subject: [PATCH 44/49] Revert "platform: Implement set underlying networks for android" This reverts commit eb4a184b7ed7becfcf82e108639d8d19cbeed6bd. --- adapter/network.go | 1 - experimental/libbox/config.go | 4 ---- experimental/libbox/platform.go | 8 -------- experimental/libbox/platform/interface.go | 1 - experimental/libbox/raw_network_android.go | 3 --- experimental/libbox/raw_network_stub.go | 7 ------- experimental/libbox/service.go | 7 ------- route/network.go | 12 ------------ 8 files changed, 43 deletions(-) delete mode 100644 experimental/libbox/raw_network_android.go delete mode 100644 experimental/libbox/raw_network_stub.go diff --git a/adapter/network.go b/adapter/network.go index 22816f07ac..00ef54b8c0 100644 --- a/adapter/network.go +++ b/adapter/network.go @@ -51,5 +51,4 @@ type NetworkInterface struct { DNSServers []string Expensive bool Constrained bool - RawNetwork any } diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go index 7d0627fe17..6a85c963ad 100644 --- a/experimental/libbox/config.go +++ b/experimental/libbox/config.go @@ -82,10 +82,6 @@ func (s *platformInterfaceStub) Interfaces() ([]adapter.NetworkInterface, error) return nil, os.ErrInvalid } -func (s *platformInterfaceStub) SetUnderlyingNetworks(networks []adapter.NetworkInterface) error { - return os.ErrInvalid -} - func (s *platformInterfaceStub) UnderNetworkExtension() bool { return false } diff --git a/experimental/libbox/platform.go b/experimental/libbox/platform.go index f4bc7ea6bf..2503ea442b 100644 --- a/experimental/libbox/platform.go +++ b/experimental/libbox/platform.go @@ -18,7 +18,6 @@ type PlatformInterface interface { StartDefaultInterfaceMonitor(listener InterfaceUpdateListener) error CloseDefaultInterfaceMonitor(listener InterfaceUpdateListener) error GetInterfaces() (NetworkInterfaceIterator, error) - SetUnderlyingNetworks(networks RawNetworkIterator) error UnderNetworkExtension() bool IncludeAllNetworks() bool ReadWIFIState() *WIFIState @@ -52,8 +51,6 @@ type NetworkInterface struct { Type int32 DNSServer StringIterator Metered bool - - RawNetwork RawNetwork } type WIFIState struct { @@ -70,11 +67,6 @@ type NetworkInterfaceIterator interface { HasNext() bool } -type RawNetworkIterator interface { - Next() RawNetwork - HasNext() bool -} - type Notification struct { Identifier string TypeName string diff --git a/experimental/libbox/platform/interface.go b/experimental/libbox/platform/interface.go index 1684d88cc5..eda51b4888 100644 --- a/experimental/libbox/platform/interface.go +++ b/experimental/libbox/platform/interface.go @@ -16,7 +16,6 @@ type Interface interface { UpdateRouteOptions(options *tun.Options, platformOptions option.TunPlatformOptions) error CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor Interfaces() ([]adapter.NetworkInterface, error) - SetUnderlyingNetworks(networks []adapter.NetworkInterface) error UnderNetworkExtension() bool IncludeAllNetworks() bool ClearDNSCache() diff --git a/experimental/libbox/raw_network_android.go b/experimental/libbox/raw_network_android.go deleted file mode 100644 index b8d38aaa9a..0000000000 --- a/experimental/libbox/raw_network_android.go +++ /dev/null @@ -1,3 +0,0 @@ -package libbox - -type RawNetwork interface{} diff --git a/experimental/libbox/raw_network_stub.go b/experimental/libbox/raw_network_stub.go deleted file mode 100644 index 6e23135c04..0000000000 --- a/experimental/libbox/raw_network_stub.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !android - -package libbox - -type RawNetwork interface { - stub() -} diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index 613b2e66fe..4906c57131 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -220,18 +220,11 @@ func (w *platformInterfaceWrapper) Interfaces() ([]adapter.NetworkInterface, err DNSServers: iteratorToArray[string](netInterface.DNSServer), Expensive: netInterface.Metered || isDefault && w.isExpensive, Constrained: isDefault && w.isConstrained, - RawNetwork: netInterface.RawNetwork, }) } return interfaces, nil } -func (w *platformInterfaceWrapper) SetUnderlyingNetworks(networks []adapter.NetworkInterface) error { - return w.iif.SetUnderlyingNetworks(newIterator(common.Map(networks, func(it adapter.NetworkInterface) RawNetwork { - return it.RawNetwork.(RawNetwork) - }))) -} - func (w *platformInterfaceWrapper) UnderNetworkExtension() bool { return w.iif.UnderNetworkExtension() } diff --git a/route/network.go b/route/network.go index 05dffb019b..97d165f119 100644 --- a/route/network.go +++ b/route/network.go @@ -237,9 +237,6 @@ func (r *NetworkManager) UpdateInterfaces() error { newInterfaces := common.Filter(interfaces, func(it adapter.NetworkInterface) bool { return it.Flags&net.FlagUp != 0 }) - for _, networkInterface := range newInterfaces { - networkInterface.RawNetwork = nil - } r.networkInterfaces.Store(newInterfaces) if len(newInterfaces) > 0 && !slices.EqualFunc(oldInterfaces, newInterfaces, func(oldInterface adapter.NetworkInterface, newInterface adapter.NetworkInterface) bool { return oldInterface.Interface.Index == newInterface.Interface.Index && @@ -260,15 +257,6 @@ func (r *NetworkManager) UpdateInterfaces() error { } return F.ToString(it.Name, " (", strings.Join(options, ", "), ")") }), ", ")) - if C.IsAndroid { - err = r.platformInterface.SetUnderlyingNetworks(newInterfaces) - if err != nil { - r.logger.Error("set underlying networks: ", err) - } - } - } - for _, networkInterface := range interfaces { - networkInterface.RawNetwork = nil } return nil } From ce0fcd5c8b8eec83acad60db2bda2d0c3b6d22aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 28 Dec 2024 15:05:05 +0800 Subject: [PATCH 45/49] Fix resolve action incorrectly overwriting UDP domain destination --- route/route.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/route/route.go b/route/route.go index ca7474623b..624b228954 100644 --- a/route/route.go +++ b/route/route.go @@ -473,7 +473,7 @@ match: } if !preMatch && inputPacketConn != nil && !metadata.Destination.IsFqdn() && !metadata.Destination.Addr.IsGlobalUnicast() { var timeout time.Duration - if metadata.InboundType == C.TypeSOCKS { + if metadata.InboundType == C.TypeSOCKS || metadata.InboundType == C.TypeMixed { timeout = C.TCPTimeout } newBuffer, newPacketBuffers, newErr := r.actionSniff(ctx, metadata, &rule.RuleActionSniff{Timeout: timeout}, inputConn, inputPacketConn) @@ -572,7 +572,7 @@ func (r *Router) actionSniff( return } } else { - if !metadata.Destination.Addr.IsGlobalUnicast() { + if !metadata.Destination.IsFqdn() && !metadata.Destination.Addr.IsGlobalUnicast() { metadata.Destination = destination } if len(packetBuffers) > 0 { From 48d3021b2c95024526c2846b6cb695ec228cbcf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 29 Dec 2024 08:52:12 +0800 Subject: [PATCH 46/49] Add port hopping for hysteria2 --- docs/configuration/inbound/hysteria2.md | 11 +++++-- docs/configuration/inbound/hysteria2.zh.md | 9 ++++-- docs/configuration/outbound/hysteria2.md | 35 +++++++++++++++++++++ docs/configuration/outbound/hysteria2.zh.md | 35 +++++++++++++++++++++ go.mod | 2 +- go.sum | 4 +-- option/hysteria2.go | 12 ++++--- protocol/hysteria2/outbound.go | 3 ++ 8 files changed, 98 insertions(+), 13 deletions(-) diff --git a/docs/configuration/inbound/hysteria2.md b/docs/configuration/inbound/hysteria2.md index 6230d3154b..3b7332b064 100644 --- a/docs/configuration/inbound/hysteria2.md +++ b/docs/configuration/inbound/hysteria2.md @@ -4,7 +4,8 @@ icon: material/alert-decagram !!! quote "Changes in sing-box 1.11.0" - :material-alert: [masquerade](#masquerade) + :material-alert: [masquerade](#masquerade) + :material-alert: [ignore_client_bandwidth](#ignore_client_bandwidth) ### Structure @@ -75,9 +76,13 @@ Authentication password #### ignore_client_bandwidth -Commands the client to use the BBR flow control algorithm instead of Hysteria CC. +*When `up_mbps` and `down_mbps` are not set*: -Conflict with `up_mbps` and `down_mbps`. +Commands clients to use the BBR CC instead of Hysteria CC. + +*When `up_mbps` and `down_mbps` are set*: + +Deny clients to use the BBR CC. #### tls diff --git a/docs/configuration/inbound/hysteria2.zh.md b/docs/configuration/inbound/hysteria2.zh.md index fceacf9325..5ad5d75d6e 100644 --- a/docs/configuration/inbound/hysteria2.zh.md +++ b/docs/configuration/inbound/hysteria2.zh.md @@ -4,7 +4,8 @@ icon: material/alert-decagram !!! quote "sing-box 1.11.0 中的更改" - :material-alert: [masquerade](#masquerade) + :material-alert: [masquerade](#masquerade) + :material-alert: [ignore_client_bandwidth](#ignore_client_bandwidth) ### 结构 @@ -72,9 +73,13 @@ Hysteria 用户 #### ignore_client_bandwidth +*当 `up_mbps` 和 `down_mbps` 未设定时*: + 命令客户端使用 BBR 拥塞控制算法而不是 Hysteria CC。 -与 `up_mbps` 和 `down_mbps` 冲突。 +*当 `up_mbps` 和 `down_mbps` 已设定时*: + +禁止客户端使用 BBR 拥塞控制算法。 #### tls diff --git a/docs/configuration/outbound/hysteria2.md b/docs/configuration/outbound/hysteria2.md index ae0b96edda..77063fb481 100644 --- a/docs/configuration/outbound/hysteria2.md +++ b/docs/configuration/outbound/hysteria2.md @@ -1,3 +1,12 @@ +--- +icon: material/new-box +--- + +!!! quote "Changes in sing-box 1.11.0" + + :material-plus: [server_ports](#server_ports) + :material-plus: [hop_interval](#hop_interval) + ### Structure ```json @@ -7,6 +16,10 @@ "server": "127.0.0.1", "server_port": 1080, + "server_ports": [ + "2080:3000" + ], + "hop_interval": "", "up_mbps": 100, "down_mbps": 100, "obfs": { @@ -22,6 +35,10 @@ } ``` +!!! note "" + + You can ignore the JSON Array [] tag when the content is only one item + !!! warning "Difference from official Hysteria2" The official Hysteria2 supports an authentication method called **userpass**, @@ -44,6 +61,24 @@ The server address. The server port. +Ignored if `server_ports` is set. + +#### server_ports + +!!! question "Since sing-box 1.11.0" + +Server port range list. + +Conflicts with `server_port`. + +#### hop_interval + +!!! question "Since sing-box 1.11.0" + +Port hopping interval. + +`30s` is used by default. + #### up_mbps, down_mbps Max bandwidth, in Mbps. diff --git a/docs/configuration/outbound/hysteria2.zh.md b/docs/configuration/outbound/hysteria2.zh.md index 7176b9a6f7..0c5a631e10 100644 --- a/docs/configuration/outbound/hysteria2.zh.md +++ b/docs/configuration/outbound/hysteria2.zh.md @@ -1,3 +1,12 @@ +--- +icon: material/new-box +--- + +!!! quote "sing-box 1.11.0 中的更改" + + :material-plus: [server_ports](#server_ports) + :material-plus: [hop_interval](#hop_interval) + ### 结构 ```json @@ -7,6 +16,10 @@ "server": "127.0.0.1", "server_port": 1080, + "server_ports": [ + "2080:3000" + ], + "hop_interval": "", "up_mbps": 100, "down_mbps": 100, "obfs": { @@ -22,6 +35,10 @@ } ``` +!!! note "" + + 当内容只有一项时,可以忽略 JSON 数组 [] 标签 + !!! warning "与官方 Hysteria2 的区别" 官方程序支持一种名为 **userpass** 的验证方式, @@ -42,6 +59,24 @@ 服务器端口。 +如果设置了 `server_ports`,则忽略此项。 + +#### server_ports + +!!! question "自 sing-box 1.11.0 起" + +服务器端口范围列表。 + +与 `server_port` 冲突。 + +#### hop_interval + +!!! question "自 sing-box 1.11.0 起" + +端口跳跃间隔。 + +默认使用 `30s`。 + #### up_mbps, down_mbps 最大带宽。 diff --git a/go.mod b/go.mod index 79d467ce7d..64b028f2af 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/sagernet/sing v0.6.0-beta.9 github.com/sagernet/sing-dns v0.4.0-beta.1 github.com/sagernet/sing-mux v0.3.0-alpha.1 - github.com/sagernet/sing-quic v0.4.0-alpha.4 + github.com/sagernet/sing-quic v0.4.0-beta.3 github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks2 v0.2.0 github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 diff --git a/go.sum b/go.sum index b84a97f2b3..bd443e683e 100644 --- a/go.sum +++ b/go.sum @@ -125,8 +125,8 @@ github.com/sagernet/sing-dns v0.4.0-beta.1 h1:W1XkdhigwxDOMgMDVB+9kdomCpb7ExsZfB github.com/sagernet/sing-dns v0.4.0-beta.1/go.mod h1:8wuFcoFkWM4vJuQyg8e97LyvDwe0/Vl7G839WLcKDs8= github.com/sagernet/sing-mux v0.3.0-alpha.1 h1:IgNX5bJBpL41gGbp05pdDOvh/b5eUQ6cv9240+Ngipg= github.com/sagernet/sing-mux v0.3.0-alpha.1/go.mod h1:FTcImmdfW38Lz7b+HQ+mxxOth1lz4ao8uEnz+MwIJQE= -github.com/sagernet/sing-quic v0.4.0-alpha.4 h1:P9xAx3nIfcqb9M8jfgs0uLm+VxCcaY++FCqaBfHY3dQ= -github.com/sagernet/sing-quic v0.4.0-alpha.4/go.mod h1:h5RkKTmUhudJKzK7c87FPXD5w1bJjVyxMN9+opZcctA= +github.com/sagernet/sing-quic v0.4.0-beta.3 h1:cOBjlhVdRZmBm6hIw1GleERpnTSFdBB2htgx5kQ5uqg= +github.com/sagernet/sing-quic v0.4.0-beta.3/go.mod h1:1UNObFodd8CnS3aCT53x9cigjPSCl3P//8dfBMCwBDM= github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE= github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg= diff --git a/option/hysteria2.go b/option/hysteria2.go index 9e55ec40d3..e40110dbb6 100644 --- a/option/hysteria2.go +++ b/option/hysteria2.go @@ -111,11 +111,13 @@ type Hysteria2MasqueradeString struct { type Hysteria2OutboundOptions struct { DialerOptions ServerOptions - UpMbps int `json:"up_mbps,omitempty"` - DownMbps int `json:"down_mbps,omitempty"` - Obfs *Hysteria2Obfs `json:"obfs,omitempty"` - Password string `json:"password,omitempty"` - Network NetworkList `json:"network,omitempty"` + ServerPorts badoption.Listable[string] `json:"server_ports,omitempty"` + HopInterval badoption.Duration `json:"hop_interval,omitempty"` + UpMbps int `json:"up_mbps,omitempty"` + DownMbps int `json:"down_mbps,omitempty"` + Obfs *Hysteria2Obfs `json:"obfs,omitempty"` + Password string `json:"password,omitempty"` + Network NetworkList `json:"network,omitempty"` OutboundTLSOptionsContainer BrutalDebug bool `json:"brutal_debug,omitempty"` } diff --git a/protocol/hysteria2/outbound.go b/protocol/hysteria2/outbound.go index 068cc7f7fe..74e87b37cf 100644 --- a/protocol/hysteria2/outbound.go +++ b/protocol/hysteria2/outbound.go @@ -4,6 +4,7 @@ import ( "context" "net" "os" + "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/adapter/outbound" @@ -70,6 +71,8 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL Logger: logger, BrutalDebug: options.BrutalDebug, ServerAddress: options.ServerOptions.Build(), + ServerPorts: options.ServerPorts, + HopInterval: time.Duration(options.HopInterval), SendBPS: uint64(options.UpMbps * hysteria.MbpsToBps), ReceiveBPS: uint64(options.DownMbps * hysteria.MbpsToBps), SalamanderPassword: salamanderPassword, From d9579c26eed3800f5de18bd8d4b60629bfeef351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 18 Nov 2024 18:59:19 +0800 Subject: [PATCH 47/49] Fix tests --- test/box_test.go | 10 +++- test/brutal_test.go | 58 ++++++++++----------- test/direct_test.go | 16 +++--- test/domain_inbound_test.go | 16 +++--- test/ech_test.go | 43 ++++++++-------- test/go.mod | 27 ++++++---- test/go.sum | 89 ++++++++++++++++----------------- test/http_test.go | 16 +++--- test/hysteria2_test.go | 72 ++++++++++++++++++-------- test/hysteria_test.go | 32 ++++++------ test/inbound_detour_test.go | 26 ++++++---- test/mux_cool_test.go | 32 ++++++------ test/mux_test.go | 30 +++++------ test/naive_test.go | 20 ++++---- test/shadowsocks_legacy_test.go | 12 +++-- test/shadowsocks_test.go | 60 +++++++++++----------- test/shadowtls_test.go | 71 ++++++++++++++------------ test/ss_plugin_test.go | 12 +++-- test/tfo_test.go | 16 +++--- test/tls_test.go | 16 +++--- test/trojan_test.go | 40 ++++++++------- test/tuic_test.go | 32 ++++++------ test/v2ray_api_test.go | 12 +++-- test/v2ray_grpc_test.go | 18 ++++--- test/v2ray_transport_test.go | 58 ++++++++++----------- test/v2ray_ws_test.go | 18 ++++--- test/vmess_test.go | 32 ++++++------ test/wireguard_test.go | 51 ------------------- test/wrapper_test.go | 10 ++-- 29 files changed, 490 insertions(+), 455 deletions(-) delete mode 100644 test/wireguard_test.go diff --git a/test/box_test.go b/test/box_test.go index 621c00dafb..08b50b64c2 100644 --- a/test/box_test.go +++ b/test/box_test.go @@ -13,6 +13,7 @@ import ( "github.com/sagernet/quic-go/http3" "github.com/sagernet/sing-box" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/include" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common/bufio" "github.com/sagernet/sing/common/debug" @@ -28,6 +29,12 @@ func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } +var globalCtx context.Context + +func init() { + globalCtx = box.Context(context.Background(), include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry()) +} + func startInstance(t *testing.T, options option.Options) *box.Box { if debug.Enabled { options.Log = &option.LogOptions{ @@ -38,8 +45,7 @@ func startInstance(t *testing.T, options option.Options) *box.Box { Level: "warning", } } - // ctx := context.Background() - ctx, cancel := context.WithCancel(context.Background()) + ctx, cancel := context.WithCancel(globalCtx) var instance *box.Box var err error for retry := 0; retry < 3; retry++ { diff --git a/test/brutal_test.go b/test/brutal_test.go index ce1d2c2a8b..d0841467b0 100644 --- a/test/brutal_test.go +++ b/test/brutal_test.go @@ -7,6 +7,8 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" ) @@ -15,22 +17,22 @@ func TestBrutalShadowsocks(t *testing.T) { method := shadowaead_2022.List[0] password := mkBase64(t, 16) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowsocks, - ShadowsocksOptions: option.ShadowsocksInboundOptions{ + Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Method: method, @@ -46,14 +48,14 @@ func TestBrutalShadowsocks(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeShadowsocks, Tag: "ss-out", - ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -100,22 +102,22 @@ func TestBrutalTrojan(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") password := mkBase64(t, 16) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTrojan, - TrojanOptions: option.TrojanInboundOptions{ + Options: &option.TrojanInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TrojanUser{{Password: password}}, @@ -138,14 +140,14 @@ func TestBrutalTrojan(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTrojan, Tag: "ss-out", - TrojanOptions: option.TrojanOutboundOptions{ + Options: &option.TrojanOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -197,22 +199,22 @@ func TestBrutalTrojan(t *testing.T) { func TestBrutalVMess(t *testing.T) { user, _ := uuid.NewV4() startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVMess, - VMessOptions: option.VMessInboundOptions{ + Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{{UUID: user.String()}}, @@ -227,14 +229,14 @@ func TestBrutalVMess(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVMess, Tag: "ss-out", - VMessOptions: option.VMessOutboundOptions{ + Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -279,22 +281,22 @@ func TestBrutalVMess(t *testing.T) { func TestBrutalVLESS(t *testing.T) { user, _ := uuid.NewV4() startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVLESS, - VLESSOptions: option.VLESSInboundOptions{ + Options: &option.VLESSInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VLESSUser{{UUID: user.String()}}, @@ -326,14 +328,14 @@ func TestBrutalVLESS(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVLESS, Tag: "ss-out", - VLESSOptions: option.VLESSOutboundOptions{ + Options: &option.VLESSOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/direct_test.go b/test/direct_test.go index c4fd8c5ea3..d33defeebd 100644 --- a/test/direct_test.go +++ b/test/direct_test.go @@ -6,41 +6,43 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" ) // Since this is a feature one-off added by outsiders, I won't address these anymore. func _TestProxyProtocol(t *testing.T) { startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeDirect, - DirectOptions: option.DirectInboundOptions{ + Options: &option.DirectInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, ProxyProtocol: true, }, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeDirect, Tag: "proxy-out", - DirectOptions: option.DirectOutboundOptions{ + Options: &option.DirectOutboundOptions{ OverrideAddress: "127.0.0.1", OverridePort: serverPort, ProxyProtocol: 2, diff --git a/test/domain_inbound_test.go b/test/domain_inbound_test.go index c82b0d2974..f39cd18725 100644 --- a/test/domain_inbound_test.go +++ b/test/domain_inbound_test.go @@ -7,6 +7,8 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-dns" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" ) @@ -14,22 +16,22 @@ import ( func TestTUICDomainUDP(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTUIC, - TUICOptions: option.TUICInboundOptions{ + Options: &option.TUICInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, InboundOptions: option.InboundOptions{ DomainStrategy: option.DomainStrategy(dns.DomainStrategyUseIPv6), @@ -49,14 +51,14 @@ func TestTUICDomainUDP(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTUIC, Tag: "tuic-out", - TUICOptions: option.TUICOutboundOptions{ + Options: &option.TUICOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/ech_test.go b/test/ech_test.go index eeac1acb72..865ffba003 100644 --- a/test/ech_test.go +++ b/test/ech_test.go @@ -8,6 +8,7 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" ) @@ -16,22 +17,22 @@ func TestECH(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org", false)) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTrojan, - TrojanOptions: option.TrojanInboundOptions{ + Options: &option.TrojanInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TrojanUser{ @@ -55,14 +56,14 @@ func TestECH(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTrojan, Tag: "trojan-out", - TrojanOptions: option.TrojanOutboundOptions{ + Options: &option.TrojanOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -109,22 +110,22 @@ func TestECHQUIC(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org", false)) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTUIC, - TUICOptions: option.TUICInboundOptions{ + Options: &option.TUICInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TUICUser{{ @@ -145,14 +146,14 @@ func TestECHQUIC(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTUIC, Tag: "tuic-out", - TUICOptions: option.TUICOutboundOptions{ + Options: &option.TUICOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -199,22 +200,22 @@ func TestECHHysteria2(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") echConfig, echKey := common.Must2(tls.ECHKeygenDefault("not.example.org", false)) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeHysteria2, - Hysteria2Options: option.Hysteria2InboundOptions{ + Options: &option.Hysteria2InboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.Hysteria2User{{ @@ -235,14 +236,14 @@ func TestECHHysteria2(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeHysteria2, Tag: "hy2-out", - Hysteria2Options: option.Hysteria2OutboundOptions{ + Options: &option.Hysteria2OutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/go.mod b/test/go.mod index 458ff8800b..ab1608530c 100644 --- a/test/go.mod +++ b/test/go.mod @@ -13,9 +13,9 @@ require ( github.com/docker/go-connections v0.5.0 github.com/gofrs/uuid/v5 v5.3.0 github.com/sagernet/quic-go v0.48.2-beta.1 - github.com/sagernet/sing v0.6.0-beta.2 + github.com/sagernet/sing v0.6.0-beta.9 github.com/sagernet/sing-dns v0.4.0-beta.1 - github.com/sagernet/sing-quic v0.4.0-alpha.4 + github.com/sagernet/sing-quic v0.4.0-beta.3 github.com/sagernet/sing-shadowsocks v0.2.7 github.com/sagernet/sing-shadowsocks2 v0.2.0 github.com/spyzhov/ajson v0.9.4 @@ -25,8 +25,8 @@ require ( ) require ( - berty.tech/go-libtor v1.0.385 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/ajg/form v1.5.1 // indirect github.com/andybalholm/brotli v1.0.6 // indirect github.com/caddyserver/certmagic v0.20.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect @@ -38,6 +38,7 @@ require ( github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-chi/chi/v5 v5.1.0 // indirect + github.com/go-chi/render v1.0.3 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect @@ -49,6 +50,7 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect github.com/hashicorp/yamux v0.1.2 // indirect + github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect github.com/josharian/native v1.1.0 // indirect github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect @@ -65,16 +67,17 @@ require ( github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect github.com/onsi/ginkgo/v2 v2.9.7 // indirect - github.com/ooni/go-libtor v1.1.8 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/oschwald/maxminddb-golang v1.12.0 // indirect + github.com/pierrec/lz4/v4 v4.1.14 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.4.1 // indirect github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 // indirect + github.com/sagernet/cors v1.2.1 // indirect github.com/sagernet/fswatch v0.1.1 // indirect github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff // indirect github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect @@ -82,12 +85,13 @@ require ( github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect github.com/sagernet/sing-mux v0.3.0-alpha.1 // indirect github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 // indirect - github.com/sagernet/sing-tun v0.6.0-beta.2 // indirect - github.com/sagernet/sing-vmess v0.1.12 // indirect + github.com/sagernet/sing-tun v0.6.0-beta.7 // indirect + github.com/sagernet/sing-vmess v0.2.0-beta.2 // indirect github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect github.com/sagernet/utls v1.6.7 // indirect - github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8 // indirect + github.com/sagernet/wireguard-go v0.0.1-beta.5 // indirect github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect + github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect github.com/vishvananda/netns v0.0.4 // indirect github.com/zeebo/blake3 v0.2.3 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect @@ -99,14 +103,15 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect - golang.org/x/crypto v0.29.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.20.0 // indirect - golang.org/x/sync v0.9.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.7.0 // indirect golang.org/x/tools v0.24.0 // indirect + golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 // indirect google.golang.org/grpc v1.67.1 // indirect google.golang.org/protobuf v1.35.1 // indirect diff --git a/test/go.sum b/test/go.sum index 5fc5292c9c..ef3cf96045 100644 --- a/test/go.sum +++ b/test/go.sum @@ -1,9 +1,9 @@ -berty.tech/go-libtor v1.0.385 h1:RWK94C3hZj6Z2GdvePpHJLnWYobFr3bY/OdUJ5aoEXw= -berty.tech/go-libtor v1.0.385/go.mod h1:9swOOQVb+kmvuAlsgWUK/4c52pm69AdbJsxLzk+fJEw= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc= @@ -14,7 +14,6 @@ github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vc github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/cretz/bine v0.1.0/go.mod h1:6PF6fWAvYtwjRGkAuDEJeWNOv3a2hUouSP/yRYXmvHw= github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo= github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -34,6 +33,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -65,6 +66,9 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjw github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= +github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= +github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= +github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -107,14 +111,14 @@ github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss= github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU= github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= -github.com/ooni/go-libtor v1.1.8 h1:Wo3V3DVTxl5vZdxtQakqYP+DAHx7pPtAFSl1bnAa08w= -github.com/ooni/go-libtor v1.1.8/go.mod h1:q1YyLwRD9GeMyeerVvwc0vJ2YgwDLTp2bdVcrh/JXyI= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs= github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY= +github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= +github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -127,51 +131,45 @@ github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkk github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM= github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 h1:YbmpqPQEMdlk9oFSKYWRqVuu9qzNiOayIonKmv1gCXY= github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1/go.mod h1:J2yAxTFPDjrDPhuAi9aWFz2L3ox9it4qAluBBbN0H5k= +github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ= +github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI= github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs= github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o= -github.com/sagernet/gvisor v0.0.0-20241021032506-a4324256e4a3 h1:RxEz7LhPNiF/gX/Hg+OXr5lqsM9iVAgmaK1L1vzlDRM= -github.com/sagernet/gvisor v0.0.0-20241021032506-a4324256e4a3/go.mod h1:ehZwnT2UpmOWAHFL48XdBhnd4Qu4hN2O3Ji0us3ZHMw= +github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff h1:mlohw3360Wg1BNGook/UHnISXhUx4Gd/3tVLs5T0nSs= github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff/go.mod h1:ehZwnT2UpmOWAHFL48XdBhnd4Qu4hN2O3Ji0us3ZHMw= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis= github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I= github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8= -github.com/sagernet/quic-go v0.48.0-beta.1 h1:86hQZrmuoARI3BpDRkQaP0iAVpywA4YsRrzJPYuPKWg= -github.com/sagernet/quic-go v0.48.0-beta.1/go.mod h1:1WgdDIVD1Gybp40JTWketeSfKA/+or9YMLaG5VeTk4k= +github.com/sagernet/quic-go v0.48.2-beta.1 h1:W0plrLWa1XtOWDTdX3CJwxmQuxkya12nN5BRGZ87kEg= github.com/sagernet/quic-go v0.48.2-beta.1/go.mod h1:1WgdDIVD1Gybp40JTWketeSfKA/+or9YMLaG5VeTk4k= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= -github.com/sagernet/sing v0.5.0-rc.4.0.20241022031908-cd17884118cb h1:3IhGq2UmcbQfAcuqyE8RYKFapqEEa3eItS/MrZr+5l8= -github.com/sagernet/sing v0.5.0-rc.4.0.20241022031908-cd17884118cb/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= -github.com/sagernet/sing v0.6.0-beta.2/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= -github.com/sagernet/sing-dns v0.3.0-rc.2.0.20241021154031-a59e0fbba3ce h1:OfpxE5qnXMyU/9LtNgX4M7bGP11lJx4s+KZ3Sijb0HE= -github.com/sagernet/sing-dns v0.3.0-rc.2.0.20241021154031-a59e0fbba3ce/go.mod h1:TqLIelI+FAbVEdiTRolhGLOwvhVjY7oT+wezlOJUQ7M= +github.com/sagernet/sing v0.6.0-beta.9 h1:P8lKa5hN53fRNAVCIKy5cWd6/kLO5c4slhdsfehSmHs= +github.com/sagernet/sing v0.6.0-beta.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= +github.com/sagernet/sing-dns v0.4.0-beta.1 h1:W1XkdhigwxDOMgMDVB+9kdomCpb7ExsZfB4acPcTZFY= github.com/sagernet/sing-dns v0.4.0-beta.1/go.mod h1:8wuFcoFkWM4vJuQyg8e97LyvDwe0/Vl7G839WLcKDs8= -github.com/sagernet/sing-mux v0.2.1-0.20241020175909-fe6153f7a9ec h1:6Fd/VsEsw9qIjaGi1IBTZSb4b4v5JYtNcoiBtGsQC48= -github.com/sagernet/sing-mux v0.2.1-0.20241020175909-fe6153f7a9ec/go.mod h1:RSwqqHwbtTOX3vs6ms8vMtBGH/0ZNyLm/uwt6TlmR84= +github.com/sagernet/sing-mux v0.3.0-alpha.1 h1:IgNX5bJBpL41gGbp05pdDOvh/b5eUQ6cv9240+Ngipg= github.com/sagernet/sing-mux v0.3.0-alpha.1/go.mod h1:FTcImmdfW38Lz7b+HQ+mxxOth1lz4ao8uEnz+MwIJQE= -github.com/sagernet/sing-quic v0.3.0-rc.1 h1:SlzL1yfEAKJyRduub8vzOVtbyTLAX7RZEEBZxO5utts= -github.com/sagernet/sing-quic v0.3.0-rc.1/go.mod h1:uX+aUHA0fgIN6U3WViseDpSdTQriViZ7qz0Wbsf1mNQ= -github.com/sagernet/sing-quic v0.4.0-alpha.4/go.mod h1:h5RkKTmUhudJKzK7c87FPXD5w1bJjVyxMN9+opZcctA= +github.com/sagernet/sing-quic v0.4.0-beta.3 h1:cOBjlhVdRZmBm6hIw1GleERpnTSFdBB2htgx5kQ5uqg= +github.com/sagernet/sing-quic v0.4.0-beta.3/go.mod h1:1UNObFodd8CnS3aCT53x9cigjPSCl3P//8dfBMCwBDM= github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE= github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg= github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= -github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k= -github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4= +github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 h1:RPrpgAdkP5td0vLfS5ldvYosFjSsZtRPxiyLV6jyKg0= github.com/sagernet/sing-shadowtls v0.2.0-alpha.2/go.mod h1:0j5XlzKxaWRIEjc1uiSKmVoWb0k+L9QgZVb876+thZA= -github.com/sagernet/sing-tun v0.4.0-rc.4.0.20241021153919-9ae45181180d h1:zWcIQM3eAKJGzy7zhqkO7zm7ZI890OdR4vSwx2mevS0= -github.com/sagernet/sing-tun v0.4.0-rc.4.0.20241021153919-9ae45181180d/go.mod h1:Xhv+Mz2nE7HZTwResni8EtTa7AMJz/S6uQLT5lV23M8= -github.com/sagernet/sing-tun v0.6.0-beta.2/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE= -github.com/sagernet/sing-vmess v0.1.12 h1:2gFD8JJb+eTFMoa8FIVMnknEi+vCSfaiTXTfEYAYAPg= -github.com/sagernet/sing-vmess v0.1.12/go.mod h1:luTSsfyBGAc9VhtCqwjR+dt1QgqBhuYBCONB/POhF8I= +github.com/sagernet/sing-tun v0.6.0-beta.7 h1:FCSX8oGBqb0H57AAvfGeeH/jMGYWCOg6XWkN/oeES+0= +github.com/sagernet/sing-tun v0.6.0-beta.7/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE= +github.com/sagernet/sing-vmess v0.2.0-beta.2 h1:obAkAL35X7ql4RnGzDg4dBYIRpGXRKqcN4LyLZpZGSs= +github.com/sagernet/sing-vmess v0.2.0-beta.2/go.mod h1:HGhf9XUdeE2iOWrX0hQNFgXPbKyGlzpeYFyX0c/pykk= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8= github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM= -github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8 h1:R0OMYAScomNAVpTfbHFpxqJpvwuhxSRi+g6z7gZhABs= -github.com/sagernet/wireguard-go v0.0.0-20231215174105-89dec3b2f3e8/go.mod h1:K4J7/npM+VAMUeUmTa2JaA02JmyheP0GpRBOUvn3ecc= +github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc= +github.com/sagernet/wireguard-go v0.0.1-beta.5/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -179,11 +177,12 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/spyzhov/ajson v0.9.4 h1:MVibcTCgO7DY4IlskdqIlCmDOsUOZ9P7oKj8ifdcf84= github.com/spyzhov/ajson v0.9.4/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA= +github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -219,13 +218,11 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= -golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -238,37 +235,33 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= -golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -281,6 +274,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= +golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg= google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M= google.golang.org/genproto/googleapis/rpc v0.0.0-20241007155032-5fefd90f89a9 h1:QCqS/PdaHTSWGvupk2F/ehwHtGc0/GYkT+3GAcR1CCc= diff --git a/test/http_test.go b/test/http_test.go index 7e72400534..0e9185ef7f 100644 --- a/test/http_test.go +++ b/test/http_test.go @@ -6,39 +6,41 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" ) func TestHTTPSelf(t *testing.T) { startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeMixed, - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeHTTP, Tag: "http-out", - HTTPOptions: option.HTTPOutboundOptions{ + Options: &option.HTTPOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/hysteria2_test.go b/test/hysteria2_test.go index 665da552c3..115af4a7b6 100644 --- a/test/hysteria2_test.go +++ b/test/hysteria2_test.go @@ -3,22 +3,36 @@ package main import ( "net/netip" "testing" + "time" C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-quic/hysteria2" + "github.com/sagernet/sing/common" + F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json/badoption" ) func TestHysteria2Self(t *testing.T) { t.Run("self", func(t *testing.T) { - testHysteria2Self(t, "") + testHysteria2Self(t, "", false) }) t.Run("self-salamander", func(t *testing.T) { - testHysteria2Self(t, "password") + testHysteria2Self(t, "password", false) + }) + t.Run("self-hop", func(t *testing.T) { + testHysteria2Self(t, "", true) + }) + t.Run("self-hop-salamander", func(t *testing.T) { + testHysteria2Self(t, "password", true) }) } -func testHysteria2Self(t *testing.T, salamanderPassword string) { +func TestHysteria2Hop(t *testing.T) { + testHysteria2Self(t, "password", true) +} + +func testHysteria2Self(t *testing.T, salamanderPassword string, portHop bool) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") var obfs *option.Hysteria2Obfs if salamanderPassword != "" { @@ -27,23 +41,31 @@ func testHysteria2Self(t *testing.T, salamanderPassword string) { Password: salamanderPassword, } } + var ( + serverPorts []string + hopInterval time.Duration + ) + if portHop { + serverPorts = []string{F.ToString(serverPort, ":", serverPort)} + hopInterval = 5 * time.Second + } startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeHysteria2, - Hysteria2Options: option.Hysteria2InboundOptions{ + Options: &option.Hysteria2InboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, UpMbps: 100, @@ -63,22 +85,24 @@ func testHysteria2Self(t *testing.T, salamanderPassword string) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeHysteria2, Tag: "hy2-out", - Hysteria2Options: option.Hysteria2OutboundOptions{ + Options: &option.Hysteria2OutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, }, - UpMbps: 100, - DownMbps: 100, - Obfs: obfs, - Password: "password", + ServerPorts: serverPorts, + HopInterval: badoption.Duration(hopInterval), + UpMbps: 100, + DownMbps: 100, + Obfs: obfs, + Password: "password", OutboundTLSOptionsContainer: option.OutboundTLSOptionsContainer{ TLS: &option.OutboundTLSOptions{ Enabled: true, @@ -110,17 +134,21 @@ func testHysteria2Self(t *testing.T, salamanderPassword string) { }, }) testSuitLargeUDP(t, clientPort, testPort) + if portHop { + time.Sleep(5 * time.Second) + testSuitLargeUDP(t, clientPort, testPort) + } } func TestHysteria2Inbound(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeHysteria2, - Hysteria2Options: option.Hysteria2InboundOptions{ + Options: &option.Hysteria2InboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Obfs: &option.Hysteria2Obfs{ @@ -167,21 +195,21 @@ func TestHysteria2Outbound(t *testing.T) { }, }) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeHysteria2, - Hysteria2Options: option.Hysteria2OutboundOptions{ + Options: &option.Hysteria2OutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/hysteria_test.go b/test/hysteria_test.go index dce00390a4..a7f73deadb 100644 --- a/test/hysteria_test.go +++ b/test/hysteria_test.go @@ -6,27 +6,29 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" ) func TestHysteriaSelf(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeHysteria, - HysteriaOptions: option.HysteriaInboundOptions{ + Options: &option.HysteriaInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, UpMbps: 100, @@ -46,14 +48,14 @@ func TestHysteriaSelf(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeHysteria, Tag: "hy-out", - HysteriaOptions: option.HysteriaOutboundOptions{ + Options: &option.HysteriaOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -98,12 +100,12 @@ func TestHysteriaSelf(t *testing.T) { func TestHysteriaInbound(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeHysteria, - HysteriaOptions: option.HysteriaInboundOptions{ + Options: &option.HysteriaInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, UpMbps: 100, @@ -149,21 +151,21 @@ func TestHysteriaOutbound(t *testing.T) { }, }) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeHysteria, - HysteriaOptions: option.HysteriaOutboundOptions{ + Options: &option.HysteriaOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/inbound_detour_test.go b/test/inbound_detour_test.go index c26c81a799..93c283aac7 100644 --- a/test/inbound_detour_test.go +++ b/test/inbound_detour_test.go @@ -7,30 +7,34 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" ) func TestChainedInbound(t *testing.T) { method := shadowaead_2022.List[0] password := mkBase64(t, 16) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowsocks, - ShadowsocksOptions: option.ShadowsocksInboundOptions{ + Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, - Detour: "detour", + InboundOptions: option.InboundOptions{ + Detour: "detour", + }, }, Method: method, Password: password, @@ -39,9 +43,9 @@ func TestChainedInbound(t *testing.T) { { Type: C.TypeShadowsocks, Tag: "detour", - ShadowsocksOptions: option.ShadowsocksInboundOptions{ + Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: otherPort, }, Method: method, @@ -49,14 +53,14 @@ func TestChainedInbound(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeShadowsocks, Tag: "ss-out", - ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + Options: &option.ShadowsocksOutboundOptions{ Method: method, Password: password, DialerOptions: option.DialerOptions{ @@ -67,7 +71,7 @@ func TestChainedInbound(t *testing.T) { { Type: C.TypeShadowsocks, Tag: "detour-out", - ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/mux_cool_test.go b/test/mux_cool_test.go index e72f244fc5..ed42a05945 100644 --- a/test/mux_cool_test.go +++ b/test/mux_cool_test.go @@ -7,6 +7,8 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" "github.com/spyzhov/ajson" "github.com/stretchr/testify/require" @@ -37,12 +39,12 @@ func TestMuxCoolServer(t *testing.T) { }) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeVMess, - VMessOptions: option.VMessInboundOptions{ + Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ @@ -81,21 +83,21 @@ func TestMuxCoolClient(t *testing.T) { }) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeVMess, - VMessOptions: option.VMessOutboundOptions{ + Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -112,22 +114,22 @@ func TestMuxCoolClient(t *testing.T) { func TestMuxCoolSelf(t *testing.T) { user := newUUID() startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVMess, - VMessOptions: option.VMessInboundOptions{ + Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ @@ -139,14 +141,14 @@ func TestMuxCoolSelf(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVMess, Tag: "vmess-out", - VMessOptions: option.VMessOutboundOptions{ + Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/mux_test.go b/test/mux_test.go index 335def2e93..6454d19c7f 100644 --- a/test/mux_test.go +++ b/test/mux_test.go @@ -7,6 +7,8 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" ) @@ -55,22 +57,22 @@ func testShadowsocksMux(t *testing.T, options option.OutboundMultiplexOptions) { method := shadowaead_2022.List[0] password := mkBase64(t, 16) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowsocks, - ShadowsocksOptions: option.ShadowsocksInboundOptions{ + Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Method: method, @@ -81,14 +83,14 @@ func testShadowsocksMux(t *testing.T, options option.OutboundMultiplexOptions) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeShadowsocks, Tag: "ss-out", - ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -125,22 +127,22 @@ func testShadowsocksMux(t *testing.T, options option.OutboundMultiplexOptions) { func testVMessMux(t *testing.T, options option.OutboundMultiplexOptions) { user, _ := uuid.NewV4() startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVMess, - VMessOptions: option.VMessInboundOptions{ + Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ @@ -154,14 +156,14 @@ func testVMessMux(t *testing.T, options option.OutboundMultiplexOptions) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVMess, Tag: "vmess-out", - VMessOptions: option.VMessOutboundOptions{ + Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/naive_test.go b/test/naive_test.go index fe3e7dce93..9dbe985588 100644 --- a/test/naive_test.go +++ b/test/naive_test.go @@ -6,19 +6,21 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" "github.com/sagernet/sing/common/auth" + "github.com/sagernet/sing/common/json/badoption" "github.com/sagernet/sing/common/network" ) func TestNaiveInboundWithNginx(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeNaive, - NaiveOptions: option.NaiveInboundOptions{ + Options: &option.NaiveInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: otherPort, }, Users: []auth.User{ @@ -59,12 +61,12 @@ func TestNaiveInboundWithNginx(t *testing.T) { func TestNaiveInbound(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeNaive, - NaiveOptions: option.NaiveInboundOptions{ + Options: &option.NaiveInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []auth.User{ @@ -103,12 +105,12 @@ func TestNaiveInbound(t *testing.T) { func TestNaiveHTTP3Inbound(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeNaive, - NaiveOptions: option.NaiveInboundOptions{ + Options: &option.NaiveInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []auth.User{ diff --git a/test/shadowsocks_legacy_test.go b/test/shadowsocks_legacy_test.go index ae6f38e400..8182e6cb23 100644 --- a/test/shadowsocks_legacy_test.go +++ b/test/shadowsocks_legacy_test.go @@ -7,7 +7,9 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks2/shadowstream" + "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json/badoption" ) func TestShadowsocksLegacy(t *testing.T) { @@ -24,21 +26,21 @@ func testShadowsocksLegacy(t *testing.T, method string) { }, }) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeShadowsocks, - ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/shadowsocks_test.go b/test/shadowsocks_test.go index 0f7af7650d..3f526b106c 100644 --- a/test/shadowsocks_test.go +++ b/test/shadowsocks_test.go @@ -9,7 +9,9 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json/badoption" "github.com/stretchr/testify/require" ) @@ -99,12 +101,12 @@ func testShadowsocksInboundWithShadowsocksRust(t *testing.T, method string, pass Cmd: []string{"-s", F.ToString("127.0.0.1:", serverPort), "-b", F.ToString("0.0.0.0:", clientPort), "-m", method, "-k", password, "-U"}, }) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeShadowsocks, - ShadowsocksOptions: option.ShadowsocksInboundOptions{ + Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Method: method, @@ -124,21 +126,21 @@ func testShadowsocksOutboundWithShadowsocksRust(t *testing.T, method string, pas Cmd: []string{"-s", F.ToString("0.0.0.0:", serverPort), "-m", method, "-k", password, "-U"}, }) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeShadowsocks, - ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -154,22 +156,22 @@ func testShadowsocksOutboundWithShadowsocksRust(t *testing.T, method string, pas func testShadowsocksSelf(t *testing.T, method string, password string) { startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowsocks, - ShadowsocksOptions: option.ShadowsocksInboundOptions{ + Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Method: method, @@ -177,14 +179,14 @@ func testShadowsocksSelf(t *testing.T, method string, password string) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeShadowsocks, Tag: "ss-out", - ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -221,22 +223,22 @@ func TestShadowsocksUoT(t *testing.T) { method := shadowaead_2022.List[0] password := mkBase64(t, 16) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowsocks, - ShadowsocksOptions: option.ShadowsocksInboundOptions{ + Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Method: method, @@ -244,14 +246,14 @@ func TestShadowsocksUoT(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeShadowsocks, Tag: "ss-out", - ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -289,22 +291,22 @@ func TestShadowsocksUoT(t *testing.T) { func testShadowsocks2022EIH(t *testing.T, method string, password string) { startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowsocks, - ShadowsocksOptions: option.ShadowsocksInboundOptions{ + Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Method: method, @@ -317,14 +319,14 @@ func testShadowsocks2022EIH(t *testing.T, method string, password string) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeShadowsocks, Tag: "ss-out", - ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/shadowtls_test.go b/test/shadowtls_test.go index 71e8d9fa2a..f64492ee61 100644 --- a/test/shadowtls_test.go +++ b/test/shadowtls_test.go @@ -10,7 +10,9 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks/shadowaead_2022" + "github.com/sagernet/sing/common" F "github.com/sagernet/sing/common/format" + "github.com/sagernet/sing/common/json/badoption" "github.com/stretchr/testify/require" ) @@ -37,12 +39,12 @@ func testShadowTLS(t *testing.T, version int, password string, utlsEanbled bool) method := shadowaead_2022.List[0] ssPassword := mkBase64(t, 16) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, @@ -50,11 +52,14 @@ func testShadowTLS(t *testing.T, version int, password string, utlsEanbled bool) { Type: C.TypeShadowTLS, Tag: "in", - ShadowTLSOptions: option.ShadowTLSInboundOptions{ + Options: &option.ShadowTLSInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, - Detour: "detour", + + InboundOptions: option.InboundOptions{ + Detour: "detour", + }, }, Handshake: option.ShadowTLSHandshakeOptions{ ServerOptions: option.ServerOptions{ @@ -70,9 +75,9 @@ func testShadowTLS(t *testing.T, version int, password string, utlsEanbled bool) { Type: C.TypeShadowsocks, Tag: "detour", - ShadowsocksOptions: option.ShadowsocksInboundOptions{ + Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: otherPort, }, Method: method, @@ -80,10 +85,10 @@ func testShadowTLS(t *testing.T, version int, password string, utlsEanbled bool) }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeShadowsocks, - ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + Options: &option.ShadowsocksOutboundOptions{ Method: method, Password: ssPassword, DialerOptions: option.DialerOptions{ @@ -94,7 +99,7 @@ func testShadowTLS(t *testing.T, version int, password string, utlsEanbled bool) { Type: C.TypeShadowTLS, Tag: "detour", - ShadowTLSOptions: option.ShadowTLSOutboundOptions{ + Options: &option.ShadowTLSOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -142,12 +147,12 @@ func testShadowTLS(t *testing.T, version int, password string, utlsEanbled bool) func TestShadowTLSFallback(t *testing.T) { startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeShadowTLS, - ShadowTLSOptions: option.ShadowTLSInboundOptions{ + Options: &option.ShadowTLSInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Handshake: option.ShadowTLSHandshakeOptions{ @@ -189,24 +194,26 @@ func TestShadowTLSInbound(t *testing.T) { Cmd: []string{"--v3", "--threads", "1", "client", "--listen", "0.0.0.0:" + F.ToString(otherPort), "--server", "127.0.0.1:" + F.ToString(serverPort), "--sni", "google.com", "--password", password}, }) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowTLS, - ShadowTLSOptions: option.ShadowTLSInboundOptions{ + Options: &option.ShadowTLSInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, - Detour: "detour", + InboundOptions: option.InboundOptions{ + Detour: "detour", + }, }, Handshake: option.ShadowTLSHandshakeOptions{ ServerOptions: option.ServerOptions{ @@ -223,23 +230,23 @@ func TestShadowTLSInbound(t *testing.T) { { Type: C.TypeShadowsocks, Tag: "detour", - ShadowsocksOptions: option.ShadowsocksInboundOptions{ + Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), }, Method: method, Password: password, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeShadowsocks, Tag: "out", - ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: otherPort, @@ -283,12 +290,12 @@ func TestShadowTLSOutbound(t *testing.T) { Env: []string{"RUST_LOG=trace"}, }) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, @@ -296,9 +303,9 @@ func TestShadowTLSOutbound(t *testing.T) { { Type: C.TypeShadowsocks, Tag: "detour", - ShadowsocksOptions: option.ShadowsocksInboundOptions{ + Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: otherPort, }, Method: method, @@ -306,10 +313,10 @@ func TestShadowTLSOutbound(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeShadowsocks, - ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + Options: &option.ShadowsocksOutboundOptions{ Method: method, Password: password, DialerOptions: option.DialerOptions{ @@ -320,7 +327,7 @@ func TestShadowTLSOutbound(t *testing.T) { { Type: C.TypeShadowTLS, Tag: "detour", - ShadowTLSOptions: option.ShadowTLSOutboundOptions{ + Options: &option.ShadowTLSOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/ss_plugin_test.go b/test/ss_plugin_test.go index 3f837b4e92..f5d1863986 100644 --- a/test/ss_plugin_test.go +++ b/test/ss_plugin_test.go @@ -6,6 +6,8 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" ) func TestShadowsocksObfs(t *testing.T) { @@ -33,21 +35,21 @@ func testShadowsocksPlugin(t *testing.T, name string, opts string, args string) }, }) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeShadowsocks, - ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/tfo_test.go b/test/tfo_test.go index 458a936d6b..d74dbfb66c 100644 --- a/test/tfo_test.go +++ b/test/tfo_test.go @@ -7,28 +7,30 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-shadowsocks/shadowaead" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" ) func TestTCPSlowOpen(t *testing.T) { method := shadowaead.List[0] password := mkBase64(t, 16) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeShadowsocks, - ShadowsocksOptions: option.ShadowsocksInboundOptions{ + Options: &option.ShadowsocksInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, TCPFastOpen: true, }, @@ -37,14 +39,14 @@ func TestTCPSlowOpen(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeShadowsocks, Tag: "ss-out", - ShadowsocksOptions: option.ShadowsocksOutboundOptions{ + Options: &option.ShadowsocksOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/tls_test.go b/test/tls_test.go index b42d924f42..5aaf37c314 100644 --- a/test/tls_test.go +++ b/test/tls_test.go @@ -6,27 +6,29 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" ) func TestUTLS(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTrojan, - TrojanOptions: option.TrojanInboundOptions{ + Options: &option.TrojanInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TrojanUser{ @@ -46,14 +48,14 @@ func TestUTLS(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTrojan, Tag: "trojan-out", - TrojanOptions: option.TrojanOutboundOptions{ + Options: &option.TrojanOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/trojan_test.go b/test/trojan_test.go index 1a206c66a4..cceed4077c 100644 --- a/test/trojan_test.go +++ b/test/trojan_test.go @@ -6,6 +6,8 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" ) func TestTrojanOutbound(t *testing.T) { @@ -20,21 +22,21 @@ func TestTrojanOutbound(t *testing.T) { }, }) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeTrojan, - TrojanOptions: option.TrojanOutboundOptions{ + Options: &option.TrojanOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -57,22 +59,22 @@ func TestTrojanOutbound(t *testing.T) { func TestTrojanSelf(t *testing.T) { _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTrojan, - TrojanOptions: option.TrojanInboundOptions{ + Options: &option.TrojanInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TrojanUser{ @@ -92,14 +94,14 @@ func TestTrojanSelf(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTrojan, Tag: "trojan-out", - TrojanOptions: option.TrojanOutboundOptions{ + Options: &option.TrojanOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -140,22 +142,22 @@ func TestTrojanSelf(t *testing.T) { func TestTrojanPlainSelf(t *testing.T) { startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTrojan, - TrojanOptions: option.TrojanInboundOptions{ + Options: &option.TrojanInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TrojanUser{ @@ -167,14 +169,14 @@ func TestTrojanPlainSelf(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTrojan, Tag: "trojan-out", - TrojanOptions: option.TrojanOutboundOptions{ + Options: &option.TrojanOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/tuic_test.go b/test/tuic_test.go index 41fb759997..d5f13bece9 100644 --- a/test/tuic_test.go +++ b/test/tuic_test.go @@ -6,6 +6,8 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" ) @@ -29,22 +31,22 @@ func testTUICSelf(t *testing.T, udpStream bool, zeroRTTHandshake bool) { udpRelayMode = "quic" } startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTUIC, - TUICOptions: option.TUICInboundOptions{ + Options: &option.TUICInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TUICUser{{ @@ -62,14 +64,14 @@ func testTUICSelf(t *testing.T, udpStream bool, zeroRTTHandshake bool) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTUIC, Tag: "tuic-out", - TUICOptions: option.TUICOutboundOptions{ + Options: &option.TUICOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -113,12 +115,12 @@ func testTUICSelf(t *testing.T, udpStream bool, zeroRTTHandshake bool) { func TestTUICInbound(t *testing.T) { caPem, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeTUIC, - TUICOptions: option.TUICInboundOptions{ + Options: &option.TUICInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TUICUser{{ @@ -160,21 +162,21 @@ func TestTUICOutbound(t *testing.T) { }, }) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeTUIC, - TUICOptions: option.TUICOutboundOptions{ + Options: &option.TUICOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/v2ray_api_test.go b/test/v2ray_api_test.go index cd7ae2c445..2225703255 100644 --- a/test/v2ray_api_test.go +++ b/test/v2ray_api_test.go @@ -1,5 +1,6 @@ package main +/* import ( "context" "net/netip" @@ -8,25 +9,27 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental/v2rayapi" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" "github.com/stretchr/testify/require" ) func TestV2RayAPI(t *testing.T) { i := startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, Tag: "out", @@ -54,3 +57,4 @@ func TestV2RayAPI(t *testing.T) { require.Equal(t, count, stat.Value) } } +*/ diff --git a/test/v2ray_grpc_test.go b/test/v2ray_grpc_test.go index 5cf875435d..884cc42e0b 100644 --- a/test/v2ray_grpc_test.go +++ b/test/v2ray_grpc_test.go @@ -7,6 +7,8 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" "github.com/spyzhov/ajson" @@ -27,12 +29,12 @@ func testV2RayGRPCInbound(t *testing.T, forceLite bool) { require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeVMess, - VMessOptions: option.VMessInboundOptions{ + Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ @@ -126,23 +128,23 @@ func testV2RayGRPCOutbound(t *testing.T, forceLite bool) { }, }) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeVMess, Tag: "vmess-out", - VMessOptions: option.VMessOutboundOptions{ + Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/v2ray_transport_test.go b/test/v2ray_transport_test.go index 27074e78e1..b32a5cafe6 100644 --- a/test/v2ray_transport_test.go +++ b/test/v2ray_transport_test.go @@ -6,6 +6,8 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" "github.com/stretchr/testify/require" @@ -44,22 +46,22 @@ func testVMessTransportSelf(t *testing.T, server *option.V2RayTransportOptions, require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVMess, - VMessOptions: option.VMessInboundOptions{ + Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ @@ -80,14 +82,14 @@ func testVMessTransportSelf(t *testing.T, server *option.V2RayTransportOptions, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVMess, Tag: "vmess-out", - VMessOptions: option.VMessOutboundOptions{ + Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -133,22 +135,22 @@ func testTrojanTransportSelf(t *testing.T, server *option.V2RayTransportOptions, require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeTrojan, - TrojanOptions: option.TrojanInboundOptions{ + Options: &option.TrojanInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.TrojanUser{ @@ -169,14 +171,14 @@ func testTrojanTransportSelf(t *testing.T, server *option.V2RayTransportOptions, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeTrojan, Tag: "vmess-out", - TrojanOptions: option.TrojanOutboundOptions{ + Options: &option.TrojanOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -224,22 +226,22 @@ func TestVMessQUICSelf(t *testing.T) { require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVMess, - VMessOptions: option.VMessInboundOptions{ + Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ @@ -260,14 +262,14 @@ func TestVMessQUICSelf(t *testing.T) { }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVMess, Tag: "vmess-out", - VMessOptions: option.VMessOutboundOptions{ + Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -312,22 +314,22 @@ func testV2RayTransportNOTLSSelf(t *testing.T, transport *option.V2RayTransportO user, err := uuid.DefaultGenerator.NewV4() require.NoError(t, err) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVMess, - VMessOptions: option.VMessInboundOptions{ + Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ @@ -340,14 +342,14 @@ func testV2RayTransportNOTLSSelf(t *testing.T, transport *option.V2RayTransportO }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVMess, Tag: "vmess-out", - VMessOptions: option.VMessOutboundOptions{ + Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/v2ray_ws_test.go b/test/v2ray_ws_test.go index de8d4bdce6..4db4b372e3 100644 --- a/test/v2ray_ws_test.go +++ b/test/v2ray_ws_test.go @@ -7,6 +7,8 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" "github.com/spyzhov/ajson" @@ -61,12 +63,12 @@ func testV2RayWebsocketInbound(t *testing.T, maxEarlyData uint32, earlyDataHeade require.NoError(t, err) _, certPem, keyPem := createSelfSignedCertificate(t, "example.org") startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeVMess, - VMessOptions: option.VMessInboundOptions{ + Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ @@ -158,23 +160,23 @@ func testV2RayWebsocketOutbound(t *testing.T, maxEarlyData uint32, earlyDataHead }, }) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeVMess, Tag: "vmess-out", - VMessOptions: option.VMessOutboundOptions{ + Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/vmess_test.go b/test/vmess_test.go index 9f81d9a015..4da76c6e08 100644 --- a/test/vmess_test.go +++ b/test/vmess_test.go @@ -7,6 +7,8 @@ import ( C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/json/badoption" "github.com/gofrs/uuid/v5" "github.com/spyzhov/ajson" @@ -181,12 +183,12 @@ func testVMessInboundWithV2Ray(t *testing.T, security string, alterId int, authe }) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeVMess, - VMessOptions: option.VMessInboundOptions{ + Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ @@ -229,21 +231,21 @@ func testVMessOutboundWithV2Ray(t *testing.T, security string, globalPadding boo }) startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeVMess, - VMessOptions: option.VMessOutboundOptions{ + Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, @@ -263,22 +265,22 @@ func testVMessOutboundWithV2Ray(t *testing.T, security string, globalPadding boo func testVMessSelf(t *testing.T, security string, alterId int, globalPadding bool, authenticatedLength bool, packetAddr bool) { user := newUUID() startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ + Inbounds: []option.Inbound{ { Type: C.TypeMixed, Tag: "mixed-in", - MixedOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: clientPort, }, }, }, { Type: C.TypeVMess, - VMessOptions: option.VMessInboundOptions{ + Options: &option.VMessInboundOptions{ ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), + Listen: common.Ptr(badoption.Addr(netip.IPv4Unspecified())), ListenPort: serverPort, }, Users: []option.VMessUser{ @@ -291,14 +293,14 @@ func testVMessSelf(t *testing.T, security string, alterId int, globalPadding boo }, }, }, - LegacyOutbounds: []option.LegacyOutbound{ + Outbounds: []option.Outbound{ { Type: C.TypeDirect, }, { Type: C.TypeVMess, Tag: "vmess-out", - VMessOptions: option.VMessOutboundOptions{ + Options: &option.VMessOutboundOptions{ ServerOptions: option.ServerOptions{ Server: "127.0.0.1", ServerPort: serverPort, diff --git a/test/wireguard_test.go b/test/wireguard_test.go deleted file mode 100644 index 70c0e5a558..0000000000 --- a/test/wireguard_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "net/netip" - "testing" - "time" - - C "github.com/sagernet/sing-box/constant" - "github.com/sagernet/sing-box/option" -) - -func _TestWireGuard(t *testing.T) { - startDockerContainer(t, DockerOptions{ - Image: ImageBoringTun, - Cap: []string{"MKNOD", "NET_ADMIN", "NET_RAW"}, - Ports: []uint16{serverPort, testPort}, - Bind: map[string]string{ - "wireguard.conf": "/etc/wireguard/wg0.conf", - }, - Cmd: []string{"wg0"}, - }) - time.Sleep(5 * time.Second) - startInstance(t, option.Options{ - Inbounds: []option.LegacyInbound{ - { - Type: C.TypeMixed, - MixedOptions: option.HTTPMixedInboundOptions{ - ListenOptions: option.ListenOptions{ - Listen: option.NewListenAddress(netip.IPv4Unspecified()), - ListenPort: clientPort, - }, - }, - }, - }, - LegacyOutbounds: []option.LegacyOutbound{ - { - Type: C.TypeWireGuard, - WireGuardOptions: option.WireGuardOutboundOptions{ - ServerOptions: option.ServerOptions{ - Server: "127.0.0.1", - ServerPort: serverPort, - }, - LocalAddress: []netip.Prefix{netip.MustParsePrefix("10.0.0.2/32")}, - PrivateKey: "qGnwlkZljMxeECW8fbwAWdvgntnbK7B8UmMFl3zM0mk=", - PeerPublicKey: "QsdcBm+oJw2oNv0cIFXLIq1E850lgTBonup4qnKEQBg=", - }, - }, - }, - }) - testSuitWg(t, clientPort, testPort) -} diff --git a/test/wrapper_test.go b/test/wrapper_test.go index a7c23f335e..9eb9ed7873 100644 --- a/test/wrapper_test.go +++ b/test/wrapper_test.go @@ -10,9 +10,9 @@ import ( ) func TestOptionsWrapper(t *testing.T) { - inbound := option.LegacyInbound{ + inbound := option.Inbound{ Type: C.TypeHTTP, - HTTPOptions: option.HTTPMixedInboundOptions{ + Options: &option.HTTPMixedInboundOptions{ InboundTLSOptionsContainer: option.InboundTLSOptionsContainer{ TLS: &option.InboundTLSOptions{ Enabled: true, @@ -20,13 +20,11 @@ func TestOptionsWrapper(t *testing.T) { }, }, } - rawOptions, err := inbound.RawOptions() - require.NoError(t, err) - tlsOptionsWrapper, loaded := rawOptions.(option.InboundTLSOptionsWrapper) + tlsOptionsWrapper, loaded := inbound.Options.(option.InboundTLSOptionsWrapper) require.True(t, loaded, "find inbound tls options") tlsOptions := tlsOptionsWrapper.TakeInboundTLSOptions() require.NotNil(t, tlsOptions, "find inbound tls options") tlsOptions.Enabled = false tlsOptionsWrapper.ReplaceInboundTLSOptions(tlsOptions) - require.False(t, inbound.HTTPOptions.TLS.Enabled, "replace tls enabled") + require.False(t, inbound.Options.(*option.HTTPMixedInboundOptions).TLS.Enabled, "replace tls enabled") } From e483c909b4a18b174dcc4ffcc47f6d44c6f11aa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Mon, 18 Nov 2024 19:00:56 +0800 Subject: [PATCH 48/49] documentation: Bump version --- docs/changelog.md | 222 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 220 insertions(+), 2 deletions(-) diff --git a/docs/changelog.md b/docs/changelog.md index e3f020e49c..255e5a65ef 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,16 +2,234 @@ icon: material/alert-decagram --- -### 1.10.6 +#### 1.11.0-beta.20 +* Hysteria2 `ignore_client_bandwidth` behavior update **1** * Fixes and improvements +**1**: + +When `up_mbps` and `down_mbps` are set, `ignore_client_bandwidth` instead denies clients from using BBR CC. + +See [Hysteria2](/configuration/inbound/hysteria2/#ignore_client_bandwidth). + +#### 1.11.0-beta.17 + +* Add port hopping support for Hysteria2 **1** +* Fixes and improvements + +**1**: + +See [Hysteria2](/configuration/outbound/hysteria2/). + +#### 1.11.0-beta.14 + +* Allow adding route (exclude) address sets to routes **1** +* Fixes and improvements + +**1**: + +When `auto_redirect` is not enabled, directly add `route[_exclude]_address_set` +to tun routes (equivalent to `route[_exclude]_address`). + +Note that it **doesn't work on the Android graphical client** due to +the Android VpnService not being able to handle a large number of routes (DeadSystemException), +but otherwise it works fine on all command line clients and Apple platforms. + +See [route_address_set](/configuration/inbound/tun/#route_address_set) and +[route_exclude_address_set](/configuration/inbound/tun/#route_exclude_address_set). + +#### 1.11.0-beta.12 + +* Add `rule-set merge` command +* Fixes and improvements + +#### 1.11.0-beta.3 + +* Add more masquerade options for hysteria2 **1** +* Fixes and improvements + +**1**: + +See [Hysteria2](/configuration/inbound/hysteria2/#masquerade). + +#### 1.11.0-alpha.25 + +* Update quic-go to v0.48.2 +* Fixes and improvements + +#### 1.11.0-alpha.22 + +* Add UDP timeout route option **1** +* Fixes and improvements + +**1**: + +See [Rule Action](/configuration/route/rule_action/#udp_timeout). + +#### 1.11.0-alpha.20 + +* Add UDP GSO support for WireGuard +* Make GSO adaptive **1** + +**1**: + +For WireGuard outbound and endpoint, GSO will be automatically enabled when available, +see [WireGuard Outbound](/configuration/outbound/wireguard/#gso). + +For TUN, GSO has been removed, +see [Deprecated](/deprecated/#gso-option-in-tun). + +#### 1.11.0-alpha.19 + +* Upgrade WireGuard outbound to endpoint **1** +* Fixes and improvements + +**1**: + +The new WireGuard endpoint combines inbound and outbound capabilities, +and the old outbound will be removed in sing-box 1.13.0. + +See [Endpoint](/configuration/endpoint/), [WireGuard Endpoint](/configuration/endpoint/wireguard/) +and [Migrate WireGuard outbound fields to route options](/migration/#migrate-wireguard-outbound-to-endpoint). + ### 1.10.2 * Add deprecated warnings * Fix proxying websocket connections in HTTP/mixed inbounds * Fixes and improvements +#### 1.11.0-alpha.18 + +* Fixes and improvements + +#### 1.11.0-alpha.16 + +* Add `cache_capacity` DNS option **1** +* Add `override_address` and `override_port` route options **2** +* Fixes and improvements + +**1**: + +See [DNS](/configuration/dns/#cache_capacity). + +**2**: + +See [Rule Action](/configuration/route/#override_address) and +[Migrate destination override fields to route options](/migration/#migrate-destination-override-fields-to-route-options). + +#### 1.11.0-alpha.15 + +* Improve multi network dialing **1** +* Fixes and improvements + +**1**: + +New options allow you to configure the network strategy flexibly. + +See [Dial Fields](/configuration/shared/dial/#network_strategy), +[Rule Action](/configuration/route/rule_action/#network_strategy) +and [Route](/configuration/route/#default_network_strategy). + +#### 1.11.0-alpha.14 + +* Add multi network dialing **1** +* Fixes and improvements + +**1**: + +Similar to Surge's strategy. + +New options allow you to connect using multiple network interfaces, +prefer or only use one type of interface, +and configure a timeout to fallback to other interfaces. + +See [Dial Fields](/configuration/shared/dial/#network_strategy), +[Rule Action](/configuration/route/rule_action/#network_strategy) +and [Route](/configuration/route/#default_network_strategy). + +#### 1.11.0-alpha.13 + +* Fixes and improvements + +#### 1.11.0-alpha.12 + +* Merge route options to route actions **1** +* Add `network_type`, `network_is_expensive` and `network_is_constrainted` rule items **2** +* Fixes and improvements + +**1**: + +Route options in DNS route actions will no longer be considered deprecated, +see [DNS Route Action](/configuration/dns/rule_action/). + +Also, now `udp_disable_domain_unmapping` and `udp_connect` can also be configured in route action, +see [Route Action](/configuration/route/rule_action/). + +**2**: + +When using in graphical clients, new routing rule items allow you to match on +network type (WIFI, cellular, etc.), whether the network is expensive, and whether Low Data Mode is enabled. + +See [Route Rule](/configuration/route/rule/), [DNS Route Rule](/configuration/dns/rule/) +and [Headless Rule](/configuration/rule-set/headless-rule/). + +#### 1.11.0-alpha.9 + +* Improve tun compatibility **1** +* Fixes and improvements + +**1**: + +When `gvisor` tun stack is enabled, even if the request passes routing, +if the outbound connection establishment fails, +the connection still does not need to be established and a TCP RST is replied. + +#### 1.11.0-alpha.7 + +* Introducing rule actions **1** + +**1**: + +New rule actions replace legacy inbound fields and special outbound fields, +and can be used for pre-matching **2**. + +See [Rule](/configuration/route/rule/), +[Rule Action](/configuration/route/rule_action/), +[DNS Rule](/configuration/dns/rule/) and +[DNS Rule Action](/configuration/dns/rule_action/). + +For migration, see +[Migrate legacy special outbounds to rule actions](/migration/#migrate-legacy-special-outbounds-to-rule-actions), +[Migrate legacy inbound fields to rule actions](/migration/#migrate-legacy-inbound-fields-to-rule-actions) +and [Migrate legacy DNS route options to rule actions](/migration/#migrate-legacy-dns-route-options-to-rule-actions). + +**2**: + +Similar to Surge's pre-matching. + +Specifically, new rule actions allow you to reject connections with +TCP RST (for TCP connections) and ICMP port unreachable (for UDP packets) +before connection established to improve tun's compatibility. + +See [Rule Action](/configuration/route/rule_action/). + +#### 1.11.0-alpha.6 + +* Update quic-go to v0.48.1 +* Set gateway for tun correctly +* Fixes and improvements + +#### 1.11.0-alpha.2 + +* Add warnings for usage of deprecated features +* Fixes and improvements + +#### 1.11.0-alpha.1 + +* Update quic-go to v0.48.0 +* Fixes and improvements + ### 1.10.1 * Fixes and improvements @@ -87,7 +305,7 @@ allows you to write headless rules directly without creating a rule-set file. **8**: -With the new access control options, not only can you allow Clash dashboards +With new access control options, not only can you allow Clash dashboards to access the Clash API on your local network, you can also manually limit the websites that can access the API instead of allowing everyone. From 99ffb46638c5596e325d48b7a4bfbf241ad133f1 Mon Sep 17 00:00:00 2001 From: Milinda Brantini Date: Fri, 3 Jan 2025 20:54:27 +0800 Subject: [PATCH 49/49] docs: merge inet4_address and inet6_address into address TUN address fields are merged inet4_address and inet6_address are merged into address, inet4_route_address and inet6_route_address are merged into route_address, inet4_route_exclude_address and inet6_route_exclude_address are merged into route_exclude_address. Signed-off-by: Milinda Brantini --- docs/manual/proxy/client.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/manual/proxy/client.md b/docs/manual/proxy/client.md index 1cf5a1ce45..6cab573b2a 100644 --- a/docs/manual/proxy/client.md +++ b/docs/manual/proxy/client.md @@ -113,7 +113,9 @@ flowchart TB "inbounds": [ { "type": "tun", - "inet4_address": "172.19.0.1/30", + "address": [ + "172.19.0.1/30" + ], "auto_route": true, "strict_route": false } @@ -173,8 +175,10 @@ flowchart TB "inbounds": [ { "type": "tun", - "inet4_address": "172.19.0.1/30", - "inet6_address": "fdfe:dcba:9876::1/126", + "address": [ + "172.19.0.1/30", + "fdfe:dcba:9876::1/126" + ], "auto_route": true, "strict_route": false } @@ -251,8 +255,10 @@ flowchart TB "inbounds": [ { "type": "tun", - "inet4_address": "172.19.0.1/30", - "inet6_address": "fdfe:dcba:9876::1/126", + "address": [ + "172.19.0.1/30", + "fdfe:dcba:9876::1/126" + ], "auto_route": true, "strict_route": true } @@ -591,4 +597,4 @@ flowchart TB ] } } - ``` \ No newline at end of file + ```