From 79084fa79883527d5f70d009a46a9ecd715ada3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sun, 19 Jul 2026 13:02:55 +0800 Subject: [PATCH] Add stateless DNS hijack --- flow.go | 1 + flow_dispatch.go | 7 ++++ flow_dns.go | 89 +++++++++++++++++++++++++++++++++++++++++++++ stack_gvisor_udp.go | 20 +++++++--- tun.go | 2 + 5 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 flow_dns.go diff --git a/flow.go b/flow.go index 1b1f34d..c735fc9 100644 --- a/flow.go +++ b/flow.go @@ -21,6 +21,7 @@ const ( ActionReject ActionDrop ActionBypass + ActionHijackDNS ) type FlowTracker interface { diff --git a/flow_dispatch.go b/flow_dispatch.go index 3f71751..c5e6ea6 100644 --- a/flow_dispatch.go +++ b/flow_dispatch.go @@ -291,6 +291,13 @@ func (d *ForwardDispatcher) judgeAndInstall(key flowKey, packet *forwardPacket, case ActionDrop: d.installSimple(key, ActionDrop, packet.protocol, now) return true + case ActionHijackDNS: + if packet.protocol == uint8(header.UDPProtocolNumber) { + d.hijackDNSPacket(packet) + return true + } + d.installSimple(key, ActionAccept, packet.protocol, now) + return false default: d.installSimple(key, ActionAccept, packet.protocol, now) return false diff --git a/flow_dns.go b/flow_dns.go new file mode 100644 index 0000000..ee9a936 --- /dev/null +++ b/flow_dns.go @@ -0,0 +1,89 @@ +package tun + +import ( + "net/netip" + + "github.com/sagernet/sing-tun/gtcpip/checksum" + "github.com/sagernet/sing-tun/gtcpip/header" + "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" +) + +func (d *ForwardDispatcher) hijackDNSPacket(packet *forwardPacket) { + writer := &dnsResponseWriter{ + writeback: d.writeback, + source: packet.source, + } + d.handler.NewDNSPacket(header.UDP(packet.transport).Payload(), M.SocksaddrFromNetIP(packet.source), M.SocksaddrFromNetIP(packet.destination), writer) +} + +var _ N.PacketWriter = (*dnsResponseWriter)(nil) + +type dnsResponseWriter struct { + writeback ForwardWriteback + source netip.AddrPort +} + +func (w *dnsResponseWriter) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error { + defer buffer.Release() + if !destination.IsIP() { + return E.New("invalid destination: ", destination) + } + sourceAddr := w.source.Addr().Unmap() + destinationAddr := destination.Addr.Unmap() + headroom := w.writeback.ReturnHeadroom() + udpLen := header.UDPMinimumSize + buffer.Len() + var ( + packet []byte + udpHdr header.UDP + ipHdr header.Network + ) + if sourceAddr.Is4() { + if !destinationAddr.Is4() { + return E.New("send IPv6 packet to IPv4 connection") + } + size := header.IPv4MinimumSize + udpLen + packet = make([]byte, headroom+size) + inet4Hdr := header.IPv4(packet[headroom:]) + inet4Hdr.Encode(&header.IPv4Fields{ + TotalLength: uint16(size), + TTL: synthesizedTTL, + Protocol: uint8(header.UDPProtocolNumber), + SrcAddr: destinationAddr, + DstAddr: sourceAddr, + }) + udpHdr = header.UDP(inet4Hdr.Payload()) + ipHdr = inet4Hdr + } else { + if destinationAddr.Is4() { + destinationAddr = netip.AddrFrom16(destinationAddr.As16()) + } + size := header.IPv6MinimumSize + udpLen + packet = make([]byte, headroom+size) + inet6Hdr := header.IPv6(packet[headroom:]) + inet6Hdr.Encode(&header.IPv6Fields{ + PayloadLength: uint16(udpLen), + TransportProtocol: header.UDPProtocolNumber, + HopLimit: synthesizedTTL, + SrcAddr: destinationAddr, + DstAddr: sourceAddr, + }) + udpHdr = header.UDP(inet6Hdr.Payload()) + ipHdr = inet6Hdr + } + udpHdr.Encode(&header.UDPFields{ + SrcPort: destination.Port, + DstPort: w.source.Port(), + Length: uint16(udpLen), + }) + copy(udpHdr.Payload(), buffer.Bytes()) + udpHdr.SetChecksum(^checksum.Checksum(udpHdr.Payload(), udpHdr.CalculateChecksum( + header.PseudoHeaderChecksum(header.UDPProtocolNumber, ipHdr.SourceAddressSlice(), ipHdr.DestinationAddressSlice(), uint16(udpLen)), + ))) + if inet4Hdr, isInet4 := ipHdr.(header.IPv4); isInet4 { + inet4Hdr.SetChecksum(^inet4Hdr.CalculateChecksum()) + } + return w.writeback.WriteReturnPackets([][]byte{packet}) +} diff --git a/stack_gvisor_udp.go b/stack_gvisor_udp.go index 3dce60a..5cd0c93 100644 --- a/stack_gvisor_udp.go +++ b/stack_gvisor_udp.go @@ -71,18 +71,26 @@ func (f *UDPForwarder) PreparePacketConnection(source M.Socksaddr, destination M firstPacket = append(firstPacket[:len(firstPacket):len(firstPacket)], view.AsSlice()...) } }) + var sourceNetwork tcpip.NetworkProtocolNumber + if source.Addr.Is4() { + sourceNetwork = header.IPv4ProtocolNumber + } else { + sourceNetwork = header.IPv6ProtocolNumber + } switch f.handler.JudgeFlow(uint8(header.UDPProtocolNumber), source.AddrPort(), destination.AddrPort(), firstPacket).Action { case ActionReject: gWriteUnreachable(f.stack, userData.(*stack.PacketBuffer)) return false, nil, nil, nil case ActionDrop: return false, nil, nil, nil - } - var sourceNetwork tcpip.NetworkProtocolNumber - if source.Addr.Is4() { - sourceNetwork = header.IPv4ProtocolNumber - } else { - sourceNetwork = header.IPv6ProtocolNumber + case ActionHijackDNS: + f.handler.NewDNSPacket(firstPacket, source, destination, &UDPBackWriter{ + stack: f.stack, + source: AddressFromAddr(source.Addr), + sourcePort: source.Port, + sourceNetwork: sourceNetwork, + }) + return false, nil, nil, nil } writer := &UDPBackWriter{ stack: f.stack, diff --git a/tun.go b/tun.go index 14344b6..e770122 100644 --- a/tun.go +++ b/tun.go @@ -14,12 +14,14 @@ 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" "github.com/sagernet/sing/common/ranges" ) type Handler interface { JudgeFlow(network uint8, source netip.AddrPort, destination netip.AddrPort, firstPacket []byte) FlowVerdict + NewDNSPacket(payload []byte, source M.Socksaddr, destination M.Socksaddr, writer N.PacketWriter) N.TCPConnectionHandlerEx N.UDPConnectionHandlerEx }