[net] x/net: add in-memory fake-DNS package to test custom resolvers

312 views
Skip to first unread message

Antonio Ojea (Gerrit)

unread,
Sep 7, 2021, 1:01:37 PM9/7/21
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Antonio Ojea has uploaded this change for review.

View Change

x/net: add in-memory fake-DNS package to test custom resolvers

quoting from bradfitx:

"We just need an easy way to wire up high-level Lookup func literals
into fake in-memory DNS-speaking func implementations to assign to
the Resolver.Dial field."

Add a new in-memory packet network connection Hairpin, that loops
packet through it. It allows to set a hook to process these packets.

Add a ResolverStub that allows to override current Lookup func
literals, and use it to process DNS packages.

Using the ResolverStub and the Hairpin connection we can override
the Resolver.Dial field and use it for testing or implement
custom resolver logic.

Signed-off-by: Antonio Ojea <antonio.o...@gmail.com>
Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
---
A resolver/conn.go
A resolver/conn_test.go
A resolver/example_test.go
A resolver/resolver.go
4 files changed, 1,092 insertions(+), 0 deletions(-)

diff --git a/resolver/conn.go b/resolver/conn.go
new file mode 100644
index 0000000..61d3bae
--- /dev/null
+++ b/resolver/conn.go
@@ -0,0 +1,331 @@
+package resolver
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net"
+ "os"
+ "sync"
+ "syscall"
+ "time"
+)
+
+const maxPacketSize = 1024
+
+// packetHandlerFn signature for the function used to process the packets
+type packetHandlerFn func(b []byte) []byte
+
+type hairpin struct {
+ wrMu sync.Mutex // Serialize Write operations
+
+ readCh chan []byte // Used to communicate Write and Read
+
+ once sync.Once // Protects closing the connection
+ done chan struct{}
+
+ readDeadline connDeadline
+ writeDeadline connDeadline
+
+ localAddr net.Addr
+ remoteAddr net.Addr
+
+ // hook for processing connections packets
+ // packet are copied directly if not set
+ packetHandler packetHandlerFn
+}
+
+// implement PacketConn interface
+var _ net.PacketConn = &hairpin{}
+
+// Hairpin creates a synchronous, in-memory, packet network connection
+// implementing the Conn interface. Reads on the connection are matched
+// with writes, and packets may be processed by the provided hook, if exist
+// or copied directly; only one packet is buffered to avoid deadlocks.
+func Hairpin(fn packetHandlerFn) *hairpin {
+ return &hairpin{
+ readCh: make(chan []byte, maxPacketSize),
+ done: make(chan struct{}),
+ readDeadline: makeConnDeadline(),
+ writeDeadline: makeConnDeadline(),
+ packetHandler: fn,
+ }
+
+}
+
+// connection parameters (copied from net.Pipe)
+// https://cs.opensource.google/go/go/+/refs/tags/go1.17:src/net/pipe.go;bpv=0;bpt=1
+
+// connDeadline is an abstraction for handling timeouts.
+type connDeadline struct {
+ mu sync.Mutex // Guards timer and cancel
+ timer *time.Timer
+ cancel chan struct{} // Must be non-nil
+}
+
+func makeConnDeadline() connDeadline {
+ return connDeadline{cancel: make(chan struct{})}
+}
+
+// set sets the point in time when the deadline will time out.
+// A timeout event is signaled by closing the channel returned by waiter.
+// Once a timeout has occurred, the deadline can be refreshed by specifying a
+// t value in the future.
+//
+// A zero value for t prevents timeout.
+func (d *connDeadline) set(t time.Time) {
+ d.mu.Lock()
+ defer d.mu.Unlock()
+
+ if d.timer != nil && !d.timer.Stop() {
+ <-d.cancel // Wait for the timer callback to finish and close cancel
+ }
+ d.timer = nil
+
+ // Time is zero, then there is no deadline.
+ closed := isClosedChan(d.cancel)
+ if t.IsZero() {
+ if closed {
+ d.cancel = make(chan struct{})
+ }
+ return
+ }
+
+ // Time in the future, setup a timer to cancel in the future.
+ if dur := time.Until(t); dur > 0 {
+ if closed {
+ d.cancel = make(chan struct{})
+ }
+ d.timer = time.AfterFunc(dur, func() {
+ close(d.cancel)
+ })
+ return
+ }
+
+ // Time in the past, so close immediately.
+ if !closed {
+ close(d.cancel)
+ }
+}
+
+// wait returns a channel that is closed when the deadline is exceeded.
+func (d *connDeadline) wait() chan struct{} {
+ d.mu.Lock()
+ defer d.mu.Unlock()
+ return d.cancel
+}
+
+func isClosedChan(c <-chan struct{}) bool {
+ select {
+ case <-c:
+ return true
+ default:
+ return false
+ }
+}
+
+type hairpinAddress struct {
+ addr string
+}
+
+func (h hairpinAddress) Network() string {
+ if h.addr != "" {
+ return h.addr
+ }
+ return "Hairpin"
+}
+func (h hairpinAddress) String() string {
+ if h.addr != "" {
+ return h.addr
+ }
+ return "Hairpin"
+}
+
+func (h *hairpin) SetLocalAddr(addr net.Addr) {
+ h.localAddr = addr
+}
+func (h *hairpin) SetRemoteAddr(addr net.Addr) {
+ h.remoteAddr = addr
+}
+
+func (h *hairpin) LocalAddr() net.Addr {
+ if h.localAddr != nil {
+ return h.localAddr
+ }
+ return hairpinAddress{}
+}
+func (h *hairpin) RemoteAddr() net.Addr {
+ if h.remoteAddr != nil {
+ return h.remoteAddr
+ }
+ return hairpinAddress{}
+}
+
+func (h *hairpin) Read(b []byte) (int, error) {
+ n, _, err := h.ReadFrom(b)
+ return n, err
+}
+func (h *hairpin) ReadFrom(b []byte) (int, net.Addr, error) {
+ n, err := h.read(b)
+ if err != nil && err != io.EOF && err != io.ErrClosedPipe {
+ err = &net.OpError{Op: "read", Net: "Hairpin", Err: err}
+ }
+ return n, hairpinAddress{}, err
+}
+
+func (h *hairpin) read(b []byte) (n int, err error) {
+ switch {
+ case isClosedChan(h.done):
+ return 0, io.ErrClosedPipe
+ case isClosedChan(h.readDeadline.wait()):
+ return 0, os.ErrDeadlineExceeded
+ }
+
+ select {
+ case bw := <-h.readCh:
+ if h.packetHandler != nil {
+ output := h.packetHandler(bw)
+ // nil means the server is closing the connection
+ if output == nil {
+ return 0, io.EOF
+ }
+ nr := copy(b, output)
+ return nr, nil
+ }
+ // bw was copied on write
+ b = bw
+ return len(b), nil
+ case <-h.done:
+ return 0, io.EOF
+ case <-h.readDeadline.wait():
+ return 0, os.ErrDeadlineExceeded
+ }
+}
+
+func (h *hairpin) Write(b []byte) (int, error) {
+ return h.WriteTo(b, hairpinAddress{})
+}
+
+func (h *hairpin) WriteTo(b []byte, _ net.Addr) (int, error) {
+ n, err := h.write(b)
+ if err != nil && err != io.ErrClosedPipe {
+ err = &net.OpError{Op: "write", Net: "Hairpin", Err: err}
+ }
+ return n, err
+}
+
+func (h *hairpin) write(b []byte) (n int, err error) {
+ if len(b) > maxPacketSize {
+ return 0, syscall.EMSGSIZE
+ }
+
+ switch {
+ case isClosedChan(h.done):
+ return 0, io.ErrClosedPipe
+ case isClosedChan(h.writeDeadline.wait()):
+ return 0, os.ErrDeadlineExceeded
+ }
+
+ select {
+ case <-h.done:
+ return n, io.ErrClosedPipe
+ case <-h.writeDeadline.wait():
+ return n, os.ErrDeadlineExceeded
+ default:
+ }
+
+ // Copy the buffer and ensure entirety of b is written together
+ // so the process handler does not mutate the input.
+ h.wrMu.Lock()
+ defer h.wrMu.Unlock()
+ packet := make([]byte, len(b))
+ nr := copy(packet, b)
+ h.readCh <- packet
+ if nr != len(b) {
+ return nr, io.ErrShortWrite
+ }
+ return len(b), nil
+}
+
+func (h *hairpin) SetDeadline(t time.Time) error {
+ if isClosedChan(h.done) {
+ return io.ErrClosedPipe
+ }
+ h.readDeadline.set(t)
+ h.writeDeadline.set(t)
+ return nil
+}
+
+func (h *hairpin) SetReadDeadline(t time.Time) error {
+ if isClosedChan(h.done) {
+ return io.ErrClosedPipe
+ }
+ h.readDeadline.set(t)
+ return nil
+}
+
+func (h *hairpin) SetWriteDeadline(t time.Time) error {
+ if isClosedChan(h.done) {
+ return io.ErrClosedPipe
+ }
+ h.writeDeadline.set(t)
+ return nil
+}
+
+func (l *hairpin) Close() error {
+ l.once.Do(func() { close(l.done) })
+ return nil
+}
+
+// Dialer
+type HairpinDialer struct {
+ PacketHandler packetHandlerFn
+}
+
+// Dial creates an in memory connection that is processed by the packet handler
+func (h *HairpinDialer) Dial(ctx context.Context, network, address string) (net.Conn, error) {
+ conn := Hairpin(h.PacketHandler)
+ conn.SetRemoteAddr(hairpinAddress{
+ addr: address,
+ })
+ return conn, nil
+}
+
+// Listener
+type HairpinListener struct {
+ connPool []net.Conn
+ address string
+
+ PacketHandler packetHandlerFn
+}
+
+var _ net.Listener = &HairpinListener{}
+
+func (h *HairpinListener) Accept() (net.Conn, error) {
+ conn := Hairpin(h.PacketHandler)
+ conn.SetLocalAddr(hairpinAddress{
+ addr: h.address,
+ })
+ return conn, nil
+}
+
+func (h *HairpinListener) Close() error {
+ var aggError error
+ for _, c := range h.connPool {
+ if err := c.Close(); err != nil {
+ aggError = fmt.Errorf("%w", err)
+ }
+ }
+ return aggError
+}
+
+func (h *HairpinListener) Addr() net.Addr {
+ return hairpinAddress{
+ addr: h.address,
+ }
+}
+
+func (h *HairpinListener) Listen(network, address string) (net.Listener, error) {
+ h.address = address
+ return h, nil
+}
diff --git a/resolver/conn_test.go b/resolver/conn_test.go
new file mode 100644
index 0000000..7a38e28
--- /dev/null
+++ b/resolver/conn_test.go
@@ -0,0 +1,470 @@
+// Copyright 2016 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// ref: https://github.com/golang/net/blob/master/nettest/conntest.go
+package resolver
+
+import (
+ "bytes"
+ "encoding/binary"
+ "io"
+ "io/ioutil"
+ "math/rand"
+ "net"
+ "runtime"
+ "sync"
+ "testing"
+ "time"
+)
+
+var (
+ aLongTimeAgo = time.Unix(233431200, 0)
+ neverTimeout = time.Time{}
+)
+
+// TestBasicIO tests that the data sent on c is properly received back on c.
+func TestBasicIO(t *testing.T) {
+ ph := func(b []byte) []byte {
+ if bytes.Equal(b, []byte{0x00}) {
+ return nil
+ }
+ return b
+ }
+ c := Hairpin(ph)
+ want := make([]byte, 1<<20)
+ rand.New(rand.NewSource(0)).Read(want)
+ dataCh := make(chan []byte)
+
+ go func() {
+ rd := bytes.NewReader(want)
+ if err := chunkedCopy(c, rd); err != nil {
+ t.Errorf("unexpected buffer Write error: %v", err)
+ }
+ // we can't close the connection directly until the reader has finished
+ // so we indicate the server to close it after he has processed all the packets
+ c.Write([]byte{0x00})
+ }()
+
+ go func() {
+ wr := new(bytes.Buffer)
+ if err := chunkedCopy(wr, c); err != nil {
+ t.Errorf("unexpected buffer Read error: %v", err)
+ }
+
+ dataCh <- wr.Bytes()
+ }()
+
+ if got := <-dataCh; !bytes.Equal(got, want) {
+ t.Errorf("transmitted data differs, got: %d bytes want: %d bytes", len(got), len(want))
+ }
+
+}
+
+// testPingPong tests that the two endpoints can synchronously send data to
+// each other in a typical request-response pattern.
+func TestPingPong(t *testing.T) {
+ var prev uint64
+ ph := func(buf []byte) []byte {
+ v := binary.LittleEndian.Uint64(buf)
+ binary.LittleEndian.PutUint64(buf, v+1)
+ if prev != 0 && prev+2 != v {
+ t.Errorf("mismatching value: got %d, want %d", v, prev+2)
+ }
+ prev = v
+ // stop processing once we get 1000 pings
+ if v == 1000 {
+ return nil
+ }
+ return buf
+ }
+ c := Hairpin(ph)
+
+ var wg sync.WaitGroup
+ defer wg.Wait()
+
+ pingPonger := func(c net.Conn) {
+ defer wg.Done()
+ buf := make([]byte, 8)
+ var prev uint64
+ for {
+ if _, err := io.ReadFull(c, buf); err != nil {
+ if err == io.EOF {
+ break
+ }
+ t.Errorf("unexpected Read error: %v", err)
+ }
+
+ v := binary.LittleEndian.Uint64(buf)
+ binary.LittleEndian.PutUint64(buf, v+1)
+ if prev != 0 && prev+2 != v {
+ t.Errorf("mismatching value: got %d, want %d", v, prev+2)
+ }
+ prev = v
+ if v == 1001 {
+ break
+ }
+
+ if _, err := c.Write(buf); err != nil {
+ t.Logf("unexpected Write error: %v", err)
+ }
+
+ }
+ if err := c.Close(); err != nil {
+ t.Errorf("unexpected Close error: %v", err)
+ }
+ }
+
+ wg.Add(1)
+ go pingPonger(c)
+
+ // Start off the chain reaction.
+ if _, err := c.Write(make([]byte, 8)); err != nil {
+ t.Errorf("unexpected c.Write error: %v", err)
+ }
+}
+
+// TestRacyRead tests that it is safe to mutate the input Read buffer
+// immediately after cancelation has occurred.
+func TestRacyRead(t *testing.T) {
+ c := Hairpin(nil)
+
+ go chunkedCopy(c, rand.New(rand.NewSource(0)))
+
+ var wg sync.WaitGroup
+ defer wg.Wait()
+
+ c.SetReadDeadline(time.Now().Add(time.Millisecond))
+ for i := 0; i < 10; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+
+ b1 := make([]byte, 1024)
+ b2 := make([]byte, 1024)
+ for j := 0; j < 100; j++ {
+ _, err := c.Read(b1)
+ copy(b1, b2) // Mutate b1 to trigger potential race
+ if err != nil {
+ checkForTimeoutError(t, err)
+ c.SetReadDeadline(time.Now().Add(time.Millisecond))
+ }
+ }
+ }()
+ }
+}
+
+// TestRacyWrite tests that it is safe to mutate the input Write buffer
+// immediately after cancelation has occurred.
+func TestRacyWrite(t *testing.T) {
+ c := Hairpin(nil)
+
+ go chunkedCopy(ioutil.Discard, c)
+
+ var wg sync.WaitGroup
+ defer wg.Wait()
+
+ c.SetWriteDeadline(time.Now().Add(time.Millisecond))
+ for i := 0; i < 10; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+
+ b1 := make([]byte, 1024)
+ b2 := make([]byte, 1024)
+ for j := 0; j < 100; j++ {
+ _, err := c.Write(b1)
+ copy(b1, b2) // Mutate b1 to trigger potential race
+ if err != nil {
+ checkForTimeoutError(t, err)
+ c.SetWriteDeadline(time.Now().Add(time.Millisecond))
+ }
+ }
+ }()
+ }
+}
+
+// testReadTimeout tests that Read timeouts do not affect Write.
+func TestReadTimeout(t *testing.T) {
+ c := Hairpin(nil)
+ go chunkedCopy(ioutil.Discard, c)
+
+ c.SetReadDeadline(aLongTimeAgo)
+ _, err := c.Read(make([]byte, 1024))
+ checkForTimeoutError(t, err)
+ if _, err := c.Write(make([]byte, 1024)); err != nil {
+ t.Errorf("unexpected Write error: %v", err)
+ }
+}
+
+// testWriteTimeout tests that Write timeouts do not affect Read.
+func testWriteTimeout(t *testing.T) {
+ c := Hairpin(nil)
+
+ go chunkedCopy(c, rand.New(rand.NewSource(0)))
+
+ c.SetWriteDeadline(aLongTimeAgo)
+ _, err := c.Write(make([]byte, 1024))
+ checkForTimeoutError(t, err)
+ if _, err := c.Read(make([]byte, 1024)); err != nil {
+ t.Errorf("unexpected Read error: %v", err)
+ }
+}
+
+// testPastTimeout tests that a deadline set in the past immediately times out
+// Read and Write requests.
+func TestPastTimeout(t *testing.T) {
+ c := Hairpin(nil)
+
+ go chunkedCopy(c, c)
+
+ testRoundtrip(t, c)
+
+ c.SetDeadline(aLongTimeAgo)
+ n, err := c.Write(make([]byte, 1024))
+ if n != 0 {
+ t.Errorf("unexpected Write count: got %d, want 0", n)
+ }
+ checkForTimeoutError(t, err)
+ n, err = c.Read(make([]byte, 1024))
+ if n != 0 {
+ t.Errorf("unexpected Read count: got %d, want 0", n)
+ }
+ checkForTimeoutError(t, err)
+
+ testRoundtrip(t, c)
+}
+
+// testPresentTimeout tests that a past deadline set while there are pending
+// Read and Write operations immediately times out those operations.
+func testPresentTimeout(t *testing.T) {
+ c := Hairpin(nil)
+
+ var wg sync.WaitGroup
+ defer wg.Wait()
+ wg.Add(3)
+
+ deadlineSet := make(chan bool, 1)
+ go func() {
+ defer wg.Done()
+ time.Sleep(100 * time.Millisecond)
+ deadlineSet <- true
+ c.SetReadDeadline(aLongTimeAgo)
+ c.SetWriteDeadline(aLongTimeAgo)
+ }()
+ go func() {
+ defer wg.Done()
+ n, err := c.Read(make([]byte, 1024))
+ if n != 0 {
+ t.Errorf("unexpected Read count: got %d, want 0", n)
+ }
+ checkForTimeoutError(t, err)
+ if len(deadlineSet) == 0 {
+ t.Error("Read timed out before deadline is set")
+ }
+ }()
+ go func() {
+ defer wg.Done()
+ var err error
+ for err == nil {
+ _, err = c.Write(make([]byte, 1024))
+ }
+ checkForTimeoutError(t, err)
+ if len(deadlineSet) == 0 {
+ t.Error("Write timed out before deadline is set")
+ }
+ }()
+}
+
+// testFutureTimeout tests that a future deadline will eventually time out
+// Read and Write operations.
+func testFutureTimeout(t *testing.T) {
+ ph := func(b []byte) []byte {
+ return b
+ }
+ c := Hairpin(ph)
+ var wg sync.WaitGroup
+ wg.Add(2)
+
+ c.SetDeadline(time.Now().Add(100 * time.Millisecond))
+ go func() {
+ defer wg.Done()
+ _, err := c.Read(make([]byte, 1024))
+ checkForTimeoutError(t, err)
+ }()
+ go func() {
+ defer wg.Done()
+ var err error
+ for err == nil {
+ _, err = c.Write(make([]byte, 1024))
+ }
+ checkForTimeoutError(t, err)
+ }()
+ wg.Wait()
+
+ go chunkedCopy(c, c)
+ resyncConn(t, c)
+ testRoundtrip(t, c)
+}
+
+// testCloseTimeout tests that calling Close immediately times out pending
+// Read and Write operations.
+func TestCloseTimeout(t *testing.T) {
+ ph := func(b []byte) []byte {
+ return b
+ }
+ c := Hairpin(ph)
+ go chunkedCopy(c, c)
+
+ var wg sync.WaitGroup
+ defer wg.Wait()
+ wg.Add(3)
+
+ // Test for cancelation upon connection closure.
+ c.SetDeadline(neverTimeout)
+ go func() {
+ defer wg.Done()
+ time.Sleep(100 * time.Millisecond)
+ c.Close()
+ }()
+ go func() {
+ defer wg.Done()
+ var err error
+ buf := make([]byte, 1024)
+ for err == nil {
+ _, err = c.Read(buf)
+ }
+ }()
+ go func() {
+ defer wg.Done()
+ var err error
+ buf := make([]byte, 1024)
+ for err == nil {
+ _, err = c.Write(buf)
+ }
+ }()
+}
+
+// testConcurrentMethods tests that the methods of net.Conn can safely
+// be called concurrently.
+func TestConcurrentMethods(t *testing.T) {
+ ph := func(b []byte) []byte {
+ return b
+ }
+ c := Hairpin(ph)
+ if runtime.GOOS == "plan9" {
+ t.Skip("skipping on plan9; see https://golang.org/issue/20489")
+ }
+ go chunkedCopy(c, c)
+
+ // The results of the calls may be nonsensical, but this should
+ // not trigger a race detector warning.
+ var wg sync.WaitGroup
+ for i := 0; i < 100; i++ {
+ wg.Add(7)
+ go func() {
+ defer wg.Done()
+ c.Read(make([]byte, 1024))
+ }()
+ go func() {
+ defer wg.Done()
+ c.Write(make([]byte, 1024))
+ }()
+ go func() {
+ defer wg.Done()
+ c.SetDeadline(time.Now().Add(10 * time.Millisecond))
+ }()
+ go func() {
+ defer wg.Done()
+ c.SetReadDeadline(aLongTimeAgo)
+ }()
+ go func() {
+ defer wg.Done()
+ c.SetWriteDeadline(aLongTimeAgo)
+ }()
+ go func() {
+ defer wg.Done()
+ c.LocalAddr()
+ }()
+ go func() {
+ defer wg.Done()
+ c.RemoteAddr()
+ }()
+ }
+ wg.Wait() // At worst, the deadline is set 10ms into the future
+
+ resyncConn(t, c)
+ testRoundtrip(t, c)
+}
+
+// checkForTimeoutError checks that the error satisfies the Error interface
+// and that Timeout returns true.
+func checkForTimeoutError(t *testing.T, err error) {
+ t.Helper()
+ if nerr, ok := err.(net.Error); ok {
+ if !nerr.Timeout() {
+ t.Errorf("err.Timeout() = false, want true")
+ }
+ } else {
+ t.Errorf("got %T, want net.Error", err)
+ }
+}
+
+// testRoundtrip writes something into c and reads it back.
+// It assumes that everything written into c is echoed back to itself.
+func testRoundtrip(t *testing.T, c net.Conn) {
+ t.Helper()
+
+ if err := c.SetDeadline(neverTimeout); err != nil {
+ t.Errorf("roundtrip SetDeadline error: %v", err)
+ }
+
+ const s = "Hello, world!"
+ buf := []byte(s)
+ if _, err := c.Write(buf); err != nil {
+ t.Errorf("roundtrip Write error: %v", err)
+ }
+ if _, err := io.ReadFull(c, buf); err != nil {
+ t.Errorf("roundtrip Read error: %v", err)
+ }
+ if string(buf) != s {
+ t.Errorf("roundtrip data mismatch: got %q, want %q", buf, s)
+ }
+}
+
+// resyncConn resynchronizes the connection into a sane state.
+// It assumes that everything written into c is echoed back to itself.
+// It assumes that 0xff is not currently on the wire or in the read buffer.
+func resyncConn(t *testing.T, c net.Conn) {
+ t.Helper()
+ c.SetDeadline(neverTimeout)
+ errCh := make(chan error)
+ go func() {
+ _, err := c.Write([]byte{0xff})
+ errCh <- err
+ }()
+ buf := make([]byte, 1024)
+ for {
+ n, err := c.Read(buf)
+ if n > 0 && bytes.IndexByte(buf[:n], 0xff) == n-1 {
+ break
+ }
+ if err != nil {
+ t.Errorf("unexpected Read error: %v", err)
+ break
+ }
+ }
+ if err := <-errCh; err != nil {
+ t.Errorf("unexpected Write error: %v", err)
+ }
+}
+
+// chunkedCopy copies from r to w in fixed-width chunks to avoid
+// causing a Write that exceeds the maximum packet size for packet-based
+// connections like "unixpacket".
+// We assume that the maximum packet size is at least 1024.
+func chunkedCopy(w io.Writer, r io.Reader) error {
+ b := make([]byte, 1024)
+ _, err := io.CopyBuffer(struct{ io.Writer }{w}, struct{ io.Reader }{r}, b)
+ return err
+}
diff --git a/resolver/example_test.go b/resolver/example_test.go
new file mode 100644
index 0000000..b22c06d
--- /dev/null
+++ b/resolver/example_test.go
@@ -0,0 +1,89 @@
+package resolver
+
+import (
+ "context"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "net"
+ "net/http"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+)
+
+const wantName = "testhost.testdomain"
+
+func Test_ExampleCustomResolver(t *testing.T) {
+ http.HandleFunc("/", func(res http.ResponseWriter, req *http.Request) {
+ fmt.Fprint(res, "Hello World!")
+ })
+
+ listener, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ panic(err)
+ }
+ defer listener.Close()
+ go http.Serve(listener, nil)
+
+ // override lookupIP
+ resolver := NewInMemoryResolver(&ResolverStub{
+ LookupIP: myLookupIP,
+ })
+
+ // use the new resolver for an http connection
+ tr := &http.Transport{
+ Proxy: http.ProxyFromEnvironment,
+ DialContext: (&net.Dialer{
+ Timeout: 30 * time.Second,
+ KeepAlive: 30 * time.Second,
+ Resolver: resolver,
+ }).DialContext,
+ }
+
+ client := &http.Client{
+ Transport: tr,
+ }
+
+ // Connect directly to the listener
+ url := "http://" + listener.Addr().String()
+ if err := connect(client, url); err != nil {
+ panic(err)
+ }
+
+ // Connect to the custom domain and check it redirects to localhost
+ url = "http://" + wantName + ":" + strconv.Itoa(listener.Addr().(*net.TCPAddr).Port)
+ if err := connect(client, url); err != nil {
+ panic(err)
+ }
+
+ // Output:
+ // Got response 200 from http://127.0.0.1:44997: Hello World!
+ // Got response 200 from http://testhost.testdomain:44997: Hello World!
+}
+
+func myLookupIP(ctx context.Context, network, host string) ([]net.IP, error) {
+ // fqdn appends a dot
+ if wantName == strings.TrimSuffix(host, ".") {
+ return []net.IP{net.ParseIP("127.0.0.1")}, nil
+ }
+ return net.DefaultResolver.LookupIP(ctx, network, host)
+
+}
+
+func connect(client *http.Client, url string) error {
+ resp, err := client.Get(url)
+ if err != nil {
+ log.Fatalf("Failed get: %s", err)
+ }
+ defer resp.Body.Close()
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return err
+ }
+ fmt.Printf(
+ "Got response %d from %s: %s\n",
+ resp.StatusCode, url, string(body))
+ return nil
+}
diff --git a/resolver/resolver.go b/resolver/resolver.go
new file mode 100644
index 0000000..f1f7779
--- /dev/null
+++ b/resolver/resolver.go
@@ -0,0 +1,202 @@
+package resolver
+
+import (
+ "context"
+ "net"
+
+ "golang.org/x/net/dns/dnsmessage"
+)
+
+// ResolverStub process dns packets executing the corresponding functions overrides if set
+type ResolverStub struct {
+ LookupAddr func(ctx context.Context, addr string) (names []string, err error)
+ LookupCNAME func(ctx context.Context, host string) (cname string, err error)
+ LookupHost func(ctx context.Context, host string) (addrs []string, err error)
+ LookupIP func(ctx context.Context, network, host string) ([]net.IP, error)
+ LookupMX func(ctx context.Context, name string) ([]*net.MX, error)
+ LookupNS func(ctx context.Context, name string) ([]*net.NS, error)
+ LookupPort func(ctx context.Context, network, service string) (port int, err error)
+ LookupSRV func(ctx context.Context, service, proto, name string) (cname string, addrs []*net.SRV, err error)
+ LookupTXT func(ctx context.Context, name string) ([]string, error)
+}
+
+// ProcessDNSRequest is used by the Hairpin connection to process the packets
+// transforming a DNS request to the corresponding Golang Lookup functions
+func (r *ResolverStub) ProcessDNSRequest(b []byte) []byte {
+ var p dnsmessage.Parser
+ hdr, err := p.Start(b)
+ if err != nil {
+ return dnsErrorMessage(dnsmessage.RCodeFormatError)
+ }
+
+ // Only support 1 question, the code in dnsmessage says
+ // https://cs.opensource.google/go/x/net/+/e898025e:dns/dnsmessage/message.go
+ // Multiple questions are valid according to the spec,
+ // but servers don't actually support them. There will
+ // be at most one question here.
+ questions, err := p.AllQuestions()
+ if err != nil {
+ return dnsErrorMessage(dnsmessage.RCodeFormatError)
+ }
+ if len(questions) > 1 {
+ return dnsErrorMessage(dnsmessage.RCodeNotImplemented)
+ } else if len(questions) == 0 {
+ return dnsErrorMessage(dnsmessage.RCodeFormatError)
+ }
+ q := questions[0]
+
+ // Create the answer restricted to 512 bytes (Section 4.2.1 RFC 1035)
+ buf := make([]byte, 2, 514)
+ answer := dnsmessage.NewBuilder(buf,
+ dnsmessage.Header{
+ ID: hdr.ID,
+ Response: true,
+ Authoritative: true,
+ })
+ answer.EnableCompression()
+
+ err = answer.StartQuestions()
+ if err != nil {
+ return dnsErrorMessage(dnsmessage.RCodeServerFailure)
+ }
+ answer.Question(q)
+
+ err = answer.StartAnswers()
+ if err != nil {
+ return dnsErrorMessage(dnsmessage.RCodeServerFailure)
+ }
+ switch q.Type {
+ case dnsmessage.TypeA:
+ if r.LookupIP == nil {
+ return dnsErrorMessage(dnsmessage.RCodeNotImplemented)
+ }
+ addrs, err := r.LookupIP(context.Background(), "ip4", q.Name.String())
+ if err != nil {
+ return dnsErrorMessage(dnsmessage.RCodeServerFailure)
+ }
+
+ for _, ip := range addrs {
+ a := ip.To4()
+ if a == nil {
+ continue
+ }
+ err = answer.AResource(
+ dnsmessage.ResourceHeader{
+ Name: q.Name,
+ Class: q.Class,
+ TTL: 86400,
+ },
+ dnsmessage.AResource{
+ A: [4]byte{a[0], a[1], a[2], a[3]},
+ },
+ )
+ if err != nil {
+ return dnsErrorMessage(dnsmessage.RCodeServerFailure)
+ }
+ }
+
+ case dnsmessage.TypeAAAA:
+ if r.LookupIP == nil {
+ return dnsErrorMessage(dnsmessage.RCodeNotImplemented)
+ }
+ addrs, err := r.LookupIP(context.Background(), "ip6", q.Name.String())
+ if err != nil {
+ return dnsErrorMessage(dnsmessage.RCodeServerFailure)
+ }
+
+ for _, ip := range addrs {
+ if ip.To16() == nil || ip.To4() != nil {
+ continue
+ }
+ var aaaa [16]byte
+ copy(aaaa[:], ip.To16())
+ err = answer.AAAAResource(
+ dnsmessage.ResourceHeader{
+ Name: q.Name,
+ Class: q.Class,
+ TTL: 86400,
+ },
+ dnsmessage.AAAAResource{
+ AAAA: aaaa,
+ },
+ )
+ if err != nil {
+ return dnsErrorMessage(dnsmessage.RCodeServerFailure)
+ }
+ }
+
+ case dnsmessage.TypeNS:
+ if r.LookupNS == nil {
+ return dnsErrorMessage(dnsmessage.RCodeNotImplemented)
+ }
+ // TODO
+ case dnsmessage.TypeCNAME:
+ if r.LookupCNAME == nil {
+ return dnsErrorMessage(dnsmessage.RCodeNotImplemented)
+ }
+ // TODO
+ case dnsmessage.TypeSOA:
+ return dnsErrorMessage(dnsmessage.RCodeNotImplemented)
+ case dnsmessage.TypePTR:
+ if r.LookupAddr == nil {
+ return dnsErrorMessage(dnsmessage.RCodeNotImplemented)
+ }
+ // TODO
+ case dnsmessage.TypeMX:
+ if r.LookupMX == nil {
+ return dnsErrorMessage(dnsmessage.RCodeNotImplemented)
+ }
+ // TODO
+ case dnsmessage.TypeTXT:
+ if r.LookupTXT == nil {
+ return dnsErrorMessage(dnsmessage.RCodeNotImplemented)
+ }
+ // TODO
+ case dnsmessage.TypeSRV:
+ if r.LookupSRV == nil {
+ return dnsErrorMessage(dnsmessage.RCodeNotImplemented)
+ }
+ // TODO
+ case dnsmessage.TypeOPT:
+ return dnsErrorMessage(dnsmessage.RCodeNotImplemented)
+ default:
+ return dnsErrorMessage(dnsmessage.RCodeNotImplemented)
+ }
+
+ buf, err = answer.Finish()
+ if err != nil {
+ return dnsErrorMessage(dnsmessage.RCodeServerFailure)
+ }
+ return buf[2:]
+}
+
+// dnsErrorMessage return an encoded dns error message
+func dnsErrorMessage(rcode dnsmessage.RCode) []byte {
+ msg := dnsmessage.Message{
+ Header: dnsmessage.Header{
+ Response: true,
+ Authoritative: true,
+ RCode: rcode,
+ },
+ }
+ buf, err := msg.Pack()
+ if err != nil {
+ panic(err)
+ }
+ return buf
+}
+
+// NewInMemoryResolver receives a ResolverStub object with the override functions
+// and returns a resolver that can be used as custom Resolver implementation
+func NewInMemoryResolver(d *ResolverStub) *net.Resolver {
+ if d == nil {
+ return net.DefaultResolver
+ }
+
+ return &net.Resolver{
+ PreferGo: true,
+ Dial: (&HairpinDialer{
+ PacketHandler: d.ProcessDNSRequest,
+ }).Dial,
+ }
+}

