This commit is contained in:
2026-05-16 00:18:06 -03:00
commit 92941e68a2
66 changed files with 10352 additions and 0 deletions

276
internal/dnsttclient/api.go Normal file
View File

@@ -0,0 +1,276 @@
package dnsttclient
import (
"context"
"crypto/tls"
"fmt"
"io"
"log"
"net"
"net/http"
"strings"
"time"
utls "github.com/refraction-networking/utls"
"github.com/xtaci/kcp-go/v5"
"github.com/xtaci/smux"
"socksrevivepc/internal/dnsttcore/dns"
"socksrevivepc/internal/dnsttcore/noise"
"socksrevivepc/internal/dnsttcore/turbotunnel"
)
// Options configures the embedded DNSTT client.
type Options struct {
ResolverType string
ResolverAddress string
PublicKeyHex string
Domain string
LocalAddress string
UTLSDistribution string
StartupTimeout time.Duration
LogWriter io.Writer
}
// Client is a running embedded DNSTT client instance.
type Client struct {
cancel context.CancelFunc
done chan struct{}
}
// Stop shuts down the listener and DNS transport.
func (c *Client) Stop() {
if c == nil {
return
}
c.cancel()
select {
case <-c.done:
case <-time.After(3 * time.Second):
}
}
// Start starts an embedded DNSTT client and waits until the local TCP listener
// is open. It does not require dnstt-client.exe.
func Start(parent context.Context, opts Options) (*Client, error) {
if opts.LogWriter != nil {
log.SetOutput(opts.LogWriter)
log.SetFlags(log.LstdFlags | log.LUTC)
}
if opts.StartupTimeout <= 0 {
opts.StartupTimeout = 5 * time.Second
}
ctx, cancel := context.WithCancel(parent)
client := &Client{cancel: cancel, done: make(chan struct{})}
ready := make(chan struct{})
errCh := make(chan error, 1)
go func() {
defer close(client.done)
errCh <- runOptions(ctx, opts, ready)
}()
select {
case <-ready:
return client, nil
case err := <-errCh:
cancel()
if err == nil {
err = fmt.Errorf("embedded dnstt stopped during startup")
}
return nil, err
case <-time.After(opts.StartupTimeout):
// Keep the client alive. The local listener usually opens first, but on
// slow networks the Noise/smux session can need a little more time.
return client, nil
case <-parent.Done():
cancel()
return nil, parent.Err()
}
}
func runOptions(ctx context.Context, opts Options, ready chan<- struct{}) error {
resolverType := strings.ToLower(strings.TrimSpace(opts.ResolverType))
resolverAddress := strings.TrimSpace(opts.ResolverAddress)
if resolverType == "" {
resolverType = "doh"
}
if resolverAddress == "" {
return fmt.Errorf("dnstt resolver is required")
}
if opts.PublicKeyHex == "" {
return fmt.Errorf("dnstt public key is required")
}
if opts.Domain == "" {
return fmt.Errorf("dnstt domain is required")
}
if opts.LocalAddress == "" {
opts.LocalAddress = "127.0.0.1:2222"
}
if opts.UTLSDistribution == "" {
opts.UTLSDistribution = "4*random,3*Firefox_120,1*Firefox_105,3*Chrome_120,1*Chrome_102,1*iOS_14,1*iOS_13"
}
domain, err := dns.ParseName(opts.Domain)
if err != nil {
return fmt.Errorf("invalid dnstt domain: %w", err)
}
localAddr, err := net.ResolveTCPAddr("tcp", opts.LocalAddress)
if err != nil {
return fmt.Errorf("invalid dnstt local address: %w", err)
}
pubkey, err := noise.DecodeKey(opts.PublicKeyHex)
if err != nil {
return fmt.Errorf("dnstt public key format error: %w", err)
}
tlsConfig, err := loadTLSConfig()
if err != nil {
return fmt.Errorf("dnstt TLS config error: %w", err)
}
utlsClientHelloID, err := sampleUTLSDistribution(opts.UTLSDistribution)
if err != nil {
return fmt.Errorf("dnstt uTLS profile error: %w", err)
}
if utlsClientHelloID != nil {
log.Printf("dnstt uTLS fingerprint %s %s", utlsClientHelloID.Client, utlsClientHelloID.Version)
}
remoteAddr, pconn, err := makePacketConn(resolverType, resolverAddress, tlsConfig, utlsClientHelloID)
if err != nil {
return err
}
pconn = NewDNSPacketConn(pconn, remoteAddr, domain)
return runContext(ctx, pubkey, domain, localAddr, remoteAddr, pconn, ready)
}
func makePacketConn(resolverType, resolverAddress string, tlsConfig *tls.Config, utlsClientHelloID *utls.ClientHelloID) (net.Addr, net.PacketConn, error) {
switch resolverType {
case "doh", "https":
addr := turbotunnel.DummyAddr{}
var rt http.RoundTripper
if utlsClientHelloID == nil {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.Proxy = nil
transport.TLSClientConfig = tlsConfig.Clone()
baseDialContext := transport.DialContext
if baseDialContext == nil {
baseDialContext = (&net.Dialer{}).DialContext
}
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
if network == "tcp" {
resolvedAddr, _, err := resolveAddrIPv4(ctx, addr)
if err != nil {
return nil, err
}
addr = resolvedAddr
network = "tcp4"
}
return baseDialContext(ctx, network, addr)
}
rt = transport
} else {
utlsConfig := &utls.Config{RootCAs: tlsConfig.RootCAs, MinVersion: tlsConfig.MinVersion}
rt = NewUTLSRoundTripper(utlsConfig, utlsClientHelloID, true)
}
pconn, err := NewHTTPPacketConn(rt, resolverAddress, 32)
return addr, pconn, err
case "dot", "tls":
addr := turbotunnel.DummyAddr{}
var dialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error)
if utlsClientHelloID == nil {
dialTLSContext = (&tls.Dialer{Config: tlsConfig.Clone()}).DialContext
} else {
utlsConfig := &utls.Config{RootCAs: tlsConfig.RootCAs, MinVersion: tlsConfig.MinVersion}
dialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return utlsDialContext(ctx, network, addr, utlsConfig, utlsClientHelloID)
}
}
pconn, err := NewTLSPacketConn(resolverAddress, dialTLSContext)
return addr, pconn, err
case "udp", "dns":
addr, err := net.ResolveUDPAddr("udp", resolverAddress)
if err != nil {
return nil, nil, err
}
pconn, err := net.ListenUDP("udp", nil)
return addr, pconn, err
default:
return nil, nil, fmt.Errorf("unknown dnstt resolver type %q", resolverType)
}
}
func runContext(ctx context.Context, pubkey []byte, domain dns.Name, localAddr *net.TCPAddr, remoteAddr net.Addr, pconn net.PacketConn, ready chan<- struct{}) error {
defer pconn.Close()
ln, err := net.ListenTCP("tcp", localAddr)
if err != nil {
return fmt.Errorf("opening dnstt local listener: %w", err)
}
defer ln.Close()
go func() {
<-ctx.Done()
_ = ln.Close()
_ = pconn.Close()
}()
close(ready)
log.Printf("dnstt local listener ready at %s", ln.Addr())
mtu := dnsNameCapacity(domain) - 8 - 1 - numPadding - 1
if mtu < 80 {
return fmt.Errorf("domain %s leaves only %d bytes for payload", domain, mtu)
}
log.Printf("dnstt effective MTU %d", mtu)
conn, err := kcp.NewConn2(remoteAddr, nil, 0, 0, pconn)
if err != nil {
return fmt.Errorf("opening dnstt KCP connection: %w", err)
}
defer func() {
log.Printf("end dnstt session %08x", conn.GetConv())
conn.Close()
}()
log.Printf("begin dnstt session %08x", conn.GetConv())
conn.SetStreamMode(true)
conn.SetNoDelay(0, 0, 0, 1)
conn.SetWindowSize(turbotunnel.QueueSize/2, turbotunnel.QueueSize/2)
if rc := conn.SetMtu(mtu); !rc {
return fmt.Errorf("setting dnstt MTU failed")
}
rw, err := noise.NewClient(conn, pubkey)
if err != nil {
return err
}
smuxConfig := smux.DefaultConfig()
smuxConfig.Version = 2
smuxConfig.KeepAliveTimeout = idleTimeout
smuxConfig.MaxStreamBuffer = 1 * 1024 * 1024
sess, err := smux.Client(rw, smuxConfig)
if err != nil {
return fmt.Errorf("opening dnstt smux session: %w", err)
}
defer sess.Close()
for {
local, err := ln.Accept()
if err != nil {
select {
case <-ctx.Done():
return nil
default:
}
if err, ok := err.(net.Error); ok && err.Temporary() {
continue
}
return err
}
go func() {
defer local.Close()
err := handle(local.(*net.TCPConn), sess, conn.GetConv())
if err != nil {
log.Printf("dnstt handle: %v", err)
}
}()
}
}

