diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 2cf2958..7245988 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -2,6 +2,3 @@ name-template: '$RESOLVED_VERSION' template: | ## Changes - $CHANGES - - $CONTRIBUTORS diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a13c8ef..4b000b1 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -41,6 +41,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG_NAME: ${{ needs.draft_release.outputs.tag_name }} + CGO_ENABLED: 0 steps: - uses: actions/checkout@v4 - name: Setup Go diff --git a/.gitignore b/.gitignore index 9baf0db..4555912 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ spoof-dpi spoof-dpi-* spoof-dpi.* !*/spoof-dpi/ -.DS_Store out/** + +.DS_Store +.idea/ diff --git a/build b/build deleted file mode 100755 index cf1893b..0000000 --- a/build +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -docker run --rm \ - -it \ - --workdir /app/out \ - -v ./:/app \ - golang:1.21-alpine \ - sh /app/make-releases.sh diff --git a/dns/addrselect/LICENSE b/dns/addrselect/LICENSE new file mode 100644 index 0000000..2a7cf70 --- /dev/null +++ b/dns/addrselect/LICENSE @@ -0,0 +1,27 @@ +Copyright 2009 The Go Authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google LLC nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/dns/addrselect/addrselect.go b/dns/addrselect/addrselect.go new file mode 100644 index 0000000..957f8e3 --- /dev/null +++ b/dns/addrselect/addrselect.go @@ -0,0 +1,377 @@ +package addrselect + +import ( + "net" + "net/netip" + "sort" +) + +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Minimal RFC 6724 address selection. + +func SortByRFC6724(addrs []net.IPAddr) { + if len(addrs) < 2 { + return + } + sortByRFC6724withSrcs(addrs, srcAddrs(addrs)) +} + +func sortByRFC6724withSrcs(addrs []net.IPAddr, srcs []netip.Addr) { + if len(addrs) != len(srcs) { + panic("internal error") + } + addrAttr := make([]ipAttr, len(addrs)) + srcAttr := make([]ipAttr, len(srcs)) + for i, v := range addrs { + addrAttrIP, _ := netip.AddrFromSlice(v.IP) + addrAttr[i] = ipAttrOf(addrAttrIP) + srcAttr[i] = ipAttrOf(srcs[i]) + } + sort.Stable(&byRFC6724{ + addrs: addrs, + addrAttr: addrAttr, + srcs: srcs, + srcAttr: srcAttr, + }) +} + +// srcAddrs tries to UDP-connect to each address to see if it has a +// route. (This doesn't send any packets). The destination port +// number is irrelevant. +func srcAddrs(addrs []net.IPAddr) []netip.Addr { + srcs := make([]netip.Addr, len(addrs)) + dst := net.UDPAddr{Port: 9} + for i := range addrs { + dst.IP = addrs[i].IP + dst.Zone = addrs[i].Zone + c, err := net.DialUDP("udp", nil, &dst) + if err == nil { + if src, ok := c.LocalAddr().(*net.UDPAddr); ok { + srcs[i], _ = netip.AddrFromSlice(src.IP) + } + c.Close() + } + } + return srcs +} + +type ipAttr struct { + Scope scope + Precedence uint8 + Label uint8 +} + +func ipAttrOf(ip netip.Addr) ipAttr { + if !ip.IsValid() { + return ipAttr{} + } + match := rfc6724policyTable.Classify(ip) + return ipAttr{ + Scope: classifyScope(ip), + Precedence: match.Precedence, + Label: match.Label, + } +} + +type byRFC6724 struct { + addrs []net.IPAddr // addrs to sort + addrAttr []ipAttr + srcs []netip.Addr // or not valid addr if unreachable + srcAttr []ipAttr +} + +func (s *byRFC6724) Len() int { return len(s.addrs) } + +func (s *byRFC6724) Swap(i, j int) { + s.addrs[i], s.addrs[j] = s.addrs[j], s.addrs[i] + s.srcs[i], s.srcs[j] = s.srcs[j], s.srcs[i] + s.addrAttr[i], s.addrAttr[j] = s.addrAttr[j], s.addrAttr[i] + s.srcAttr[i], s.srcAttr[j] = s.srcAttr[j], s.srcAttr[i] +} + +// Less reports whether i is a better destination address for this +// host than j. +// +// The algorithm and variable names comes from RFC 6724 section 6. +func (s *byRFC6724) Less(i, j int) bool { + DA := s.addrs[i].IP + DB := s.addrs[j].IP + SourceDA := s.srcs[i] + SourceDB := s.srcs[j] + attrDA := &s.addrAttr[i] + attrDB := &s.addrAttr[j] + attrSourceDA := &s.srcAttr[i] + attrSourceDB := &s.srcAttr[j] + + const preferDA = true + const preferDB = false + + // Rule 1: Avoid unusable destinations. + // If DB is known to be unreachable or if Source(DB) is undefined, then + // prefer DA. Similarly, if DA is known to be unreachable or if + // Source(DA) is undefined, then prefer DB. + if !SourceDA.IsValid() && !SourceDB.IsValid() { + return false // "equal" + } + if !SourceDB.IsValid() { + return preferDA + } + if !SourceDA.IsValid() { + return preferDB + } + + // Rule 2: Prefer matching scope. + // If Scope(DA) = Scope(Source(DA)) and Scope(DB) <> Scope(Source(DB)), + // then prefer DA. Similarly, if Scope(DA) <> Scope(Source(DA)) and + // Scope(DB) = Scope(Source(DB)), then prefer DB. + if attrDA.Scope == attrSourceDA.Scope && attrDB.Scope != attrSourceDB.Scope { + return preferDA + } + if attrDA.Scope != attrSourceDA.Scope && attrDB.Scope == attrSourceDB.Scope { + return preferDB + } + + // Rule 3: Avoid deprecated addresses. + // If Source(DA) is deprecated and Source(DB) is not, then prefer DB. + // Similarly, if Source(DA) is not deprecated and Source(DB) is + // deprecated, then prefer DA. + + // TODO(bradfitz): implement? low priority for now. + + // Rule 4: Prefer home addresses. + // If Source(DA) is simultaneously a home address and care-of address + // and Source(DB) is not, then prefer DA. Similarly, if Source(DB) is + // simultaneously a home address and care-of address and Source(DA) is + // not, then prefer DB. + + // TODO(bradfitz): implement? low priority for now. + + // Rule 5: Prefer matching label. + // If Label(Source(DA)) = Label(DA) and Label(Source(DB)) <> Label(DB), + // then prefer DA. Similarly, if Label(Source(DA)) <> Label(DA) and + // Label(Source(DB)) = Label(DB), then prefer DB. + if attrSourceDA.Label == attrDA.Label && + attrSourceDB.Label != attrDB.Label { + return preferDA + } + if attrSourceDA.Label != attrDA.Label && + attrSourceDB.Label == attrDB.Label { + return preferDB + } + + // Rule 6: Prefer higher precedence. + // If Precedence(DA) > Precedence(DB), then prefer DA. Similarly, if + // Precedence(DA) < Precedence(DB), then prefer DB. + if attrDA.Precedence > attrDB.Precedence { + return preferDA + } + if attrDA.Precedence < attrDB.Precedence { + return preferDB + } + + // Rule 7: Prefer native transport. + // If DA is reached via an encapsulating transition mechanism (e.g., + // IPv6 in IPv4) and DB is not, then prefer DB. Similarly, if DB is + // reached via encapsulation and DA is not, then prefer DA. + + // TODO(bradfitz): implement? low priority for now. + + // Rule 8: Prefer smaller scope. + // If Scope(DA) < Scope(DB), then prefer DA. Similarly, if Scope(DA) > + // Scope(DB), then prefer DB. + if attrDA.Scope < attrDB.Scope { + return preferDA + } + if attrDA.Scope > attrDB.Scope { + return preferDB + } + + // Rule 9: Use the longest matching prefix. + // When DA and DB belong to the same address family (both are IPv6 or + // both are IPv4 [but see below]): If CommonPrefixLen(Source(DA), DA) > + // CommonPrefixLen(Source(DB), DB), then prefer DA. Similarly, if + // CommonPrefixLen(Source(DA), DA) < CommonPrefixLen(Source(DB), DB), + // then prefer DB. + // + // However, applying this rule to IPv4 addresses causes + // problems (see issues 13283 and 18518), so limit to IPv6. + if DA.To4() == nil && DB.To4() == nil { + commonA := commonPrefixLen(SourceDA, DA) + commonB := commonPrefixLen(SourceDB, DB) + + if commonA > commonB { + return preferDA + } + if commonA < commonB { + return preferDB + } + } + + // Rule 10: Otherwise, leave the order unchanged. + // If DA preceded DB in the original list, prefer DA. + // Otherwise, prefer DB. + return false // "equal" +} + +type policyTableEntry struct { + Prefix netip.Prefix + Precedence uint8 + Label uint8 +} + +type policyTable []policyTableEntry + +// RFC 6724 section 2.1. +// Items are sorted by the size of their Prefix.Mask.Size, +var rfc6724policyTable = policyTable{ + { + // "::1/128" + Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01}), 128), + Precedence: 50, + Label: 0, + }, + { + // "::ffff:0:0/96" + // IPv4-compatible, etc. + Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}), 96), + Precedence: 35, + Label: 4, + }, + { + // "::/96" + Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 96), + Precedence: 1, + Label: 3, + }, + { + // "2001::/32" + // Teredo + Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0x20, 0x01}), 32), + Precedence: 5, + Label: 5, + }, + { + // "2002::/16" + // 6to4 + Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0x20, 0x02}), 16), + Precedence: 30, + Label: 2, + }, + { + // "3ffe::/16" + Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0x3f, 0xfe}), 16), + Precedence: 1, + Label: 12, + }, + { + // "fec0::/10" + Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0xfe, 0xc0}), 10), + Precedence: 1, + Label: 11, + }, + { + // "fc00::/7" + Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{0xfc}), 7), + Precedence: 3, + Label: 13, + }, + { + // "::/0" + Prefix: netip.PrefixFrom(netip.AddrFrom16([16]byte{}), 0), + Precedence: 40, + Label: 1, + }, +} + +// Classify returns the policyTableEntry of the entry with the longest +// matching prefix that contains ip. +// The table t must be sorted from largest mask size to smallest. +func (t policyTable) Classify(ip netip.Addr) policyTableEntry { + // Prefix.Contains() will not match an IPv6 prefix for an IPv4 address. + if ip.Is4() { + ip = netip.AddrFrom16(ip.As16()) + } + for _, ent := range t { + if ent.Prefix.Contains(ip) { + return ent + } + } + return policyTableEntry{} +} + +// RFC 6724 section 3.1. +type scope uint8 + +const ( + scopeInterfaceLocal scope = 0x1 + scopeLinkLocal scope = 0x2 + scopeAdminLocal scope = 0x4 + scopeSiteLocal scope = 0x5 + scopeOrgLocal scope = 0x8 + scopeGlobal scope = 0xe +) + +func classifyScope(ip netip.Addr) scope { + if ip.IsLoopback() || ip.IsLinkLocalUnicast() { + return scopeLinkLocal + } + ipv6 := ip.Is6() && !ip.Is4In6() + ipv6AsBytes := ip.As16() + if ipv6 && ip.IsMulticast() { + return scope(ipv6AsBytes[1] & 0xf) + } + // Site-local addresses are defined in RFC 3513 section 2.5.6 + // (and deprecated in RFC 3879). + if ipv6 && ipv6AsBytes[0] == 0xfe && ipv6AsBytes[1]&0xc0 == 0xc0 { + return scopeSiteLocal + } + return scopeGlobal +} + +// commonPrefixLen reports the length of the longest prefix (looking +// at the most significant, or leftmost, bits) that the +// two addresses have in common, up to the length of a's prefix (i.e., +// the portion of the address not including the interface ID). +// +// If a or b is an IPv4 address as an IPv6 address, the IPv4 addresses +// are compared (with max common prefix length of 32). +// If a and b are different IP versions, 0 is returned. +// +// See https://tools.ietf.org/html/rfc6724#section-2.2 +func commonPrefixLen(a netip.Addr, b net.IP) (cpl int) { + if b4 := b.To4(); b4 != nil { + b = b4 + } + aAsSlice := a.AsSlice() + if len(aAsSlice) != len(b) { + return 0 + } + // If IPv6, only up to the prefix (first 64 bits) + if len(aAsSlice) > 8 { + aAsSlice = aAsSlice[:8] + b = b[:8] + } + for len(aAsSlice) > 0 { + if aAsSlice[0] == b[0] { + cpl += 8 + aAsSlice = aAsSlice[1:] + b = b[1:] + continue + } + bits := 8 + ab, bb := aAsSlice[0], b[0] + for { + ab >>= 1 + bb >>= 1 + bits-- + if ab == bb { + cpl += bits + return + } + } + } + return +} diff --git a/dns/dns.go b/dns/dns.go index eee15c6..a5ea456 100644 --- a/dns/dns.go +++ b/dns/dns.go @@ -2,109 +2,91 @@ package dns import ( "context" - "errors" + "fmt" "net" - "regexp" "strconv" "time" "github.com/miekg/dns" log "github.com/sirupsen/logrus" + "github.com/xvzc/SpoofDPI/dns/resolver" "github.com/xvzc/SpoofDPI/util" ) -type DnsResolver struct { - host string - port string - enableDoh bool +type Resolver interface { + Resolve(ctx context.Context, host string, qTypes []uint16) ([]net.IPAddr, error) + String() string } -func NewResolver(config *util.Config) *DnsResolver { - return &DnsResolver{ - host: *config.DnsAddr, - port: strconv.Itoa(*config.DnsPort), - enableDoh: *config.EnableDoh, +type Dns struct { + host string + port string + systemClient Resolver + generalClient Resolver + dohClient Resolver +} + +func NewDns(config *util.Config) *Dns { + addr := *config.DnsAddr + port := strconv.Itoa(*config.DnsPort) + + return &Dns{ + host: *config.DnsAddr, + port: port, + systemClient: resolver.NewSystemResolver(), + generalClient: resolver.NewGeneralResolver(net.JoinHostPort(addr, port)), + dohClient: resolver.NewDOHResolver(addr), } } -func (d *DnsResolver) Lookup(domain string, useSystemDns bool) (string, error) { - ipRegex := "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$" - - if r, _ := regexp.MatchString(ipRegex, domain); r { - return domain, nil - } - - if useSystemDns { - log.Debug("[DNS] ", domain, " resolving with system dns") - return systemLookup(domain) - } - - if d.enableDoh { - log.Debug("[DNS] ", domain, " resolving with dns over https") - return dohLookup(d.host, domain) - } - - log.Debug("[DNS] ", domain, " resolving with custom dns") - return customLookup(d.host, d.port, domain) -} - -func customLookup(host string, port string, domain string) (string, error) { - - dnsServer := host + ":" + port - - msg := new(dns.Msg) - msg.SetQuestion(dns.Fqdn(domain), dns.TypeA) - - c := new(dns.Client) - - response, _, err := c.Exchange(msg, dnsServer) - if err != nil { - return "", errors.New("could not resolve the domain(custom)") - } - - for _, answer := range response.Answer { - if record, ok := answer.(*dns.A); ok { - return record.A.String(), nil - } - } - - return "", errors.New("no record found(custom)") - -} - -func systemLookup(domain string) (string, error) { - systemResolver := net.Resolver{PreferGo: true} - ips, err := systemResolver.LookupIPAddr(context.Background(), domain) - if err != nil { - return "", errors.New("could not resolve the domain(system)") - } - - for _, ip := range ips { +func (d *Dns) ResolveHost(host string, enableDoh bool, useSystemDns bool) (string, error) { + if ip, err := parseIpAddr(host); err == nil { return ip.String(), nil } - return "", errors.New("no record found(system)") -} - -func dohLookup(host string, domain string) (string, error) { + clt := d.clientFactory(enableDoh, useSystemDns) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - client := getDOHClient(host) + log.Debugf("[DNS] resolving %s using %s", host, clt) + t := time.Now() - msg := new(dns.Msg) - msg.SetQuestion(dns.Fqdn(domain), dns.TypeA) - - response, err := client.dohExchange(ctx, msg) + addrs, err := clt.Resolve(ctx, host, []uint16{dns.TypeAAAA, dns.TypeA}) + // addrs, err := clt.Resolve(ctx, host, []uint16{dns.TypeAAAA}) if err != nil { - return "", errors.New("could not resolve the domain(doh)") + return "", fmt.Errorf("%s: %w", clt, err) } - for _, answer := range response.Answer { - if record, ok := answer.(*dns.A); ok { - return record.A.String(), nil - } + if len(addrs) > 0 { + d := time.Since(t).Milliseconds() + log.Debugf("[DNS] resolved %s from %s in %d ms", addrs[0].String(), host, d) + return addrs[0].String(), nil } - return "", errors.New("no record found(doh)") + return "", fmt.Errorf("could not resolve %s using %s", host, clt) +} + +func (d *Dns) clientFactory(enableDoh bool, useSystemDns bool) Resolver { + if useSystemDns { + return d.systemClient + } + + if enableDoh { + return d.dohClient + } + + return d.generalClient +} + +func parseIpAddr(addr string) (*net.IPAddr, error) { + ip := net.ParseIP(addr) + if ip == nil { + return nil, fmt.Errorf("%s is not an ip address", addr) + } + + ipAddr := &net.IPAddr{ + IP: ip, + } + + return ipAddr, nil } diff --git a/dns/doh.go b/dns/doh.go deleted file mode 100644 index d597642..0000000 --- a/dns/doh.go +++ /dev/null @@ -1,106 +0,0 @@ -package dns - -import ( - "bytes" - "context" - "encoding/base64" - "errors" - "fmt" - "net" - "net/http" - "regexp" - "sync" - "time" - - "github.com/miekg/dns" -) - -type DOHClient struct { - upstream string - httpClient *http.Client -} - -var dohClient *DOHClient -var clientOnce sync.Once - -func getDOHClient(host string) *DOHClient { - if dohClient != nil { - return dohClient - } - - clientOnce.Do(func() { - h := &http.Client{ - Timeout: 5 * time.Second, - Transport: &http.Transport{ - DialContext: (&net.Dialer{ - Timeout: 3 * time.Second, - KeepAlive: 30 * time.Second, - }).DialContext, - TLSHandshakeTimeout: 5 * time.Second, - MaxIdleConnsPerHost: 100, - MaxIdleConns: 100, - }, - } - - host = regexp.MustCompile(`^https:\/\/|\/dns-query$`).ReplaceAllString(host, "") - dohClient = &DOHClient{ - upstream: "https://" + host + "/dns-query", - httpClient: h, - } - }) - - return dohClient -} - -func (d *DOHClient) dohQuery(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { - pack, err := msg.Pack() - if err != nil { - return nil, err - } - - url := fmt.Sprintf("%s?dns=%s", d.upstream, base64.RawStdEncoding.EncodeToString(pack)) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, err - } - - req = req.WithContext(ctx) - req.Header.Set("Accept", "application/dns-message") - - resp, err := d.httpClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, errors.New("doh status error") - } - - buf := bytes.Buffer{} - _, err = buf.ReadFrom(resp.Body) - if err != nil { - return nil, err - } - - resultMsg := new(dns.Msg) - err = resultMsg.Unpack(buf.Bytes()) - if err != nil { - return nil, err - } - - return resultMsg, nil -} - -func (d *DOHClient) dohExchange(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { - res, err := d.dohQuery(ctx, msg) - if err != nil { - return nil, err - } - - if res.Rcode != dns.RcodeSuccess { - return nil, errors.New("doh rcode wasn't successful") - } - - return res, nil -} diff --git a/dns/resolver/doh.go b/dns/resolver/doh.go new file mode 100644 index 0000000..c9350a5 --- /dev/null +++ b/dns/resolver/doh.go @@ -0,0 +1,95 @@ +package resolver + +import ( + "bytes" + "context" + "encoding/base64" + "errors" + "fmt" + "net" + "net/http" + "regexp" + "time" + + "github.com/miekg/dns" +) + +type DOHResolver struct { + upstream string + client *http.Client +} + +func NewDOHResolver(host string) *DOHResolver { + c := &http.Client{ + Timeout: 5 * time.Second, + Transport: &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 3 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + TLSHandshakeTimeout: 5 * time.Second, + MaxIdleConnsPerHost: 100, + MaxIdleConns: 100, + }, + } + + host = regexp.MustCompile(`^https:\/\/|\/dns-query$`).ReplaceAllString(host, "") + return &DOHResolver{ + upstream: "https://" + host + "/dns-query", + client: c, + } +} + +func (r *DOHResolver) Resolve(ctx context.Context, host string, qTypes []uint16) ([]net.IPAddr, error) { + resultCh := lookupAllTypes(ctx, host, qTypes, r.exchange) + addrs, err := processResults(ctx, resultCh) + return addrs, err +} + +func (r *DOHResolver) String() string { + return fmt.Sprintf("doh resolver(%s)", r.upstream) +} + +func (r *DOHResolver) exchange(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { + pack, err := msg.Pack() + if err != nil { + return nil, err + } + + url := fmt.Sprintf("%s?dns=%s", r.upstream, base64.RawStdEncoding.EncodeToString(pack)) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + req = req.WithContext(ctx) + req.Header.Set("Accept", "application/dns-message") + + resp, err := r.client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, errors.New("doh status error") + } + + buf := bytes.Buffer{} + _, err = buf.ReadFrom(resp.Body) + if err != nil { + return nil, err + } + + resultMsg := new(dns.Msg) + err = resultMsg.Unpack(buf.Bytes()) + if err != nil { + return nil, err + } + + if resultMsg.Rcode != dns.RcodeSuccess { + return nil, errors.New("doh rcode wasn't successful") + } + + return resultMsg, nil +} diff --git a/dns/resolver/general.go b/dns/resolver/general.go new file mode 100644 index 0000000..9c8b49f --- /dev/null +++ b/dns/resolver/general.go @@ -0,0 +1,36 @@ +package resolver + +import ( + "context" + "fmt" + "net" + + "github.com/miekg/dns" +) + +type GeneralResolver struct { + client *dns.Client + server string +} + +func NewGeneralResolver(server string) *GeneralResolver { + return &GeneralResolver{ + client: &dns.Client{}, + server: server, + } +} + +func (r *GeneralResolver) Resolve(ctx context.Context, host string, qTypes []uint16) ([]net.IPAddr, error) { + resultCh := lookupAllTypes(ctx, host, qTypes, r.exchange) + addrs, err := processResults(ctx, resultCh) + return addrs, err +} + +func (r *GeneralResolver) String() string { + return fmt.Sprintf("general resolver(%s)", r.server) +} + +func (r *GeneralResolver) exchange(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { + resp, _, err := r.client.Exchange(msg, r.server) + return resp, err +} diff --git a/dns/resolver/resolver.go b/dns/resolver/resolver.go new file mode 100644 index 0000000..298a101 --- /dev/null +++ b/dns/resolver/resolver.go @@ -0,0 +1,114 @@ +package resolver + +import ( + "context" + "errors" + "fmt" + "net" + "strconv" + "sync" + + "github.com/miekg/dns" + "github.com/xvzc/SpoofDPI/dns/addrselect" +) + +type exchangeFunc = func(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) + +type DNSResult struct { + msg *dns.Msg + err error +} + +func recordTypeIDToName(id uint16) string { + switch id { + case 1: + return "A" + case 28: + return "AAAA" + } + return strconv.FormatUint(uint64(id), 10) +} + +func parseAddrsFromMsg(msg *dns.Msg) []net.IPAddr { + var addrs []net.IPAddr + + for _, record := range msg.Answer { + switch ipRecord := record.(type) { + case *dns.A: + addrs = append(addrs, net.IPAddr{IP: ipRecord.A}) + case *dns.AAAA: + addrs = append(addrs, net.IPAddr{IP: ipRecord.AAAA}) + } + } + return addrs +} + +func sortAddrs(addrs []net.IPAddr) { + addrselect.SortByRFC6724(addrs) +} + +func lookupAllTypes(ctx context.Context, host string, qTypes []uint16, exchange exchangeFunc) <-chan *DNSResult { + var wg sync.WaitGroup + resCh := make(chan *DNSResult) + + for _, qType := range qTypes { + wg.Add(1) + go func(qType uint16) { + defer wg.Done() + select { + case <-ctx.Done(): + return + case resCh <- lookupType(ctx, host, qType, exchange): + } + }(qType) + } + + go func() { + wg.Wait() + close(resCh) + }() + + return resCh +} + +func lookupType(ctx context.Context, host string, queryType uint16, exchange exchangeFunc) *DNSResult { + msg := newMsg(host, queryType) + resp, err := exchange(ctx, msg) + if err != nil { + queryName := recordTypeIDToName(queryType) + err = fmt.Errorf("resolving %s, query type %s: %w", host, queryName, err) + return &DNSResult{err: err} + } + return &DNSResult{msg: resp} +} + +func newMsg(host string, qType uint16) *dns.Msg { + msg := new(dns.Msg) + msg.SetQuestion(dns.Fqdn(host), qType) + return msg +} + +func processResults(ctx context.Context, resCh <-chan *DNSResult) ([]net.IPAddr, error) { + var errs []error + var addrs []net.IPAddr + + for result := range resCh { + if result.err != nil { + errs = append(errs, result.err) + continue + } + resultAddrs := parseAddrsFromMsg(result.msg) + addrs = append(addrs, resultAddrs...) + } + select { + case <-ctx.Done(): + return nil, errors.New("canceled") + default: + if len(addrs) == 0 { + return addrs, errors.Join(errs...) + } + } + + sortAddrs(addrs) + return addrs, nil +} diff --git a/dns/resolver/system.go b/dns/resolver/system.go new file mode 100644 index 0000000..4af716e --- /dev/null +++ b/dns/resolver/system.go @@ -0,0 +1,28 @@ +package resolver + +import ( + "context" + "net" +) + +type SystemResolver struct { + *net.Resolver +} + +func NewSystemResolver() *SystemResolver { + return &SystemResolver{ + &net.Resolver{PreferGo: true}, + } +} + +func (r *SystemResolver) String() string { + return "system resolver" +} + +func (r *SystemResolver) Resolve(ctx context.Context, host string, _ []uint16) ([]net.IPAddr, error) { + addrs, err := r.LookupIPAddr(ctx, host) + if err != nil { + return []net.IPAddr{}, err + } + return addrs, nil +} diff --git a/make-releases.sh b/make-releases.sh deleted file mode 100644 index 84f349e..0000000 --- a/make-releases.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -for osarch in 'darwin/amd64' 'darwin/arm64' 'linux/amd64' 'linux/arm' 'linux/arm64' 'linux/mips' 'linux/mipsle' ; do - GOOS=${osarch%/*} GOARCH=${osarch#*/} go build -ldflags="-w -s" github.com/xvzc/SpoofDPI/cmd/spoof-dpi && - tar -zcvf spoof-dpi-${osarch%/*}-${osarch#*/}.tar.gz ./spoof-dpi && - rm -rf ./spoof-dpi -done - -for osarch in 'windows/amd64'; do - GOOS=${osarch%/*} GOARCH=${osarch#*/} go build -o spoof-dpi-${osarch%/*}-${osarch#*/}.exe -ldflags="-w -s" github.com/xvzc/SpoofDPI/cmd/spoof-dpi -done diff --git a/packet/https.go b/packet/https.go index 49abeb8..7174417 100644 --- a/packet/https.go +++ b/packet/https.go @@ -2,12 +2,14 @@ package packet import ( "encoding/binary" + "fmt" "io" ) type TLSMessageType byte const ( + TLSMaxPayloadLen uint16 = 16384 // 16 KB TLSHeaderLen = 5 TLSInvalid TLSMessageType = 0x0 TLSChangeCipherSpec TLSMessageType = 0x14 @@ -42,7 +44,10 @@ func ReadTLSMessage(r io.Reader) (*TLSMessage, error) { ProtoVersion: binary.BigEndian.Uint16(rawHeader[1:3]), PayloadLen: binary.BigEndian.Uint16(rawHeader[3:5]), } - + if header.PayloadLen > TLSMaxPayloadLen { + // Corrupted header? Check integer overflow + return nil, fmt.Errorf("invalid TLS header. Type: %x, ProtoVersion: %x, PayloadLen: %x", header.Type, header.ProtoVersion, header.PayloadLen) + } raw := make([]byte, header.PayloadLen+TLSHeaderLen) copy(raw[0:TLSHeaderLen], rawHeader[:]) _, err = io.ReadFull(r, raw[TLSHeaderLen:]) @@ -62,5 +67,7 @@ func ReadTLSMessage(r io.Reader) (*TLSMessage, error) { func (m *TLSMessage) IsClientHello() bool { // According to RFC 8446 section 4. // first byte (Raw[5]) of handshake message should be 0x1 - means client_hello - return m.Header.Type == TLSHandshake && m.Raw[5] == 0x01 + return len(m.Raw) > TLSHeaderLen && + m.Header.Type == TLSHandshake && + m.Raw[5] == 0x01 } diff --git a/proxy/http.go b/proxy/http.go index 7763ad8..97944aa 100644 --- a/proxy/http.go +++ b/proxy/http.go @@ -18,7 +18,7 @@ func (pxy *Proxy) handleHttp(lConn *net.TCPConn, pkt *packet.HttpPacket, ip stri if pkt.Port() != "" { port, err = strconv.Atoi(pkt.Port()) if err != nil { - log.Debug("[HTTP] error while parsing port for ", pkt.Domain(), " aborting..") + log.Debugf("[HTTP] error while parsing port for %s aborting..", pkt.Domain()) } } @@ -29,17 +29,17 @@ func (pxy *Proxy) handleHttp(lConn *net.TCPConn, pkt *packet.HttpPacket, ip stri return } - log.Debug("[HTTP] new connection to the server ", rConn.LocalAddr(), " -> ", pkt.Domain()) + log.Debugf("[HTTP] new connection to the server %s -> %s", rConn.LocalAddr(), pkt.Domain()) go Serve(rConn, lConn, "[HTTP]", pkt.Domain(), lConn.RemoteAddr().String(), pxy.timeout) _, err = rConn.Write(pkt.Raw()) if err != nil { - log.Debug("[HTTP] error sending request to ", pkt.Domain(), err) + log.Debugf("[HTTP] error sending request to %s: %s", pkt.Domain(), err) return } - log.Debug("[HTTP] sent a request to ", pkt.Domain()) + log.Debugf("[HTTP] sent a request to %s", pkt.Domain()) go Serve(lConn, rConn, "[HTTP]", lConn.RemoteAddr().String(), pkt.Domain(), pxy.timeout) } diff --git a/proxy/https.go b/proxy/https.go index 4a615ad..efc4163 100644 --- a/proxy/https.go +++ b/proxy/https.go @@ -15,7 +15,7 @@ func (pxy *Proxy) handleHttps(lConn *net.TCPConn, exploit bool, initPkt *packet. if initPkt.Port() != "" { port, err = strconv.Atoi(initPkt.Port()) if err != nil { - log.Debug("[HTTPS] error while parsing port for ", initPkt.Domain(), " aborting..") + log.Debugf("[HTTPS] error parsing port for %s aborting..", initPkt.Domain()) } } @@ -26,40 +26,40 @@ func (pxy *Proxy) handleHttps(lConn *net.TCPConn, exploit bool, initPkt *packet. return } - log.Debug("[HTTPS] new connection to the server ", rConn.LocalAddr(), " -> ", initPkt.Domain()) + log.Debugf("[HTTPS] new connection to the server %s -> %s", rConn.LocalAddr(), initPkt.Domain()) _, err = lConn.Write([]byte(initPkt.Version() + " 200 Connection Established\r\n\r\n")) if err != nil { - log.Debug("[HTTPS] error sending 200 connection established to the client: ", err) + log.Debugf("[HTTPS] error sending 200 connection established to the client: %s", err) return } - log.Debug("[HTTPS] sent connection estabalished to ", lConn.RemoteAddr()) + log.Debugf("[HTTPS] sent connection estabalished to %s", lConn.RemoteAddr()) // Read client hello m, err := packet.ReadTLSMessage(lConn) if err != nil || !m.IsClientHello() { - log.Debug("[HTTPS] error reading client hello from ", lConn.RemoteAddr().String(), " ", err) + log.Debugf("[HTTPS] error reading client hello from %s: %s", lConn.RemoteAddr().String(), err) return } clientHello := m.Raw - log.Debug("[HTTPS] client sent hello ", len(clientHello), "bytes") + log.Debugf("[HTTPS] client sent hello %d bytes", len(clientHello)) // Generate a go routine that reads from the server go Serve(rConn, lConn, "[HTTPS]", initPkt.Domain(), lConn.RemoteAddr().String(), pxy.timeout) if exploit { - log.Debug("[HTTPS] writing chunked client hello to ", initPkt.Domain()) + log.Debugf("[HTTPS] writing chunked client hello to %s", initPkt.Domain()) chunks := splitInChunks(clientHello, pxy.windowSize) if _, err := writeChunks(rConn, chunks); err != nil { - log.Debug("[HTTPS] error writing chunked client hello to ", initPkt.Domain(), err) + log.Debugf("[HTTPS] error writing chunked client hello to %s: %s", initPkt.Domain(), err) return } } else { - log.Debug("[HTTPS] writing plain client hello to ", initPkt.Domain()) + log.Debugf("[HTTPS] writing plain client hello to %s", initPkt.Domain()) if _, err := rConn.Write(clientHello); err != nil { - log.Debug("[HTTPS] error writing plain client hello to ", initPkt.Domain(), err) + log.Debugf("[HTTPS] error writing plain client hello to %s: %s", initPkt.Domain(), err) return } } @@ -71,7 +71,7 @@ func splitInChunks(bytes []byte, size int) [][]byte { var chunks [][]byte var raw []byte = bytes - log.Debug("[HTTPS] window-size: ", size) + log.Debugf("[HTTPS] window-size: %d", size) if size > 0 { for { diff --git a/proxy/proxy.go b/proxy/proxy.go index d82ba6a..c2beec6 100644 --- a/proxy/proxy.go +++ b/proxy/proxy.go @@ -17,8 +17,9 @@ type Proxy struct { addr string port int timeout int - resolver *dns.DnsResolver + resolver *dns.Dns windowSize int + enableDoh bool allowedPattern []*regexp.Regexp } @@ -28,13 +29,14 @@ func New(config *util.Config) *Proxy { port: *config.Port, timeout: *config.Timeout, windowSize: *config.WindowSize, + enableDoh: *config.EnableDoh, allowedPattern: config.AllowedPatterns, - resolver: dns.NewResolver(config), + resolver: dns.NewDns(config), } } func (pxy *Proxy) Start() { - l, err := net.ListenTCP("tcp4", &net.TCPAddr{IP: net.ParseIP(pxy.addr), Port: pxy.port}) + l, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP(pxy.addr), Port: pxy.port}) if err != nil { log.Fatal("[PROXY] error creating listener: ", err) os.Exit(1) @@ -75,7 +77,7 @@ func (pxy *Proxy) Start() { matched := pxy.patternMatches([]byte(pkt.Domain())) useSystemDns := !matched - ip, err := pxy.resolver.Lookup(pkt.Domain(), useSystemDns) + ip, err := pxy.resolver.ResolveHost(pkt.Domain(), pxy.enableDoh, useSystemDns) if err != nil { log.Debug("[PROXY] error while dns lookup: ", pkt.Domain(), " ", err) conn.Write([]byte(pkt.Version() + " 502 Bad Gateway\r\n\r\n")) @@ -114,11 +116,6 @@ func (pxy *Proxy) patternMatches(bytes []byte) bool { } func isLoopedRequest(ip net.IP) bool { - // we don't handle IPv6 at all it seems - if ip.To4() == nil { - return false - } - if ip.IsLoopback() { return true } @@ -133,7 +130,7 @@ func isLoopedRequest(ip net.IP) bool { for _, addr := range addr { if ipnet, ok := addr.(*net.IPNet); ok { - if ipnet.IP.To4() != nil && ipnet.IP.To4().Equal(ip) { + if ipnet.IP.Equal(ip) { return true } } diff --git a/proxy/server.go b/proxy/server.go index a23cb6e..233839a 100644 --- a/proxy/server.go +++ b/proxy/server.go @@ -37,10 +37,9 @@ func Serve(from *net.TCPConn, to *net.TCPConn, proto string, fd string, td strin from.Close() to.Close() - log.Debug("[HTTPS] closing proxy connection: ", fd, " -> ", td) + log.Debugf("%s closing proxy connection: %s -> %s", proto, fd, td) }() - proto += " " buf := make([]byte, BufferSize) for { if timeout > 0 { @@ -52,15 +51,15 @@ func Serve(from *net.TCPConn, to *net.TCPConn, proto string, fd string, td strin bytesRead, err := ReadBytes(from, buf) if err != nil { if err == io.EOF { - log.Debug(proto, "finished reading from", fd) + log.Debugf("%s finished reading from %s", proto, fd) return } - log.Debug(proto, "error reading from ", fd, " ", err) + log.Debugf("%s error reading from %s: %s", proto, fd, err) return } if _, err := to.Write(bytesRead); err != nil { - log.Debug(proto, "error Writing to ", td) + log.Debugf("%s error Writing to %s", proto, td) return } } diff --git a/version/VERSION b/version/VERSION index e57ab91..e1352ad 100644 --- a/version/VERSION +++ b/version/VERSION @@ -1 +1 @@ -v0.10.8 +v0.10.10