| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314 |
- // 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.
- package main
- import (
- "flag"
- "fmt"
- "io"
- "log"
- "net"
- "os"
- "sync"
- "time"
- "github.com/xtaci/kcp-go/v5"
- "github.com/xtaci/smux"
- "www.bamsoftware.com/git/dnstt.git/dns"
- "www.bamsoftware.com/git/dnstt.git/noise"
- "www.bamsoftware.com/git/dnstt.git/turbotunnel"
- )
- // smux streams will be closed after this much time without receiving data.
- const idleTimeout = 10 * 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)
- }
- 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 {
- log.Printf("stream %08x:%d copy stream←local: %v\n", 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 && err != io.ErrClosedPipe {
- log.Printf("stream %08x:%d copy local←stream: %v\n", 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\n", 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
- )
- 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
- 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\n", err)
- }
- }()
- }
- }
- func main() {
- var dohURL string
- var dotAddr string
- var pubkeyFilename string
- var pubkeyString string
- var udpAddr 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()
- }
- 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.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)
- }
- // 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{}
- pconn, err := NewHTTPPacketConn(dohURL, 32)
- return addr, pconn, err
- }},
- // -dot
- {dotAddr, func(s string) (net.Addr, net.PacketConn, error) {
- addr := turbotunnel.DummyAddr{}
- pconn, err := NewTLSPacketConn(dotAddr)
- 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)
- }
- }
|