View File

@@ -0,0 +1,22 @@
package dnsttclient
import (
"crypto/tls"
"crypto/x509"
)
func loadTLSConfig() (*tls.Config, error) {
pool, err := x509.SystemCertPool()
if err != nil {
pool = nil
}
config := &tls.Config{
MinVersion: tls.VersionTLS12,
}
if pool != nil {
config.RootCAs = pool
}
return config, nil
}

407
internal/dnsttclient/dns.go Normal file
View File

@@ -0,0 +1,407 @@
package dnsttclient
import (
"bytes"
"crypto/rand"
"encoding/base32"
"encoding/binary"
"fmt"
"io"
"log"
"net"
"time"
"socksrevivepc/internal/dnsttcore/dns"
"socksrevivepc/internal/dnsttcore/turbotunnel"
)
const (
// How many bytes of random padding to insert into queries.
numPadding = 3
// In an otherwise empty polling query, insert even more random padding,
// to reduce the chance of a cache hit. Cannot be greater than 31,
// because the prefix codes indicating padding start at 224.
numPaddingForPoll = 8
// sendLoop has a poll timer that automatically sends an empty polling
// query when a certain amount of time has elapsed without a send. The
// poll timer is initially set to initPollDelay. It increases by a
// factor of pollDelayMultiplier every time the poll timer expires, up
// to a maximum of maxPollDelay. The poll timer is reset to
// initPollDelay whenever an a send occurs that is not the result of the
// poll timer expiring.
initPollDelay = 500 * time.Millisecond
maxPollDelay = 10 * time.Second
pollDelayMultiplier = 2.0
// A limit on the number of empty poll requests we may send in a burst
// as a result of receiving data.
pollLimit = 16
)
// base32Encoding is a base32 encoding without padding.
var base32Encoding = base32.StdEncoding.WithPadding(base32.NoPadding)
// DNSPacketConn provides a packet-sending and -receiving interface over various
// forms of DNS. It handles the details of how packets and padding are encoded
// as a DNS name in the Question section of an upstream query, and as a TXT RR
// in downstream responses.
//
// DNSPacketConn does not handle the mechanics of actually sending and receiving
// encoded DNS messages. That is rather the responsibility of some other
// net.PacketConn such as net.UDPConn, HTTPPacketConn, or TLSPacketConn, one of
// which must be provided to NewDNSPacketConn.
//
// We don't have a need to match up a query and a response by ID. Queries and
// responses are vehicles for carrying data and for our purposes don't need to
// be correlated. When sending a query, we generate a random ID, and when
// receiving a response, we ignore the ID.
type DNSPacketConn struct {
clientID turbotunnel.ClientID
domain dns.Name
// Sending on pollChan permits sendLoop to send an empty polling query.
// sendLoop also does its own polling according to a time schedule.
pollChan chan struct{}
// QueuePacketConn is the direct receiver of ReadFrom and WriteTo calls.
// recvLoop and sendLoop take the messages out of the receive and send
// queues and actually put them on the network.
*turbotunnel.QueuePacketConn
}
// NewDNSPacketConn creates a new DNSPacketConn. transport, through its WriteTo
// and ReadFrom methods, handles the actual sending and receiving the DNS
// messages encoded by DNSPacketConn. addr is the address to be passed to
// transport.WriteTo whenever a message needs to be sent.
func NewDNSPacketConn(transport net.PacketConn, addr net.Addr, domain dns.Name) *DNSPacketConn {
// Generate a new random ClientID.
clientID := turbotunnel.NewClientID()
c := &DNSPacketConn{
clientID: clientID,
domain: domain,
pollChan: make(chan struct{}, pollLimit),
QueuePacketConn: turbotunnel.NewQueuePacketConn(clientID, 0),
}
go func() {
err := c.recvLoop(transport)
if err != nil {
log.Printf("recvLoop: %v", err)
}
}()
go func() {
err := c.sendLoop(transport, addr)
if err != nil {
log.Printf("sendLoop: %v", err)
}
}()
return c
}
// dnsResponsePayload extracts the downstream payload of a DNS response, encoded
// into the RDATA of a TXT RR. It returns nil if the message doesn't pass format
// checks, or if the name in its Question entry is not a subdomain of domain.
func dnsResponsePayload(resp *dns.Message, domain dns.Name) []byte {
if resp.Flags&0x8000 != 0x8000 {
// QR != 1, this is not a response.
return nil
}
if resp.Flags&0x000f != dns.RcodeNoError {
return nil
}
if len(resp.Answer) != 1 {
return nil
}
answer := resp.Answer[0]
_, ok := answer.Name.TrimSuffix(domain)
if !ok {
// Not the name we are expecting.
return nil
}
if answer.Type != dns.RRTypeTXT {
// We only support TYPE == TXT.
return nil
}
payload, err := dns.DecodeRDataTXT(answer.Data)
if err != nil {
return nil
}
return payload
}
// nextPacket reads the next length-prefixed packet from r. It returns a nil
// error only when a complete packet was read. It returns io.EOF only when there
// were 0 bytes remaining to read from r. It returns io.ErrUnexpectedEOF when
// EOF occurs in the middle of an encoded packet.
func nextPacket(r *bytes.Reader) ([]byte, error) {
for {
var n uint16
err := binary.Read(r, binary.BigEndian, &n)
if err != nil {
// We may return a real io.EOF only here.
return nil, err
}
p := make([]byte, n)
_, err = io.ReadFull(r, p)
// Here we must change io.EOF to io.ErrUnexpectedEOF.
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
return p, err
}
}
// recvLoop repeatedly calls transport.ReadFrom to receive a DNS message,
// extracts its payload and breaks it into packets, and stores the packets in a
// queue to be returned from a future call to c.ReadFrom.
//
// Whenever we receive a DNS response containing at least one data packet, we
// send on c.pollChan to permit sendLoop to send an immediate polling queries.
// KCP itself will also send an ACK packet for incoming data, which is
// effectively a second poll. Therefore, each time we receive data, we send up
// to 2 polling queries (or 1 + f polling queries, if KCP only ACKs an f
// fraction of incoming data). We say "up to" because sendLoop will discard an
// empty polling query if it has an organic non-empty packet to send (this goes
// also for KCP's organic ACK packets).
//
// The intuition behind polling immediately after receiving is that if server
// has just had something to send, it may have more to send, and in order for
// the server to send anything, we must give it a query to respond to. The
// intuition behind polling *2 times* (or 1 + f times) is similar to TCP slow
// start: we want to maintain some number of queries "in flight", and the faster
// the server is sending, the higher that number should be. If we polled only
// once for each received packet, we would tend to have only one query in flight
// at a time, ping-pong style. The first polling query replaces the in-flight
// query that has just finished its duty in returning data to us; the second
// grows the effective in-flight window proportional to the rate at which
// data-carrying responses are being received. Compare to Eq. (2) of
// https://tools.ietf.org/html/rfc5681#section-3.1. The differences are that we
// count messages, not bytes, and we don't maintain an explicit window. If a
// response comes back without data, or if a query or response is dropped by the
// network, then we don't poll again, which decreases the effective in-flight
// window.
func (c *DNSPacketConn) recvLoop(transport net.PacketConn) error {
for {
var buf [4096]byte
n, addr, err := transport.ReadFrom(buf[:])
if err != nil {
if err, ok := err.(net.Error); ok && err.Temporary() {
log.Printf("ReadFrom temporary error: %v", err)
continue
}
return err
}
// Got a response. Try to parse it as a DNS message.
resp, err := dns.MessageFromWireFormat(buf[:n])
if err != nil {
log.Printf("MessageFromWireFormat: %v", err)
continue
}
payload := dnsResponsePayload(&resp, c.domain)
// Pull out the packets contained in the payload.
r := bytes.NewReader(payload)
any := false
for {
p, err := nextPacket(r)
if err != nil {
break
}
any = true
c.QueuePacketConn.QueueIncoming(p, addr)
}
// If the payload contained one or more packets, permit sendLoop
// to poll immediately. ACKs on received data will effectively
// serve as another stream of polls whose rate is proportional
// to the rate of incoming packets.
if any {
select {
case c.pollChan <- struct{}{}:
default:
}
}
}
}
// chunks breaks p into non-empty subslices of at most n bytes, greedily so that
// only final subslice has length < n.
func chunks(p []byte, n int) [][]byte {
var result [][]byte
for len(p) > 0 {
sz := len(p)
if sz > n {
sz = n
}
result = append(result, p[:sz])
p = p[sz:]
}
return result
}
// send sends p as a single packet encoded into a DNS query, using
// transport.WriteTo(query, addr). The length of p must be less than 224 bytes.
//
// Here is an example of how a packet is encoded into a DNS name, using
//
// p = "supercalifragilisticexpialidocious"
// c.clientID = "CLIENTID"
// domain = "t.example.com"
//
// as the input.
//
// 0. Start with the raw packet contents.
//
// supercalifragilisticexpialidocious
//
// 1. Length-prefix the packet and add random padding. A length prefix L < 0xe0
// means a data packet of L bytes. A length prefix L ≥ 0xe0 means padding
// of L 0xe0 bytes (not counting the length of the length prefix itself).
//
// \xe3\xd9\xa3\x15\x22supercalifragilisticexpialidocious
//
// 2. Prefix the ClientID.
//
// CLIENTID\xe3\xd9\xa3\x15\x22supercalifragilisticexpialidocious
//
// 3. Base32-encode, without padding and in lower case.
//
// ingesrkokreujy6zumkse43vobsxey3bnruwm4tbm5uwy2ltoruwgzlyobuwc3djmrxwg2lpovzq
//
// 4. Break into labels of at most 63 octets.
//
// ingesrkokreujy6zumkse43vobsxey3bnruwm4tbm5uwy2ltoruwgzlyobuwc3d.jmrxwg2lpovzq
//
// 5. Append the domain.
//
// ingesrkokreujy6zumkse43vobsxey3bnruwm4tbm5uwy2ltoruwgzlyobuwc3d.jmrxwg2lpovzq.t.example.com
func (c *DNSPacketConn) send(transport net.PacketConn, p []byte, addr net.Addr) error {
var decoded []byte
{
if len(p) >= 224 {
return fmt.Errorf("too long")
}
var buf bytes.Buffer
// ClientID
buf.Write(c.clientID[:])
n := numPadding
if len(p) == 0 {
n = numPaddingForPoll
}
// Padding / cache inhibition
buf.WriteByte(byte(224 + n))
io.CopyN(&buf, rand.Reader, int64(n))
// Packet contents
if len(p) > 0 {
buf.WriteByte(byte(len(p)))
buf.Write(p)
}
decoded = buf.Bytes()
}
encoded := make([]byte, base32Encoding.EncodedLen(len(decoded)))
base32Encoding.Encode(encoded, decoded)
encoded = bytes.ToLower(encoded)
labels := chunks(encoded, 63)
labels = append(labels, c.domain...)
name, err := dns.NewName(labels)
if err != nil {
return err
}
var id uint16
binary.Read(rand.Reader, binary.BigEndian, &id)
query := &dns.Message{
ID: id,
Flags: 0x0100, // QR = 0, RD = 1
Question: []dns.Question{
{
Name: name,
Type: dns.RRTypeTXT,
Class: dns.ClassIN,
},
},
// EDNS(0)
Additional: []dns.RR{
{
Name: dns.Name{},
Type: dns.RRTypeOPT,
Class: 4096, // requester's UDP payload size
TTL: 0, // extended RCODE and flags
Data: []byte{},
},
},
}
buf, err := query.WireFormat()
if err != nil {
return err
}
_, err = transport.WriteTo(buf, addr)
return err
}
// sendLoop takes packets that have been written using c.WriteTo, and sends them
// on the network using send. It also does polling with empty packets when
// requested by pollChan or after a timeout.
func (c *DNSPacketConn) sendLoop(transport net.PacketConn, addr net.Addr) error {
pollDelay := initPollDelay
pollTimer := time.NewTimer(pollDelay)
for {
var p []byte
outgoing := c.QueuePacketConn.OutgoingQueue(addr)
pollTimerExpired := false
// Prioritize sending an actual data packet from outgoing. Only
// consider a poll when outgoing is empty.
select {
case p = <-outgoing:
default:
select {
case p = <-outgoing:
case <-c.pollChan:
case <-pollTimer.C:
pollTimerExpired = true
}
}
if len(p) > 0 {
// A data-carrying packet displaces one pending poll
// opportunity, if any.
select {
case <-c.pollChan:
default:
}
}
if pollTimerExpired {
// We're polling because it's been a while since we last
// polled. Increase the poll delay.
pollDelay = time.Duration(float64(pollDelay) * pollDelayMultiplier)
if pollDelay > maxPollDelay {
pollDelay = maxPollDelay
}
} else {
// We're sending an actual data packet, or we're polling
// in response to a received packet. Reset the poll
// delay to initial.
if !pollTimer.Stop() {
<-pollTimer.C
}
pollDelay = initPollDelay
}
pollTimer.Reset(pollDelay)
// Unlike in the server, in the client we assume that because
// the data capacity of queries is so limited, it's not worth
// trying to send more than one packet per query.
err := c.send(transport, p, addr)
if err != nil {
log.Printf("send: %v", err)
continue
}
}
}

