diff --git a/CHANGELOG b/CHANGELOG index c638625..1c23fd4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,24 @@ +## v1.20260501.0 + +Fixed an error in dnstt-server that could cause the server to stop +processing traffic, while still running. Rarely, a sendto system call in +the sendLoop function would cause sendLoop to return with an error. +Meanwhile, recvLoop would keep running and processing incoming queries, +but as sendLoop had finished, the server would cease to send back +responses. In live testing, one possible cause of a sendto error was the +Linux conntrack table becoming full from heavy traffic and the outgoing +packet being blocked by a default DENY rule in the local firewall; other +causes may have been possible. This release contains two changes to fix +the problem: (1) sendto errors, other than net.ErrClosed, are now only +logged and do not cause sendLoop to terminate; (2) if sendLoop (or any +other top-level goroutine) does return, it causes the whole process to +exit, which will make any failure noticeable and avoid the "running but +not working" failure mode. + +## v1.20241021.0 + +Added a CC0 COPYING file. + ## v1.20240513.0 Updated utls to v1.6.6. Added a "random" fingerprint ID that maps to diff --git a/README b/README index d71f9c0..05d6337 100644 --- a/README +++ b/README @@ -239,6 +239,43 @@ tunnel-client$ curl --proxy socks5h://127.0.0.1:7000/ https://wtfismyip.com/text ``` +### Dante SOCKS proxy + +With the Dante SOCKS proxy (https://www.inet.no/dante/), a minimal +configuration (/etc/danted.conf) may look like the following: + +``` +internal: lo port = 8000 +external: eth0 + +clientmethod: none +socksmethod: none + +user.privileged: proxy +user.unprivileged: nobody + +client pass { + from: lo to: 0/0 +} + +socks block { + from: 0.0.0.0/0 to: lo +} + +socks block { + from: ::/0 to: lo +} + +socks pass { + from: lo to: 0.0.0.0/0 + command: connect +} +``` + +See https://www.inet.no/dante/doc/latest/config/server.html for more +examples. + + ### Tor bridge You can run a Tor bridge on the tunnel server and tunnel the connection diff --git a/dnstt-client/lib/dns.go b/dnstt-client/lib/dns.go index c4ffedd..53f9494 100644 --- a/dnstt-client/lib/dns.go +++ b/dnstt-client/lib/dns.go @@ -187,11 +187,6 @@ func (c *DNSPacketConn) recvLoop(transport net.PacketConn) error { var buf [4096]byte n, addr, err := transport.ReadFrom(buf[:]) if err != nil { - //goland:noinspection GoDeprecation - if err, ok := err.(net.Error); ok && err.Temporary() { - log.Printf("ReadFrom temporary error: %v", err) - continue - } return err } @@ -257,29 +252,29 @@ func chunks(p []byte, n int) [][]byte { // // 0. Start with the raw packet contents. // -// supercalifragilisticexpialidocious +// supercalifragilisticexpialidocious // // 1. Length-prefix the packet and add random padding. A length prefix L < 0xe0 // means a data packet of L bytes. A length prefix L ≥ 0xe0 means padding // of L − 0xe0 bytes (not counting the length of the length prefix itself). // -// \xe3\xd9\xa3\x15\x22supercalifragilisticexpialidocious +// \xe3\xd9\xa3\x15\x22supercalifragilisticexpialidocious // // 2. Prefix the ClientID. // -// CLIENTID\xe3\xd9\xa3\x15\x22supercalifragilisticexpialidocious +// CLIENTID\xe3\xd9\xa3\x15\x22supercalifragilisticexpialidocious // // 3. Base32-encode, without padding and in lower case. // -// ingesrkokreujy6zumkse43vobsxey3bnruwm4tbm5uwy2ltoruwgzlyobuwc3djmrxwg2lpovzq +// ingesrkokreujy6zumkse43vobsxey3bnruwm4tbm5uwy2ltoruwgzlyobuwc3djmrxwg2lpovzq // // 4. Break into labels of at most 63 octets. // -// ingesrkokreujy6zumkse43vobsxey3bnruwm4tbm5uwy2ltoruwgzlyobuwc3d.jmrxwg2lpovzq +// ingesrkokreujy6zumkse43vobsxey3bnruwm4tbm5uwy2ltoruwgzlyobuwc3d.jmrxwg2lpovzq // // 5. Append the domain. // -// ingesrkokreujy6zumkse43vobsxey3bnruwm4tbm5uwy2ltoruwgzlyobuwc3d.jmrxwg2lpovzq.t.example.com +// ingesrkokreujy6zumkse43vobsxey3bnruwm4tbm5uwy2ltoruwgzlyobuwc3d.jmrxwg2lpovzq.t.example.com func (c *DNSPacketConn) send(transport net.PacketConn, p []byte, addr net.Addr) error { var decoded []byte { diff --git a/dnstt-server/main.go b/dnstt-server/main.go index 4480980..c9868e8 100644 --- a/dnstt-server/main.go +++ b/dnstt-server/main.go @@ -269,6 +269,7 @@ func acceptStreams(conn *kcp.UDPSession, privkey []byte, upstream string) error if err != nil { //goland:noinspection GoDeprecation if err, ok := err.(net.Error); ok && err.Temporary() { + log.Printf("AcceptStream temporary error: %v", err) continue } return err @@ -295,6 +296,7 @@ func acceptSessions(ln *kcp.Listener, privkey []byte, mtu int, upstream string) if err != nil { //goland:noinspection GoDeprecation if err, ok := err.(net.Error); ok && err.Temporary() { + log.Printf("AcceptKCP temporary error: %v", err) continue } return err @@ -516,11 +518,6 @@ func recvLoop(domain dns.Name, dnsConn net.PacketConn, ttConn *turbotunnel.Queue var buf [4096]byte n, addr, err := dnsConn.ReadFrom(buf[:]) if err != nil { - //goland:noinspection GoDeprecation - if err, ok := err.(net.Error); ok && err.Temporary() { - log.Printf("ReadFrom temporary error: %v", err) - continue - } return err } @@ -606,6 +603,7 @@ func sendLoop(dnsConn net.PacketConn, ttConn *turbotunnel.QueuePacketConn, ch <- // overflow the capacity of the DNS response, we stash // to be bundled into a future response. timer := time.NewTimer(maxResponseDelay) + timerExpired := false for { var p []byte unstash := ttConn.Unstash(rec.ClientID) @@ -626,6 +624,7 @@ func sendLoop(dnsConn net.PacketConn, ttConn *turbotunnel.QueuePacketConn, ch <- case p = <-unstash: case p = <-outgoing: case <-timer.C: + timerExpired = true case nextRec = <-ch: } } @@ -634,7 +633,11 @@ func sendLoop(dnsConn net.PacketConn, ttConn *turbotunnel.QueuePacketConn, ch <- // only. The second and later packets must be // immediately available or they will be omitted // from this bundle. + if !timerExpired && !timer.Stop() { + <-timer.C + } timer.Reset(0) + timerExpired = false if len(p) == 0 { // timer expired or receive on ch, we @@ -660,7 +663,9 @@ func sendLoop(dnsConn net.PacketConn, ttConn *turbotunnel.QueuePacketConn, ch <- _ = binary.Write(&payload, binary.BigEndian, uint16(len(p))) payload.Write(p) } - timer.Stop() + if !timerExpired && !timer.Stop() { + <-timer.C + } rec.Resp.Answer[0].Data = dns.EncodeRDataTXT(payload.Bytes()) } @@ -681,12 +686,14 @@ func sendLoop(dnsConn net.PacketConn, ttConn *turbotunnel.QueuePacketConn, ch <- // Now we actually send the message as a UDP packet. _, err = dnsConn.WriteTo(buf, rec.Addr) if err != nil { - //goland:noinspection GoDeprecation - if err, ok := err.(net.Error); ok && err.Temporary() { - log.Printf("WriteTo temporary error: %v", err) - continue + // net.ErrClosed means we'll never be able to send on + // dnsConn, so terminate the loop. Treat all other + // errors as temporary and simply log them. + if errors.Is(err, net.ErrClosed) { + return err } - return err + log.Printf("WriteTo error: %v", err) + continue } } return nil @@ -814,7 +821,15 @@ func run(privkey []byte, domain dns.Name, upstream string, dnsConn net.PacketCon defer func() { _ = ln.Close() }() + + // We will run acceptSessions, sendLoop, and recvLoop concurrently. The + // first one to finish closes the done channel. + doneChan := make(chan struct{}) + var doneOnce sync.Once + done := func() { doneOnce.Do(func() { close(doneChan) }) } + go func() { + defer done() err := acceptSessions(ln, privkey, mtu, upstream) if err != nil { log.Printf("acceptSessions: %v", err) @@ -828,13 +843,26 @@ func run(privkey []byte, domain dns.Name, upstream string, dnsConn net.PacketCon // for each response to collect downstream data before being evicted by // another response that needs to be sent. go func() { + defer done() err := sendLoop(dnsConn, ttConn, ch, maxEncodedPayload) if err != nil { log.Printf("sendLoop: %v", err) } }() - return recvLoop(domain, dnsConn, ttConn, ch) + go func() { + defer done() + err := recvLoop(domain, dnsConn, ttConn, ch) + if err != nil { + log.Printf("recvLoop: %v", err) + } + }() + + // Wait for any of acceptSessions, sendLoop, or recvLoop to return. In + // normal operation, we don't expect any of these to return. + <-doneChan + + return nil } func main() {