To view, visit change 347850. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
Gerrit-Change-Number: 347850
Gerrit-PatchSet: 1
Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
Gerrit-MessageType: newchange

Antonio Ojea (Gerrit)

unread,
Sep 7, 2021, 1:03:50 PM9/7/21
to goph...@pubsubhelper.golang.org, Brad Fitzpatrick, golang-co...@googlegroups.com

Attention is currently required from: Brad Fitzpatrick.

View Change

1 comment:

To view, visit change 347850. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
Gerrit-Change-Number: 347850
Gerrit-PatchSet: 1
Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-Comment-Date: Tue, 07 Sep 2021 17:03:44 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
Gerrit-MessageType: comment

Ian Gudger (Gerrit)

unread,
Sep 7, 2021, 1:15:26 PM9/7/21
to Antonio Ojea, goph...@pubsubhelper.golang.org, Brad Fitzpatrick, golang-co...@googlegroups.com

Attention is currently required from: Antonio Ojea, Brad Fitzpatrick.

View Change

2 comments:

    • // Hairpin creates a synchronous, in-memory, packet network connection

    • // implementing the Conn interface. Reads on the connection are matched

    • // with writes, and packets may be processed by the provided hook, if exist

    • // or copied directly; only one packet is buffered to avoid deadlocks.

To view, visit change 347850. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
Gerrit-Change-Number: 347850
Gerrit-PatchSet: 1
Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-CC: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Antonio Ojea <antonio.o...@gmail.com>
Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-Comment-Date: Tue, 07 Sep 2021 17:15:21 +0000