View File

@@ -0,0 +1,176 @@
package dnsttclient
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"strconv"
"sync"
"time"
"socksrevivepc/internal/dnsttcore/turbotunnel"
)
// A default Retry-After delay to use when there is no explicit Retry-After
// header in an HTTP response.
const defaultRetryAfter = 10 * time.Second
// HTTPPacketConn is an HTTP-based transport for DNS messages, used for DNS over
// HTTPS (DoH). Its WriteTo and ReadFrom methods exchange DNS messages over HTTP
// requests and responses.
//
// HTTPPacketConn deals only with already formatted DNS messages. It does not
// handle encoding information into the messages. That is rather the
// responsibility of DNSPacketConn.
//
// https://tools.ietf.org/html/rfc8484
type HTTPPacketConn struct {
// client is the http.Client used to make requests. We use this instead
// of http.DefaultClient in order to support setting a timeout and a
// uTLS fingerprint.
client *http.Client
// urlString is the URL to which HTTP requests will be sent, for example
// "https://doh.example/dns-query".
urlString string
// notBefore, if not zero, is a time before which we may not send any
// queries; queries are buffered or dropped until that time. notBefore
// is set when we get a 429 Too Many Requests HTTP response or other
// unexpected status code that causes us to need to slow down. It is set
// according to the Retry-After header if available, otherwise it is set
// to defaultRetryAfter in the future. notBeforeLock controls access to
// notBefore.
notBefore time.Time
notBeforeLock sync.RWMutex
// QueuePacketConn is the direct receiver of ReadFrom and WriteTo calls.
// sendLoop, via send, removes messages from the outgoing queue that
// were placed there by WriteTo, and inserts messages into the incoming
// queue to be returned from ReadFrom.
*turbotunnel.QueuePacketConn
}
// NewHTTPPacketConn creates a new HTTPPacketConn configured to use the HTTP
// server at urlString as a DNS over HTTP resolver. client is the http.Client
// that will be used to make requests. urlString should include any necessary
// path components; e.g., "/dns-query". numSenders is the number of concurrent
// sender-receiver goroutines to run.
func NewHTTPPacketConn(rt http.RoundTripper, urlString string, numSenders int) (*HTTPPacketConn, error) {
c := &HTTPPacketConn{
client: &http.Client{
Transport: rt,
Timeout: 1 * time.Minute,
},
urlString: urlString,
QueuePacketConn: turbotunnel.NewQueuePacketConn(turbotunnel.DummyAddr{}, 0),
}
for i := 0; i < numSenders; i++ {
go c.sendLoop()
}
return c, nil
}
// send sends a message in an HTTP request, and queues the body HTTP response to
// be returned from a future call to ReadFrom.
func (c *HTTPPacketConn) send(p []byte) error {
req, err := http.NewRequest("POST", c.urlString, bytes.NewReader(p))
if err != nil {
return err
}
req.Header.Set("Accept", "application/dns-message")
req.Header.Set("Content-Type", "application/dns-message")
req.Header.Set("User-Agent", "") // Disable default "Go-http-client/1.1".
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
switch resp.StatusCode {
case http.StatusOK:
if ct := resp.Header.Get("Content-Type"); ct != "application/dns-message" {
return fmt.Errorf("unknown HTTP response Content-Type %+q", ct)
}
body, err := ioutil.ReadAll(io.LimitReader(resp.Body, 64000))
if err == nil {
c.QueuePacketConn.QueueIncoming(body, turbotunnel.DummyAddr{})
}
// Ignore err != nil; don't report an error if we at least
// managed to send.
default:
// We primarily are thinking of 429 Too Many Requests here, but
// any other unexpected response codes will also cause us to
// rate-limit ourselves and emit a log message.
// https://developers.google.com/speed/public-dns/docs/doh/#errors
now := time.Now()
var retryAfter time.Time
if value := resp.Header.Get("Retry-After"); value != "" {
var err error
retryAfter, err = parseRetryAfter(value, now)
if err != nil {
log.Printf("cannot parse Retry-After value %+q", value)
}
}
if retryAfter.IsZero() {
// Supply a default.
retryAfter = now.Add(defaultRetryAfter)
}
if retryAfter.Before(now) {
log.Printf("got %+q, but Retry-After is %v in the past",
resp.Status, now.Sub(retryAfter))
} else {
c.notBeforeLock.Lock()
if retryAfter.Before(c.notBefore) {
log.Printf("got %+q, but Retry-After is %v earlier than already received Retry-After",
resp.Status, c.notBefore.Sub(retryAfter))
} else {
log.Printf("got %+q; ceasing sending for %v",
resp.Status, retryAfter.Sub(now))
c.notBefore = retryAfter
}
c.notBeforeLock.Unlock()
}
}
return nil
}
// sendLoop loops over the contents of the outgoing queue and passes them to
// send. It drops packets while c.notBefore is in the future.
func (c *HTTPPacketConn) sendLoop() {
for p := range c.QueuePacketConn.OutgoingQueue(turbotunnel.DummyAddr{}) {
// Stop sending while we are rate-limiting ourselves (as a
// result of a Retry-After response header, for example).
c.notBeforeLock.RLock()
notBefore := c.notBefore
c.notBeforeLock.RUnlock()
if wait := notBefore.Sub(time.Now()); wait > 0 {
// Drop it.
continue
}
err := c.send(p)
if err != nil {
log.Printf("sendLoop: %v", err)
}
}
}
// parseRetryAfter parses the value of a Retry-After header as an absolute
// time.Time.
func parseRetryAfter(value string, now time.Time) (time.Time, error) {
// May be a date string or an integer number of seconds.
// https://tools.ietf.org/html/rfc7231#section-7.1.3
if t, err := http.ParseTime(value); err == nil {
return t, nil
}
i, err := strconv.ParseUint(value, 10, 32)
if err != nil {
return time.Time{}, err
}
return now.Add(time.Duration(i) * time.Second), nil
}

