Merge branch 'refs/heads/main' into integration-test/main

This commit is contained in:
LiquidTheDangerous 2024-08-21 04:24:47 +05:00
commit 52887b5435
19 changed files with 778 additions and 241 deletions

View File

@ -2,6 +2,3 @@ name-template: '$RESOLVED_VERSION'
template: |
## Changes
$CHANGES
$CONTRIBUTORS

View File

@ -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

4
.gitignore vendored
View File

@ -2,5 +2,7 @@ spoof-dpi
spoof-dpi-*
spoof-dpi.*
!*/spoof-dpi/
.DS_Store
out/**
.DS_Store
.idea/

8
build
View File

@ -1,8 +0,0 @@
#!/bin/sh
docker run --rm \
-it \
--workdir /app/out \
-v ./:/app \
golang:1.21-alpine \
sh /app/make-releases.sh

27
dns/addrselect/LICENSE Normal file
View File

@ -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.

View File

@ -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
}

View File

@ -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 {
type Resolver interface {
Resolve(ctx context.Context, host string, qTypes []uint16) ([]net.IPAddr, error)
String() string
}
type Dns struct {
host string
port string
enableDoh bool
systemClient Resolver
generalClient Resolver
dohClient Resolver
}
func NewResolver(config *util.Config) *DnsResolver {
return &DnsResolver{
func NewDns(config *util.Config) *Dns {
addr := *config.DnsAddr
port := strconv.Itoa(*config.DnsPort)
return &Dns{
host: *config.DnsAddr,
port: strconv.Itoa(*config.DnsPort),
enableDoh: *config.EnableDoh,
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
}

View File

@ -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
}

95
dns/resolver/doh.go Normal file
View File

@ -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
}

36
dns/resolver/general.go Normal file
View File

@ -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
}

114
dns/resolver/resolver.go Normal file
View File

@ -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
}

28
dns/resolver/system.go Normal file
View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -1 +1 @@
v0.10.8
v0.10.10