1 // dnstt-client is the client end of a DNS tunnel.
4 // dnstt-client [-doh URL|-dot ADDR|-udp ADDR] -pubkey-file PUBKEYFILE DOMAIN LOCALADDR
7 // dnstt-client -doh https://resolver.example/dns-query -pubkey-file server.pub t.example.com 127.0.0.1:7000
8 // dnstt-client -dot resolver.example:853 -pubkey-file server.pub t.example.com 127.0.0.1:7000
10 // The program supports DNS over HTTPS (DoH), DNS over TLS (DoT), and UDP DNS.
11 // Use one of these options:
12 // -doh https://resolver.example/dns-query
13 // -dot resolver.example:853
14 // -udp resolver.example:53
16 // You can give the server's public key as a file or as a hex string. Use
17 // "dnstt-server -gen-key" to get the public key.
18 // -pubkey-file server.pub
19 // -pubkey 0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff
21 // DOMAIN is the root of the DNS zone reserved for the tunnel. See README for
22 // instructions on setting it up.
24 // LOCALADDR is the TCP address that will listen for connections and forward
25 // them over the tunnel.
38 "github.com/xtaci/kcp-go/v5"
39 "github.com/xtaci/smux"
40 "www.bamsoftware.com/git/dnstt.git/dns"
41 "www.bamsoftware.com/git/dnstt.git/noise"
42 "www.bamsoftware.com/git/dnstt.git/turbotunnel"
45 // smux streams will be closed after this much time without receiving data.
46 const idleTimeout
= 2 * time
.Minute
48 // dnsNameCapacity returns the number of bytes remaining for encoded data after
49 // including domain in a DNS name.
50 func dnsNameCapacity(domain dns
.Name
) int {
51 // Names must be 255 octets or shorter in total length.
52 // https://tools.ietf.org/html/rfc1035#section-2.3.4
54 // Subtract the length of the null terminator.
56 for _
, label
:= range domain
{
57 // Subtract the length of the label and the length octet.
58 capacity
-= len(label
) + 1
60 // Each label may be up to 63 bytes long and requires 64 bytes to
62 capacity
= capacity
* 63 / 64
63 // Base32 expands every 5 bytes to 8.
64 capacity
= capacity
* 5 / 8
68 // readKeyFromFile reads a key from a named file.
69 func readKeyFromFile(filename
string) ([]byte, error
) {
70 f
, err
:= os
.Open(filename
)
75 return noise
.ReadKey(f
)
78 func handle(local
*net
.TCPConn
, sess
*smux
.Session
, conv
uint32) error
{
79 stream
, err
:= sess
.OpenStream()
81 return fmt
.Errorf("session %08x opening stream: %v", conv
, err
)
84 log
.Printf("end stream %08x:%d", conv
, stream
.ID())
87 log
.Printf("begin stream %08x:%d", conv
, stream
.ID())
93 _
, err
:= io
.Copy(stream
, local
)
95 // smux Stream.Write may return io.EOF.
99 log
.Printf("stream %08x:%d copy stream←local: %v", conv
, stream
.ID(), err
)
106 _
, err
:= io
.Copy(local
, stream
)
108 // smux Stream.WriteTo may return io.EOF.
111 if err
!= nil && err
!= io
.ErrClosedPipe
{
112 log
.Printf("stream %08x:%d copy local←stream: %v", conv
, stream
.ID(), err
)
121 func run(pubkey
[]byte, domain dns
.Name
, localAddr
*net
.TCPAddr
, remoteAddr net
.Addr
, pconn net
.PacketConn
) error
{
124 ln
, err
:= net
.ListenTCP("tcp", localAddr
)
126 return fmt
.Errorf("opening local listener: %v", err
)
130 mtu
:= dnsNameCapacity(domain
) - 8 - 1 - numPadding
- 1 // clientid + padding length prefix + padding + data length prefix
132 return fmt
.Errorf("domain %s leaves only %d bytes for payload", domain
, mtu
)
134 log
.Printf("effective MTU %d", mtu
)
136 // Open a KCP conn on the PacketConn.
137 conn
, err
:= kcp
.NewConn2(remoteAddr
, nil, 0, 0, pconn
)
139 return fmt
.Errorf("opening KCP conn: %v", err
)
142 log
.Printf("end session %08x", conn
.GetConv())
145 log
.Printf("begin session %08x", conn
.GetConv())
146 // Permit coalescing the payloads of consecutive sends.
147 conn
.SetStreamMode(true)
148 // Disable the dynamic congestion window (limit only by the maximum of
149 // local and remote static windows).
151 0, // default nodelay
152 0, // default interval
154 1, // nc=1 => congestion window off
156 if rc
:= conn
.SetMtu(mtu
); !rc
{
160 // Put a Noise channel on top of the KCP conn.
161 rw
, err
:= noise
.NewClient(conn
, pubkey
)
166 // Start a smux session on the Noise channel.
167 smuxConfig
:= smux
.DefaultConfig()
168 smuxConfig
.Version
= 2
169 smuxConfig
.KeepAliveTimeout
= idleTimeout
170 sess
, err
:= smux
.Client(rw
, smuxConfig
)
172 return fmt
.Errorf("opening smux session: %v", err
)
177 local
, err
:= ln
.Accept()
179 if err
, ok
:= err
.(net
.Error
); ok
&& err
.Temporary() {
186 err
:= handle(local
.(*net
.TCPConn
), sess
, conn
.GetConv())
188 log
.Printf("handle: %v", err
)
197 var pubkeyFilename
string
198 var pubkeyString
string
201 flag
.Usage
= func() {
202 fmt
.Fprintf(flag
.CommandLine
.Output(), `Usage:
203 %[1]s [-doh URL|-dot ADDR|-udp ADDR] -pubkey-file PUBKEYFILE DOMAIN LOCALADDR
206 %[1]s -doh https://resolver.example/dns-query -pubkey-file server.pub t.example.com 127.0.0.1:7000
207 %[1]s -dot resolver.example:853 -pubkey-file server.pub t.example.com 127.0.0.1:7000
212 flag
.StringVar(&dohURL
, "doh", "", "URL of DoH resolver")
213 flag
.StringVar(&dotAddr
, "dot", "", "address of DoT resolver")
214 flag
.StringVar(&pubkeyString
, "pubkey", "", fmt
.Sprintf("server public key (%d hex digits)", noise
.KeyLen
*2))
215 flag
.StringVar(&pubkeyFilename
, "pubkey-file", "", "read server public key from file")
216 flag
.StringVar(&udpAddr
, "udp", "", "address of UDP DNS resolver")
219 log
.SetFlags(log
.LstdFlags | log
.LUTC
)
221 if flag
.NArg() != 2 {
225 domain
, err
:= dns
.ParseName(flag
.Arg(0))
227 fmt
.Fprintf(os
.Stderr
, "invalid domain %+q: %v\n", flag
.Arg(0), err
)
230 localAddr
, err
:= net
.ResolveTCPAddr("tcp", flag
.Arg(1))
232 fmt
.Fprintln(os
.Stderr
, err
)
237 if pubkeyFilename
!= "" && pubkeyString
!= "" {
238 fmt
.Fprintf(os
.Stderr
, "only one of -pubkey and -pubkey-file may be used\n")
240 } else if pubkeyFilename
!= "" {
242 pubkey
, err
= readKeyFromFile(pubkeyFilename
)
244 fmt
.Fprintf(os
.Stderr
, "cannot read pubkey from file: %v\n", err
)
247 } else if pubkeyString
!= "" {
249 pubkey
, err
= noise
.DecodeKey(pubkeyString
)
251 fmt
.Fprintf(os
.Stderr
, "pubkey format error: %v\n", err
)
255 if len(pubkey
) == 0 {
256 fmt
.Fprintf(os
.Stderr
, "the -pubkey or -pubkey-file option is required\n")
260 // Iterate over the remote resolver address options and select one and
262 var remoteAddr net
.Addr
263 var pconn net
.PacketConn
264 for _
, opt
:= range []struct {
266 f
func(string) (net
.Addr
, net
.PacketConn
, error
)
269 {dohURL
, func(s
string) (net
.Addr
, net
.PacketConn
, error
) {
270 addr
:= turbotunnel
.DummyAddr
{}
271 pconn
, err
:= NewHTTPPacketConn(dohURL
, 32)
272 return addr
, pconn
, err
275 {dotAddr
, func(s
string) (net
.Addr
, net
.PacketConn
, error
) {
276 addr
:= turbotunnel
.DummyAddr
{}
277 pconn
, err
:= NewTLSPacketConn(dotAddr
)
278 return addr
, pconn
, err
281 {udpAddr
, func(s
string) (net
.Addr
, net
.PacketConn
, error
) {
282 addr
, err
:= net
.ResolveUDPAddr("udp", s
)
286 pconn
, err
:= net
.ListenUDP("udp", nil)
287 return addr
, pconn
, err
294 fmt
.Fprintf(os
.Stderr
, "only one of -doh, -dot, and -udp may be given\n")
298 remoteAddr
, pconn
, err
= opt
.f(opt
.s
)
300 fmt
.Fprintln(os
.Stderr
, err
)
305 fmt
.Fprintf(os
.Stderr
, "one of -doh, -dot, or -udp is required\n")
309 pconn
= NewDNSPacketConn(pconn
, remoteAddr
, domain
)
310 err
= run(pubkey
, domain
, localAddr
, remoteAddr
, pconn
)