View File

@@ -0,0 +1,445 @@
// dnstt-client is the client end of a DNS tunnel.
//
// Usage:
//
// dnstt-client [-doh URL|-dot ADDR|-udp ADDR] -pubkey-file PUBKEYFILE DOMAIN LOCALADDR
//
// Examples:
//
// dnstt-client -doh https://resolver.example/dns-query -pubkey-file server.pub t.example.com 127.0.0.1:7000
// dnstt-client -dot resolver.example:853 -pubkey-file server.pub t.example.com 127.0.0.1:7000
//
// The program supports DNS over HTTPS (DoH), DNS over TLS (DoT), and UDP DNS.
// Use one of these options:
//
// -doh https://resolver.example/dns-query
// -dot resolver.example:853
// -udp resolver.example:53
//
// You can give the server's public key as a file or as a hex string. Use
// "dnstt-server -gen-key" to get the public key.
//
// -pubkey-file server.pub
// -pubkey 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff
//
// DOMAIN is the root of the DNS zone reserved for the tunnel. See README for
// instructions on setting it up.
//
// LOCALADDR is the TCP address that will listen for connections and forward
// them over the tunnel.
//
// In -doh and -dot modes, the program's TLS fingerprint is camouflaged with
// uTLS by default. The specific TLS fingerprint is selected randomly from a
// weighted distribution. You can set your own distribution (or specific single
// fingerprint) using the -utls option. The special value "none" disables uTLS.
//
// -utls '3*Firefox,2*Chrome,1*iOS'
// -utls Firefox
// -utls none
package dnsttclient
import (
"context"
"crypto/tls"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"strings"
"sync"
"time"
utls "github.com/refraction-networking/utls"
"github.com/xtaci/kcp-go/v5"
"github.com/xtaci/smux"
"socksrevivepc/internal/dnsttcore/dns"
"socksrevivepc/internal/dnsttcore/noise"
"socksrevivepc/internal/dnsttcore/turbotunnel"
)
// smux streams will be closed after this much time without receiving data.
const idleTimeout = 2 * time.Minute
// dnsNameCapacity returns the number of bytes remaining for encoded data after
// including domain in a DNS name.
func dnsNameCapacity(domain dns.Name) int {
// Names must be 255 octets or shorter in total length.
// https://tools.ietf.org/html/rfc1035#section-2.3.4
capacity := 255
// Subtract the length of the null terminator.
capacity -= 1
for _, label := range domain {
// Subtract the length of the label and the length octet.
capacity -= len(label) + 1
}
// Each label may be up to 63 bytes long and requires 64 bytes to
// encode.
capacity = capacity * 63 / 64
// Base32 expands every 5 bytes to 8.
capacity = capacity * 5 / 8
return capacity
}
// readKeyFromFile reads a key from a named file.
func readKeyFromFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
return noise.ReadKey(f)
}
// sampleUTLSDistribution parses a weighted uTLS Client Hello ID distribution
// string of the form "3*Firefox,2*Chrome,1*iOS", matches each label to a
// utls.ClientHelloID from utlsClientHelloIDMap, and randomly samples one
// utls.ClientHelloID from the distribution.
func sampleUTLSDistribution(spec string) (*utls.ClientHelloID, error) {
weights, labels, err := parseWeightedList(spec)
if err != nil {
return nil, err
}
ids := make([]*utls.ClientHelloID, 0, len(labels))
for _, label := range labels {
var id *utls.ClientHelloID
if label == "none" {
id = nil
} else {
id = utlsLookup(label)
if id == nil {
return nil, fmt.Errorf("unknown TLS fingerprint %q", label)
}
}
ids = append(ids, id)
}
return ids[sampleWeighted(weights)], nil
}
func handle(local *net.TCPConn, sess *smux.Session, conv uint32) error {
stream, err := sess.OpenStream()
if err != nil {
return fmt.Errorf("session %08x opening stream: %v", conv, err)
}
defer func() {
log.Printf("end stream %08x:%d", conv, stream.ID())
stream.Close()
}()
log.Printf("begin stream %08x:%d", conv, stream.ID())
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
_, err := io.Copy(stream, local)
if err == io.EOF {
// smux Stream.Write may return io.EOF.
err = nil
}
if err != nil && !errors.Is(err, io.ErrClosedPipe) {
log.Printf("stream %08x:%d copy stream←local: %v", conv, stream.ID(), err)
}
local.CloseRead()
stream.Close()
}()
go func() {
defer wg.Done()
_, err := io.Copy(local, stream)
if err == io.EOF {
// smux Stream.WriteTo may return io.EOF.
err = nil
}
if err != nil && !errors.Is(err, io.ErrClosedPipe) {
log.Printf("stream %08x:%d copy local←stream: %v", conv, stream.ID(), err)
}
local.CloseWrite()
}()
wg.Wait()
return err
}
func run(pubkey []byte, domain dns.Name, localAddr *net.TCPAddr, remoteAddr net.Addr, pconn net.PacketConn) error {
defer pconn.Close()
ln, err := net.ListenTCP("tcp", localAddr)
if err != nil {
return fmt.Errorf("opening local listener: %v", err)
}
defer ln.Close()
mtu := dnsNameCapacity(domain) - 8 - 1 - numPadding - 1 // clientid + padding length prefix + padding + data length prefix
if mtu < 80 {
return fmt.Errorf("domain %s leaves only %d bytes for payload", domain, mtu)
}
log.Printf("effective MTU %d", mtu)
// Open a KCP conn on the PacketConn.
conn, err := kcp.NewConn2(remoteAddr, nil, 0, 0, pconn)
if err != nil {
return fmt.Errorf("opening KCP conn: %v", err)
}
defer func() {
log.Printf("end session %08x", conn.GetConv())
conn.Close()
}()
log.Printf("begin session %08x", conn.GetConv())
// Permit coalescing the payloads of consecutive sends.
conn.SetStreamMode(true)
// Disable the dynamic congestion window (limit only by the maximum of
// local and remote static windows).
conn.SetNoDelay(
0, // default nodelay
0, // default interval
0, // default resend
1, // nc=1 => congestion window off
)
conn.SetWindowSize(turbotunnel.QueueSize/2, turbotunnel.QueueSize/2)
if rc := conn.SetMtu(mtu); !rc {
panic(rc)
}
// Put a Noise channel on top of the KCP conn.
rw, err := noise.NewClient(conn, pubkey)
if err != nil {
return err
}
// Start a smux session on the Noise channel.
smuxConfig := smux.DefaultConfig()
smuxConfig.Version = 2
smuxConfig.KeepAliveTimeout = idleTimeout
smuxConfig.MaxStreamBuffer = 1 * 1024 * 1024 // default is 65536
sess, err := smux.Client(rw, smuxConfig)
if err != nil {
return fmt.Errorf("opening smux session: %v", err)
}
defer sess.Close()
for {
local, err := ln.Accept()
if err != nil {
if err, ok := err.(net.Error); ok && err.Temporary() {
continue
}
return err
}
go func() {
defer local.Close()
err := handle(local.(*net.TCPConn), sess, conn.GetConv())
if err != nil {
log.Printf("handle: %v", err)
}
}()
}
}
func main() {
var dohURL string
var dotAddr string
var pubkeyFilename string
var pubkeyString string
var udpAddr string
var utlsDistribution string
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), `Usage:
%[1]s [-doh URL|-dot ADDR|-udp ADDR] -pubkey-file PUBKEYFILE DOMAIN LOCALADDR
Examples:
%[1]s -doh https://resolver.example/dns-query -pubkey-file server.pub t.example.com 127.0.0.1:7000
%[1]s -dot resolver.example:853 -pubkey-file server.pub t.example.com 127.0.0.1:7000
`, os.Args[0])
flag.PrintDefaults()
labels := make([]string, 0, len(utlsClientHelloIDMap))
labels = append(labels, "none")
for _, entry := range utlsClientHelloIDMap {
labels = append(labels, entry.Label)
}
fmt.Fprintf(flag.CommandLine.Output(), `
Known TLS fingerprints for -utls are:
`)
i := 0
for i < len(labels) {
var line strings.Builder
fmt.Fprintf(&line, " %s", labels[i])
w := 2 + len(labels[i])
i++
for i < len(labels) && w+1+len(labels[i]) <= 72 {
fmt.Fprintf(&line, " %s", labels[i])
w += 1 + len(labels[i])
i++
}
fmt.Fprintln(flag.CommandLine.Output(), line.String())
}
}
flag.StringVar(&dohURL, "doh", "", "URL of DoH resolver")
flag.StringVar(&dotAddr, "dot", "", "address of DoT resolver")
flag.StringVar(&pubkeyString, "pubkey", "", fmt.Sprintf("server public key (%d hex digits)", noise.KeyLen*2))
flag.StringVar(&pubkeyFilename, "pubkey-file", "", "read server public key from file")
flag.StringVar(&udpAddr, "udp", "", "address of UDP DNS resolver")
flag.StringVar(&utlsDistribution, "utls",
"4*random,3*Firefox_120,1*Firefox_105,3*Chrome_120,1*Chrome_102,1*iOS_14,1*iOS_13",
"choose TLS fingerprint from weighted distribution")
flag.Parse()
log.SetFlags(log.LstdFlags | log.LUTC)
if flag.NArg() != 2 {
flag.Usage()
os.Exit(1)
}
domain, err := dns.ParseName(flag.Arg(0))
if err != nil {
fmt.Fprintf(os.Stderr, "invalid domain %+q: %v\n", flag.Arg(0), err)
os.Exit(1)
}
localAddr, err := net.ResolveTCPAddr("tcp", flag.Arg(1))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
var pubkey []byte
if pubkeyFilename != "" && pubkeyString != "" {
fmt.Fprintf(os.Stderr, "only one of -pubkey and -pubkey-file may be used\n")
os.Exit(1)
} else if pubkeyFilename != "" {
var err error
pubkey, err = readKeyFromFile(pubkeyFilename)
if err != nil {
fmt.Fprintf(os.Stderr, "cannot read pubkey from file: %v\n", err)
os.Exit(1)
}
} else if pubkeyString != "" {
var err error
pubkey, err = noise.DecodeKey(pubkeyString)
if err != nil {
fmt.Fprintf(os.Stderr, "pubkey format error: %v\n", err)
os.Exit(1)
}
}
if len(pubkey) == 0 {
fmt.Fprintf(os.Stderr, "the -pubkey or -pubkey-file option is required\n")
os.Exit(1)
}
tlsConfig, err := loadTLSConfig()
if err != nil {
fmt.Fprintf(os.Stderr, "TLS config error: %v\n", err)
os.Exit(1)
}
utlsClientHelloID, err := sampleUTLSDistribution(utlsDistribution)
if err != nil {
fmt.Fprintf(os.Stderr, "parsing -utls: %v\n", err)
os.Exit(1)
}
if utlsClientHelloID != nil {
log.Printf("uTLS fingerprint %s %s", utlsClientHelloID.Client, utlsClientHelloID.Version)
}
// Iterate over the remote resolver address options and select one and
// only one.
var remoteAddr net.Addr
var pconn net.PacketConn
for _, opt := range []struct {
s string
f func(string) (net.Addr, net.PacketConn, error)
}{
// -doh
{dohURL, func(s string) (net.Addr, net.PacketConn, error) {
addr := turbotunnel.DummyAddr{}
var rt http.RoundTripper
if utlsClientHelloID == nil {
transport := http.DefaultTransport.(*http.Transport).Clone()
// Disable DefaultTransport's default Proxy =
// ProxyFromEnvironment setting, for conformity
// with utlsRoundTripper and with DoT mode,
// which do not take a proxy from the
// environment.
transport.Proxy = nil
transport.TLSClientConfig = tlsConfig.Clone()
baseDialContext := transport.DialContext
if baseDialContext == nil {
baseDialContext = (&net.Dialer{}).DialContext
}
transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
if network == "tcp" {
resolvedAddr, _, err := resolveAddrIPv4(ctx, addr)
if err != nil {
return nil, err
}
addr = resolvedAddr
network = "tcp4"
}
return baseDialContext(ctx, network, addr)
}
rt = transport
} else {
utlsConfig := &utls.Config{
RootCAs: tlsConfig.RootCAs,
MinVersion: tlsConfig.MinVersion,
}
rt = NewUTLSRoundTripper(utlsConfig, utlsClientHelloID, true)
}
pconn, err := NewHTTPPacketConn(rt, dohURL, 32)
return addr, pconn, err
}},
// -dot
{dotAddr, func(s string) (net.Addr, net.PacketConn, error) {
addr := turbotunnel.DummyAddr{}
var dialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error)
if utlsClientHelloID == nil {
dialTLSContext = (&tls.Dialer{Config: tlsConfig.Clone()}).DialContext
} else {
utlsConfig := &utls.Config{
RootCAs: tlsConfig.RootCAs,
MinVersion: tlsConfig.MinVersion,
}
dialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
return utlsDialContext(ctx, network, addr, utlsConfig, utlsClientHelloID)
}
}
pconn, err := NewTLSPacketConn(dotAddr, dialTLSContext)
return addr, pconn, err
}},
// -udp
{udpAddr, func(s string) (net.Addr, net.PacketConn, error) {
addr, err := net.ResolveUDPAddr("udp", s)
if err != nil {
return nil, nil, err
}
pconn, err := net.ListenUDP("udp", nil)
return addr, pconn, err
}},
} {
if opt.s == "" {
continue
}
if pconn != nil {
fmt.Fprintf(os.Stderr, "only one of -doh, -dot, and -udp may be given\n")
os.Exit(1)
}
var err error
remoteAddr, pconn, err = opt.f(opt.s)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
if pconn == nil {
fmt.Fprintf(os.Stderr, "one of -doh, -dot, or -udp is required\n")
os.Exit(1)
}
pconn = NewDNSPacketConn(pconn, remoteAddr, domain)
err = run(pubkey, domain, localAddr, remoteAddr, pconn)
if err != nil {
log.Fatal(err)
}
}

