mirror of
https://github.com/daeuniverse/dae.git
synced 2025-07-07 00:42:22 +07:00
feat: add DNS rush-answer filter
This commit is contained in:
@ -7,6 +7,7 @@ package control
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/cilium/ebpf"
|
"github.com/cilium/ebpf"
|
||||||
"github.com/mohae/deepcopy"
|
"github.com/mohae/deepcopy"
|
||||||
@ -21,6 +22,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
SuspectedRushAnswerError = fmt.Errorf("suspected DNS rush-answer")
|
||||||
|
NotAdapableQuestionTypeError = fmt.Errorf("not adaptable question type")
|
||||||
|
)
|
||||||
|
|
||||||
type dnsCache struct {
|
type dnsCache struct {
|
||||||
DomainBitmap [consts.MaxMatchSetLen / 32]uint32
|
DomainBitmap [consts.MaxMatchSetLen / 32]uint32
|
||||||
Answers []dnsmessage.Resource
|
Answers []dnsmessage.Resource
|
||||||
@ -135,23 +141,79 @@ func FlipDnsQuestionCase(dm *dnsmessage.Message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnsureAdditionalOpt makes sure there is additional record OPT in the request.
|
||||||
|
func EnsureAdditionalOpt(dm *dnsmessage.Message, isReqAdd bool) (bool, error) {
|
||||||
|
// Check healthy resp.
|
||||||
|
if isReqAdd == dm.Response || dm.RCode != dnsmessage.RCodeSuccess || len(dm.Questions) == 0 {
|
||||||
|
return false, NotAdapableQuestionTypeError
|
||||||
|
}
|
||||||
|
q := dm.Questions[0]
|
||||||
|
switch q.Type {
|
||||||
|
case dnsmessage.TypeA, dnsmessage.TypeAAAA:
|
||||||
|
default:
|
||||||
|
return false, NotAdapableQuestionTypeError
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ad := range dm.Additionals {
|
||||||
|
if ad.Header.Type == dnsmessage.TypeOPT {
|
||||||
|
// Already has additional record OPT.
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isReqAdd {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
// Add one.
|
||||||
|
dm.Additionals = append(dm.Additionals, dnsmessage.Resource{
|
||||||
|
Header: dnsmessage.ResourceHeader{
|
||||||
|
Name: dnsmessage.MustNewName("."),
|
||||||
|
Type: dnsmessage.TypeOPT,
|
||||||
|
Class: 512, TTL: 0, Length: 0,
|
||||||
|
},
|
||||||
|
Body: &dnsmessage.OPTResource{
|
||||||
|
Options: nil,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
// DnsRespHandler handle DNS resp. This function should be invoked when cache miss.
|
// DnsRespHandler handle DNS resp. This function should be invoked when cache miss.
|
||||||
func (c *ControlPlane) DnsRespHandler(data []byte) (newData []byte, err error) {
|
func (c *ControlPlane) DnsRespHandler(data []byte) (newData []byte, err error) {
|
||||||
var msg dnsmessage.Message
|
var msg dnsmessage.Message
|
||||||
if err = msg.Unpack(data); err != nil {
|
if err = msg.Unpack(data); err != nil {
|
||||||
return nil, fmt.Errorf("unpack dns pkt: %w", err)
|
return nil, fmt.Errorf("unpack dns pkt: %w", err)
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
exist, e := EnsureAdditionalOpt(&msg, false)
|
||||||
|
if e != nil && !errors.Is(e, NotAdapableQuestionTypeError) {
|
||||||
|
c.log.Warnf("EnsureAdditionalOpt: %v", e)
|
||||||
|
}
|
||||||
|
if e == nil && !exist {
|
||||||
|
// Additional record OPT in the request was ensured, and in normal case the resp should also set it.
|
||||||
|
// This DNS packet may be a rush-answer, and we should reject it.
|
||||||
|
// Note that additional record OPT may not be supported by home router either.
|
||||||
|
err = SuspectedRushAnswerError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
FlipDnsQuestionCase(&msg)
|
FlipDnsQuestionCase(&msg)
|
||||||
// Check healthy resp.
|
// Check healthy resp.
|
||||||
if !msg.Response || msg.RCode != dnsmessage.RCodeSuccess || len(msg.Questions) == 0 {
|
if !msg.Response || msg.RCode != dnsmessage.RCodeSuccess || len(msg.Questions) == 0 {
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
q := msg.Questions[0]
|
q := msg.Questions[0]
|
||||||
// Align question and answer Name.
|
// Align Name.
|
||||||
if len(msg.Answers) > 0 &&
|
if len(msg.Answers) > 0 &&
|
||||||
strings.EqualFold(msg.Answers[0].Header.Name.String(), q.Name.String()) {
|
strings.EqualFold(msg.Answers[0].Header.Name.String(), q.Name.String()) {
|
||||||
msg.Answers[0].Header.Name.Data = q.Name.Data
|
msg.Answers[0].Header.Name.Data = q.Name.Data
|
||||||
}
|
}
|
||||||
|
for i := range msg.Additionals {
|
||||||
|
if strings.EqualFold(msg.Additionals[i].Header.Name.String(), q.Name.String()) {
|
||||||
|
msg.Additionals[i].Header.Name.Data = q.Name.Data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check req type.
|
// Check req type.
|
||||||
switch q.Type {
|
switch q.Type {
|
||||||
case dnsmessage.TypeA, dnsmessage.TypeAAAA:
|
case dnsmessage.TypeA, dnsmessage.TypeAAAA:
|
||||||
|
@ -7,6 +7,7 @@ package control
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mzz2017/softwind/pool"
|
"github.com/mzz2017/softwind/pool"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
@ -84,9 +85,21 @@ func (c *ControlPlane) RelayToUDP(lConn *net.UDPConn, to netip.AddrPort, isDNS b
|
|||||||
if isDNS {
|
if isDNS {
|
||||||
data, err = c.DnsRespHandler(data)
|
data, err = c.DnsRespHandler(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, SuspectedRushAnswerError) {
|
||||||
|
if from.Addr().IsPrivate() {
|
||||||
|
// Additional record OPT may not be supported by home router.
|
||||||
|
// And we should trust home devices even if they make rush-answer.
|
||||||
|
c.log.Tracef("DnsRespHandler: received %v", err)
|
||||||
|
err = nil
|
||||||
|
goto sendToClient
|
||||||
|
}
|
||||||
|
// Reject DNS rush-answer.
|
||||||
|
return err
|
||||||
|
}
|
||||||
c.log.Debugf("DnsRespHandler: %v", err)
|
c.log.Debugf("DnsRespHandler: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
sendToClient:
|
||||||
if dummyFrom != nil {
|
if dummyFrom != nil {
|
||||||
from = *dummyFrom
|
from = *dummyFrom
|
||||||
}
|
}
|
||||||
@ -117,6 +130,7 @@ func (c *ControlPlane) handlePkt(data []byte, lConn *net.UDPConn, lAddrPort neti
|
|||||||
dest := addrHdr.Dest
|
dest := addrHdr.Dest
|
||||||
if isDns {
|
if isDns {
|
||||||
if resp := c.LookupDnsRespCache(dnsMessage); resp != nil {
|
if resp := c.LookupDnsRespCache(dnsMessage); resp != nil {
|
||||||
|
// Send cache to client directly.
|
||||||
if err = sendPktWithHdr(resp, dest, lConn, lAddrPort); err != nil {
|
if err = sendPktWithHdr(resp, dest, lConn, lAddrPort); err != nil {
|
||||||
return fmt.Errorf("failed to write cached DNS resp: %w", err)
|
return fmt.Errorf("failed to write cached DNS resp: %w", err)
|
||||||
}
|
}
|
||||||
@ -127,18 +141,25 @@ func (c *ControlPlane) handlePkt(data []byte, lConn *net.UDPConn, lAddrPort neti
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
} else {
|
}
|
||||||
c.log.Tracef("Modify dns target %v to upstream: %v", RefineAddrPortToShow(dest), c.dnsUpstream)
|
|
||||||
// Modify dns target to upstream.
|
|
||||||
// NOTICE: Routing was calculated in advance by the eBPF program.
|
|
||||||
dummyFrom = &addrHdr.Dest
|
|
||||||
dest = c.dnsUpstream
|
|
||||||
|
|
||||||
// Flip dns question to reduce dns pollution.
|
// Need to make a DNS request.
|
||||||
FlipDnsQuestionCase(dnsMessage)
|
c.log.Tracef("Modify dns target %v to upstream: %v", RefineAddrPortToShow(dest), c.dnsUpstream)
|
||||||
if data, err = dnsMessage.Pack(); err != nil {
|
// Modify dns target to upstream.
|
||||||
return fmt.Errorf("pack flipped dns packet: %w", err)
|
// NOTICE: Routing was calculated in advance by the eBPF program.
|
||||||
}
|
dummyFrom = &addrHdr.Dest
|
||||||
|
dest = c.dnsUpstream
|
||||||
|
|
||||||
|
// Flip dns question to reduce dns pollution.
|
||||||
|
FlipDnsQuestionCase(dnsMessage)
|
||||||
|
// Make sure there is additional record OPT in the request to filter DNS rush-answer in the response process.
|
||||||
|
// Because rush-answer has no resp OPT. We can distinguish them from multiple responses.
|
||||||
|
// Note that additional record OPT may not be supported by home router either.
|
||||||
|
_, _ = EnsureAdditionalOpt(dnsMessage, true)
|
||||||
|
|
||||||
|
// Re-pack DNS packet.
|
||||||
|
if data, err = dnsMessage.Pack(); err != nil {
|
||||||
|
return fmt.Errorf("pack flipped dns packet: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
package control
|
package control
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/mzz2017/softwind/pool"
|
"github.com/mzz2017/softwind/pool"
|
||||||
"github.com/v2rayA/dae/component/outbound/dialer"
|
"github.com/v2rayA/dae/component/outbound/dialer"
|
||||||
@ -40,6 +41,9 @@ func (ue *UdpEndpoint) start() {
|
|||||||
ue.deadlineTimer.Reset(ue.NatTimeout)
|
ue.deadlineTimer.Reset(ue.NatTimeout)
|
||||||
ue.mu.Unlock()
|
ue.mu.Unlock()
|
||||||
if err = ue.handler(buf[:n], from.(*net.UDPAddr).AddrPort()); err != nil {
|
if err = ue.handler(buf[:n], from.(*net.UDPAddr).AddrPort()); err != nil {
|
||||||
|
if errors.Is(err, SuspectedRushAnswerError) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,7 @@ global {
|
|||||||
# lan_interface: docker0
|
# lan_interface: docker0
|
||||||
|
|
||||||
# The WAN interface to bind. Use it if you want to proxy localhost
|
# The WAN interface to bind. Use it if you want to proxy localhost
|
||||||
# Multiple interfaces split by ","
|
# Multiple interfaces split by ",".
|
||||||
wan_interface: wlp5s0
|
wan_interface: wlp5s0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +63,8 @@ group {
|
|||||||
routing {
|
routing {
|
||||||
# See routing.md for full examples.
|
# See routing.md for full examples.
|
||||||
|
|
||||||
ip(8.8.8.8, 1.1.1.1) && port(53) -> my_group
|
# dae arms DNS rush-answer filter so we can use 8.8.8.8 regardless of DNS pollution.
|
||||||
|
ip(8.8.8.8) && port(53) -> direct
|
||||||
|
|
||||||
pname(firefox) && domain(ip.sb) -> direct
|
pname(firefox) && domain(ip.sb) -> direct
|
||||||
pname(curl) && domain(ip.sb) -> my_group
|
pname(curl) && domain(ip.sb) -> my_group
|
||||||
|
Reference in New Issue
Block a user