Skip to content

Commit

Permalink
feat(network): add DNS fallback (truncated) PacketProxy (#26)
Browse files Browse the repository at this point in the history
I added `dnstruncated.NewPacketProxy()` to the SDK. This type can be used when the remote Shadowsocks server **doesn't support UDP traffic at all**. By using this `PacketProxy`, we will always set the [`TC` (truncated) bit](https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.1) in the DNS responses for all UDP DNS requests. So a well-implemented DNS client (e.g. the browser, the `dig` command) should retry the same DNS request over TCP.

Related PR: #24 , #27
  • Loading branch information
jyyi1 committed Jul 24, 2023
1 parent fc75f57 commit 35b8cde
Show file tree
Hide file tree
Showing 10 changed files with 487 additions and 10 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Alpha tasks:
- Network-level libraries
- [x] Add IP Device abstraction
- [x] Add IP Device implementation based on go-tun2socks (LWIP)
- [ ] Add UDP handler to fallback to DNS-over-TCP
- [x] Add UDP handler to fallback to DNS-over-TCP
- [x] Add DelegatePacketProxy for runtime PacketProxy replacement


Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.20

require (
github.com/eycorsican/go-tun2socks v1.16.11
github.com/google/gopacket v1.1.19
github.com/shadowsocks/go-shadowsocks2 v0.1.5
github.com/stretchr/testify v1.8.2
golang.org/x/crypto v0.7.0
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eycorsican/go-tun2socks v1.16.11 h1:+hJDNgisrYaGEqoSxhdikMgMJ4Ilfwm/IZDrWRrbaH8=
github.com/eycorsican/go-tun2socks v1.16.11/go.mod h1:wgB2BFT8ZaPKyKOQ/5dljMG/YIow+AIXyq4KBwJ5sGQ=
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
Expand All @@ -23,17 +25,25 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191021144547-ec77196f6094/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
35 changes: 35 additions & 0 deletions network/dnstruncate/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2023 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/*
Package dnstruncate functions as an alternative implementation that handles DNS requests if the remote server doesn't
support UDP traffic. This is done by always setting the TC (truncated) bit in the DNS response header; it tells the
caller to resend the DNS request using TCP instead of UDP. As a result, no UDP requests are made to the remote server.
This implementation is ported from the [go-tun2socks' dnsfallback.NewUDPHandler].
Note that UDP traffic that are not DNS requests are dropped.
To create a [network.PacketProxy] that handles DNS requests locally:
proxy, err := dnstruncate.NewPacketProxy()
if err != nil {
// handle error
}
This `proxy` can then be used in, for example, lwip2transport.ConfigureDevice.
[go-tun2socks' dnsfallback.NewUDPHandler]: https://github.com/eycorsican/go-tun2socks/blob/master/proxy/dnsfallback/udp.go
*/
package dnstruncate
154 changes: 154 additions & 0 deletions network/dnstruncate/packet_proxy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Copyright 2023 Jigsaw Operations LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dnstruncate

import (
"errors"
"fmt"
"net"
"net/netip"
"sync/atomic"

"github.com/Jigsaw-Code/outline-internal-sdk/internal/slicepool"
"github.com/Jigsaw-Code/outline-internal-sdk/network"
)

// From [RFC 1035], the DNS message header contains the following fields:
//
// 1 1 1 1 1 1
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
//
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | ID |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// |QR| Opcode |AA|TC|RD|RA| Z | RCODE |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | QDCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | ANCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | NSCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
// | ARCOUNT |
// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
//
// [RFC 1035]: https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.1
const (
standardDNSPort = uint16(53) // https://datatracker.ietf.org/doc/html/rfc1035#section-4.2
dnsUdpMinMsgLen = 12 // A DNS message must at least contain the header
dnsUdpMaxMsgLen = 512 // https://datatracker.ietf.org/doc/html/rfc1035#section-2.3.4

dnsUdpAnswerByte = 2 // The byte in the header containing QR and TC bit
dnsUdpResponseBit = uint8(0x80) // The QR bit within dnsUdpAnswerByte
dnsUdpTruncatedBit = uint8(0x02) // The TC bit within dnsUdpAnswerByte
dnsUdpRCodeByte = 3 // The byte in the header containing RCODE
dnsUdpRCodeMask = uint8(0x0f) // The RCODE bits within dnsUdpRCodeByte
dnsQDCntStartByte = 4 // The starting byte of QDCOUNT
dnsQDCntEndByte = 5 // The ending byte (inclusive) of QDCOUNT
dnsARCntStartByte = 6 // The starting byte of ANCOUNT
dnsARCntEndByte = 7 // The ending byte (inclusive) of ANCOUNT
)

// packetBufferPool is used to create buffers to modify DNS requests
var packetBufferPool = slicepool.MakePool(dnsUdpMaxMsgLen)

// dnsTruncateProxy is a network.PacketProxy that create dnsTruncateRequestHandler to handle DNS requests locally.
//
// Multiple goroutines may invoke methods on a dnsTruncateProxy simultaneously.
type dnsTruncateProxy struct {
}

// dnsTruncateRequestHandler is a network.PacketRequestSender that handles DNS requests in UDP protocol locally,
// without sending the requests to the actual DNS resolver. It sets the TC (truncated) bit in the DNS response header
// to tell the caller to resend the DNS request over TCP.
//
// Multiple goroutines may invoke methods on a dnsTruncateProxy simultaneously.
type dnsTruncateRequestHandler struct {
closed atomic.Bool
respWriter network.PacketResponseReceiver
}

// Compilation guard against interface implementation
var _ network.PacketProxy = (*dnsTruncateProxy)(nil)
var _ network.PacketRequestSender = (*dnsTruncateRequestHandler)(nil)

// NewPacketProxy creates a new [network.PacketProxy] that can be used to handle DNS requests if the remote proxy
// doesn't support UDP traffic. It sets the TC (truncated) bit in the DNS response header to tell the caller to resend
// the DNS request over TCP.
//
// This [network.PacketProxy] should only be used if the remote proxy server doesn't support UDP traffic at all. Note
// that all other non-DNS UDP packets will be dropped by this [network.PacketProxy].
func NewPacketProxy() (network.PacketProxy, error) {
return &dnsTruncateProxy{}, nil
}

// NewSession implements [network.PacketProxy].NewSession(). It creates a new [network.PacketRequestSender] that will
// set the TC (truncated) bit and write the response to `respWriter`.
func (p *dnsTruncateProxy) NewSession(respWriter network.PacketResponseReceiver) (network.PacketRequestSender, error) {
if respWriter == nil {
return nil, errors.New("respWriter is required")
}
return &dnsTruncateRequestHandler{
respWriter: respWriter,
}, nil
}

// Close implements [network.PacketRequestSender].Close(), and it closes the corresponding
// [network.PacketResponseReceiver].
func (h *dnsTruncateRequestHandler) Close() error {
if !h.closed.CompareAndSwap(false, true) {
return network.ErrClosed
}
h.respWriter.Close()
return nil
}

// WriteTo implements [network.PacketRequestSender].WriteTo(). It parses a packet from p, and determines whether it is
// a valid DNS request. If so, it will write the DNS response with TC (truncated) bit set to the corresponding
// [network.PacketResponseReceiver] passed to NewSession. If it is not a valid DNS request, the packet will be
// discarded and returns an error.
func (h *dnsTruncateRequestHandler) WriteTo(p []byte, destination netip.AddrPort) (int, error) {
if h.closed.Load() {
return 0, network.ErrClosed
}
if destination.Port() != standardDNSPort {
return 0, fmt.Errorf("UDP traffic to non-DNS port %v is not supported: %w", destination.Port(), network.ErrPortUnreachable)
}
if len(p) < dnsUdpMinMsgLen {
return 0, fmt.Errorf("invalid DNS message of length %v, it must be at least %v bytes", len(p), dnsUdpMinMsgLen)
}

// Allocate buffer from slicepool, because `go build -gcflags="-m"` shows a local array will escape to heap
slice := packetBufferPool.LazySlice()
buf := slice.Acquire()
defer slice.Release()

// We need to copy p into buf because "WriteTo must not modify p, even temporarily".
n := copy(buf, p)

// Set "Response", "Truncated" and "NoError"
// Note: gopacket is a good library doing this kind of things. But it will increase the binary size a lot.
// If we decide to use gopacket in the future, please evaluate the binary size and runtime memory consumption.
buf[dnsUdpAnswerByte] |= (dnsUdpResponseBit | dnsUdpTruncatedBit)
buf[dnsUdpRCodeByte] &= ^dnsUdpRCodeMask

// Copy QDCOUNT to ANCOUNT. This is an incorrect workaround for some DNS clients (such as Windows 7);
// because without these clients won't retry over TCP.
//
// For reference: https://github.com/eycorsican/go-tun2socks/blob/master/proxy/dnsfallback/udp.go#L59-L63
copy(buf[dnsARCntStartByte:dnsARCntEndByte+1], buf[dnsQDCntStartByte:dnsQDCntEndByte+1])

return h.respWriter.WriteFrom(buf[:n], net.UDPAddrFromAddrPort(destination))
}
Loading

0 comments on commit 35b8cde

Please sign in to comment.