Antonio Ojea (Gerrit)

unread,
Sep 7, 2021, 4:05:51 PM9/7/21
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Attention is currently required from: Antonio Ojea, Brad Fitzpatrick.

Antonio Ojea uploaded patch set #2 to this change.

View Change

x/net: add in-memory fake-DNS package to test custom resolvers

quoting from bradfitx:

"We just need an easy way to wire up high-level Lookup func literals
into fake in-memory DNS-speaking func implementations to assign to
the Resolver.Dial field."

Add a new in-memory packet network connection Hairpin, that loops
packet through it. It allows to set a hook to process these packets.

Add a ResolverStub that allows to override current Lookup func
literals, and use it to process DNS packages.

Using the ResolverStub and the Hairpin connection we can override
the Resolver.Dial field and use it for testing or implement
custom resolver logic.

Signed-off-by: Antonio Ojea <antonio.o...@gmail.com>
Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
---
A resolver/conn.go
A resolver/conn_test.go
A resolver/example_test.go
A resolver/resolver.go
4 files changed, 1,096 insertions(+), 0 deletions(-)

To view, visit change 347850. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
Gerrit-Change-Number: 347850
Gerrit-PatchSet: 2
Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-CC: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Antonio Ojea <antonio.o...@gmail.com>
Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-MessageType: newpatchset

Antonio Ojea (Gerrit)

unread,
Sep 7, 2021, 4:06:33 PM9/7/21
to goph...@pubsubhelper.golang.org, Ian Gudger, Brad Fitzpatrick, golang-co...@googlegroups.com

Attention is currently required from: Ian Gudger, Brad Fitzpatrick.

View Change

2 comments:

  • File resolver/conn.go:

    • Patch Set #1, Line 41:

      // Hairpin creates a synchronous, in-memory, packet network connection
      // implementing the Conn interface. Reads on the connection are matched
      // with writes, and packets may be processed by the provided hook, if exist
      // or copied directly; only one packet is buffered to avoid deadlocks.

    • Can you add how this is different from net. […]

      Ack

  • File resolver/conn_test.go:

    • The tests in nettest consider a connection between 2 endpoints, however, Hairpin just loop through it the packets, so it can not be tested with nettest.
      I've copied the tests from nettest/conntest.go and I've modifed it to run it here.

To view, visit change 347850. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
Gerrit-Change-Number: 347850
Gerrit-PatchSet: 2
Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-CC: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-Comment-Date: Tue, 07 Sep 2021 20:06:27 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
Comment-In-Reply-To: Ian Gudger <i...@iangudger.com>
Gerrit-MessageType: comment

Ian Gudger (Gerrit)

unread,
Sep 7, 2021, 4:44:20 PM9/7/21
to Antonio Ojea, goph...@pubsubhelper.golang.org, Brad Fitzpatrick, golang-co...@googlegroups.com

Attention is currently required from: Antonio Ojea, Brad Fitzpatrick.

View Change

1 comment:

  • File resolver/conn_test.go:

    • The tests in nettest consider a connection between 2 endpoints, however, Hairpin just loop through i […]

      Two things to consider:

      1. net.Pipe works both as a stream oriented and message oriented transport.
      2. The Go DNS resolver has RFC mandated limits in place that prevent it from supporting all possible DNS lookups over UDP.

To view, visit change 347850. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
Gerrit-Change-Number: 347850
Gerrit-PatchSet: 2
Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-CC: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Antonio Ojea <antonio.o...@gmail.com>
Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-Comment-Date: Tue, 07 Sep 2021 20:44:16 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
Comment-In-Reply-To: Antonio Ojea <antonio.o...@gmail.com>

Antonio Ojea (Gerrit)

unread,
Sep 7, 2021, 5:01:01 PM9/7/21
to goph...@pubsubhelper.golang.org, Ian Gudger, Brad Fitzpatrick, golang-co...@googlegroups.com

Attention is currently required from: Ian Gudger, Brad Fitzpatrick.

View Change

1 comment:

  • File resolver/conn_test.go:

    • Two things to consider: […]

      I've considered both too, but packetConn was much simpler, do you prefer to rework it to support both?

To view, visit change 347850. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
Gerrit-Change-Number: 347850
Gerrit-PatchSet: 2
Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-CC: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-Comment-Date: Tue, 07 Sep 2021 21:00:54 +0000

Antonio Ojea (Gerrit)

unread,
Sep 8, 2021, 7:21:09 AM9/8/21
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Attention is currently required from: Ian Gudger, Brad Fitzpatrick.

Antonio Ojea uploaded patch set #3 to this change.

View Change

x/net: add in-memory fake-DNS package to test custom resolvers

quoting from bradfitx:

"We just need an easy way to wire up high-level Lookup func literals
into fake in-memory DNS-speaking func implementations to assign to
the Resolver.Dial field."

Add a new in-memory stream network connection Hairpin, that loops

packet through it. It allows to set a hook to process these packets.

Add a ResolverStub that allows to override current Lookup func
literals, and use it to process DNS packages.

Using the ResolverStub and the Hairpin connection we can override
the Resolver.Dial field and use it for testing or implement
custom resolver logic.

Signed-off-by: Antonio Ojea <antonio.o...@gmail.com>
Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
---
A resolver/conn.go
A resolver/conn_test.go
A resolver/example_test.go
A resolver/resolver.go
4 files changed, 1,105 insertions(+), 0 deletions(-)

To view, visit change 347850. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
Gerrit-Change-Number: 347850
Gerrit-PatchSet: 3
Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-CC: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-MessageType: newpatchset

Antonio Ojea (Gerrit)

unread,
Sep 8, 2021, 7:23:02 AM9/8/21
to goph...@pubsubhelper.golang.org, Ian Gudger, Brad Fitzpatrick, golang-co...@googlegroups.com

Attention is currently required from: Ian Gudger, Brad Fitzpatrick.

View Change

1 comment:

  • File resolver/conn_test.go:

    • I've considered both too, but packetConn was much simpler, do you prefer to rework it to support bot […]

      I've implemented Hairpin as a stream connection, PTAL

To view, visit change 347850. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
Gerrit-Change-Number: 347850
Gerrit-PatchSet: 3
Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-CC: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-Comment-Date: Wed, 08 Sep 2021 11:22:57 +0000

Antonio Ojea (Gerrit)

unread,
Sep 8, 2021, 7:34:35 AM9/8/21
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Attention is currently required from: Ian Gudger, Brad Fitzpatrick.

Antonio Ojea uploaded patch set #4 to this change.

View Change

x/net: add in-memory fake-DNS package to test custom resolvers

quoting from bradfitx:

"We just need an easy way to wire up high-level Lookup func literals
into fake in-memory DNS-speaking func implementations to assign to
the Resolver.Dial field."

Add a new in-memory stream network connection Hairpin, that loops
packet through it. It allows to set a hook to process these packets.

Add a ResolverStub that allows to override current Lookup func
literals, and use it to process DNS packages.

Using the ResolverStub and the Hairpin connection we can override
the Resolver.Dial field and use it for testing or implement
custom resolver logic.

Signed-off-by: Antonio Ojea <antonio.o...@gmail.com>
Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
---
A resolver/conn.go
A resolver/conn_test.go
A resolver/example_test.go
A resolver/resolver.go
4 files changed, 1,097 insertions(+), 0 deletions(-)

To view, visit change 347850. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
Gerrit-Change-Number: 347850
Gerrit-PatchSet: 4
Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-CC: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-MessageType: newpatchset

