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
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
151 changes: 151 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,151 @@
// 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/network"
)

var (
errInvalidResolver = errors.New("invalid DNS resolver's port number, it must be 53")
errInvalidDNSMsg = errors.New("invalid DNS message")
)

// 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(0x10) // 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
)

// 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("DNS resolver is not a valid UDP address: %w", err)
}
if resolverAddr.Port != dnsServerPort {
return 0, errInvalidResolver
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
}
if len(p) < dnsUdpMinMsgLen {
return 0, errInvalidDNSMsg
}

buf := make([]byte, dnsUdpMaxMsgLen)
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
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)
}
17 changes: 17 additions & 0 deletions network/packet_proxy_mock.go
jyyi1 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package network

import "net"

type PacketProxy interface {
NewSession(PacketResponseReceiver) (PacketRequestSender, error)
}

type PacketRequestSender interface {
WriteTo(p []byte, destination net.Addr) (int, error)
Close() error
}

type PacketResponseReceiver interface {
WriteFrom(p []byte, source net.Addr) (int, error)
Close() error
}