134
internal/dnsttclient/tls.go Normal file
View File

@@ -0,0 +1,134 @@
package dnsttclient
import (
"bufio"
"context"
"encoding/binary"
"io"
"log"
"net"
"sync"
"time"
"socksrevivepc/internal/dnsttcore/turbotunnel"
)
const dialTimeout = 30 * time.Second
// TLSPacketConn is a TLS- and TCP-based transport for DNS messages, used for
// DNS over TLS (DoT). Its WriteTo and ReadFrom methods exchange DNS messages
// over a TLS channel, prefixing each message with a two-octet length field as
// in DNS over TCP.
//
// TLSPacketConn deals only with already formatted DNS messages. It does not
// handle encoding information into the messages. That is rather the
// responsibility of DNSPacketConn.
//
// https://tools.ietf.org/html/rfc7858
type TLSPacketConn struct {
// QueuePacketConn is the direct receiver of ReadFrom and WriteTo calls.
// recvLoop and sendLoop take the messages out of the receive and send
// queues and actually put them on the network.
*turbotunnel.QueuePacketConn
}
// NewTLSPacketConn creates a new TLSPacketConn configured to use the TLS
// server at addr as a DNS over TLS resolver. It maintains a TLS connection to
// the resolver, reconnecting as necessary. It closes the connection if any
// reconnection attempt fails.
func NewTLSPacketConn(addr string, dialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error)) (*TLSPacketConn, error) {
dial := func() (net.Conn, error) {
ctx, cancel := context.WithTimeout(context.Background(), dialTimeout)
defer cancel()
return dialTLSContext(ctx, "tcp", addr)
}
// We maintain one TLS connection at a time, redialing it whenever it
// becomes disconnected. We do the first dial here, outside the
// goroutine, so that any immediate and permanent connection errors are
// reported directly to the caller of NewTLSPacketConn.
conn, err := dial()
if err != nil {
return nil, err
}
c := &TLSPacketConn{
QueuePacketConn: turbotunnel.NewQueuePacketConn(turbotunnel.DummyAddr{}, 0),
}
go func() {
defer c.Close()
for {
var wg sync.WaitGroup
wg.Add(2)
go func() {
err := c.recvLoop(conn)
if err != nil {
log.Printf("recvLoop: %v", err)
}
wg.Done()
}()
go func() {
err := c.sendLoop(conn)
if err != nil {
log.Printf("sendLoop: %v", err)
}
wg.Done()
}()
wg.Wait()
conn.Close()
// Whenever the TLS connection dies, redial a new one.
conn, err = dial()
if err != nil {
log.Printf("dial tls: %v", err)
break
}
}
}()
return c, nil
}
// recvLoop reads length-prefixed messages from conn and passes them to the
// incoming queue.
func (c *TLSPacketConn) recvLoop(conn net.Conn) error {
br := bufio.NewReader(conn)
for {
var length uint16
err := binary.Read(br, binary.BigEndian, &length)
if err != nil {
if err == io.EOF {
err = nil
}
return err
}
p := make([]byte, int(length))
_, err = io.ReadFull(br, p)
if err != nil {
return err
}
c.QueuePacketConn.QueueIncoming(p, turbotunnel.DummyAddr{})
}
}
// sendLoop reads messages from the outgoing queue and writes them,
// length-prefixed, to conn.
func (c *TLSPacketConn) sendLoop(conn net.Conn) error {
bw := bufio.NewWriter(conn)
for p := range c.QueuePacketConn.OutgoingQueue(turbotunnel.DummyAddr{}) {
length := uint16(len(p))
if int(length) != len(p) {
panic(len(p))
}
err := binary.Write(bw, binary.BigEndian, &length)
if err != nil {
return err
}
_, err = bw.Write(p)
if err != nil {
return err
}
err = bw.Flush()
if err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,337 @@
package dnsttclient
// Support code for TLS camouflage using uTLS.
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"
utls "github.com/refraction-networking/utls"
"golang.org/x/net/http2"
)
// utlsClientHelloIDMap is a correspondence between human-readable labels and
// supported utls.ClientHelloIDs.
var utlsClientHelloIDMap = []struct {
Label string
ID *utls.ClientHelloID
}{
{"random", &utls.HelloRandomizedALPN},
{"Firefox", &utls.HelloFirefox_Auto},
{"Firefox_55", &utls.HelloFirefox_55},
{"Firefox_56", &utls.HelloFirefox_56},
{"Firefox_63", &utls.HelloFirefox_63},
{"Firefox_65", &utls.HelloFirefox_65},
{"Firefox_99", &utls.HelloFirefox_99},
{"Firefox_102", &utls.HelloFirefox_102},
{"Firefox_105", &utls.HelloFirefox_105},
{"Firefox_120", &utls.HelloFirefox_120},
{"Chrome", &utls.HelloChrome_Auto},
{"Chrome_58", &utls.HelloChrome_58},
{"Chrome_62", &utls.HelloChrome_62},
{"Chrome_70", &utls.HelloChrome_70},
{"Chrome_72", &utls.HelloChrome_72},
{"Chrome_83", &utls.HelloChrome_83},
{"Chrome_87", &utls.HelloChrome_87},
{"Chrome_96", &utls.HelloChrome_96},
{"Chrome_100", &utls.HelloChrome_100},
{"Chrome_102", &utls.HelloChrome_102},
{"Chrome_120", &utls.HelloChrome_120},
{"iOS", &utls.HelloIOS_Auto},
{"iOS_11_1", &utls.HelloIOS_11_1},
{"iOS_12_1", &utls.HelloIOS_12_1},
{"iOS_13", &utls.HelloIOS_13},
{"iOS_14", &utls.HelloIOS_14},
}
// utlsLookup returns a *utls.ClientHelloID from utlsClientHelloIDMap by a
// case-insensitive label match, or nil if there is no match.
func utlsLookup(label string) *utls.ClientHelloID {
for _, entry := range utlsClientHelloIDMap {
if strings.ToLower(label) == strings.ToLower(entry.Label) {
return entry.ID
}
}
return nil
}
var bootstrapResolverAddrs = []string{
"8.8.8.8:53",
"1.1.1.1:53",
"8.8.4.4:53",
"1.0.0.1:53",
}
func lookupHostIPv4(ctx context.Context, host string) ([]net.IP, error) {
var lastErr error
for _, resolverAddr := range bootstrapResolverAddrs {
resolverAddr := resolverAddr
r := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := &net.Dialer{Timeout: 5 * time.Second}
return d.DialContext(ctx, "udp4", resolverAddr)
},
}
ips, err := r.LookupIP(ctx, "ip4", host)
if err == nil && len(ips) > 0 {
return ips, nil
}
if err != nil {
lastErr = err
}
}
if lastErr == nil {
lastErr = fmt.Errorf("no A records returned")
}
return nil, fmt.Errorf("bootstrap IPv4 lookup failed for %s: %w", host, lastErr)
}
func resolveAddrIPv4(ctx context.Context, addr string) (string, string, error) {
host, port, err := net.SplitHostPort(addr)
if err != nil {
return "", "", err
}
if ip := net.ParseIP(host); ip != nil {
ip4 := ip.To4()
if ip4 == nil {
return "", "", fmt.Errorf("IPv6 address %s cannot be used for forced IPv4 dial", host)
}
return net.JoinHostPort(ip4.String(), port), host, nil
}
ips, err := lookupHostIPv4(ctx, host)
if err != nil {
return "", host, err
}
return net.JoinHostPort(ips[0].String(), port), host, nil
}
// utlsDialContext connects to the given network address and initiates a TLS
// handshake with the provided ClientHelloID, and returns the resulting TLS
// connection.
func utlsDialContext(ctx context.Context, network, addr string, config *utls.Config, id *utls.ClientHelloID) (*utls.UConn, error) {
return utlsDialContextWithOptions(ctx, network, addr, config, id, false)
}
func utlsDialContextWithOptions(ctx context.Context, network, addr string, config *utls.Config, id *utls.ClientHelloID, forceIPv4 bool) (*utls.UConn, error) {
originalHost, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
if forceIPv4 {
addr, _, err = resolveAddrIPv4(ctx, addr)
if err != nil {
return nil, err
}
}
// Set the SNI from the original addr host, if not already set.
if config == nil {
config = &utls.Config{}
}
if config.ServerName == "" {
config = config.Clone()
config.ServerName = originalHost
}
dialer := &net.Dialer{}
if forceIPv4 && network == "tcp" {
network = "tcp4"
}
conn, err := dialer.DialContext(ctx, network, addr)
if err != nil {
return nil, err
}
uconn := utls.UClient(conn, config, *id)
// We must call Handshake before returning, or else the UConn may not
// actually use the selected ClientHelloID. It depends on whether a Read
// or a Write happens first. If a Read happens first, the connection
// will use the normal crypto/tls fingerprint. If a Write happens first,
// it will use the selected fingerprint as expected.
// https://github.com/refraction-networking/utls/issues/75
err = uconn.Handshake()
if err != nil {
uconn.Close()
return nil, err
}
return uconn, nil
}
// The goal of utlsRoundTripper is: provide an http.RoundTripper abstraction
// that retains the features of http.Transport (e.g., persistent connections and
// HTTP/2 support), while making TLS connections using uTLS in place of
// crypto/tls. The challenge is: while http.Transport provides a DialTLSContext
// hook, setting it to non-nil disables automatic HTTP/2 support in the client.
// Most of the uTLS fingerprints contain an ALPN extension containing "h2";
// i.e., they declare support for HTTP/2. If the server also supports HTTP/2,
// then uTLS may negotiate an HTTP/2 connection without the http.Transport
// knowing it, which leads to an HTTP/1.1 client speaking to an HTTP/2 server, a
// protocol error.
//
// The code here uses an idea adapted from meek_lite in obfs4proxy:
// https://gitlab.com/yawning/obfs4/commit/4d453dab2120082b00bf6e63ab4aaeeda6b8d8a3
// Instead of setting DialTLSContext on an http.Transport and exposing it
// directly, we expose a wrapper type, utlsRoundTripper, which contains within
// it either an http.Transport or an http2.Transport. The first time a caller
// calls RoundTrip on the wrapper, we initiate a uTLS connection
// (bootstrapConn), then peek at the ALPN-negotiated protocol: if "h2", create
// an internal http2.Transport; otherwise, create an internal http.Transport. In
// either case, set DialTLSContext (or DialTLS for http2.Transport) on the
// created Transport to a function that dials using uTLS. As a special case, the
// first time the DialTLS callback is called, it reuses bootstrapConn (the one
// made to peek at the ALPN), rather than make a new connection.
//
// Subsequent calls to RoundTripper on the wrapper just pass the requests though
// the previously created http.Transport or http2.Transport. We assume that in
// future RoundTrips, the ALPN-negotiated protocol will remain the same as it
// was in the initial RoundTrip. At this point it is the http.Transport or
// http2.Transport calling DialTLSContext, not us, so we cannot dynamically swap
// the underlying transport based on the ALPN.
//
// https://bugs.torproject.org/tpo/anti-censorship/pluggable-transports/meek/29077
// https://github.com/refraction-networking/utls/issues/16
// utlsRoundTripper is an http.RoundTripper that uses uTLS (with a specified
// ClientHelloID) to make TLS connections.
//
// Can only be reused among servers which negotiate the same ALPN.
type utlsRoundTripper struct {
clientHelloID *utls.ClientHelloID
config *utls.Config
forceIPv4 bool
innerLock sync.Mutex
inner http.RoundTripper
}
// NewUTLSRoundTripper creates a utlsRoundTripper with the given TLS
// configuration and ClientHelloID.
func NewUTLSRoundTripper(config *utls.Config, id *utls.ClientHelloID, forceIPv4 bool) *utlsRoundTripper {
return &utlsRoundTripper{
clientHelloID: id,
config: config,
forceIPv4: forceIPv4,
// inner will be set in the first call to RoundTrip.
}
}
func (rt *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
switch req.URL.Scheme {
case "http":
// If http, don't invoke uTLS; just pass it to an ordinary http.Transport.
return http.DefaultTransport.RoundTrip(req)
case "https":
default:
return nil, fmt.Errorf("unsupported URL scheme %q", req.URL.Scheme)
}
var err error
rt.innerLock.Lock()
if rt.inner == nil {
// On the first call, make an http.Transport or http2.Transport
// as appropriate.
rt.inner, err = makeRoundTripper(req, rt.config, rt.clientHelloID, rt.forceIPv4)
}
rt.innerLock.Unlock()
if err != nil {
return nil, err
}
// Forward the request to the inner http.Transport or http2.Transport.
return rt.inner.RoundTrip(req)
}
// makeRoundTripper makes a bootstrap TLS configuration using the given TLS
// configuration and ClientHelloID, and creates an http.Transport or
// http2.Transport, depending on the negotated ALPN. The Transport is set up to
// make future TLS connections using the same TLS configuration and
// ClientHelloID.
func makeRoundTripper(req *http.Request, config *utls.Config, id *utls.ClientHelloID, forceIPv4 bool) (http.RoundTripper, error) {
addr, err := addrForDial(req.URL)
if err != nil {
return nil, err
}
bootstrapConn, err := utlsDialContextWithOptions(req.Context(), "tcp", addr, config, id, forceIPv4)
if err != nil {
return nil, err
}
// Peek at the ALPN-negotiated protocol.
protocol := bootstrapConn.ConnectionState().NegotiatedProtocol
// Protects bootstrapConn.
var lock sync.Mutex
// This is the callback for future dials done by the inner
// http.Transport or http2.Transport.
dialTLSContext := func(ctx context.Context, network, addr string) (net.Conn, error) {
lock.Lock()
defer lock.Unlock()
// On the first dial, reuse bootstrapConn.
if bootstrapConn != nil {
uconn := bootstrapConn
bootstrapConn = nil
return uconn, nil
}
// Later dials make a new connection.
uconn, err := utlsDialContextWithOptions(ctx, "tcp", addr, config, id, forceIPv4)
if err != nil {
return nil, err
}
if uconn.ConnectionState().NegotiatedProtocol != protocol {
return nil, fmt.Errorf("unexpected switch from ALPN %q to %q",
protocol, uconn.ConnectionState().NegotiatedProtocol)
}
return uconn, nil
}
// Construct an http.Transport or http2.Transport depending on ALPN.
switch protocol {
case http2.NextProtoTLS:
// Unfortunately http2.Transport does not expose the same
// configuration options as http.Transport with regard to
// timeouts, etc., so we are at the mercy of the defaults.
// https://github.com/golang/go/issues/16581
return &http2.Transport{
DialTLS: func(network, addr string, _ *tls.Config) (net.Conn, error) {
// Ignore the *tls.Config parameter; use our
// static config instead.
return dialTLSContext(context.Background(), network, addr)
},
}, nil
default:
// With http.Transport, copy important default fields from
// http.DefaultTransport, such as TLSHandshakeTimeout and
// IdleConnTimeout, before overriding DialTLSContext.
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.DialTLSContext = dialTLSContext
return tr, nil
}
}
// addrForDial extracts a host:port address from a URL, suitable for dialing.
func addrForDial(url *url.URL) (string, error) {
host := url.Hostname()
// net/http would use golang.org/x/net/idna here, to convert a possible
// internationalized domain name to ASCII.
port := url.Port()
if port == "" {
// No port? Use the default for the scheme.
switch url.Scheme {
case "http":
port = "80"
case "https":
port = "443"
default:
return "", fmt.Errorf("unsupported URL scheme %q", url.Scheme)
}
}
return net.JoinHostPort(host, port), nil
}

