feat: add DNS rush-answer filter

This commit is contained in:
mzz2017 2023-02-04 20:53:29 +08:00
parent dcf8021500
commit 5118a80bca
4 changed files with 102 additions and 14 deletions

View File

@ -7,6 +7,7 @@ package control
import (
"encoding/binary"
"errors"
"fmt"
"github.com/cilium/ebpf"
"github.com/mohae/deepcopy"
@ -21,6 +22,11 @@ import (
"time"
)
var (
SuspectedRushAnswerError = fmt.Errorf("suspected DNS rush-answer")
NotAdapableQuestionTypeError = fmt.Errorf("not adaptable question type")
)
type dnsCache struct {
DomainBitmap [consts.MaxMatchSetLen / 32]uint32
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.
func (c *ControlPlane) DnsRespHandler(data []byte) (newData []byte, err error) {
var msg dnsmessage.Message
if err = msg.Unpack(data); err != nil {
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)
// Check healthy resp.
if !msg.Response || msg.RCode != dnsmessage.RCodeSuccess || len(msg.Questions) == 0 {
return data, nil
}
q := msg.Questions[0]
// Align question and answer Name.
// Align Name.
if len(msg.Answers) > 0 &&
strings.EqualFold(msg.Answers[0].Header.Name.String(), q.Name.String()) {
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.
switch q.Type {
case dnsmessage.TypeA, dnsmessage.TypeAAAA:

View File

@ -7,6 +7,7 @@ package control
import (
"encoding/binary"
"errors"
"fmt"
"github.com/mzz2017/softwind/pool"
"github.com/sirupsen/logrus"
@ -84,9 +85,21 @@ func (c *ControlPlane) RelayToUDP(lConn *net.UDPConn, to netip.AddrPort, isDNS b
if isDNS {
data, err = c.DnsRespHandler(data)
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)
}
}
sendToClient:
if dummyFrom != nil {
from = *dummyFrom
}
@ -117,6 +130,7 @@ func (c *ControlPlane) handlePkt(data []byte, lConn *net.UDPConn, lAddrPort neti
dest := addrHdr.Dest
if isDns {
if resp := c.LookupDnsRespCache(dnsMessage); resp != nil {
// Send cache to client directly.
if err = sendPktWithHdr(resp, dest, lConn, lAddrPort); err != nil {
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
} 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.
FlipDnsQuestionCase(dnsMessage)
if data, err = dnsMessage.Pack(); err != nil {
return fmt.Errorf("pack flipped dns packet: %w", err)
}
// Need to make a DNS request.
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.
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)
}
}

View File

@ -6,6 +6,7 @@
package control
import (
"errors"
"fmt"
"github.com/mzz2017/softwind/pool"
"github.com/v2rayA/dae/component/outbound/dialer"
@ -40,6 +41,9 @@ func (ue *UdpEndpoint) start() {
ue.deadlineTimer.Reset(ue.NatTimeout)
ue.mu.Unlock()
if err = ue.handler(buf[:n], from.(*net.UDPAddr).AddrPort()); err != nil {
if errors.Is(err, SuspectedRushAnswerError) {
continue
}
break
}
}

View File

@ -17,7 +17,7 @@ global {
# lan_interface: docker0
# The WAN interface to bind. Use it if you want to proxy localhost
# Multiple interfaces split by ","
# Multiple interfaces split by ",".
wan_interface: wlp5s0
}
@ -63,7 +63,8 @@ group {
routing {
# 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(curl) && domain(ip.sb) -> my_group