Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(network): add DNS fallback (truncated) PacketProxy #26

Merged
merged 10 commits into from
Jul 24, 2023
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
- [ ] Add DelegatePacketProxy


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
157 changes: 157 additions & 0 deletions network/dnstruncate/packet_proxy.go
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
// 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"
"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 (
dnsServerPort = 53 // https://datatracker.ietf.org/doc/html/rfc1035#section-4.2
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
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() // TODO(junyi): potential inf loop (rw.Close -> req.Close -> rw.Close ...)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you figure out the loop?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is no problem for this (because I've checked closed), but I will create another one with both the fix and a reproducing test case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still see the TODO there

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a reproducing test in #30 , will fix the potential issue in that pr.

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 net.Addr) (int, error) {
if h.closed.Load() {
return 0, network.ErrClosed
}
resolverAddr, err := net.ResolveUDPAddr("udp", destination.String())
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return 0, fmt.Errorf("non-UDP %w, the DNS resolver is not a valid UDP address: %w", network.ErrUnsupported, err)
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
}
if resolverAddr.Port != dnsServerPort {
return 0, fmt.Errorf("non-DNS UDP %w, target server's port is %v rather than %v",
network.ErrUnsupported, resolverAddr.Port, dnsServerPort)
}
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
if len(p) < dnsUdpMinMsgLen {
return 0, fmt.Errorf("invalid DNS %w, message length is %v bytes, it must be at least %v bytes",
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
network.ErrUnsupported, 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"
buf[dnsUdpAnswerByte] |= (dnsUdpResponseBit | dnsUdpTruncatedBit)
fortuna marked this conversation as resolved.
Show resolved Hide resolved
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], destination)
}
Loading