1 // dnstt-client is the client end of a DNS tunnel.
5 // dnstt-client [-doh URL|-dot ADDR|-udp ADDR] -pubkey-file PUBKEYFILE DOMAIN LOCALADDR
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'
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
73 // Subtract the length of the null terminator.
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
81 capacity
= capacity
* 63 / 64
82 // Base32 expands every 5 bytes to 8.
83 capacity
= capacity
* 5 / 8
87 // readKeyFromFile reads a key from a named file.
88 func readKeyFromFile(filename
string) ([]byte, error
) {
89 f
, err
:= os
.Open(filename
)
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
)
106 ids
:= make([]*utls
.ClientHelloID
, 0, len(labels
))
107 for _
, label
:= range labels
{
108 var id
*utls
.ClientHelloID
112 id
= utlsLookup(label
)
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()
125 return fmt
.Errorf("session %08x opening stream: %v", conv
, err
)
128 log
.Printf("end stream %08x:%d", conv
, stream
.ID())
131 log
.Printf("begin stream %08x:%d", conv
, stream
.ID())
133 var wg sync
.WaitGroup
137 _
, err
:= io
.Copy(stream
, local
)
139 // smux Stream.Write may return io.EOF.
142 if err
!= nil && !errors
.Is(err
, io
.ErrClosedPipe
) {
143 log
.Printf("stream %08x:%d copy stream←local: %v", conv
, stream
.ID(), err
)
150 _
, err
:= io
.Copy(local
, stream
)
152 // smux Stream.WriteTo may return io.EOF.
155 if err
!= nil && !errors
.Is(err
, io
.ErrClosedPipe
) {
156 log
.Printf("stream %08x:%d copy local←stream: %v", conv
, stream
.ID(), err
)
165 func run(pubkey
[]byte, domain dns
.Name
, localAddr
*net
.TCPAddr
, remoteAddr net
.Addr
, pconn net
.PacketConn
) error
{
168 ln
, err
:= net
.ListenTCP("tcp", localAddr
)
170 return fmt
.Errorf("opening local listener: %v", err
)
174 mtu
:= dnsNameCapacity(domain
) - 8 - 1 - numPadding
- 1 // clientid + padding length prefix + padding + data length prefix
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
)
183 return fmt
.Errorf("opening KCP conn: %v", err
)
186 log
.Printf("end session %08x", conn
.GetConv())
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).
195 0, // default nodelay
196 0, // default interval
198 1, // nc=1 => congestion window off
200 conn
.SetWindowSize(turbotunnel
.QueueSize
/2, turbotunnel
.QueueSize
/2)
201 if rc
:= conn
.SetMtu(mtu
); !rc
{
205 // Put a Noise channel on top of the KCP conn.
206 rw
, err
:= noise
.NewClient(conn
, pubkey
)
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
)
218 return fmt
.Errorf("opening smux session: %v", err
)
223 local
, err
:= ln
.Accept()
225 if err
, ok
:= err
.(net
.Error
); ok
&& err
.Temporary() {
232 err
:= handle(local
.(*net
.TCPConn
), sess
, conn
.GetConv())
234 log
.Printf("handle: %v", err
)
243 var pubkeyFilename
string
244 var pubkeyString
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
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
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:
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")
290 log
.SetFlags(log
.LstdFlags | log
.LUTC
)
292 if flag
.NArg() != 2 {
296 domain
, err
:= dns
.ParseName(flag
.Arg(0))
298 fmt
.Fprintf(os
.Stderr
, "invalid domain %+q: %v\n", flag
.Arg(0), err
)
301 localAddr
, err
:= net
.ResolveTCPAddr("tcp", flag
.Arg(1))
303 fmt
.Fprintln(os
.Stderr
, err
)
308 if pubkeyFilename
!= "" && pubkeyString
!= "" {
309 fmt
.Fprintf(os
.Stderr
, "only one of -pubkey and -pubkey-file may be used\n")
311 } else if pubkeyFilename
!= "" {
313 pubkey
, err
= readKeyFromFile(pubkeyFilename
)
315 fmt
.Fprintf(os
.Stderr
, "cannot read pubkey from file: %v\n", err
)
318 } else if pubkeyString
!= "" {
320 pubkey
, err
= noise
.DecodeKey(pubkeyString
)
322 fmt
.Fprintf(os
.Stderr
, "pubkey format error: %v\n", err
)
326 if len(pubkey
) == 0 {
327 fmt
.Fprintf(os
.Stderr
, "the -pubkey or -pubkey-file option is required\n")
331 utlsClientHelloID
, err
:= sampleUTLSDistribution(utlsDistribution
)
333 fmt
.Fprintf(os
.Stderr
, "parsing -utls: %v\n", err
)
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
342 var remoteAddr net
.Addr
343 var pconn net
.PacketConn
344 for _
, opt
:= range []struct {
346 f
func(string) (net
.Addr
, net
.PacketConn
, error
)
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
359 transport
.Proxy
= nil
362 rt
= NewUTLSRoundTripper(nil, utlsClientHelloID
)
364 pconn
, err
:= NewHTTPPacketConn(rt
, dohURL
, 32)
365 return addr
, pconn
, err
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
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
382 {udpAddr
, func(s
string) (net
.Addr
, net
.PacketConn
, error
) {
383 addr
, err
:= net
.ResolveUDPAddr("udp", s
)
387 pconn
, err
:= net
.ListenUDP("udp", nil)
388 return addr
, pconn
, err
395 fmt
.Fprintf(os
.Stderr
, "only one of -doh, -dot, and -udp may be given\n")
399 remoteAddr
, pconn
, err
= opt
.f(opt
.s
)
401 fmt
.Fprintln(os
.Stderr
, err
)
406 fmt
.Fprintf(os
.Stderr
, "one of -doh, -dot, or -udp is required\n")
410 pconn
= NewDNSPacketConn(pconn
, remoteAddr
, domain
)
411 err
= run(pubkey
, domain
, localAddr
, remoteAddr
, pconn
)