Antonio Ojea (Gerrit)

unread,
Sep 15, 2021, 8:17:56 AM9/15/21
to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

Attention is currently required from: Ian Gudger, Brad Fitzpatrick.

Antonio Ojea uploaded patch set #5 to this change.

View Change

x/net: add in-memory fake-DNS package to test custom resolvers

quoting from bradfitx:

"We just need an easy way to wire up high-level Lookup func literals
into fake in-memory DNS-speaking func implementations to assign to
the Resolver.Dial field."

Add a new fake-DNS resolver that uses an in memory, half-duplex
connection, that allows to set a hook to process DNS messages.

The resolver adds its own hook to this connection to process the
DNS packets and chose the corredponding Lookup functions, this allows
to override current golang Lookup func literals.


Signed-off-by: Antonio Ojea <antonio.o...@gmail.com>
Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
---
A dns/resolver/dnsdial.go
A dns/resolver/dnsdial_test.go
A dns/resolver/example_test.go
A dns/resolver/resolver.go
A dns/resolver/resolver_test.go
5 files changed, 1,674 insertions(+), 0 deletions(-)

To view, visit change 347850. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
Gerrit-Change-Number: 347850
Gerrit-PatchSet: 5

Antonio Ojea (Gerrit)

unread,
Sep 15, 2021, 8:23:11 AM9/15/21
to goph...@pubsubhelper.golang.org, Ian Gudger, Brad Fitzpatrick, golang-co...@googlegroups.com

Attention is currently required from: Ian Gudger, Brad Fitzpatrick.

View Change

2 comments:

  • Patchset:

    • Patch Set #5:

      Hi Brad, Ian, I´ve updated the patch to simplify the approach and added a bunch of tests.
      Can you PTAL and provide feedback?

  • File dns/resolver/resolver_test.go:

To view, visit change 347850. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
Gerrit-Change-Number: 347850
Gerrit-PatchSet: 5
Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-CC: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-Comment-Date: Wed, 15 Sep 2021 12:23:05 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No
Gerrit-MessageType: comment

Antonio Ojea (Gerrit)

unread,
Sep 27, 2021, 4:59:20 AM9/27/21
to goph...@pubsubhelper.golang.org, Ian Gudger, Brad Fitzpatrick, golang-co...@googlegroups.com

Attention is currently required from: Ian Gudger, Brad Fitzpatrick.

View Change

1 comment:

  • File resolver/conn_test.go:

    • I've implemented Hairpin as a stream connection, PTAL

      Added both stream and message connection transport

To view, visit change 347850. To unsubscribe, or for help writing mail filters, visit settings.

Gerrit-Project: net
Gerrit-Branch: master
Gerrit-Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
Gerrit-Change-Number: 347850
Gerrit-PatchSet: 5
Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-CC: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Ian Gudger <i...@iangudger.com>
Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
Gerrit-Comment-Date: Mon, 27 Sep 2021 08:59:14 +0000
Gerrit-HasComments: Yes
Gerrit-Has-Labels: No

Antonio Ojea (Gerrit)

unread,
Sep 27, 2021, 9:36:08 AM9/27/21
to goph...@pubsubhelper.golang.org, Ian Gudger, Brad Fitzpatrick, golang-co...@googlegroups.com

Attention is currently required from: Ian Gudger, Brad Fitzpatrick.

View Change

