diff --git a/component/control/dns.go b/component/control/dns.go index 71d6c04..a34ef63 100644 --- a/component/control/dns.go +++ b/component/control/dns.go @@ -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: diff --git a/component/control/udp.go b/component/control/udp.go index a92cd91..e140b8b 100644 --- a/component/control/udp.go +++ b/component/control/udp.go @@ -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) } } diff --git a/component/control/udp_endpoint.go b/component/control/udp_endpoint.go index 693fc28..a94ec04 100644 --- a/component/control/udp_endpoint.go +++ b/component/control/udp_endpoint.go @@ -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 } } diff --git a/example.dae b/example.dae index 81cca35..8fd59a0 100644 --- a/example.dae +++ b/example.dae @@ -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