Launch
This commit is contained in:
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user