2 comments:

  • Patchset:

    • File dns/resolver/resolver_test.go:

      • I have to figure out why this is failing, the synthetic test TestDNSLargeDialTCP in dnsdial_test. […]

        I've found the problem ,golang cast the connection to decide if is UDP or TCP, I need to create two different connections

    To view, visit change 347850. To unsubscribe, or for help writing mail filters, visit settings.

    Gerrit-Project: net
    Gerrit-Branch: master
    Gerrit-Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
    Gerrit-Change-Number: 347850
    Gerrit-PatchSet: 6
    Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-CC: Ian Gudger <i...@iangudger.com>
    Gerrit-Attention: Ian Gudger <i...@iangudger.com>
    Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Comment-Date: Mon, 27 Sep 2021 13:36:01 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: No
    Comment-In-Reply-To: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-MessageType: comment

    Antonio Ojea (Gerrit)

    unread,
    Sep 28, 2021, 9:29:06 AM9/28/21
    to goph...@pubsubhelper.golang.org, golang-co...@googlegroups.com

    Antonio Ojea has uploaded this change for review.

    View Change

    x/net: add in-memory fake-DNS package to test custom resolvers

    Add a new in memory-DNS resolver that allows to override current
    golang Lookup func literals.

    The DNS resolver uses a "hairpin" connection, that is a synchronous,
    in-memory and half-duplex connection. The connection process the
    packets that are written, and sends the output back through the same
    connection.

    Signed-off-by: Antonio Ojea <antonio.o...@gmail.com>
    Change-Id: I5d4742c1539e5d5c0bae1b68c9ca4f0a288d6762
    ---
    A dns/resolver/example_test.go
    A dns/resolver/resolver_unix.go
    A dns/resolver/resolver_unix_test.go
    A netutil/hairpin/README.md
    A netutil/hairpin/conn.go
    A netutil/hairpin/hairpin.go
    A netutil/hairpin/hairpin_test.go
    A netutil/hairpin/packet_hairpin.go
    A netutil/hairpin/packet_hairpin_test.go
    9 files changed, 2,013 insertions(+), 0 deletions(-)

    diff --git a/dns/resolver/example_test.go b/dns/resolver/example_test.go
    new file mode 100644
    index 0000000..ca26feb
    --- /dev/null
    +++ b/dns/resolver/example_test.go
    @@ -0,0 +1,85 @@
    +	f := &MemResolver{
    + LookupIP: myLookupIP,

    + }
    + // override lookupIP
    +	resolver := NewMemoryResolver(f)

    + // use the new resolver for an http connection
    + tr := &http.Transport{
    + Proxy: http.ProxyFromEnvironment,
    + DialContext: (&net.Dialer{
    + Timeout: 30 * time.Second,
    + KeepAlive: 30 * time.Second,
    + Resolver: resolver,
    + }).DialContext,
    + }
    + client := &http.Client{
    + Transport: tr,
    + }
    + // Connect directly to the listener
    + url := "http://" + listener.Addr().String()
    + if err := connect(client, url); err != nil {
    + panic(err)
    + }
    + // Connect to the custom domain and check it redirects to localhost
    + url = "http://" + wantName + ":" + strconv.Itoa(listener.Addr().(*net.TCPAddr).Port)
    + if err := connect(client, url); err != nil {
    + panic(err)
    + }
    +	// Connect to an external server
    + url = "http://www.google.es"

    + if err := connect(client, url); err != nil {
    + panic(err)
    + }
    +	// Output:
    + // Got response 200 from http://127.0.0.1:44997: Hello World!
    + // Got response 200 from http://testhost.testdomain:44997: Hello World!
    +}
    +func myLookupIP(ctx context.Context, network, host string) ([]net.IP, error) {
    + // fqdn appends a dot
    + if wantName == strings.TrimSuffix(host, ".") {
    + return []net.IP{net.ParseIP("127.0.0.1")}, nil
    + }
    + return net.DefaultResolver.LookupIP(ctx, network, host)
    +}
    +func connect(client *http.Client, url string) error {
    + resp, err := client.Get(url)
    + if err != nil {
    + log.Fatalf("Failed get: %s", err)
    + }
    + defer resp.Body.Close()
    + body, err := ioutil.ReadAll(resp.Body)
    + if err != nil {
    + return err
    + }
    + fmt.Printf(
    + "Got response %d from %s: %s\n",
    +		resp.StatusCode, url, string(body)[:12])
    + return nil
    +}
    diff --git a/dns/resolver/resolver_unix.go b/dns/resolver/resolver_unix.go
    new file mode 100644
    index 0000000..f063d80
    --- /dev/null
    +++ b/dns/resolver/resolver_unix.go
    @@ -0,0 +1,440 @@
    +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
    +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
    +

    +package resolver
    +
    +import (
    + "context"
    +	"encoding/binary"
    + "net"
    + "strings"
    +
    + "golang.org/x/net/dns/dnsmessage"
    + "golang.org/x/net/netutil/hairpin"
    +)
    +
    +const ttl = 300
    +
    +// MemResolver implement an in memory resolver that receives DNS questions and
    +// executes the corresponding Lookup functions. If the corresponding Lookup
    +// function is not present, it uses the DefaultResolver ones.
    +type MemResolver struct {

    + LookupAddr func(ctx context.Context, addr string) (names []string, err error)
    + LookupCNAME func(ctx context.Context, host string) (cname string, err error)
    + LookupHost func(ctx context.Context, host string) (addrs []string, err error)
    + LookupIP func(ctx context.Context, network, host string) ([]net.IP, error)
    + LookupMX func(ctx context.Context, name string) ([]*net.MX, error)
    + LookupNS func(ctx context.Context, name string) ([]*net.NS, error)
    + LookupPort func(ctx context.Context, network, service string) (port int, err error)
    + LookupSRV func(ctx context.Context, service, proto, name string) (cname string, addrs []*net.SRV, err error)
    + LookupTXT func(ctx context.Context, name string) ([]string, error)
    +	// Add new lookup functions here
    + // LookupSOA https://github.com/golang/go/issues/35061
    +
    +}
    +
    +func (r *MemResolver) dnsStreamRoundTrip(b []byte) []byte {
    + // As per RFC 1035, TCP DNS messages are preceded by a 16 bit size, skip first 2 bytes.
    + b = b[2:]
    +

    + var p dnsmessage.Parser
    + hdr, err := p.Start(b)
    + if err != nil {
    +		return dnsErrorMessage(hdr.ID, dnsmessage.RCodeFormatError, dnsmessage.Question{})
    + }
    + // Only support 1 question, ref:

    + // https://cs.opensource.google/go/x/net/+/e898025e:dns/dnsmessage/message.go
    + // Multiple questions are valid according to the spec,
    + // but servers don't actually support them. There will
    + // be at most one question here.
    + questions, err := p.AllQuestions()
    + if err != nil {
    +		return dnsErrorMessage(hdr.ID, dnsmessage.RCodeFormatError, dnsmessage.Question{})

    + }
    + if len(questions) > 1 {
    +		return dnsErrorMessage(hdr.ID, dnsmessage.RCodeNotImplemented, dnsmessage.Question{})

    + } else if len(questions) == 0 {
    +		return dnsErrorMessage(hdr.ID, dnsmessage.RCodeFormatError, dnsmessage.Question{})
    + }
    +
    + b = r.ProcessDNSRequest(hdr.ID, questions[0])
    + hdrLen := make([]byte, 2)
    + binary.BigEndian.PutUint16(hdrLen, uint16(len(b)))
    + return append(hdrLen, b...)
    +}
    +
    +func (r *MemResolver) dnsPacketRoundTrip(b []byte) []byte {

    + var p dnsmessage.Parser
    + hdr, err := p.Start(b)
    + if err != nil {
    +		return dnsErrorMessage(hdr.ID, dnsmessage.RCodeFormatError, dnsmessage.Question{})
    + }
    + // RFC1035 max 512 bytes for UDP
    + if len(b) > 512 {
    + return dnsErrorMessage(hdr.ID, dnsmessage.RCodeFormatError, dnsmessage.Question{})
    + }
    +
    + // Only support 1 question, ref:

    + // https://cs.opensource.google/go/x/net/+/e898025e:dns/dnsmessage/message.go
    + // Multiple questions are valid according to the spec,
    + // but servers don't actually support them. There will
    + // be at most one question here.
    + questions, err := p.AllQuestions()
    + if err != nil {
    +		return dnsErrorMessage(hdr.ID, dnsmessage.RCodeFormatError, dnsmessage.Question{})

    + }
    + if len(questions) > 1 {
    +		return dnsErrorMessage(hdr.ID, dnsmessage.RCodeNotImplemented, dnsmessage.Question{})

    + } else if len(questions) == 0 {
    +		return dnsErrorMessage(hdr.ID, dnsmessage.RCodeFormatError, dnsmessage.Question{})
    + }
    +
    + answer := r.ProcessDNSRequest(hdr.ID, questions[0])
    + // Return a truncated packet if the answer is too big
    + if len(answer) > 512 {
    + answer = dnsTruncatedMessage(hdr.ID, questions[0])
    + }
    +
    + return answer
    +}
    +
    +// ProcessDNSRequest implements dnsHandlerFunc so it can be used in a MemResolver
    +// transforming a DNS request to the corresponding Golang Lookup functions.
    +func (r *MemResolver) ProcessDNSRequest(id uint16, q dnsmessage.Question) []byte {
    + // DNS packet length is encoded in 2 bytes
    + buf := []byte{}

    + answer := dnsmessage.NewBuilder(buf,
    + dnsmessage.Header{
    +			ID:            id,

    + Response: true,
    + Authoritative: true,
    + })
    + answer.EnableCompression()
    + err := answer.StartQuestions()
    + if err != nil {
    +		return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + answer.Question(q)

    + err = answer.StartAnswers()
    + if err != nil {
    +		return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)

    + }
    + switch q.Type {
    + case dnsmessage.TypeA:
    +		addrs, err := r.lookupIP(context.Background(), "ip4", q.Name.String())

    + if err != nil {
    +			return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)

    + }
    + for _, ip := range addrs {
    + a := ip.To4()
    + if a == nil {
    + continue
    + }
    + err = answer.AResource(
    + dnsmessage.ResourceHeader{
    + Name: q.Name,
    + Class: q.Class,
    +					TTL:   ttl,

    + },
    + dnsmessage.AResource{
    + A: [4]byte{a[0], a[1], a[2], a[3]},
    + },
    + )
    + if err != nil {
    +				return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)

    + }
    + }
    + case dnsmessage.TypeAAAA:
    +		addrs, err := r.lookupIP(context.Background(), "ip6", q.Name.String())

    + if err != nil {
    +			return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)

    + }
    + for _, ip := range addrs {
    + if ip.To16() == nil || ip.To4() != nil {
    + continue
    + }
    + var aaaa [16]byte
    + copy(aaaa[:], ip.To16())
    + err = answer.AAAAResource(
    + dnsmessage.ResourceHeader{
    + Name: q.Name,
    + Class: q.Class,
    +					TTL:   ttl,

    + },
    + dnsmessage.AAAAResource{
    + AAAA: aaaa,
    + },
    + )
    + if err != nil {
    +				return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)

    + }
    + }
    + case dnsmessage.TypeNS:
    +		nsList, err := r.lookupNS(context.Background(), q.Name.String())

    + if err != nil {
    +			return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + for _, ns := range nsList {
    + name, err := dnsmessage.NewName(ns.Host)

    + if err != nil {
    +				return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + err = answer.NSResource(

    + dnsmessage.ResourceHeader{
    + Name: q.Name,
    + Class: q.Class,
    +					TTL:   ttl,
    + },
    + dnsmessage.NSResource{
    + NS: name,

    + },
    + )
    + if err != nil {
    +				return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + }
    + case dnsmessage.TypeCNAME:
    + cname, err := r.lookupCNAME(context.Background(), q.Name.String())

    + if err != nil {
    +			return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + name, err := dnsmessage.NewName(cname)

    + if err != nil {
    +			return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + err = answer.CNAMEResource(

    + dnsmessage.ResourceHeader{
    + Name: q.Name,
    + Class: q.Class,
    +				TTL:   ttl,
    + },
    + dnsmessage.CNAMEResource{
    + CNAME: name,

    + },
    + )
    + if err != nil {
    +			return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + case dnsmessage.TypeSOA:

    + // TODO
    + case dnsmessage.TypeMX:
    +		mxList, err := r.lookupMX(context.Background(), q.Name.String())

    + if err != nil {
    +			return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + for _, mx := range mxList {
    + name, err := dnsmessage.NewName(mx.Host)

    + if err != nil {
    +				return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + err = answer.MXResource(

    + dnsmessage.ResourceHeader{
    + Name: q.Name,
    + Class: q.Class,
    +					TTL:   ttl,
    + },
    + dnsmessage.MXResource{
    + MX: name,
    + Pref: mx.Pref,

    + },
    + )
    + if err != nil {
    +				return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + }
    + case dnsmessage.TypeTXT:
    + // You can enter a value of up to 255 characters in one string in a TXT record.
    + // You can add multiple strings of 255 characters in a single TXT record.
    + txt, err := r.lookupTXT(context.Background(), q.Name.String())

    + if err != nil {
    +			return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + err = answer.TXTResource(

    + dnsmessage.ResourceHeader{
    + Name: q.Name,
    + Class: q.Class,
    +				TTL:   ttl,
    + },
    + dnsmessage.TXTResource{
    + TXT: txt,

    + },
    + )
    + if err != nil {
    +			return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + case dnsmessage.TypeSRV:
    + // WIP
    + _, srvList, err := r.lookupSRV(context.Background(), "", "", q.Name.String())

    + if err != nil {
    +			return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + for _, srv := range srvList {
    + target, err := dnsmessage.NewName(srv.Target)

    + if err != nil {
    +				return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + err = answer.SRVResource(

    + dnsmessage.ResourceHeader{
    + Name: q.Name,
    + Class: q.Class,
    +					TTL:   ttl,
    + },
    + dnsmessage.SRVResource{
    + Target: target,
    + Priority: srv.Priority,
    + Weight: srv.Weight,
    + Port: srv.Port,

    + },
    + )
    + if err != nil {
    +				return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + }
    + case dnsmessage.TypePTR:
    + names, err := r.LookupAddr(context.Background(), q.Name.String())

    + if err != nil {
    +			return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + for _, n := range names {
    + name, err := dnsmessage.NewName(n)

    + if err != nil {
    +				return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + err = answer.PTRResource(

    + dnsmessage.ResourceHeader{
    + Name: q.Name,
    + Class: q.Class,
    +					TTL:   ttl,
    + },
    + dnsmessage.PTRResource{
    + PTR: name,

    + },
    + )
    + if err != nil {
    +				return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)
    + }
    + }
    + default:
    + return dnsErrorMessage(id, dnsmessage.RCodeNotImplemented, q)

    + }
    + buf, err = answer.Finish()
    + if err != nil {
    +		return dnsErrorMessage(id, dnsmessage.RCodeServerFailure, q)

    + }
    + return buf
    +}
    +func (r *MemResolver) lookupAddr(ctx context.Context, addr string) (names []string, err error) {
    + if r.LookupAddr != nil {
    + return r.LookupAddr(ctx, addr)
    + }
    + return net.DefaultResolver.LookupAddr(ctx, addr)
    +}
    +func (r *MemResolver) lookupCNAME(ctx context.Context, host string) (cname string, err error) {
    + if r.LookupCNAME != nil {
    + return r.LookupCNAME(ctx, host)
    + }
    + return net.DefaultResolver.LookupCNAME(ctx, host)
    +}
    +func (r *MemResolver) lookupHost(ctx context.Context, host string) (addrs []string, err error) {
    + if r.LookupHost != nil {
    + return r.LookupHost(ctx, host)
    + }
    + return net.DefaultResolver.LookupHost(ctx, host)
    +}
    +func (r *MemResolver) lookupIP(ctx context.Context, network, host string) ([]net.IP, error) {
    + if r.LookupIP != nil {
    + return r.LookupIP(ctx, network, host)

    + }
    + return net.DefaultResolver.LookupIP(ctx, network, host)
    +}
    +func (r *MemResolver) lookupMX(ctx context.Context, name string) ([]*net.MX, error) {
    + if r.LookupMX != nil {
    + return r.LookupMX(ctx, name)
    + }
    + return net.DefaultResolver.LookupMX(ctx, name)
    +}
    +func (r *MemResolver) lookupNS(ctx context.Context, name string) ([]*net.NS, error) {
    + if r.LookupNS != nil {
    + return r.LookupNS(ctx, name)
    + }
    + return net.DefaultResolver.LookupNS(ctx, name)
    +}
    +func (r *MemResolver) lookupPort(ctx context.Context, network, service string) (port int, err error) {
    + if r.LookupPort != nil {
    + return r.LookupPort(ctx, network, service)
    + }
    + return net.DefaultResolver.LookupPort(ctx, network, service)
    +}
    +func (r *MemResolver) lookupSRV(ctx context.Context, service, proto, name string) (cname string, addrs []*net.SRV, err error) {
    + if r.LookupSRV != nil {
    + return r.LookupSRV(ctx, service, proto, name)
    + }
    + return net.DefaultResolver.LookupSRV(ctx, service, proto, name)
    +}
    +func (r *MemResolver) lookupTXT(ctx context.Context, name string) ([]string, error) {
    + if r.LookupTXT != nil {
    + return r.LookupTXT(ctx, name)
    + }
    + return net.DefaultResolver.LookupTXT(ctx, name)

    +}
    +
    +// Dial creates an in memory connection
    +func (r *MemResolver) Dial(ctx context.Context, network, address string) (net.Conn, error) {
    + if strings.Contains(network, "tcp") {
    + h := hairpin.HairpinDialer{
    + PacketHandler: r.dnsStreamRoundTrip,
    + }
    + return h.Dial(ctx, network, address)
    + }
    + h := hairpin.PacketHairpinDialer{
    + PacketHandler: r.dnsPacketRoundTrip,
    + }
    + return h.Dial(ctx, network, address)
    +}
    +
    +// MemoryResolver returns an in-memory resolver that can override golang Lookup
    +// functions.
    +func NewMemoryResolver(r *MemResolver) *net.Resolver {
    + if r == nil {
    + r = &MemResolver{}

    + }
    + return &net.Resolver{
    + PreferGo: true,
    +		Dial:     r.Dial,
    + }

    +}
    +
    +// dnsErrorMessage return an encoded dns error message
    +func dnsErrorMessage(id uint16, rcode dnsmessage.RCode, q dnsmessage.Question) []byte {

    + msg := dnsmessage.Message{
    + Header: dnsmessage.Header{
    +			ID:            id,

    + Response: true,
    + Authoritative: true,
    + RCode: rcode,
    + },
    +		Questions: []dnsmessage.Question{q},

    + }
    + buf, err := msg.Pack()
    + if err != nil {
    + panic(err)
    + }
    + return buf
    +}
    +
    +func dnsTruncatedMessage(id uint16, q dnsmessage.Question) []byte {

    + msg := dnsmessage.Message{
    + Header: dnsmessage.Header{
    +			ID:            id,

    + Response: true,
    + Authoritative: true,
    +			Truncated:     true,
    + },
    + Questions: []dnsmessage.Question{q},

    + }
    + buf, err := msg.Pack()
    + if err != nil {
    + panic(err)
    + }
    + return buf
    +}
    diff --git a/dns/resolver/resolver_unix_test.go b/dns/resolver/resolver_unix_test.go
    new file mode 100644
    index 0000000..7f95014
    --- /dev/null
    +++ b/dns/resolver/resolver_unix_test.go
    @@ -0,0 +1,293 @@
    +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
    +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
    +
    +package resolver
    +
    +// https://github.com/golang/go/blob/master/src/net/lookup_test.go

    +import (
    + "context"
    + "fmt"
    +	"net"
    + "strings"
    + "testing"
    +)
    +
    +func hasSuffixFold(s, suffix string) bool {
    + return strings.HasSuffix(strings.ToLower(s), strings.ToLower(suffix))
    +}
    +func TestLookupNS(t *testing.T) {
    + t.Parallel()
    + var lookupGmailNSTests = []struct {
    + name, host string
    + }{
    + {"gmail.com", "google.com."},
    + {"gmail.com.", "google.com."},
    + {"gmail1.com.", "google2.com."},
    + }
    + f := &MemResolver{

    + LookupNS: func(ctx context.Context, name string) ([]*net.NS, error) {
    +			switch name {
    + case "gmail.com.":
    + return []*net.NS{&net.NS{Host: "google.com."}}, nil
    + case "gmail1.com.":
    + return []*net.NS{&net.NS{Host: "google2.com."}, &net.NS{Host: "Google2.com."}}, nil
    + default:
    + return nil, fmt.Errorf("error")
    + }
    + },
    + }
    + r := NewMemoryResolver(f)
    + for i := 0; i < len(lookupGmailNSTests); i++ {
    + tt := lookupGmailNSTests[i]
    + nss, err := r.LookupNS(context.Background(), tt.name)

    + if err != nil {
    +			t.Fatal(err)
    + }
    + if len(nss) == 0 {
    + t.Error("got no record")
    + }
    + for _, ns := range nss {
    + if !hasSuffixFold(ns.Host, tt.host) {
    + t.Errorf("got %v; want a record containing %s", ns, tt.host)
    + }
    + }
    + }
    +}
    +func TestLookupTXT(t *testing.T) {
    + t.Parallel()
    + var lookupGmailTXTTests = []struct {
    + name, txt, host string
    + }{
    + {"gmail.com", "spf", "fakegoogle.com"},
    + {"gmail.com.", "spf", "fakegoogle.com"},
    + {"gmail1.com.", "spf", "fakegoogle2.com"},
    + }
    + f := &MemResolver{

    + LookupTXT: func(ctx context.Context, name string) ([]string, error) {
    +			switch name {
    + case "gmail.com.":
    + return []string{"spf", "google.com", "fakegoogle.com"}, nil
    + case "gmail1.com.":
    + return []string{"spf", "google.com", "fakegoogle2.com"}, nil
    + default:
    + return nil, fmt.Errorf("error")
    + }
    + },
    + }
    + r := NewMemoryResolver(f)
    + for i := 0; i < len(lookupGmailTXTTests); i++ {
    + tt := lookupGmailTXTTests[i]
    + txts, err := r.LookupTXT(context.Background(), tt.name)

    + if err != nil {
    +			t.Fatal(err)
    + }
    + if len(txts) == 0 {
    + t.Error("got no record")
    + }
    + found := false
    + for _, txt := range txts {
    + if strings.Contains(txt, tt.txt) && (strings.HasSuffix(txt, tt.host) || strings.HasSuffix(txt, tt.host+".")) {
    + found = true
    + break
    + }
    + }
    + if !found {
    + t.Errorf("got %v; want a record containing %s, %s", txts, tt.txt, tt.host)
    + }
    + }
    +}
    +func TestLookupAddr(t *testing.T) {
    + t.Parallel()
    + var lookupGooglePublicDNSAddrTests = []string{
    + "8.8.8.8",
    + "8.8.4.4",
    + "2001:4860:4860::8888",
    + "2001:4860:4860::8844",
    + }
    + f := &MemResolver{

    + LookupAddr: func(ctx context.Context, addr string) (names []string, err error) {
    +			switch addr {
    + default:
    + return []string{"test.google.com.", "test.golang.com."}, nil
    + }
    + },
    + }
    + r := NewMemoryResolver(f)
    + for _, ip := range lookupGooglePublicDNSAddrTests {
    + names, err := r.LookupAddr(context.Background(), ip)

    + if err != nil {
    +			t.Fatal(err)
    + }
    + if len(names) != 2 {
    + t.Errorf("expected 2 records, got %d records", len(names))
    + }
    + if names[0] != "test.google.com." {
    + t.Errorf("got %q; want a record test.google.com.", names[0])
    + }
    + if names[1] != "test.golang.com." {
    + t.Errorf("got %q; want a record test.golang.com.", names[1])
    + }
    + }
    +}
    +func TestLookupLongTXT(t *testing.T) {
    + // resolver_test.go:164: lookup golang.rsc.io on 127.0.0.53:53: cannot unmarshal DNS message
    + want := []string{
    + strings.Repeat("abcde12345", 25), // 10 * 25 = 250
    + strings.Repeat("abcde12345", 25), // 10 * 25 = 250
    + strings.Repeat("abcde12345", 25), // 10 * 25 = 250
    + strings.Repeat("abcde12345", 25), // 10 * 25 = 250
    + }
    + f := &MemResolver{

    + LookupTXT: func(ctx context.Context, name string) ([]string, error) {
    +			switch name {
    + default:
    + return want, nil
    + }
    + },
    + }
    + r := NewMemoryResolver(f)
    + txts, err := r.LookupTXT(context.Background(), "golang.rsc.io")

    + if err != nil {
    +		t.Fatal(err)
    + }
    + // golang concatenate returned strings
    + var sb strings.Builder
    + for i := range want {
    + sb.WriteString(want[i])
    + }
    + if txts[0] != sb.String() {
    + t.Fatalf("LookupTXT golang.rsc.io incorrect\nhave %q\nwant %q", txts, sb.String())
    + }
    +}
    +func TestLookupIP(t *testing.T) {
    + t.Parallel()
    + var lookupGoogleIPTests = []struct {
    + name string
    + network string
    + }{
    + {"google.com", "ip4"},
    + {"google.com.", "ip"},
    + {"fakegoogle.com.", "ip6"},
    + }
    + f := &MemResolver{

    + LookupIP: func(ctx context.Context, network, host string) ([]net.IP, error) {
    +			if network == "ip6" {
    + return []net.IP{net.ParseIP("2001:db8::1")}, nil
    + }
    + switch host {
    + default:
    + return []net.IP{net.ParseIP("127.8.8.8"), net.ParseIP("169.254.0.1")}, nil
    + }
    + },
    + }
    + r := NewMemoryResolver(f)
    + for _, tt := range lookupGoogleIPTests {
    + ips, err := r.LookupIP(context.Background(), tt.network, tt.name)

    + if err != nil {
    +			t.Fatal(err)
    + }
    + if len(ips) == 0 {
    + t.Error("got no record")
    + }
    + if tt.network == "ip6" {
    + if ips[0].String() != "2001:db8::1" {
    + t.Errorf("got %v; want IP 2001:db8::1", ips[0])
    + }
    + return
    + }
    + if ips[0].String() != "127.8.8.8" {
    + t.Errorf("got %v; want IP 127.8.8.8", ips[0])
    + }
    + if ips[1].String() != "169.254.0.1" {
    + t.Errorf("got %v; want IP 169.254.0.1", ips[1])
    + }
    + }
    +}
    +func TestLookupCNAME(t *testing.T) {
    + t.Parallel()
    + // This is actually doing A and AAAA requests
    + t.Skip()
    + var lookupCNAMETests = []struct {
    + name, cname string
    + }{
    + {"www.iana.org", "icann.org."},
    + {"www.iana.org.", "icann.org."},
    + {"www.google.com", "google.com."},
    + }
    + f := &MemResolver{
    + LookupCNAME: func(ctx context.Context, host string) (string, error) {
    + switch {
    + case strings.Contains(host, "iana"):
    + return "icann.org.", nil
    + default:
    + return "google.com.", nil
    + }
    + },

    + LookupIP: func(ctx context.Context, network, host string) ([]net.IP, error) {
    +			return []net.IP{net.ParseIP("127.8.8.8"), net.ParseIP("169.254.0.1")}, nil
    + },
    + }
    + r := NewMemoryResolver(f)
    + for i := 0; i < len(lookupCNAMETests); i++ {
    + tt := lookupCNAMETests[i]
    + cname, err := r.LookupCNAME(context.Background(), tt.name)

    + if err != nil {
    +			t.Fatal(err)
    + }
    + if !hasSuffixFold(cname, tt.cname) {
    + t.Errorf("got %s; want a record containing %s", cname, tt.cname)
    + }
    + }
    +}
    +func TestLookupMX(t *testing.T) {
    + t.Parallel()
    + var lookupMXTests = []struct {
    + name, host string
    + }{
    + {"gmail.com", "google.com."},
    + {"gmail2.com.", "google2.com."},
    + }
    + f := &MemResolver{
    + LookupMX: func(ctx context.Context, host string) ([]*net.MX, error) {
    + var mxHost string
    + switch host {
    + case "gmail.com.":
    + mxHost = "google.com."
    + case "gmail2.com.":
    + mxHost = "google2.com."
    + default:
    + return nil, fmt.Errorf("error")
    + }
    + mxs := []*net.MX{
    + &net.MX{
    + Host: mxHost,
    + Pref: 10,
    + },
    + &net.MX{
    + Host: mxHost,
    + Pref: 10,
    + },
    + }
    + return mxs, nil
    + },
    + }
    + r := NewMemoryResolver(f)
    + for i := 0; i < len(lookupMXTests); i++ {
    + tt := lookupMXTests[i]
    + mxs, err := r.LookupMX(context.Background(), tt.name)

    + if err != nil {
    +			t.Fatal(err)
    + }
    + if len(mxs) == 0 {
    + t.Error("got no record")
    + }
    + for _, mx := range mxs {
    + if !hasSuffixFold(mx.Host, tt.host) {
    + t.Errorf("got %s; want a record containing %s", mx.Host, tt.name)
    + }
    + if mx.Pref != 10 {
    + t.Errorf("got %d; want a record prefix of 10", mx.Pref)
    + }
    + }
    + }
    +}
    diff --git a/netutil/hairpin/README.md b/netutil/hairpin/README.md
    new file mode 100644
    index 0000000..59ed8fd
    --- /dev/null
    +++ b/netutil/hairpin/README.md
    @@ -0,0 +1,19 @@
    +# hairpin
    +
    +Hairpin creates a synchronous, in-memory, half-duplex connection.
    +
    +Hairpin implements golang net.Conn interface and is very useful for protocol testing.
    +
    +PacketHairpin implements golang net.PacketConn interface.
    +
    +
    +## How it works
    +
    +The data written in the connection is executed by the Packet Process Handler and written to a buffer.
    +
    +If there is no Packet Process Handler defined, the data is written directly to the buffer.
    +
    +Operations are serialized, once a packet is processed, Writes() are blocked until the processed packet is Read().
    +
    +Partial Reads are allowed, but Writes() will not be unblocked until the Read() buffer is fully drained.
    +
    diff --git a/netutil/hairpin/conn.go b/netutil/hairpin/conn.go
    new file mode 100644
    index 0000000..aec6bd1
    --- /dev/null
    +++ b/netutil/hairpin/conn.go
    @@ -0,0 +1,206 @@
    +package hairpin

    +
    +import (
    + "bytes"
    +	"io"
    + "os"
    + "sync"
    + "time"
    +)
    +
    +// packetHandlerFunc signature for the function used to process the connection packets
    +type packetHandlerFunc func(b []byte) []byte
    +
    +// conn implements a synchronous, half-duplex in memory connection. Writes on
    +// the connection are processed by the packetHandler hook, if exist, and send
    +// back to the same connection.
    +type conn struct {

    + wrMu sync.Mutex // Serialize Write operations
    +	rdMu sync.Mutex // Serialize Read operation

    +
    + readCh chan []byte // Used to communicate Write and Read
    +	readBuffer bytes.Buffer

    +
    + once sync.Once // Protects closing the connection
    + done chan struct{}
    +
    + readDeadline connDeadline
    + writeDeadline connDeadline
    +
    +	packetHandler packetHandlerFunc // packet handler hook
    +}
    +
    +func newConn(fn packetHandlerFunc) conn {
    + return conn{
    + readCh: make(chan []byte, 1), // Serialize
    + readBuffer: bytes.Buffer{},

    + done: make(chan struct{}),
    + readDeadline: makeConnDeadline(),
    + writeDeadline: makeConnDeadline(),
    + packetHandler: fn,
    + }
    +}
    +
    +// connection parameters (same as net.Pipe)

    +// https://cs.opensource.google/go/go/+/refs/tags/go1.17:src/net/pipe.go;bpv=0;bpt=1
    +
    +// connDeadline is an abstraction for handling timeouts.
    +type connDeadline struct {
    + mu sync.Mutex // Guards timer and cancel
    + timer *time.Timer
    + cancel chan struct{} // Must be non-nil
    +}
    +
    +func makeConnDeadline() connDeadline {
    + return connDeadline{cancel: make(chan struct{})}
    +}
    +
    +// set sets the point in time when the deadline will time out.
    +// A timeout event is signaled by closing the channel returned by waiter.
    +// Once a timeout has occurred, the deadline can be refreshed by specifying a
    +// t value in the future.
    +//
    +// A zero value for t prevents timeout.
    +func (c *connDeadline) set(t time.Time) {
    + c.mu.Lock()
    + defer c.mu.Unlock()
    +
    + if c.timer != nil && !c.timer.Stop() {
    + <-c.cancel // Wait for the timer callback to finish and close cancel
    + }
    + c.timer = nil

    +
    + // Time is zero, then there is no deadline.
    +	closed := isClosedChan(c.cancel)

    + if t.IsZero() {
    + if closed {
    +			c.cancel = make(chan struct{})

    + }
    + return
    + }
    +
    + // Time in the future, setup a timer to cancel in the future.
    + if dur := time.Until(t); dur > 0 {
    + if closed {
    +			c.cancel = make(chan struct{})
    + }
    + c.timer = time.AfterFunc(dur, func() {
    + close(c.cancel)

    + })
    + return
    + }
    +
    + // Time in the past, so close immediately.
    + if !closed {
    +		close(c.cancel)
    + }
    +}
    +
    +// wait returns a channel that is closed when the deadline is exceedec.
    +func (c *connDeadline) wait() chan struct{} {
    + c.mu.Lock()
    + defer c.mu.Unlock()
    + return c.cancel

    +}
    +
    +func isClosedChan(c <-chan struct{}) bool {
    + select {
    + case <-c:
    + return true
    + default:
    + return false
    + }
    +}
    +
    +func (c *conn) SetDeadline(t time.Time) error {
    + if isClosedChan(c.done) {
    + return io.ErrClosedPipe
    + }
    + c.readDeadline.set(t)
    + c.writeDeadline.set(t)

    + return nil
    +}
    +
    +func (c *conn) SetReadDeadline(t time.Time) error {
    + if isClosedChan(c.done) {
    + return io.ErrClosedPipe
    + }
    + c.readDeadline.set(t)

    + return nil
    +}
    +
    +func (c *conn) SetWriteDeadline(t time.Time) error {
    + if isClosedChan(c.done) {
    + return io.ErrClosedPipe
    + }
    + c.writeDeadline.set(t)

    + return nil
    +}
    +
    +func (c *conn) Close() error {
    + c.once.Do(func() { close(c.done) })

    + return nil
    +}
    +
    +func (c *conn) Read(b []byte) (n int, err error) {
    + c.rdMu.Lock()
    + defer c.rdMu.Unlock()
    +
    + if len(b) == 0 {

    + return 0, io.EOF
    + }
    +
    +	switch {
    + case isClosedChan(c.done):
    + return 0, io.ErrClosedPipe
    + case isClosedChan(c.readDeadline.wait()):

    + return 0, os.ErrDeadlineExceeded
    + }
    +
    +	// if the buffer was drained wait for new data
    + if c.readBuffer.Len() == 0 {
    + select {
    + case <-c.done:
    + return 0, io.EOF
    + case <-c.readDeadline.wait():
    + return 0, os.ErrDeadlineExceeded
    + case bw := <-c.readCh:
    + c.readBuffer.Write(bw)
    + }
    + }
    + return c.readBuffer.Read(b)
    +}
    +
    +func (c *conn) Write(b []byte) (n int, err error) {
    + switch {
    + case isClosedChan(c.done):
    + return 0, io.ErrClosedPipe
    + case isClosedChan(c.writeDeadline.wait()):

    + return 0, os.ErrDeadlineExceeded
    + }
    +
    +	// ensure the buffer is processed together
    + c.wrMu.Lock()
    + defer c.wrMu.Unlock()
    + buf := make([]byte, len(b))
    + nr := copy(buf, b)
    +
    + select {
    + case <-c.done:
    + return n, io.ErrClosedPipe
    + case <-c.writeDeadline.wait():
    + return n, os.ErrDeadlineExceeded
    + case c.readCh <- c.transport()(buf):

    + return nr, nil
    + }
    +
    +}
    +
    +func (c *conn) transport() packetHandlerFunc {
    + if c.packetHandler != nil {
    + return c.packetHandler
    + }
    + // if no packet handler function was defined the connection
    + // sends back the packets directly
    + return func(b []byte) []byte { return b }
    +}
    diff --git a/netutil/hairpin/hairpin.go b/netutil/hairpin/hairpin.go
    new file mode 100644
    index 0000000..d5becd2
    --- /dev/null
    +++ b/netutil/hairpin/hairpin.go
    @@ -0,0 +1,97 @@
    +package hairpin

    +
    +import (
    + "context"
    + "fmt"
    + "io"
    + "net"
    +)
    +
    +type hairpin struct {
    + conn
    +}
    +
    +// hairpin implements net.Conn interface
    +var _ net.Conn = &hairpin{}

    +
    +func (h *hairpin) Read(b []byte) (int, error) {
    +	n, err := h.conn.Read(b)

    + if err != nil && err != io.EOF && err != io.ErrClosedPipe {
    + err = &net.OpError{Op: "read", Net: "Hairpin", Err: err}
    + }
    +	return n, err
    +}
    +

    +func (h *hairpin) Write(b []byte) (int, error) {
    +	n, err := h.conn.Write(b)

    + if err != nil && err != io.ErrClosedPipe {
    + err = &net.OpError{Op: "write", Net: "Hairpin", Err: err}
    + }
    + return n, err
    +}
    +
    +type hairpinAddress struct{}

    +
    +func (h hairpinAddress) Network() string {
    +	return "Hairpin"
    +}
    +func (h hairpinAddress) String() string {
    +	return "Hairpin"
    +}
    +
    +func (h *hairpin) LocalAddr() net.Addr {
    + return hairpinAddress{}
    +}

    +func (h *hairpin) RemoteAddr() net.Addr {
    +	return hairpinAddress{}
    +}
    +
    +// Hairpin creates a half-duplex, in-memory, synchronous stream connection where
    +// data written on the connection is processed by an optional hook and then read
    +// back on the same connection. Reads and Write are serialized, Writes are
    +// blocked by Reads.
    +func Hairpin(fn packetHandlerFunc) *hairpin {
    + return &hairpin{newConn(fn)}

    +}
    +
    +// Dialer
    +type HairpinDialer struct {
    +	PacketHandler packetHandlerFunc

    +}
    +
    +// Dial creates an in memory connection that is processed by the packet handler
    +func (h *HairpinDialer) Dial(ctx context.Context, network, address string) (net.Conn, error) {
    +	return Hairpin(h.PacketHandler), nil

    +}
    +
    +// Listener
    +type HairpinListener struct {
    + connPool []net.Conn
    +
    +	PacketHandler packetHandlerFunc

    +}
    +
    +var _ net.Listener = &HairpinListener{}
    +
    +func (h *HairpinListener) Accept() (net.Conn, error) {
    + conn := Hairpin(h.PacketHandler)
    +	return conn, nil
    +}
    +
    +func (h *HairpinListener) Close() error {
    + var aggError error
    + for _, c := range h.connPool {
    + if err := c.Close(); err != nil {
    + aggError = fmt.Errorf("%w", err)
    + }
    + }
    + return aggError
    +}
    +
    +func (h *HairpinListener) Addr() net.Addr {
    + return hairpinAddress{}
    +}
    +
    +func (h *HairpinListener) Listen(network, address string) (net.Listener, error) {
    +	return h, nil
    +}
    diff --git a/netutil/hairpin/hairpin_test.go b/netutil/hairpin/hairpin_test.go
    new file mode 100644
    index 0000000..46c3674
    --- /dev/null
    +++ b/netutil/hairpin/hairpin_test.go
    @@ -0,0 +1,421 @@
    +// Based on golang tests
    +// ref: https://github.com/golang/net/blob/master/nettest/conntest.go
    +
    +package hairpin

    +
    +import (
    + "bytes"
    + "encoding/binary"
    + "io"
    + "io/ioutil"
    + "math/rand"
    + "net"
    + "runtime"
    + "sync"
    + "testing"
    + "time"
    +)
    +
    +	go chunkedCopy(c, rand.New(rand.NewSource(0)))

    + var wg sync.WaitGroup
    + defer wg.Wait()
    +	c.SetReadDeadline(time.Now().Add(time.Millisecond))
    + for i := 0; i < 10; i++ {
    + wg.Add(1)
    + go func() {
    + defer wg.Done()
    + b1 := make([]byte, 1024)
    + b2 := make([]byte, 1024)
    + for j := 0; j < 100; j++ {
    + _, err := c.Read(b1)
    + copy(b1, b2) // Mutate b1 to trigger potential race
    + if err != nil {
    + checkForTimeoutError(t, err)
    + c.SetReadDeadline(time.Now().Add(time.Millisecond))
    + }
    + }
    + }()
    + }
    +}
    +
    +// TestRacyWrite tests that it is safe to mutate the input Write buffer
    +// immediately after cancelation has occurred.
    +func TestRacyWrite(t *testing.T) {
    + c := Hairpin(nil)
    +	go chunkedCopy(ioutil.Discard, c)

    + var wg sync.WaitGroup
    + defer wg.Wait()
    +// testPastTimeout tests that a deadline set in the past immediately times out
    +// Read and Write requests.
    +func TestPastTimeout(t *testing.T) {
    + c := Hairpin(nil)
    +	go chunkedCopy(c, c)
    + testRoundtrip(t, c)

    + c.SetDeadline(aLongTimeAgo)
    + n, err := c.Write(make([]byte, 1024))
    + if n != 0 {
    + t.Errorf("unexpected Write count: got %d, want 0", n)
    + }
    + checkForTimeoutError(t, err)
    + n, err = c.Read(make([]byte, 1024))
    + if n != 0 {
    + t.Errorf("unexpected Read count: got %d, want 0", n)
    + }
    + checkForTimeoutError(t, err)
    + testRoundtrip(t, c)
    +}
    +
    +// testPresentTimeout tests that a past deadline set while there are pending
    +// Read and Write operations immediately times out those operations.
    +func TestPresentTimeout(t *testing.T) {

    + ph := func(b []byte) []byte {
    +		// block until deadline is set
    + time.Sleep(200 * time.Millisecond)
    + return b
    + }
    + c := PacketHairpin(ph)
    +func TestFutureTimeout(t *testing.T) {

    + ph := func(b []byte) []byte {
    +		// block until deadline is set
    + time.Sleep(300 * time.Millisecond)
    +	var wg sync.WaitGroup
    + defer wg.Wait()
    + wg.Add(3)
    diff --git a/netutil/hairpin/packet_hairpin.go b/netutil/hairpin/packet_hairpin.go
    new file mode 100644
    index 0000000..473df79
    --- /dev/null
    +++ b/netutil/hairpin/packet_hairpin.go
    @@ -0,0 +1,96 @@
    +package hairpin

    +
    +import (
    + "context"
    + "fmt"
    + "io"
    + "net"
    +)
    +
    +type packetHairpin struct {
    + conn
    +}
    +
    +// packetHairpin implements net.PacketConn interface
    +var _ net.PacketConn = &packetHairpin{}
    +
    +func (p *packetHairpin) ReadFrom(b []byte) (int, net.Addr, error) {
    + n, err := p.conn.Read(b)

    + if err != nil && err != io.EOF && err != io.ErrClosedPipe {
    +		err = &net.OpError{Op: "read", Net: "PacketHairpin", Err: err}
    + }
    + return n, packetHairpinAddress{}, err
    +}
    +
    +func (p *packetHairpin) WriteTo(b []byte, _ net.Addr) (int, error) {
    + n, err := p.conn.Write(b)

    + if err != nil && err != io.ErrClosedPipe {
    +		err = &net.OpError{Op: "write", Net: "PacketHairpin", Err: err}

    + }
    + return n, err
    +}
    +
    +type packetHairpinAddress struct{}
    +
    +func (p packetHairpinAddress) Network() string {
    + return "packetHairpin"
    +}
    +func (p packetHairpinAddress) String() string {
    + return "packetHairpin"
    +}
    +
    +func (p *packetHairpin) LocalAddr() net.Addr {
    + return packetHairpinAddress{}
    +}
    +func (p *packetHairpin) RemoteAddr() net.Addr {
    + return packetHairpinAddress{}
    +}
    +
    +// packetHairpin creates a half-duplex, in-memory, synchronous packet connection where
    +// data written on the connection is processed by an optional hook and then read
    +// back on the same connection. Reads and Write are serialized, Writes are
    +// blocked by Reads.
    +func PacketHairpin(fn packetHandlerFunc) *packetHairpin {
    + return &packetHairpin{newConn(fn)}
    +}
    +
    +// Dialer
    +type PacketHairpinDialer struct {
    + PacketHandler packetHandlerFunc

    +}
    +
    +// Dial creates an in memory connection that is processed by the packet handler
    +func (p *PacketHairpinDialer) Dial(ctx context.Context, network, address string) (net.Conn, error) {
    + return PacketHairpin(p.PacketHandler), nil
    +}
    +
    +// Listener
    +type PacketHairpinListener struct {
    + connPool []net.PacketConn
    +
    + PacketHandler packetHandlerFunc
    +}
    +
    +var _ net.Listener = &PacketHairpinListener{}
    +
    +func (p *PacketHairpinListener) Accept() (net.Conn, error) {
    + return PacketHairpin(p.PacketHandler), nil
    +}
    +
    +func (p *PacketHairpinListener) Close() error {
    + var aggError error
    + for _, c := range p.connPool {

    + if err := c.Close(); err != nil {
    + aggError = fmt.Errorf("%w", err)
    + }
    + }
    + return aggError
    +}
    +
    +func (p *PacketHairpinListener) Addr() net.Addr {
    + return packetHairpinAddress{}
    +}
    +
    +func (p *PacketHairpinListener) Listen(network, address string) (net.Listener, error) {
    + return p, nil
    +}
    diff --git a/netutil/hairpin/packet_hairpin_test.go b/netutil/hairpin/packet_hairpin_test.go
    new file mode 100644
    index 0000000..b19621d
    --- /dev/null
    +++ b/netutil/hairpin/packet_hairpin_test.go
    @@ -0,0 +1,356 @@
    +// Based on golang tests
    +// ref: https://github.com/golang/net/blob/master/nettest/conntest.go
    +
    +package hairpin

    +
    +import (
    + "bytes"
    + "encoding/binary"
    + "io"
    + "io/ioutil"
    + "math/rand"
    + "net"
    + "runtime"
    + "sync"
    + "testing"
    + "time"
    +)
    +
    +var (
    + aLongTimeAgo = time.Unix(233431200, 0)
    + neverTimeout = time.Time{}
    +)
    +
    +// TestBasicIO tests that the data sent on c is properly received back on c.
    +func TestPacketBasicIO(t *testing.T) {

    + ph := func(b []byte) []byte {
    + if bytes.Equal(b, []byte{0x00}) {
    + return nil
    + }
    + return b
    + }
    +	c := PacketHairpin(ph)

    + want := make([]byte, 1<<20)
    + rand.New(rand.NewSource(0)).Read(want)
    + dataCh := make(chan []byte)
    + go func() {
    + rd := bytes.NewReader(want)
    + if err := chunkedCopy(c, rd); err != nil {
    + t.Errorf("unexpected buffer Write error: %v", err)
    + }
    + // we can't close the connection directly until the reader has finished
    + // so we indicate the server to close it after he has processed all the packets
    + c.Write([]byte{0x00})
    + }()
    + go func() {
    + wr := new(bytes.Buffer)
    + if err := chunkedCopy(wr, c); err != nil {
    + t.Errorf("unexpected buffer Read error: %v", err)
    + }
    + dataCh <- wr.Bytes()
    + }()
    + if got := <-dataCh; !bytes.Equal(got, want) {
    + t.Errorf("transmitted data differs, got: %d bytes want: %d bytes", len(got), len(want))
    + }
    +}
    +
    +// testPingPong tests that the two endpoints can synchronously send data to
    +// each other in a typical request-response pattern.
    +func TestPacketPingPong(t *testing.T) {

    + var prev uint64
    + ph := func(buf []byte) []byte {
    + v := binary.LittleEndian.Uint64(buf)
    + binary.LittleEndian.PutUint64(buf, v+1)
    + if prev != 0 && prev+2 != v {
    + t.Errorf("mismatching value: got %d, want %d", v, prev+2)
    + }
    + prev = v
    + // stop processing once we get 1000 pings
    + if v == 1000 {
    + return nil
    + }
    + return buf
    + }
    +	c := PacketHairpin(ph)
    +func TestPacketRacyRead(t *testing.T) {
    + c := PacketHairpin(nil)
    + go chunkedCopy(c, rand.New(rand.NewSource(0)))

    + var wg sync.WaitGroup
    + defer wg.Wait()
    +	c.SetReadDeadline(time.Now().Add(time.Millisecond))
    + for i := 0; i < 10; i++ {
    + wg.Add(1)
    + go func() {
    + defer wg.Done()
    + b1 := make([]byte, 1024)
    + b2 := make([]byte, 1024)
    + for j := 0; j < 100; j++ {
    + _, err := c.Read(b1)
    + copy(b1, b2) // Mutate b1 to trigger potential race
    + if err != nil {
    + checkForTimeoutError(t, err)
    + c.SetReadDeadline(time.Now().Add(time.Millisecond))
    + }
    + }
    + }()
    + }
    +}
    +
    +// TestRacyWrite tests that it is safe to mutate the input Write buffer
    +// immediately after cancelation has occurred.
    +func TestPacketRacyWrite(t *testing.T) {
    + c := PacketHairpin(nil)
    + go chunkedCopy(ioutil.Discard, c)

    + var wg sync.WaitGroup
    + defer wg.Wait()
    +	c.SetWriteDeadline(time.Now().Add(time.Millisecond))
    + for i := 0; i < 10; i++ {
    + wg.Add(1)
    + go func() {
    + defer wg.Done()
    + b1 := make([]byte, 1024)
    + b2 := make([]byte, 1024)
    + for j := 0; j < 100; j++ {
    + _, err := c.Write(b1)
    + copy(b1, b2) // Mutate b1 to trigger potential race
    + if err != nil {
    + checkForTimeoutError(t, err)
    + c.SetWriteDeadline(time.Now().Add(time.Millisecond))
    + }
    + }
    + }()
    + }
    +}
    +
    +// testReadTimeout tests that Read timeouts do not affect Write.
    +func TestPacketReadTimeout(t *testing.T) {
    + c := PacketHairpin(nil)
    + go chunkedCopy(ioutil.Discard, c)

    + c.SetReadDeadline(aLongTimeAgo)
    + _, err := c.Read(make([]byte, 1024))
    + checkForTimeoutError(t, err)
    + if _, err := c.Write(make([]byte, 1024)); err != nil {
    + t.Errorf("unexpected Write error: %v", err)
    + }
    +}
    +
    +// testPastTimeout tests that a deadline set in the past immediately times out
    +// Read and Write requests.
    +func TestPacketPastTimeout(t *testing.T) {
    + c := PacketHairpin(nil)
    + go chunkedCopy(c, c)
    + testRoundtrip(t, c)

    + c.SetDeadline(aLongTimeAgo)
    + n, err := c.Write(make([]byte, 1024))
    + if n != 0 {
    + t.Errorf("unexpected Write count: got %d, want 0", n)
    + }
    + checkForTimeoutError(t, err)
    + n, err = c.Read(make([]byte, 1024))
    + if n != 0 {
    + t.Errorf("unexpected Read count: got %d, want 0", n)
    + }
    + checkForTimeoutError(t, err)
    + testRoundtrip(t, c)
    +}
    +
    +// testPresentTimeout tests that a past deadline set while there are pending
    +// Read and Write operations immediately times out those operations.
    +func TestPacketPresentTimeout(t *testing.T) {

    + ph := func(b []byte) []byte {
    +		// block until deadline is set
    + time.Sleep(200 * time.Millisecond)

    + return b
    + }
    + c := Hairpin(ph)
    + var wg sync.WaitGroup
    +func TestPacketFutureTimeout(t *testing.T) {

    + ph := func(b []byte) []byte {
    +		// block until deadline is set
    + time.Sleep(300 * time.Millisecond)
    + return b
    + }
    + c := PacketHairpin(ph)

    + var wg sync.WaitGroup
    + wg.Add(2)
    + c.SetDeadline(time.Now().Add(100 * time.Millisecond))
    + go func() {
    + defer wg.Done()
    + _, err := c.Read(make([]byte, 1024))
    + checkForTimeoutError(t, err)
    + }()
    + go func() {
    + defer wg.Done()
    + var err error
    + for err == nil {
    + _, err = c.Write(make([]byte, 1024))
    + }
    + checkForTimeoutError(t, err)
    + }()
    + wg.Wait()
    + go chunkedCopy(c, c)
    + resyncConn(t, c)
    + testRoundtrip(t, c)
    +}
    +
    +// testCloseTimeout tests that calling Close immediately times out pending
    +// Read and Write operations.
    +func TestPacketCloseTimeout(t *testing.T) {

    + ph := func(b []byte) []byte {
    + return b
    + }
    +	c := PacketHairpin(ph)
    + go chunkedCopy(c, c)

    + var wg sync.WaitGroup
    + defer wg.Wait()
    + wg.Add(3)
    +func TestPacketConcurrentMethods(t *testing.T) {

    + ph := func(b []byte) []byte {
    + return b
    + }
    +	c := PacketHairpin(ph)
    +	resyncConn(t, c)
    + testRoundtrip(t, c)
    +}

    To view, visit change 352769. To unsubscribe, or for help writing mail filters, visit settings.

    Gerrit-Project: net
    Gerrit-Branch: master
    Gerrit-Change-Id: I5d4742c1539e5d5c0bae1b68c9ca4f0a288d6762
    Gerrit-Change-Number: 352769
    Gerrit-PatchSet: 1
    Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-MessageType: newchange

    Antonio Ojea (Gerrit)

    unread,
    Sep 28, 2021, 9:30:44 AM9/28/21
    to goph...@pubsubhelper.golang.org, Ian Gudger, Brad Fitzpatrick, golang-co...@googlegroups.com

    Antonio Ojea abandoned this change.

    View Change

    To view, visit change 347850. To unsubscribe, or for help writing mail filters, visit settings.

    Gerrit-Project: net
    Gerrit-Branch: master
    Gerrit-Change-Id: Ib1a23f160e2a7104c016d6b7b8c991d5be62f6c8
    Gerrit-Change-Number: 347850
    Gerrit-PatchSet: 6
    Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-CC: Ian Gudger <i...@iangudger.com>
    Gerrit-MessageType: abandon

    Antonio Ojea (Gerrit)

    unread,
    Sep 28, 2021, 9:32:13 AM9/28/21
    to goph...@pubsubhelper.golang.org, Ian Gudger, golang-co...@googlegroups.com

    Attention is currently required from: Ian Gudger.

    View Change

    1 comment:

    • Patchset:

      • Patch Set #1:

        hi Ian, I got it working for TCP and UDP, it also supports DNS fallback from UDP to TCP for truncated message (there is a test verifying it)

    To view, visit change 352769. To unsubscribe, or for help writing mail filters, visit settings.

    Gerrit-Project: net
    Gerrit-Branch: master
    Gerrit-Change-Id: I5d4742c1539e5d5c0bae1b68c9ca4f0a288d6762
    Gerrit-Change-Number: 352769
    Gerrit-PatchSet: 1
    Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-Reviewer: Ian Gudger <i...@iangudger.com>
    Gerrit-Attention: Ian Gudger <i...@iangudger.com>
    Gerrit-Comment-Date: Tue, 28 Sep 2021 13:32:09 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: No
    Gerrit-MessageType: comment

    Ian Gudger (Gerrit)

    unread,
    Sep 28, 2021, 12:57:04 PM9/28/21
    to Antonio Ojea, goph...@pubsubhelper.golang.org, Brad Fitzpatrick, golang-co...@googlegroups.com

    Attention is currently required from: Antonio Ojea, Brad Fitzpatrick.

    View Change

    3 comments:

    • File dns/resolver/resolver_unix.go:

      • Patch Set #1, Line 36: dnsStreamRoundTrip

        This is basically a simple DNS server. My preference would be adding a general purpose DNS server over a very specialized one.

      • Patch Set #1, Line 82:

        questions, err := p.AllQuestions()
        if err != nil {


      • return dnsErrorMessage(hdr.ID, dnsmessage.RCodeFormatError, dnsmessage.Question{})
        }

      • 	if len(questions) > 1 {


      • return dnsErrorMessage(hdr.ID, dnsmessage.RCodeNotImplemented, dnsmessage.Question{})

      • 	} else if len(questions) == 0 {

      • 		return dnsErrorMessage(hdr.ID, dnsmessage.RCodeFormatError, dnsmessage.Question{})
        }

    To view, visit change 352769. To unsubscribe, or for help writing mail filters, visit settings.

    Gerrit-Project: net
    Gerrit-Branch: master
    Gerrit-Change-Id: I5d4742c1539e5d5c0bae1b68c9ca4f0a288d6762
    Gerrit-Change-Number: 352769
    Gerrit-PatchSet: 1
    Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Reviewer: Ian Gudger <i...@iangudger.com>
    Gerrit-Attention: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Comment-Date: Tue, 28 Sep 2021 16:57:01 +0000

    Antonio Ojea (Gerrit)

    unread,
    Sep 28, 2021, 5:13:27 PM9/28/21
    to goph...@pubsubhelper.golang.org, Brad Fitzpatrick, Ian Gudger, golang-co...@googlegroups.com

    Attention is currently required from: Ian Gudger, Brad Fitzpatrick.

    View Change

    3 comments:

    • File dns/resolver/resolver_unix.go:

      • This is basically a simple DNS server. […]

        Yeah, this is targeting the mocking of the Lookup functions, I've tried to make it very simple: A and AAAA LookupIP, CNAME lookupCNAME, ... I've never targeted a general DNS server, sorry for the confusion

      • Patch Set #1, Line 82:

        questions, err := p.AllQuestions()
        if err != nil {
        return dnsErrorMessage(hdr.ID, dnsmessage.RCodeFormatError, dnsmessage.Question{})
        }
        if len(questions) > 1 {
        return dnsErrorMessage(hdr.ID, dnsmessage.RCodeNotImplemented, dnsmessage.Question{})
        } else if len(questions) == 0 {
        return dnsErrorMessage(hdr.ID, dnsmessage.RCodeFormatError, dnsmessage.Question{})
        }

      • Consider calling p.Question and p.SkipQuestion. […]

        nice tip, thanks

    • File netutil/hairpin/conn.go:


      • > We just need an easy way to wire up high-level Lookup func literals into fake in-memory DNS-speaking func implementations to assign to the Resolver.Dial field.

      • This "Hairpin" connection, basically converts a connection in a function, arguments are passed by Write() and the result is obtained with Read().
        This is very useful for testing, since the connection is synchronous, operations are serialized avoiding possible race conditions with multiple writes, and simplifying the server code.
        You can mock any high level protocol like DNS just with a few lines of code just like I'm doing here in the resolver_unix.go file

    To view, visit change 352769. To unsubscribe, or for help writing mail filters, visit settings.

    Gerrit-Project: net
    Gerrit-Branch: master
    Gerrit-Change-Id: I5d4742c1539e5d5c0bae1b68c9ca4f0a288d6762
    Gerrit-Change-Number: 352769
    Gerrit-PatchSet: 1
    Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Reviewer: Ian Gudger <i...@iangudger.com>
    Gerrit-Attention: Ian Gudger <i...@iangudger.com>
    Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Comment-Date: Tue, 28 Sep 2021 21:13:21 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: No

    Ian Gudger (Gerrit)

    unread,
    Sep 28, 2021, 5:14:43 PM9/28/21
    to Antonio Ojea, goph...@pubsubhelper.golang.org, Brad Fitzpatrick, golang-co...@googlegroups.com

    Attention is currently required from: Antonio Ojea, Brad Fitzpatrick.

    View Change

    1 comment:

    • File netutil/hairpin/conn.go:

      • oh, I didn't understand the first comment about net.Pipe, sorry, let me explain it better. […]

        net.Pipe is synchronous.

    To view, visit change 352769. To unsubscribe, or for help writing mail filters, visit settings.

    Gerrit-Project: net
    Gerrit-Branch: master
    Gerrit-Change-Id: I5d4742c1539e5d5c0bae1b68c9ca4f0a288d6762
    Gerrit-Change-Number: 352769
    Gerrit-PatchSet: 1
    Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Reviewer: Ian Gudger <i...@iangudger.com>
    Gerrit-Attention: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Comment-Date: Tue, 28 Sep 2021 21:14:39 +0000
    Gerrit-HasComments: Yes
    Gerrit-Has-Labels: No
    Comment-In-Reply-To: Antonio Ojea <antonio.o...@gmail.com>

    Antonio Ojea (Gerrit)

    unread,
    Sep 28, 2021, 5:26:50 PM9/28/21
    to goph...@pubsubhelper.golang.org, Brad Fitzpatrick, Ian Gudger, golang-co...@googlegroups.com

    Attention is currently required from: Ian Gudger, Brad Fitzpatrick.

    View Change

    1 comment:

    • File netutil/hairpin/conn.go:

      • net.Pipe is synchronous.

        ... but you have to create a server in one of the sides of the pipe, right?

        This connection is basically used to mock the server, you don't need to spawn a goroutine to handle the server side of the connection, just only using a function with the "business logic" and Write() -> Function() -> Read(), the tests are much simpler and you remove the server dependency, that is always much complex and may have errors.

    To view, visit change 352769. To unsubscribe, or for help writing mail filters, visit settings.

    Gerrit-Project: net
    Gerrit-Branch: master
    Gerrit-Change-Id: I5d4742c1539e5d5c0bae1b68c9ca4f0a288d6762
    Gerrit-Change-Number: 352769
    Gerrit-PatchSet: 1
    Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Reviewer: Ian Gudger <i...@iangudger.com>
    Gerrit-Attention: Ian Gudger <i...@iangudger.com>
    Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Comment-Date: Tue, 28 Sep 2021 21:26:44 +0000

    Ian Gudger (Gerrit)

    unread,
    Sep 28, 2021, 6:24:42 PM9/28/21
    to Antonio Ojea, goph...@pubsubhelper.golang.org, Brad Fitzpatrick, golang-co...@googlegroups.com

    Attention is currently required from: Antonio Ojea, Brad Fitzpatrick.

    View Change

    1 comment:

    • File netutil/hairpin/conn.go:

      • ... but you have to create a server in one of the sides of the pipe, right? […]

        It would be a lot simpler than the Hairpin connection.

    To view, visit change 352769. To unsubscribe, or for help writing mail filters, visit settings.

    Gerrit-Project: net
    Gerrit-Branch: master
    Gerrit-Change-Id: I5d4742c1539e5d5c0bae1b68c9ca4f0a288d6762
    Gerrit-Change-Number: 352769
    Gerrit-PatchSet: 1
    Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Reviewer: Ian Gudger <i...@iangudger.com>
    Gerrit-Attention: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Comment-Date: Tue, 28 Sep 2021 22:24:38 +0000

    Antonio Ojea (Gerrit)

    unread,
    Sep 29, 2021, 3:14:40 AM9/29/21
    to goph...@pubsubhelper.golang.org, Brad Fitzpatrick, Ian Gudger, golang-co...@googlegroups.com

    Attention is currently required from: Ian Gudger, Brad Fitzpatrick.

    View Change

    1 comment:

    • File netutil/hairpin/conn.go:

      • It would be a lot simpler than the Hairpin connection.

        I see your point and is fair enough, my idea of Hairpin is to use it for testing other protocols too, like SMTP per example ...

        Coming back to net.Pipe, you still have the problem of net.PacketConn if you want to mimic the fallback from UDP to TCP on large/truncated messages, the dnsclient cast the connection to use UDP

    To view, visit change 352769. To unsubscribe, or for help writing mail filters, visit settings.

    Gerrit-Project: net
    Gerrit-Branch: master
    Gerrit-Change-Id: I5d4742c1539e5d5c0bae1b68c9ca4f0a288d6762
    Gerrit-Change-Number: 352769
    Gerrit-PatchSet: 1
    Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Reviewer: Ian Gudger <i...@iangudger.com>
    Gerrit-Attention: Ian Gudger <i...@iangudger.com>
    Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Comment-Date: Wed, 29 Sep 2021 07:14:34 +0000

    Ian Gudger (Gerrit)

    unread,
    Sep 29, 2021, 10:07:57 AM9/29/21
    to Antonio Ojea, goph...@pubsubhelper.golang.org, Brad Fitzpatrick, golang-co...@googlegroups.com

    Attention is currently required from: Antonio Ojea, Brad Fitzpatrick.

    View Change

    1 comment:

    • File netutil/hairpin/conn.go:

      • I see your point and is fair enough, my idea of Hairpin is to use it for testing other protocols too […]

        That is true, but wrapping the connections in something that makes them implement net.PacketConn is pretty straightforward.

        ```
        type unbufferedPacketConn struct {
        net.Conn
        }

        var _ net.PacketConn = (*unbufferedPacketConn)(nil)

        // ReadFrom implements net.PacketConn.ReadFrom.
        func (c unbufferedPacketConn) ReadFrom(b []byte) (int, net.Addr, error) {
        n, err := c.Read(b)
        return n, c.RemoteAddr(), err
        }
        // WriteTo implements net.PacketConn.WriteTo.
        //
        // addr is ignored.
        func (c unbufferedPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) {
        return c.Write(b)
        }
        ```

    To view, visit change 352769. To unsubscribe, or for help writing mail filters, visit settings.

    Gerrit-Project: net
    Gerrit-Branch: master
    Gerrit-Change-Id: I5d4742c1539e5d5c0bae1b68c9ca4f0a288d6762
    Gerrit-Change-Number: 352769
    Gerrit-PatchSet: 1
    Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Reviewer: Ian Gudger <i...@iangudger.com>
    Gerrit-Attention: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Comment-Date: Wed, 29 Sep 2021 14:07:52 +0000

    Antonio Ojea (Gerrit)

    unread,
    Sep 29, 2021, 10:55:49 AM9/29/21
    to goph...@pubsubhelper.golang.org, Brad Fitzpatrick, Ian Gudger, golang-co...@googlegroups.com

    Attention is currently required from: Ian Gudger, Brad Fitzpatrick.

    View Change

    1 comment:

    • File netutil/hairpin/conn.go:

      • That is true, but wrapping the connections in something that makes them implement net. […]

        I see, so the glue here will be to create a custom Dial() that creates a net.Pipe() and "attach" one side of the connection to a "simple" DNS server, the other side will be used by the Resolver.

        The "simple" DNS Server can have a pool of conns and process each request, as I have now with the resolver_linux.go basically.

        Am I close?

    To view, visit change 352769. To unsubscribe, or for help writing mail filters, visit settings.

    Gerrit-Project: net
    Gerrit-Branch: master
    Gerrit-Change-Id: I5d4742c1539e5d5c0bae1b68c9ca4f0a288d6762
    Gerrit-Change-Number: 352769
    Gerrit-PatchSet: 1
    Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Reviewer: Ian Gudger <i...@iangudger.com>
    Gerrit-Attention: Ian Gudger <i...@iangudger.com>
    Gerrit-Attention: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Comment-Date: Wed, 29 Sep 2021 14:55:43 +0000

    Antonio Ojea (Gerrit)

    unread,
    Oct 13, 2021, 6:22:13 AM10/13/21
    to goph...@pubsubhelper.golang.org, Brad Fitzpatrick, Ian Gudger, golang-co...@googlegroups.com

    Antonio Ojea abandoned this change.

    View Change

    Abandoned Thanks for the feedback, I think that the main problem is that is tricky to use a custom resolver and my solution indeed seems very hacky

    To view, visit change 352769. To unsubscribe, or for help writing mail filters, visit settings.

    Gerrit-Project: net
    Gerrit-Branch: master
    Gerrit-Change-Id: I5d4742c1539e5d5c0bae1b68c9ca4f0a288d6762
    Gerrit-Change-Number: 352769
    Gerrit-PatchSet: 1
    Gerrit-Owner: Antonio Ojea <antonio.o...@gmail.com>
    Gerrit-Reviewer: Brad Fitzpatrick <brad...@tailscale.com>
    Gerrit-Reviewer: Ian Gudger <i...@iangudger.com>
    Gerrit-MessageType: abandon
    Reply all
    Reply to author
    Forward
    0 new messages