CHANGELOG for v1.20240513.0.
[dnstt.git] / dnstt-client / main.go
blob93e6576e08f6d39db11969871f92caea9417c19e
1 // dnstt-client is the client end of a DNS tunnel.
2 //
3 // Usage:
4 //
5 // dnstt-client [-doh URL|-dot ADDR|-udp ADDR] -pubkey-file PUBKEYFILE DOMAIN LOCALADDR
6 //
7 // Examples:
8 //
9 // dnstt-client -doh https://resolver.example/dns-query -pubkey-file server.pub t.example.com 127.0.0.1:7000
10 // dnstt-client -dot resolver.example:853 -pubkey-file server.pub t.example.com 127.0.0.1:7000
12 // The program supports DNS over HTTPS (DoH), DNS over TLS (DoT), and UDP DNS.
13 // Use one of these options:
15 // -doh https://resolver.example/dns-query
16 // -dot resolver.example:853
17 // -udp resolver.example:53
19 // You can give the server's public key as a file or as a hex string. Use
20 // "dnstt-server -gen-key" to get the public key.
22 // -pubkey-file server.pub
23 // -pubkey 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff
25 // DOMAIN is the root of the DNS zone reserved for the tunnel. See README for
26 // instructions on setting it up.
28 // LOCALADDR is the TCP address that will listen for connections and forward
29 // them over the tunnel.
31 // In -doh and -dot modes, the program's TLS fingerprint is camouflaged with
32 // uTLS by default. The specific TLS fingerprint is selected randomly from a
33 // weighted distribution. You can set your own distribution (or specific single
34 // fingerprint) using the -utls option. The special value "none" disables uTLS.
36 // -utls '3*Firefox,2*Chrome,1*iOS'
37 // -utls Firefox
38 // -utls none
39 package main
41 import (
42 "context"
43 "crypto/tls"
44 "errors"
45 "flag"
46 "fmt"
47 "io"
48 "log"
49 "net"
50 "net/http"
51 "os"
52 "strings"
53 "sync"
54 "time"
56 utls "github.com/refraction-networking/utls"
57 "github.com/xtaci/kcp-go/v5"
58 "github.com/xtaci/smux"
59 "www.bamsoftware.com/git/dnstt.git/dns"
60 "www.bamsoftware.com/git/dnstt.git/noise"
61 "www.bamsoftware.com/git/dnstt.git/turbotunnel"
64 // smux streams will be closed after this much time without receiving data.
65 const idleTimeout = 2 * time.Minute
67 // dnsNameCapacity returns the number of bytes remaining for encoded data after
68 // including domain in a DNS name.
69 func dnsNameCapacity(domain dns.Name) int {
70 // Names must be 255 octets or shorter in total length.
71 // https://tools.ietf.org/html/rfc1035#section-2.3.4
72 capacity := 255
73 // Subtract the length of the null terminator.
74 capacity -= 1
75 for _, label := range domain {
76 // Subtract the length of the label and the length octet.
77 capacity -= len(label) + 1
79 // Each label may be up to 63 bytes long and requires 64 bytes to
80 // encode.
81 capacity = capacity * 63 / 64
82 // Base32 expands every 5 bytes to 8.
83 capacity = capacity * 5 / 8
84 return capacity
87 // readKeyFromFile reads a key from a named file.
88 func readKeyFromFile(filename string) ([]byte, error) {
89 f, err := os.Open(filename)
90 if err != nil {
91 return nil, err
93 defer f.Close()
94 return noise.ReadKey(f)
97 // sampleUTLSDistribution parses a weighted uTLS Client Hello ID distribution
98 // string of the form "3*Firefox,2*Chrome,1*iOS", matches each label to a
99 // utls.ClientHelloID from utlsClientHelloIDMap, and randomly samples one
100 // utls.ClientHelloID from the distribution.
101 func sampleUTLSDistribution(spec string) (*utls.ClientHelloID, error) {
102 weights, labels, err := parseWeightedList(spec)
103 if err != nil {
104 return nil, err
106 ids := make([]*utls.ClientHelloID, 0, len(labels))
107 for _, label := range labels {
108 var id *utls.ClientHelloID
109 if label == "none" {
110 id = nil
111 } else {
112 id = utlsLookup(label)
113 if id == nil {
114 return nil, fmt.Errorf("unknown TLS fingerprint %q", label)
117 ids = append(ids, id)
119 return ids[sampleWeighted(weights)], nil
122 func handle(local *net.TCPConn, sess *smux.Session, conv uint32) error {
123 stream, err := sess.OpenStream()
124 if err != nil {
125 return fmt.Errorf("session %08x opening stream: %v", conv, err)
127 defer func() {
128 log.Printf("end stream %08x:%d", conv, stream.ID())
129 stream.Close()
131 log.Printf("begin stream %08x:%d", conv, stream.ID())
133 var wg sync.WaitGroup
134 wg.Add(2)
135 go func() {
136 defer wg.Done()
137 _, err := io.Copy(stream, local)
138 if err == io.EOF {
139 // smux Stream.Write may return io.EOF.
140 err = nil
142 if err != nil && !errors.Is(err, io.ErrClosedPipe) {
143 log.Printf("stream %08x:%d copy stream←local: %v", conv, stream.ID(), err)
145 local.CloseRead()
146 stream.Close()
148 go func() {
149 defer wg.Done()
150 _, err := io.Copy(local, stream)
151 if err == io.EOF {
152 // smux Stream.WriteTo may return io.EOF.
153 err = nil
155 if err != nil && !errors.Is(err, io.ErrClosedPipe) {
156 log.Printf("stream %08x:%d copy local←stream: %v", conv, stream.ID(), err)
158 local.CloseWrite()
160 wg.Wait()
162 return err
165 func run(pubkey []byte, domain dns.Name, localAddr *net.TCPAddr, remoteAddr net.Addr, pconn net.PacketConn) error {
166 defer pconn.Close()
168 ln, err := net.ListenTCP("tcp", localAddr)
169 if err != nil {
170 return fmt.Errorf("opening local listener: %v", err)
172 defer ln.Close()
174 mtu := dnsNameCapacity(domain) - 8 - 1 - numPadding - 1 // clientid + padding length prefix + padding + data length prefix
175 if mtu < 80 {
176 return fmt.Errorf("domain %s leaves only %d bytes for payload", domain, mtu)
178 log.Printf("effective MTU %d", mtu)
180 // Open a KCP conn on the PacketConn.
181 conn, err := kcp.NewConn2(remoteAddr, nil, 0, 0, pconn)
182 if err != nil {
183 return fmt.Errorf("opening KCP conn: %v", err)
185 defer func() {
186 log.Printf("end session %08x", conn.GetConv())
187 conn.Close()
189 log.Printf("begin session %08x", conn.GetConv())
190 // Permit coalescing the payloads of consecutive sends.
191 conn.SetStreamMode(true)
192 // Disable the dynamic congestion window (limit only by the maximum of
193 // local and remote static windows).
194 conn.SetNoDelay(
195 0, // default nodelay
196 0, // default interval
197 0, // default resend
198 1, // nc=1 => congestion window off
200 conn.SetWindowSize(turbotunnel.QueueSize/2, turbotunnel.QueueSize/2)
201 if rc := conn.SetMtu(mtu); !rc {
202 panic(rc)
205 // Put a Noise channel on top of the KCP conn.
206 rw, err := noise.NewClient(conn, pubkey)
207 if err != nil {
208 return err
211 // Start a smux session on the Noise channel.
212 smuxConfig := smux.DefaultConfig()
213 smuxConfig.Version = 2
214 smuxConfig.KeepAliveTimeout = idleTimeout
215 smuxConfig.MaxStreamBuffer = 1 * 1024 * 1024 // default is 65536
216 sess, err := smux.Client(rw, smuxConfig)
217 if err != nil {
218 return fmt.Errorf("opening smux session: %v", err)
220 defer sess.Close()
222 for {
223 local, err := ln.Accept()
224 if err != nil {
225 if err, ok := err.(net.Error); ok && err.Temporary() {
226 continue
228 return err
230 go func() {
231 defer local.Close()
232 err := handle(local.(*net.TCPConn), sess, conn.GetConv())
233 if err != nil {
234 log.Printf("handle: %v", err)
240 func main() {
241 var dohURL string
242 var dotAddr string
243 var pubkeyFilename string
244 var pubkeyString string
245 var udpAddr string
246 var utlsDistribution string
248 flag.Usage = func() {
249 fmt.Fprintf(flag.CommandLine.Output(), `Usage:
250 %[1]s [-doh URL|-dot ADDR|-udp ADDR] -pubkey-file PUBKEYFILE DOMAIN LOCALADDR
252 Examples:
253 %[1]s -doh https://resolver.example/dns-query -pubkey-file server.pub t.example.com 127.0.0.1:7000
254 %[1]s -dot resolver.example:853 -pubkey-file server.pub t.example.com 127.0.0.1:7000
256 `, os.Args[0])
257 flag.PrintDefaults()
258 labels := make([]string, 0, len(utlsClientHelloIDMap))
259 labels = append(labels, "none")
260 for _, entry := range utlsClientHelloIDMap {
261 labels = append(labels, entry.Label)
263 fmt.Fprintf(flag.CommandLine.Output(), `
264 Known TLS fingerprints for -utls are:
266 i := 0
267 for i < len(labels) {
268 var line strings.Builder
269 fmt.Fprintf(&line, " %s", labels[i])
270 w := 2 + len(labels[i])
272 for i < len(labels) && w+1+len(labels[i]) <= 72 {
273 fmt.Fprintf(&line, " %s", labels[i])
274 w += 1 + len(labels[i])
277 fmt.Fprintln(flag.CommandLine.Output(), line.String())
280 flag.StringVar(&dohURL, "doh", "", "URL of DoH resolver")
281 flag.StringVar(&dotAddr, "dot", "", "address of DoT resolver")
282 flag.StringVar(&pubkeyString, "pubkey", "", fmt.Sprintf("server public key (%d hex digits)", noise.KeyLen*2))
283 flag.StringVar(&pubkeyFilename, "pubkey-file", "", "read server public key from file")
284 flag.StringVar(&udpAddr, "udp", "", "address of UDP DNS resolver")
285 flag.StringVar(&utlsDistribution, "utls",
286 "4*random,3*Firefox_120,1*Firefox_105,3*Chrome_120,1*Chrome_102,1*iOS_14,1*iOS_13",
287 "choose TLS fingerprint from weighted distribution")
288 flag.Parse()
290 log.SetFlags(log.LstdFlags | log.LUTC)
292 if flag.NArg() != 2 {
293 flag.Usage()
294 os.Exit(1)
296 domain, err := dns.ParseName(flag.Arg(0))
297 if err != nil {
298 fmt.Fprintf(os.Stderr, "invalid domain %+q: %v\n", flag.Arg(0), err)
299 os.Exit(1)
301 localAddr, err := net.ResolveTCPAddr("tcp", flag.Arg(1))
302 if err != nil {
303 fmt.Fprintln(os.Stderr, err)
304 os.Exit(1)
307 var pubkey []byte
308 if pubkeyFilename != "" && pubkeyString != "" {
309 fmt.Fprintf(os.Stderr, "only one of -pubkey and -pubkey-file may be used\n")
310 os.Exit(1)
311 } else if pubkeyFilename != "" {
312 var err error
313 pubkey, err = readKeyFromFile(pubkeyFilename)
314 if err != nil {
315 fmt.Fprintf(os.Stderr, "cannot read pubkey from file: %v\n", err)
316 os.Exit(1)
318 } else if pubkeyString != "" {
319 var err error
320 pubkey, err = noise.DecodeKey(pubkeyString)
321 if err != nil {
322 fmt.Fprintf(os.Stderr, "pubkey format error: %v\n", err)
323 os.Exit(1)
326 if len(pubkey) == 0 {
327 fmt.Fprintf(os.Stderr, "the -pubkey or -pubkey-file option is required\n")
328 os.Exit(1)
331 utlsClientHelloID, err := sampleUTLSDistribution(utlsDistribution)
332 if err != nil {
333 fmt.Fprintf(os.Stderr, "parsing -utls: %v\n", err)
334 os.Exit(1)
336 if utlsClientHelloID != nil {
337 log.Printf("uTLS fingerprint %s %s", utlsClientHelloID.Client, utlsClientHelloID.Version)
340 // Iterate over the remote resolver address options and select one and
341 // only one.
342 var remoteAddr net.Addr
343 var pconn net.PacketConn
344 for _, opt := range []struct {
345 s string
346 f func(string) (net.Addr, net.PacketConn, error)
348 // -doh
349 {dohURL, func(s string) (net.Addr, net.PacketConn, error) {
350 addr := turbotunnel.DummyAddr{}
351 var rt http.RoundTripper
352 if utlsClientHelloID == nil {
353 transport := http.DefaultTransport.(*http.Transport).Clone()
354 // Disable DefaultTransport's default Proxy =
355 // ProxyFromEnvironment setting, for conformity
356 // with utlsRoundTripper and with DoT mode,
357 // which do not take a proxy from the
358 // environment.
359 transport.Proxy = nil
360 rt = transport
361 } else {
362 rt = NewUTLSRoundTripper(nil, utlsClientHelloID)
364 pconn, err := NewHTTPPacketConn(rt, dohURL, 32)
365 return addr, pconn, err
367 // -dot
368 {dotAddr, func(s string) (net.Addr, net.PacketConn, error) {
369 addr := turbotunnel.DummyAddr{}
370 var dialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error)
371 if utlsClientHelloID == nil {
372 dialTLSContext = (&tls.Dialer{}).DialContext
373 } else {
374 dialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
375 return utlsDialContext(ctx, network, addr, nil, utlsClientHelloID)
378 pconn, err := NewTLSPacketConn(dotAddr, dialTLSContext)
379 return addr, pconn, err
381 // -udp
382 {udpAddr, func(s string) (net.Addr, net.PacketConn, error) {
383 addr, err := net.ResolveUDPAddr("udp", s)
384 if err != nil {
385 return nil, nil, err
387 pconn, err := net.ListenUDP("udp", nil)
388 return addr, pconn, err
391 if opt.s == "" {
392 continue
394 if pconn != nil {
395 fmt.Fprintf(os.Stderr, "only one of -doh, -dot, and -udp may be given\n")
396 os.Exit(1)
398 var err error
399 remoteAddr, pconn, err = opt.f(opt.s)
400 if err != nil {
401 fmt.Fprintln(os.Stderr, err)
402 os.Exit(1)
405 if pconn == nil {
406 fmt.Fprintf(os.Stderr, "one of -doh, -dot, or -udp is required\n")
407 os.Exit(1)
410 pconn = NewDNSPacketConn(pconn, remoteAddr, domain)
411 err = run(pubkey, domain, localAddr, remoteAddr, pconn)
412 if err != nil {
413 log.Fatal(err)