Refactor UDP DNS hijack

This commit is contained in:
世界
2026-07-19 13:04:55 +08:00
parent fea6299fa7
commit 7163aa08e3
17 changed files with 126 additions and 62 deletions
+4
View File
@@ -19,6 +19,7 @@ type Router interface {
Lifecycle
ConnectionRouter
PreMatch(metadata InboundContext, firstPacket []byte) PreMatchResult
HijackDNSPacket(ctx context.Context, payload []byte, writer N.PacketWriter, metadata InboundContext)
ConnectionRouterEx
RuleSet(tag string) (RuleSet, bool)
Rules() []Rule
@@ -37,6 +38,7 @@ const (
PreMatchReject
PreMatchDrop
PreMatchBypass
PreMatchHijackDNS
)
type PreMatchResult struct {
@@ -92,6 +94,8 @@ func JudgeFlow(router Router, inbound string, inboundType string, network uint8,
return tun.FlowVerdict{Action: tun.ActionDrop}
case PreMatchBypass:
return tun.FlowVerdict{Action: tun.ActionBypass}
case PreMatchHijackDNS:
return tun.FlowVerdict{Action: tun.ActionHijackDNS}
default:
return tun.FlowVerdict{Action: tun.ActionAccept}
}
+1 -1
View File
@@ -53,7 +53,7 @@ require (
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-shadowtls v0.2.1
github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb
github.com/sagernet/sing-tun v0.8.12-0.20260717024008-39eed1f6361d
github.com/sagernet/sing-tun v0.8.12-0.20260719050255-7e7bd198288c
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1
github.com/sagernet/smux v1.5.50-sing-box-mod.1
+2 -2
View File
@@ -299,8 +299,8 @@ github.com/sagernet/sing-shadowtls v0.2.1 h1:ZiHZdnEnP+YS73NMsxiZmIFCwNd0M4k7PkG
github.com/sagernet/sing-shadowtls v0.2.1/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb h1:VvU2/PZqP5tbKTDq0BxkhRO8ZnKI4UJzziakgBiP2Qg=
github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb/go.mod h1:PcwzX/Xvqky0EP3kGt8OCjYb3R1pydenPHNQZcPZmXY=
github.com/sagernet/sing-tun v0.8.12-0.20260717024008-39eed1f6361d h1:UXUGxGTwotY+R+QkRGvhj/N148SDnwMh/d9Hefn5X7Y=
github.com/sagernet/sing-tun v0.8.12-0.20260717024008-39eed1f6361d/go.mod h1:F/gRq5VX1WN/OZtsvbN2JjXXuNl2ATJglHMSk1/iN9U=
github.com/sagernet/sing-tun v0.8.12-0.20260719050255-7e7bd198288c h1:jS5eWD9PyXhHY89DueRDgenfcrfpyPOuuZIoNfnBmOs=
github.com/sagernet/sing-tun v0.8.12-0.20260719050255-7e7bd198288c/go.mod h1:F/gRq5VX1WN/OZtsvbN2JjXXuNl2ATJglHMSk1/iN9U=
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb h1:KEMbfexD4DvrQGYWwx6r+AwH9Veh8z6cnBZmtCS2G+0=
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb/go.mod h1:D4CnJX3MNAAANhbQUxfIRgBdnvlTEaV7h6ojedcs+pw=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
+4
View File
@@ -420,6 +420,10 @@ func (e *Endpoint) JudgeFlow(network uint8, source netip.AddrPort, destination n
return judgeOpenConnectFlow(e.router, e.Tag(), e.Type(), e.state.Load().localAddresses, network, source, destination, firstPacket)
}
func (e *Endpoint) NewDNSPacket(payload []byte, source M.Socksaddr, destination M.Socksaddr, writer N.PacketWriter) {
e.newDNSPacket(log.ContextWithNewID(e.loopContext), e, payload, source, destination, writer)
}
func (e *Endpoint) ready() bool {
state := e.state.Load()
return state.started && state.tunnelConfigured
+12
View File
@@ -68,6 +68,18 @@ func (e *endpointBase) newPacketConnection(ctx context.Context, endpoint adapter
e.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
}
func (e *endpointBase) newDNSPacket(ctx context.Context, endpoint adapter.Endpoint, payload []byte, source M.Socksaddr, destination M.Socksaddr, writer N.PacketWriter) {
var metadata adapter.InboundContext
metadata.Inbound = endpoint.Tag()
metadata.InboundType = endpoint.Type()
metadata.Network = N.NetworkUDP
metadata.Source = source
metadata.Destination = destination
metadata.Protocol = C.ProtocolDNS
e.logger.InfoContext(ctx, "inbound DNS packet from ", source)
e.router.HijackDNSPacket(ctx, payload, writer, metadata)
}
func isEndpointLocalAddress(localAddresses []netip.Prefix, address netip.Addr) bool {
for _, localPrefix := range localAddresses {
if address == localPrefix.Addr() {
+4
View File
@@ -477,6 +477,10 @@ func (c *ClientEndpoint) JudgeFlow(network uint8, source netip.AddrPort, destina
return judgeOpenVPNFlow(c.router, c.Tag(), c.Type(), c.state.Load().localAddresses, network, source, destination, firstPacket)
}
func (c *ClientEndpoint) NewDNSPacket(payload []byte, source M.Socksaddr, destination M.Socksaddr, writer N.PacketWriter) {
c.newDNSPacket(log.ContextWithNewID(c.ctx), c, payload, source, destination, writer)
}
func (c *ClientEndpoint) ready() bool {
state := c.state.Load()
return state.started && state.tunnelConfigured
+12
View File
@@ -72,6 +72,18 @@ func (e *endpointBase) newPacketConnection(ctx context.Context, endpoint adapter
e.router.RoutePacketConnectionEx(ctx, conn, metadata, onClose)
}
func (e *endpointBase) newDNSPacket(ctx context.Context, endpoint adapter.Endpoint, payload []byte, source M.Socksaddr, destination M.Socksaddr, writer N.PacketWriter) {
var metadata adapter.InboundContext
metadata.Inbound = endpoint.Tag()
metadata.InboundType = endpoint.Type()
metadata.Network = N.NetworkUDP
metadata.Source = source
metadata.Destination = destination
metadata.Protocol = C.ProtocolDNS
e.logger.InfoContext(ctx, "inbound DNS packet from ", source)
e.router.HijackDNSPacket(ctx, payload, writer, metadata)
}
func isEndpointLocalAddress(localAddresses []netip.Prefix, address netip.Addr) bool {
for _, localPrefix := range localAddresses {
if address == localPrefix.Addr() {
+4
View File
@@ -484,6 +484,10 @@ func (s *ServerEndpoint) JudgeFlow(network uint8, source netip.AddrPort, destina
return judgeOpenVPNFlow(s.router, s.Tag(), s.Type(), s.localAddresses, network, source, destination, firstPacket)
}
func (s *ServerEndpoint) NewDNSPacket(payload []byte, source M.Socksaddr, destination M.Socksaddr, writer N.PacketWriter) {
s.newDNSPacket(log.ContextWithNewID(s.ctx), s, payload, source, destination, writer)
}
func (s *ServerEndpoint) WritePackets(packets [][]byte) error {
if !s.started.Load() {
return E.New("OpenVPN server is not ready yet")
+17
View File
@@ -6,9 +6,13 @@ import (
"net/netip"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
tsTUN "github.com/sagernet/tailscale/net/tstun"
"github.com/sagernet/tailscale/types/ipproto"
"github.com/sagernet/tailscale/wgengine/filter"
@@ -67,6 +71,19 @@ func (t *Endpoint) JudgeFlow(network uint8, source netip.AddrPort, destination n
return adapter.JudgeFlow(t.router, t.Tag(), t.Type(), network, source, destination, firstPacket)
}
func (t *Endpoint) NewDNSPacket(payload []byte, source M.Socksaddr, destination M.Socksaddr, writer N.PacketWriter) {
ctx := log.ContextWithNewID(t.ctx)
var metadata adapter.InboundContext
metadata.Inbound = t.Tag()
metadata.InboundType = t.Type()
metadata.Network = N.NetworkUDP
metadata.Source = source
metadata.Destination = destination
metadata.Protocol = C.ProtocolDNS
t.logger.InfoContext(ctx, "inbound DNS packet from ", source)
t.router.HijackDNSPacket(ctx, payload, writer, metadata)
}
func (t *Endpoint) AttachReturn(returnPath tun.Return) error {
t.returnAccess.Lock()
defer t.returnAccess.Unlock()
+21
View File
@@ -18,6 +18,7 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing-tun/gtcpip/header"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json/badoption"
@@ -516,11 +517,27 @@ func (t *Inbound) Close() error {
func (t *Inbound) JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) tun.FlowVerdict {
if slices.Contains(t.dnsHijackAddress, destination.Addr()) {
if network == uint8(header.UDPProtocolNumber) {
return tun.FlowVerdict{Action: tun.ActionHijackDNS}
}
return tun.FlowVerdict{Action: tun.ActionAccept}
}
return adapter.JudgeFlow(t.router, t.tag, C.TypeTun, network, source, destination, firstPacket)
}
func (t *Inbound) NewDNSPacket(payload []byte, source M.Socksaddr, destination M.Socksaddr, writer N.PacketWriter) {
ctx := log.ContextWithNewID(t.ctx)
var metadata adapter.InboundContext
metadata.Inbound = t.tag
metadata.InboundType = C.TypeTun
metadata.Network = N.NetworkUDP
metadata.Source = source
metadata.Destination = destination
metadata.Protocol = C.ProtocolDNS
t.logger.InfoContext(ctx, "inbound DNS packet from ", source)
t.router.HijackDNSPacket(ctx, payload, writer, metadata)
}
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
@@ -591,3 +608,7 @@ func (t *autoRedirectHandler) NewConnectionEx(ctx context.Context, conn net.Conn
func (t *autoRedirectHandler) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
panic("unexcepted")
}
func (t *autoRedirectHandler) NewDNSPacket(payload []byte, source M.Socksaddr, destination M.Socksaddr, writer N.PacketWriter) {
(*Inbound)(t).NewDNSPacket(payload, source, destination, writer)
}
+13
View File
@@ -188,6 +188,19 @@ func (w *Endpoint) JudgeFlow(network uint8, source netip.AddrPort, destination n
return adapter.JudgeFlow(w.router, w.Tag(), w.Type(), network, source, destination, firstPacket)
}
func (w *Endpoint) NewDNSPacket(payload []byte, source M.Socksaddr, destination M.Socksaddr, writer N.PacketWriter) {
ctx := log.ContextWithNewID(w.ctx)
var metadata adapter.InboundContext
metadata.Inbound = w.Tag()
metadata.InboundType = w.Type()
metadata.Network = N.NetworkUDP
metadata.Source = source
metadata.Destination = destination
metadata.Protocol = C.ProtocolDNS
w.logger.InfoContext(ctx, "inbound DNS packet from ", source)
w.router.HijackDNSPacket(ctx, payload, writer, metadata)
}
func (w *Endpoint) WritePackets(packets [][]byte) error {
if !w.started.Load() {
return E.New("WireGuard is not ready yet")
+18 -56
View File
@@ -10,12 +10,9 @@ import (
"github.com/sagernet/sing-box/dns"
dnsOutbound "github.com/sagernet/sing-box/protocol/dns"
R "github.com/sagernet/sing-box/route/rule"
"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/udpnat2"
mDNS "github.com/miekg/dns"
)
@@ -36,24 +33,6 @@ 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, onClose N.CloseHandlerFunc) error {
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.dns, r.logger, natConn, buffer, metadata, destination)
}
natConn.SetHandler(&dnsHijacker{
router: r.dns,
logger: r.logger,
conn: conn,
ctx: ctx,
metadata: metadata,
onClose: onClose,
})
return nil
}
err := dnsOutbound.NewDNSPacketConnection(ctx, r.dns, conn, packetBuffers, metadata)
N.CloseOnHandshakeFailure(conn, onClose, err)
if err != nil && !E.IsClosedOrCanceled(err) {
@@ -62,48 +41,31 @@ func (r *Router) hijackDNSPacket(ctx context.Context, conn N.PacketConn, packetB
return nil
}
func ExchangeDNSPacket(ctx context.Context, router adapter.DNSRouter, logger logger.ContextLogger, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext, destination M.Socksaddr) {
err := exchangeDNSPacket(ctx, router, conn, buffer, metadata, destination)
if err != nil && !R.IsRejected(err) && !E.IsClosedOrCanceled(err) {
logger.ErrorContext(ctx, E.Cause(err, "process DNS packet"))
}
}
func exchangeDNSPacket(ctx context.Context, router adapter.DNSRouter, conn N.PacketConn, buffer *buf.Buffer, metadata adapter.InboundContext, destination M.Socksaddr) error {
func (r *Router) HijackDNSPacket(ctx context.Context, payload []byte, writer N.PacketWriter, metadata adapter.InboundContext) {
var message mDNS.Msg
err := message.Unpack(buffer.Bytes())
buffer.Release()
err := message.Unpack(payload)
if err != nil {
return E.Cause(err, "unpack request")
r.logger.ErrorContext(ctx, E.Cause(err, "process DNS packet: unpack request"))
return
}
response, err := router.Exchange(adapter.WithContext(ctx, &metadata), &message, adapter.DNSQueryOptions{})
destination := metadata.Destination
metadata.Destination = M.Socksaddr{}
go func() {
exchangeErr := r.exchangeDNSPacket(ctx, &message, writer, metadata, destination)
if exchangeErr != nil && !R.IsRejected(exchangeErr) && !E.IsClosedOrCanceled(exchangeErr) {
r.logger.ErrorContext(ctx, E.Cause(exchangeErr, "process DNS packet"))
}
}()
}
func (r *Router) exchangeDNSPacket(ctx context.Context, message *mDNS.Msg, writer N.PacketWriter, metadata adapter.InboundContext, destination M.Socksaddr) error {
response, err := r.dns.Exchange(adapter.WithContext(ctx, &metadata), message, adapter.DNSQueryOptions{})
if err != nil {
return err
}
responseBuffer, err := dns.TruncateDNSMessage(&message, response, 1024)
responseBuffer, err := dns.TruncateDNSMessage(message, response, 1024)
if err != nil {
return err
}
err = conn.WritePacket(responseBuffer, destination)
return err
}
type dnsHijacker struct {
router adapter.DNSRouter
logger logger.ContextLogger
conn N.PacketConn
ctx context.Context
metadata adapter.InboundContext
onClose N.CloseHandlerFunc
}
func (h *dnsHijacker) NewPacketEx(buffer *buf.Buffer, destination M.Socksaddr) {
go ExchangeDNSPacket(h.ctx, h.router, h.logger, h.conn, buffer, h.metadata, destination)
}
func (h *dnsHijacker) Close() error {
if h.onClose != nil {
h.onClose(nil)
}
return nil
return writer.WritePacket(responseBuffer, destination)
}
+5
View File
@@ -391,6 +391,11 @@ func (r *Router) PreMatch(metadata adapter.InboundContext, firstPacket []byte) a
return adapter.PreMatchResult{Action: adapter.PreMatchDrop}
}
return adapter.PreMatchResult{Action: adapter.PreMatchReject}
case *R.RuleActionHijackDNS:
if metadata.Network != N.NetworkUDP {
return continueResult
}
return adapter.PreMatchResult{Action: adapter.PreMatchHijackDNS}
case *R.RuleActionResolve:
resolveErr := r.actionResolve(adapter.WithContext(ctx, &metadata), &metadata, action)
if resolveErr != nil {
+3
View File
@@ -24,6 +24,9 @@ func (r *ruleSetItemTestRouter) PreMatch(adapter.InboundContext, []byte) adapter
return adapter.PreMatchResult{}
}
func (r *ruleSetItemTestRouter) HijackDNSPacket(context.Context, []byte, N.PacketWriter, adapter.InboundContext) {
}
func (r *ruleSetItemTestRouter) RouteConnection(context.Context, net.Conn, adapter.InboundContext) error {
return nil
}
+3
View File
@@ -48,6 +48,9 @@ func (h *endpointUDPNATHandler) JudgeFlow(uint8, netip.AddrPort, netip.AddrPort,
return tun.FlowVerdict{Action: tun.ActionAccept}
}
func (h *endpointUDPNATHandler) NewDNSPacket([]byte, M.Socksaddr, M.Socksaddr, N.PacketWriter) {
}
func (h *endpointUDPNATHandler) NewConnectionEx(_ context.Context, conn net.Conn, _ M.Socksaddr, _ M.Socksaddr, onClose N.CloseHandlerFunc) {
err := conn.Close()
if onClose != nil {
+1 -1
View File
@@ -16,7 +16,7 @@ require (
github.com/sagernet/sing-quic v0.6.4-0.20260709034545-e23afe1172dc
github.com/sagernet/sing-shadowsocks v0.2.8
github.com/sagernet/sing-shadowsocks2 v0.2.1
github.com/sagernet/sing-tun v0.8.12-0.20260717024008-39eed1f6361d
github.com/sagernet/sing-tun v0.8.12-0.20260719050255-7e7bd198288c
github.com/spyzhov/ajson v0.9.4
github.com/stretchr/testify v1.11.1
go.uber.org/goleak v1.3.0
+2 -2
View File
@@ -318,8 +318,8 @@ github.com/sagernet/sing-shadowtls v0.2.1 h1:ZiHZdnEnP+YS73NMsxiZmIFCwNd0M4k7PkG
github.com/sagernet/sing-shadowtls v0.2.1/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb h1:VvU2/PZqP5tbKTDq0BxkhRO8ZnKI4UJzziakgBiP2Qg=
github.com/sagernet/sing-snell v0.0.0-20260710094516-a4e97ee24beb/go.mod h1:PcwzX/Xvqky0EP3kGt8OCjYb3R1pydenPHNQZcPZmXY=
github.com/sagernet/sing-tun v0.8.12-0.20260717024008-39eed1f6361d h1:UXUGxGTwotY+R+QkRGvhj/N148SDnwMh/d9Hefn5X7Y=
github.com/sagernet/sing-tun v0.8.12-0.20260717024008-39eed1f6361d/go.mod h1:F/gRq5VX1WN/OZtsvbN2JjXXuNl2ATJglHMSk1/iN9U=
github.com/sagernet/sing-tun v0.8.12-0.20260719050255-7e7bd198288c h1:jS5eWD9PyXhHY89DueRDgenfcrfpyPOuuZIoNfnBmOs=
github.com/sagernet/sing-tun v0.8.12-0.20260719050255-7e7bd198288c/go.mod h1:F/gRq5VX1WN/OZtsvbN2JjXXuNl2ATJglHMSk1/iN9U=
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb h1:KEMbfexD4DvrQGYWwx6r+AwH9Veh8z6cnBZmtCS2G+0=
github.com/sagernet/sing-usbip v0.0.0-20260616101517-efb91521eddb/go.mod h1:D4CnJX3MNAAANhbQUxfIRgBdnvlTEaV7h6ojedcs+pw=
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=