Skip to content

Commit

Permalink
Move to dns apis
Browse files Browse the repository at this point in the history
  • Loading branch information
fortuna committed Dec 15, 2023
1 parent 3f45245 commit da99558
Show file tree
Hide file tree
Showing 3 changed files with 16 additions and 157 deletions.
2 changes: 1 addition & 1 deletion x/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/Jigsaw-Code/outline-sdk/x
go 1.20

require (
github.com/Jigsaw-Code/outline-sdk v0.0.11-0.20231215164346-91aaff6b855f
github.com/Jigsaw-Code/outline-sdk v0.0.11-0.20231215170417-3f452458d791
github.com/miekg/dns v1.1.57
github.com/songgao/water v0.0.0-20190725173103-fd331bda3f4b
github.com/stretchr/testify v1.8.2
Expand Down
2 changes: 2 additions & 0 deletions x/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ github.com/Jigsaw-Code/outline-sdk v0.0.11-0.20231212000605-eab30ec091c7 h1:3obj
github.com/Jigsaw-Code/outline-sdk v0.0.11-0.20231212000605-eab30ec091c7/go.mod h1:C0XDPcOFq8ho9n5j0OWr1NkVL/8VVRdN5lTOgolptMg=
github.com/Jigsaw-Code/outline-sdk v0.0.11-0.20231215164346-91aaff6b855f h1:4SOzehZzIGy5rZcOTroRMZIJ3rY2AJVVIbVXIwQdCEg=
github.com/Jigsaw-Code/outline-sdk v0.0.11-0.20231215164346-91aaff6b855f/go.mod h1:nvQicUPpq1XvOme5Vp9vaohqkjn36LG3lXQm07mENWk=
github.com/Jigsaw-Code/outline-sdk v0.0.11-0.20231215170417-3f452458d791 h1:L/vWIQm9PjK//fb+XzwBpbuZHnXHTCH0oHa6urPmGG4=
github.com/Jigsaw-Code/outline-sdk v0.0.11-0.20231215170417-3f452458d791/go.mod h1:nvQicUPpq1XvOme5Vp9vaohqkjn36LG3lXQm07mENWk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
Expand Down
169 changes: 13 additions & 156 deletions x/smart/stream_dialer.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"math/rand"
"net"
"net/url"
"strings"
"sync"
"time"
"unicode"
Expand All @@ -37,7 +36,6 @@ import (
)

// TODO:
// - Use dns.dnsmessage insted of miekg
// - Add DNS caching
// - Parallelize TLS
// - Add debug logging to proxy handler
Expand Down Expand Up @@ -67,154 +65,7 @@ import (
// To test one strategy:
// go run ./x/examples/smart-proxy -v -localAddr=localhost:1080 --transport="" --domain www.rferl.org --config=<(echo '{"dns": [{"https": {"name": "doh.sb"}}]}')

// canonicalName returns the domain name in canonical form. A name in canonical
// form is lowercase and fully qualified. Only US-ASCII letters are affected. See
// Section 6.2 in RFC 4034.
func canonicalName(s string) string {
return strings.Map(func(r rune) rune {
if r >= 'A' && r <= 'Z' {
r += 'a' - 'A'
}
return r
}, s)
}

type cacheEntry struct {
domain string
ips []net.IP
expire time.Time
}

// cacheResolver is a very simple resolver.
// It doesn't use the response TTL and doesn't cache empty answers.
// It also doesn't dedup duplicate in-flight requests.
type cacheResolver struct {
resolver dns.Resolver
cache []cacheEntry
}

var _ dns.Resolver = (*cacheResolver)(nil)

func (r *cacheResolver) removeExpired() {
now := time.Now()
last := 0
for _, entry := range r.cache {
if entry.expire.After(now) {
r.cache[last] = entry
last++
}
}
r.cache = r.cache[:last]
}

func (r *cacheResolver) moveToFront(index int) {
entry := r.cache[index]
copy(r.cache[1:], r.cache[:index])
r.cache[0] = entry
}

func (r *cacheResolver) LookupIP(ctx context.Context, domain string) ([]net.IP, error) {
r.removeExpired()
canonicalDomain := canonicalName(domain)
for ei, entry := range r.cache {
if entry.domain == canonicalDomain {
r.moveToFront(ei)
return entry.ips, nil
}
}
ips, err := r.resolver.LookupIP(ctx, domain)
if err != nil {
return nil, err
}
newSize := len(r.cache) + 1
if newSize > cap(r.cache) {
newSize = cap(r.cache)
}
r.cache = r.cache[:newSize]
copy(r.cache[1:], r.cache[:newSize-1])
r.cache[0] = cacheEntry{domain: canonicalDomain, ips: ips, expire: time.Now().Add(60 * time.Second)}
return ips, nil
}

type dnsResolver struct {
rt dns.RoundTripper
}

func newQuestion(domain string, qtype dnsmessage.Type) (*dnsmessage.Question, error) {
name, err := dnsmessage.NewName(domain)
if err != nil {
return nil, fmt.Errorf("cannot parse domain name: %w", err)
}
return &dnsmessage.Question{
Name: name,
Type: qtype,
Class: dnsmessage.ClassINET,
}, nil
}

func (rt dnsResolver) LookupIP(ctx context.Context, domain string) ([]net.IP, error) {
ip := net.ParseIP(domain)
if ip != nil {
return []net.IP{ip}, nil
}
domain = dns.MakeFullyQualified(domain)
var wg sync.WaitGroup
var returnErr error
ips := []net.IP{}
// TODO: re-enable IPv6. This doesn't work when the base dialer is Shadowsocks.
// wg.Add(1)
// go func() {
// defer wg.Done()
// var reqAAAA miekgdns.Msg
// reqAAAA.SetQuestion(domain, miekgdns.TypeAAAA)
// response, err := rt.rt.RoundTripMsg(ctx, &reqAAAA)
// if err != nil {
// returnErr = err
// return
// }
// for _, answer := range response.Answer {
// if answer.Header().Rrtype != miekgdns.TypeAAAA {
// continue
// }
// if rr, ok := answer.(*miekgdns.AAAA); ok {
// ips = append(ips, rr.AAAA)
// }
// }
// }()
q, err := newQuestion(domain, dnsmessage.TypeA)
if err != nil {
return nil, err
}
response, err := rt.rt.RoundTrip(ctx, *q)
wg.Wait()
if err != nil {
returnErr = errors.Join(returnErr, err)
}
if returnErr != nil {
return nil, returnErr
}
for _, answer := range response.Answers {
if answer.Header.Type != dnsmessage.TypeA {
continue
}
if rr, ok := answer.Body.(*dnsmessage.AResource); ok {
ips = append(ips, net.IP(rr.A[:]))
}
}
if len(ips) == 0 {
return nil, errors.New("no IPs found")
}
return ips, nil
}

type funcStreamDialer func(ctx context.Context, raddr string) (transport.StreamConn, error)

var _ transport.StreamDialer = (funcStreamDialer)(nil)

func (d funcStreamDialer) Dial(ctx context.Context, raddr string) (transport.StreamConn, error) {
return d(ctx, raddr)
}

// mixCase randomizes the case of the domain letters.
func mixCase(domain string) string {
var mixed []rune
for _, r := range domain {
Expand Down Expand Up @@ -262,7 +113,7 @@ func fingerprint(pd transport.PacketDialer, sd transport.StreamDialer, testDomai
}
}

q, err := newQuestion(testDomain, dnsmessage.TypeA)
q, err := dns.NewQuestion(testDomain, dnsmessage.TypeA)
if err != nil {
log.Fatalf("failed to parse domain name: %v", err)
}
Expand Down Expand Up @@ -419,7 +270,7 @@ func (f *StrategyFinder) testDNSClient(ctx context.Context, dnsRT dns.RoundTripp

requestDomain := mixCase(testDomain)

q, err := newQuestion(requestDomain, dnsmessage.TypeA)
q, err := dns.NewQuestion(requestDomain, dnsmessage.TypeA)
if err != nil {
return nil, fmt.Errorf("failed to create question: %v", err)
}
Expand All @@ -429,10 +280,10 @@ func (f *StrategyFinder) testDNSClient(ctx context.Context, dnsRT dns.RoundTripp
}
ips, err := evaluateAResponse(*response, requestDomain)
if err != nil {
return ips, err
return ips, fmt.Errorf("failed A test: %w", err)
}

q, err = newQuestion(requestDomain, dnsmessage.TypeCNAME)
q, err = dns.NewQuestion(requestDomain, dnsmessage.TypeCNAME)
if err != nil {
return nil, fmt.Errorf("failed to create question: %v", err)
}
Expand All @@ -441,7 +292,10 @@ func (f *StrategyFinder) testDNSClient(ctx context.Context, dnsRT dns.RoundTripp
return nil, fmt.Errorf("request for CNAME query failed: %w", err)
}
err = evaluateCNAMEResponse(*response, requestDomain)
return ips, err
if err != nil {
return nil, fmt.Errorf("failed CNAME test: %w", err)
}
return ips, nil
}

type httpsEntryJSON struct {
Expand Down Expand Up @@ -622,7 +476,7 @@ func (f *StrategyFinder) findDNS(testDomain string, dnsConfig []dnsEntryJSON) (d
}
f.log(", status=ok ✅\n")
if result.RT != nil {
return &cacheResolver{resolver: &dnsResolver{result.RT}, cache: make([]cacheEntry, 0, 100)}, nil
return dns.NewCacheResolver(&dns.RoundTripResolver{result.RT}, 100), nil
} else {
return nil, nil
}
Expand Down Expand Up @@ -679,6 +533,9 @@ func (f *StrategyFinder) findTLS(testDomain string, baseDialer transport.StreamD
return nil, errors.New("could not find TLS strategy")
}

// NewDialer uses the config in configBytes to search for a strategy that unblocks the testDomain, returning a dialer with the found strategy.
// It returns an error if no strategy was found that unblocks the testDomain.
// The testDomain must be a domain with a TLS service running on port 443.
func (f *StrategyFinder) NewDialer(ctx context.Context, testDomain string, configBytes []byte) (transport.StreamDialer, error) {
var parsedConfig configJSON
err := json.Unmarshal(configBytes, &parsedConfig)
Expand Down

0 comments on commit da99558

Please sign in to comment.