Launch
This commit is contained in:
276
internal/dnsttclient/api.go
Normal file
276
internal/dnsttclient/api.go
Normal 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)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
22
internal/dnsttclient/certpool.go
Normal file
22
internal/dnsttclient/certpool.go
Normal 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
407
internal/dnsttclient/dns.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
176
internal/dnsttclient/http.go
Normal file
176
internal/dnsttclient/http.go
Normal 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
|
||||
}
|
||||
445
internal/dnsttclient/main.go
Normal file
445
internal/dnsttclient/main.go
Normal 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
134
internal/dnsttclient/tls.go
Normal 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
|
||||
}
|
||||
337
internal/dnsttclient/utls.go
Normal file
337
internal/dnsttclient/utls.go
Normal 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
|
||||
}
|
||||
201
internal/dnsttclient/weightedlist.go
Normal file
201
internal/dnsttclient/weightedlist.go
Normal 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")
|
||||
}
|
||||
Reference in New Issue
Block a user