View File

@@ -0,0 +1,201 @@
package dnsttclient
// Random selection from weighted distributions, and strings for specifying such
// distributions.
import (
cryptorand "crypto/rand"
"encoding/binary"
"fmt"
mathrand "math/rand"
"strconv"
"strings"
)
// parseWeightedList parses a list of text labels with optional numeric weights,
// and returns parallel slices of weights and labels. If a weight is omitted for
// a label, the weight is 1.
//
// An example weighted list string is "2*apple,orange,10*cookie". This example
// results in the slices [2, 1, 10] and ["apple", "orange", "cookie"].
// Bytes may be escaped by backslashes.
//
// list ::= entry ("," entry)*
// entry ::= (weight "*")? label
func parseWeightedList(s string) ([]uint32, []string, error) {
const (
kindEOF = iota
kindComma
kindAsterisk
kindText
kindError
)
type token struct {
Kind int
Text string
}
var i int
// nextToken incrementally consumes s and returns tokens.
nextToken := func() token {
if !(i < len(s)) {
return token{Kind: kindEOF}
}
if s[i] == ',' {
i++
return token{Kind: kindComma}
}
if s[i] == '*' {
i++
return token{Kind: kindAsterisk}
}
var text strings.Builder
for i < len(s) && s[i] != ',' && s[i] != '*' {
if s[i] == '\\' {
i++
if !(i < len(s)) {
return token{Kind: kindError, Text: fmt.Sprintf("%q at end of string", s[i])}
}
}
text.WriteByte(s[i])
i++
}
return token{Kind: kindText, Text: text.String()}
}
peekToken := func() token {
saved := i
t := nextToken()
i = saved
return t
}
const (
stateBeginEntry = iota
stateLabel
stateEndEntry
stateDone
stateUnexpected
)
var weights []uint32
var labels []string
var weightString, label string
var t token
for state := stateBeginEntry; state != stateDone; {
switch state {
// Beginning of a new entry (at the beginning of the input or
// after a comma).
case stateBeginEntry:
t = nextToken()
switch t.Kind {
case kindText:
// If the next token is an asterisk, this text
// represents a weight; otherwise it represents
// a label (with a weight of "1").
switch peekToken().Kind {
case kindAsterisk:
nextToken() // Consume the asterisk token.
weightString = t.Text
state = stateLabel
default:
weightString = "1"
label = t.Text
state = stateEndEntry
}
default:
state = stateUnexpected
}
// weightString is assigned and we have seen an asterisk, now
// expect a text label.
case stateLabel:
t = nextToken()
switch t.Kind {
case kindText:
label = t.Text
state = stateEndEntry
default:
state = stateUnexpected
}
// weightString and label are assigned, now emit the entry and
// expect a comma or EOF.
case stateEndEntry:
w, err := strconv.ParseUint(weightString, 10, 32)
if err != nil {
return nil, nil, err
}
weights = append(weights, uint32(w))
labels = append(labels, label)
t = nextToken()
switch t.Kind {
case kindEOF:
state = stateDone
case kindComma:
state = stateBeginEntry
default:
state = stateUnexpected
}
case stateUnexpected:
if t.Kind == kindError {
return nil, nil, fmt.Errorf("%s", t.Text)
} else {
var ttext string
switch t.Kind {
case kindEOF:
ttext = "end of string"
case kindComma:
ttext = "\",\""
case kindAsterisk:
ttext = "\"*\""
case kindText:
ttext = fmt.Sprintf("%+q", t.Text)
}
return nil, nil, fmt.Errorf("unexpected %s", ttext)
}
default:
panic(state)
}
}
return weights, labels, nil
}
// cryptoSource is a math/rand Source that reads from the crypto/rand Reader.
// The Seed method does not affect the sequence of numbers returned from the
// Int63 method.
type cryptoSource struct{}
func (s cryptoSource) Seed(_ int64) {}
func (s cryptoSource) Int63() int64 {
var n int64
err := binary.Read(cryptorand.Reader, binary.BigEndian, &n)
if err != nil {
panic(err)
}
n &= (1 << 63) - 1
return n
}
// sampleWeighted returns the index of a randomly selected element of the
// weights slice, weighted by the values stored in the slice. Panics if
// the sum of the weights is zero or does not fit in an int64.
func sampleWeighted(weights []uint32) int {
var sum int64 = 0
for _, w := range weights {
sum += int64(w)
if sum < int64(w) {
panic("weights overflow")
}
}
if sum == 0 {
panic("total weight is zero")
}
r := uint64(mathrand.New(&cryptoSource{}).Int63n(sum))
for i, w := range weights {
if r < uint64(w) {
return i
}
r -= uint64(w)
}
panic("impossible")
}