detached c747a0b

feat(dnsfwd): answer one overridden name with the listener address

commit
c747a0b650cfb08aed48148bb9467ce95dc366ec
author
brice · · 2026-10-13 07:19 UTC
parent
cbb8ad6
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +274 -5
M internal/dnsfwd/dnsfwd.go +19 -5
@@ -12,6 +12,10 @@
1212 // connections, an absolute lifetime of TCPLifetime per TCP connection (a client
1313 // cannot hold a slot by trickling bytes), a deadline on every upstream
1414 // exchange, and a cap on queries per TCP connection.
15+//
16+// One name can be overridden (Forwarder.Override, the opt-in guest forge
17+// access): queries for it are answered locally with the address of the
18+// listener, see override.go. Every other name is forwarded.
1519 package dnsfwd
1620
1721 import (
@@ -62,12 +66,18 @@ type Forwarder struct {
6266 // exists when the network is created and a missing one is an error to
6367 // report at once, not a listener on an address nobody owns. Set before Listen.
6468 FreeBind bool
69+ // Override, when set, is a host name that every listener answers itself:
70+ // A with the address the listener is bound to, and NOERROR with no data for
71+ // every other type, whatever the host's resolvers say. Case-insensitive; a
72+ // trailing dot is ignored. Set before Listen.
73+ Override string
6574
6675 mu sync.Mutex
6776 listeners map[netip.Addr]*listener
6877 }
6978
7079 type listener struct {
80+ addr netip.Addr
7181 udp net.PacketConn
7282 tcp net.Listener
7383 cancel context.CancelFunc
@@ -126,7 +136,7 @@ func (f *Forwarder) Listen(ctx context.Context, addr netip.Addr) error {
126136 return fmt.Errorf("dnsfwd: listen tcp %s: %w", hp, err)
127137 }
128138 lctx, cancel := context.WithCancel(context.WithoutCancel(ctx))
129- l := &listener{udp: pc, tcp: tl, cancel: cancel, inflight: make(chan struct{}, MaxInflight), conns: make(chan struct{}, MaxTCPConns)}
139+ l := &listener{addr: addr, udp: pc, tcp: tl, cancel: cancel, inflight: make(chan struct{}, MaxInflight), conns: make(chan struct{}, MaxTCPConns)}
130140 f.listeners[addr] = l
131141 log := zerowrap.FromCtx(ctx)
132142 l.done.Add(2)
@@ -211,7 +221,7 @@ func (f *Forwarder) serveUDP(ctx context.Context, l *listener, log zerowrap.Logg
211221 q := append([]byte(nil), buf[:n]...)
212222 go func() {
213223 defer func() { <-l.inflight }()
214- resp := f.exchange(ctx, q, false)
224+ resp := f.exchange(ctx, l, q, false)
215225 if len(resp) > 0 {
216226 _, _ = pc.WriteTo(resp, from)
217227 }
@@ -277,7 +287,7 @@ func (f *Forwarder) serveConn(ctx context.Context, l *listener, c net.Conn, dead
277287 default:
278288 return // saturated: close, the client retries
279289 }
280- resp := f.exchange(ctx, q, true)
290+ resp := f.exchange(ctx, l, q, true)
281291 <-l.inflight
282292 if len(resp) == 0 || len(resp) > 0xffff {
283293 return
@@ -293,8 +303,12 @@ func (f *Forwarder) serveConn(ctx context.Context, l *listener, c net.Conn, dead
293303
294304 // exchange relays q to the upstreams in order and returns the first answer, or
295305 // a SERVFAIL when none answers. The transport towards the upstream matches the
296-// client's: a client that fell back to TCP gets a complete answer.
297-func (f *Forwarder) exchange(ctx context.Context, q []byte, overTCP bool) []byte {
306+// client's: a client that fell back to TCP gets a complete answer. The
307+// overridden name, if any, is answered without asking the upstreams.
308+func (f *Forwarder) exchange(ctx context.Context, l *listener, q []byte, overTCP bool) []byte {
309+ if resp := f.overrideAnswer(l.addr, q); resp != nil {
310+ return resp
311+ }
298312 for _, up := range f.upstreams() {
299313 var resp []byte
300314 var err error
@@ -12,6 +12,10 @@
12 // connections, an absolute lifetime of TCPLifetime per TCP connection (a client12 // connections, an absolute lifetime of TCPLifetime per TCP connection (a client
13 // cannot hold a slot by trickling bytes), a deadline on every upstream13 // cannot hold a slot by trickling bytes), a deadline on every upstream
14 // exchange, and a cap on queries per TCP connection.14 // exchange, and a cap on queries per TCP connection.
15+//
16+// One name can be overridden (Forwarder.Override, the opt-in guest forge
17+// access): queries for it are answered locally with the address of the
18+// listener, see override.go. Every other name is forwarded.
15 package dnsfwd19 package dnsfwd
16 20
17 import (21 import (
@@ -62,12 +66,18 @@ type Forwarder struct {
62 // exists when the network is created and a missing one is an error to66 // exists when the network is created and a missing one is an error to
63 // report at once, not a listener on an address nobody owns. Set before Listen.67 // report at once, not a listener on an address nobody owns. Set before Listen.
64 FreeBind bool68 FreeBind bool
69+ // Override, when set, is a host name that every listener answers itself:
70+ // A with the address the listener is bound to, and NOERROR with no data for
71+ // every other type, whatever the host's resolvers say. Case-insensitive; a
72+ // trailing dot is ignored. Set before Listen.
73+ Override string
65 74
66 mu sync.Mutex75 mu sync.Mutex
67 listeners map[netip.Addr]*listener76 listeners map[netip.Addr]*listener
68 }77 }
69 78
70 type listener struct {79 type listener struct {
80+ addr netip.Addr
71 udp net.PacketConn81 udp net.PacketConn
72 tcp net.Listener82 tcp net.Listener
73 cancel context.CancelFunc83 cancel context.CancelFunc
@@ -126,7 +136,7 @@ func (f *Forwarder) Listen(ctx context.Context, addr netip.Addr) error {
126 return fmt.Errorf("dnsfwd: listen tcp %s: %w", hp, err)136 return fmt.Errorf("dnsfwd: listen tcp %s: %w", hp, err)
127 }137 }
128 lctx, cancel := context.WithCancel(context.WithoutCancel(ctx))138 lctx, cancel := context.WithCancel(context.WithoutCancel(ctx))
129- l := &listener{udp: pc, tcp: tl, cancel: cancel, inflight: make(chan struct{}, MaxInflight), conns: make(chan struct{}, MaxTCPConns)}139+ l := &listener{addr: addr, udp: pc, tcp: tl, cancel: cancel, inflight: make(chan struct{}, MaxInflight), conns: make(chan struct{}, MaxTCPConns)}
130 f.listeners[addr] = l140 f.listeners[addr] = l
131 log := zerowrap.FromCtx(ctx)141 log := zerowrap.FromCtx(ctx)
132 l.done.Add(2)142 l.done.Add(2)
@@ -211,7 +221,7 @@ func (f *Forwarder) serveUDP(ctx context.Context, l *listener, log zerowrap.Logg
211 q := append([]byte(nil), buf[:n]...)221 q := append([]byte(nil), buf[:n]...)
212 go func() {222 go func() {
213 defer func() { <-l.inflight }()223 defer func() { <-l.inflight }()
214- resp := f.exchange(ctx, q, false)224+ resp := f.exchange(ctx, l, q, false)
215 if len(resp) > 0 {225 if len(resp) > 0 {
216 _, _ = pc.WriteTo(resp, from)226 _, _ = pc.WriteTo(resp, from)
217 }227 }
@@ -277,7 +287,7 @@ func (f *Forwarder) serveConn(ctx context.Context, l *listener, c net.Conn, dead
277 default:287 default:
278 return // saturated: close, the client retries288 return // saturated: close, the client retries
279 }289 }
280- resp := f.exchange(ctx, q, true)290+ resp := f.exchange(ctx, l, q, true)
281 <-l.inflight291 <-l.inflight
282 if len(resp) == 0 || len(resp) > 0xffff {292 if len(resp) == 0 || len(resp) > 0xffff {
283 return293 return
@@ -293,8 +303,12 @@ func (f *Forwarder) serveConn(ctx context.Context, l *listener, c net.Conn, dead
293 303
294 // exchange relays q to the upstreams in order and returns the first answer, or304 // exchange relays q to the upstreams in order and returns the first answer, or
295 // a SERVFAIL when none answers. The transport towards the upstream matches the305 // a SERVFAIL when none answers. The transport towards the upstream matches the
296-// client's: a client that fell back to TCP gets a complete answer.306+// client's: a client that fell back to TCP gets a complete answer. The
297-func (f *Forwarder) exchange(ctx context.Context, q []byte, overTCP bool) []byte {307+// overridden name, if any, is answered without asking the upstreams.
308+func (f *Forwarder) exchange(ctx context.Context, l *listener, q []byte, overTCP bool) []byte {
309+ if resp := f.overrideAnswer(l.addr, q); resp != nil {
310+ return resp
311+ }
298 for _, up := range f.upstreams() {312 for _, up := range f.upstreams() {
299 var resp []byte313 var resp []byte
300 var err error314 var err error
M internal/dnsfwd/dnsfwd_test.go +146 -0
@@ -466,3 +466,149 @@ func TestFreeBindListensOnAnAddressTheHostDoesNotOwnYet(t *testing.T) {
466466 t.Fatal("no listener")
467467 }
468468 }
469+
470+// queryType is query with a chosen type (and optionally EDNS0).
471+func queryType(id uint16, name string, qtype uint16, edns bool) []byte {
472+ q := query(id, name)
473+ binary.BigEndian.PutUint16(q[len(q)-4:], qtype)
474+ if edns {
475+ q[11] = 1
476+ q = append(q, 0, 0, 41, 0x10, 0, 0, 0, 0, 0, 0, 0)
477+ }
478+ return q
479+}
480+
481+func udpExchange(t *testing.T, addr string, q []byte) []byte {
482+ t.Helper()
483+ c, err := net.Dial("udp", addr)
484+ if err != nil {
485+ t.Fatal(err)
486+ }
487+ defer func() { _ = c.Close() }()
488+ _ = c.SetDeadline(time.Now().Add(5 * time.Second))
489+ _, _ = c.Write(q)
490+ buf := make([]byte, 1024)
491+ n, err := c.Read(buf)
492+ if err != nil {
493+ t.Fatal(err)
494+ }
495+ return buf[:n]
496+}
497+
498+func startOverride(t *testing.T, name string, ups ...string) (*Forwarder, string) {
499+ t.Helper()
500+ f := New(func() []string { return ups }, 0)
501+ f.Override = name
502+ ip := netip.MustParseAddr("127.0.0.1")
503+ if err := f.Listen(context.Background(), ip); err != nil {
504+ t.Fatal(err)
505+ }
506+ t.Cleanup(f.CloseAll)
507+ a, _ := f.Addr(ip)
508+ return f, a.String()
509+}
510+
511+func TestOverrideAnswersAWithTheListenerAddress(t *testing.T) {
512+ var asked atomic.Int64
513+ up := fakeUpstream(t, func(q []byte) []byte { asked.Add(1); return answer(q) })
514+ // A trailing dot in the setting is ignored; on the wire a name has none.
515+ _, addr := startOverride(t, "Ermite.Example.org.", up)
516+ for _, name := range []string{"ermite.example.org", "ERMITE.example.ORG", "Ermite.Example.Org"} {
517+ r := udpExchange(t, addr, queryType(0x4242, name, 1, false))
518+ if binary.BigEndian.Uint16(r) != 0x4242 || r[2]&0x80 == 0 || r[2]&0x01 == 0 || r[3]&0x0F != 0 {
519+ t.Fatalf("%s: header %x", name, r[:hdrLen])
520+ }
521+ if an := binary.BigEndian.Uint16(r[6:]); an != 1 {
522+ t.Fatalf("%s: %d answers", name, an)
523+ }
524+ if !strings.HasSuffix(string(r), "\x00\x04\x7f\x00\x00\x01") {
525+ t.Fatalf("%s: rdata is not 127.0.0.1: %x", name, r)
526+ }
527+ }
528+ if asked.Load() != 0 {
529+ t.Fatalf("the override went upstream %d times", asked.Load())
530+ }
531+}
532+
533+func TestOverrideAAAAIsEmptyNoError(t *testing.T) {
534+ up := fakeUpstream(t, answer)
535+ _, addr := startOverride(t, "ermite.example.org", up)
536+ for _, edns := range []bool{false, true} {
537+ r := udpExchange(t, addr, queryType(7, "ermite.EXAMPLE.org", 28, edns))
538+ if r[3]&0x0F != 0 || binary.BigEndian.Uint16(r[6:]) != 0 || r[2]&0x80 == 0 {
539+ t.Fatalf("AAAA (edns %v): %x", edns, r)
540+ }
541+ if edns != (binary.BigEndian.Uint16(r[10:]) == 1) {
542+ t.Fatalf("AAAA (edns %v): arcount %d", edns, binary.BigEndian.Uint16(r[10:]))
543+ }
544+ }
545+}
546+
547+func TestOverrideForwardsEveryOtherName(t *testing.T) {
548+ var asked atomic.Int64
549+ up := fakeUpstream(t, func(q []byte) []byte { asked.Add(1); return answer(q) })
550+ _, addr := startOverride(t, "ermite.example.org", up)
551+ for _, name := range []string{"example.org", "other.example.org", "x.ermite.example.org", "ermite.example.org.evil.test", "ermite.example.or"} {
552+ r := udpExchange(t, addr, query(9, name))
553+ if string(r[len(r)-6:]) != "ANSWER" {
554+ t.Fatalf("%s was not forwarded: %x", name, r)
555+ }
556+ }
557+ if asked.Load() != 5 {
558+ t.Fatalf("%d upstream queries, want 5", asked.Load())
559+ }
560+}
561+
562+func TestOverrideOverTCPAndDisabledByDefault(t *testing.T) {
563+ up := fakeUpstream(t, answer)
564+ _, addr := startOverride(t, "ermite.example.org", up)
565+ tc, err := net.Dial("tcp", addr)
566+ if err != nil {
567+ t.Fatal(err)
568+ }
569+ defer func() { _ = tc.Close() }()
570+ _ = tc.SetDeadline(time.Now().Add(5 * time.Second))
571+ q := queryType(3, "ermite.example.org", 1, false)
572+ _, _ = tc.Write(append([]byte{byte(len(q) >> 8), byte(len(q))}, q...))
573+ var l [2]byte
574+ if _, err := io.ReadFull(tc, l[:]); err != nil {
575+ t.Fatal(err)
576+ }
577+ r := make([]byte, binary.BigEndian.Uint16(l[:]))
578+ if _, err := io.ReadFull(tc, r); err != nil {
579+ t.Fatal(err)
580+ }
581+ if binary.BigEndian.Uint16(r[6:]) != 1 || !strings.HasSuffix(string(r), "\x7f\x00\x00\x01") {
582+ t.Fatalf("tcp: %x", r)
583+ }
584+
585+ // Off by default: the name is forwarded like any other.
586+ _, addr2 := start(t, up)
587+ r2 := udpExchange(t, addr2, query(4, "ermite.example.org"))
588+ if string(r2[len(r2)-6:]) != "ANSWER" {
589+ t.Fatalf("not forwarded without Override: %x", r2)
590+ }
591+}
592+
593+func TestOverrideIgnoresMalformedAndNonQueries(t *testing.T) {
594+ f := New(func() []string { return nil }, 0)
595+ f.Override = "ermite.example.org"
596+ gw := netip.MustParseAddr("10.200.0.1")
597+ good := queryType(1, "ermite.example.org", 1, false)
598+ if f.overrideAnswer(gw, good) == nil {
599+ t.Fatal("control query not answered")
600+ }
601+ cut := good[:len(good)-3]
602+ chaos := queryType(1, "ermite.example.org", 1, false)
603+ binary.BigEndian.PutUint16(chaos[len(chaos)-2:], 3) // class CH
604+ resp := append([]byte(nil), good...)
605+ resp[2] |= 0x80 // QR set: not a query
606+ notify := append([]byte(nil), good...)
607+ notify[2] |= 4 << 3 // opcode 4
608+ ptr := append(append([]byte(nil), good[:hdrLen]...), 0xC0, 0x0C, 0, 1, 0, 1)
609+ for name, q := range map[string][]byte{"truncated": cut, "class CH": chaos, "response": resp, "opcode": notify, "pointer": ptr, "short": good[:5]} {
610+ if f.overrideAnswer(gw, q) != nil {
611+ t.Errorf("%s was answered locally", name)
612+ }
613+ }
614+}
@@ -466,3 +466,149 @@ func TestFreeBindListensOnAnAddressTheHostDoesNotOwnYet(t *testing.T) {
466 t.Fatal("no listener")466 t.Fatal("no listener")
467 }467 }
468 }468 }
469+
470+// queryType is query with a chosen type (and optionally EDNS0).
471+func queryType(id uint16, name string, qtype uint16, edns bool) []byte {
472+ q := query(id, name)
473+ binary.BigEndian.PutUint16(q[len(q)-4:], qtype)
474+ if edns {
475+ q[11] = 1
476+ q = append(q, 0, 0, 41, 0x10, 0, 0, 0, 0, 0, 0, 0)
477+ }
478+ return q
479+}
480+
481+func udpExchange(t *testing.T, addr string, q []byte) []byte {
482+ t.Helper()
483+ c, err := net.Dial("udp", addr)
484+ if err != nil {
485+ t.Fatal(err)
486+ }
487+ defer func() { _ = c.Close() }()
488+ _ = c.SetDeadline(time.Now().Add(5 * time.Second))
489+ _, _ = c.Write(q)
490+ buf := make([]byte, 1024)
491+ n, err := c.Read(buf)
492+ if err != nil {
493+ t.Fatal(err)
494+ }
495+ return buf[:n]
496+}
497+
498+func startOverride(t *testing.T, name string, ups ...string) (*Forwarder, string) {
499+ t.Helper()
500+ f := New(func() []string { return ups }, 0)
501+ f.Override = name
502+ ip := netip.MustParseAddr("127.0.0.1")
503+ if err := f.Listen(context.Background(), ip); err != nil {
504+ t.Fatal(err)
505+ }
506+ t.Cleanup(f.CloseAll)
507+ a, _ := f.Addr(ip)
508+ return f, a.String()
509+}
510+
511+func TestOverrideAnswersAWithTheListenerAddress(t *testing.T) {
512+ var asked atomic.Int64
513+ up := fakeUpstream(t, func(q []byte) []byte { asked.Add(1); return answer(q) })
514+ // A trailing dot in the setting is ignored; on the wire a name has none.
515+ _, addr := startOverride(t, "Ermite.Example.org.", up)
516+ for _, name := range []string{"ermite.example.org", "ERMITE.example.ORG", "Ermite.Example.Org"} {
517+ r := udpExchange(t, addr, queryType(0x4242, name, 1, false))
518+ if binary.BigEndian.Uint16(r) != 0x4242 || r[2]&0x80 == 0 || r[2]&0x01 == 0 || r[3]&0x0F != 0 {
519+ t.Fatalf("%s: header %x", name, r[:hdrLen])
520+ }
521+ if an := binary.BigEndian.Uint16(r[6:]); an != 1 {
522+ t.Fatalf("%s: %d answers", name, an)
523+ }
524+ if !strings.HasSuffix(string(r), "\x00\x04\x7f\x00\x00\x01") {
525+ t.Fatalf("%s: rdata is not 127.0.0.1: %x", name, r)
526+ }
527+ }
528+ if asked.Load() != 0 {
529+ t.Fatalf("the override went upstream %d times", asked.Load())
530+ }
531+}
532+
533+func TestOverrideAAAAIsEmptyNoError(t *testing.T) {
534+ up := fakeUpstream(t, answer)
535+ _, addr := startOverride(t, "ermite.example.org", up)
536+ for _, edns := range []bool{false, true} {
537+ r := udpExchange(t, addr, queryType(7, "ermite.EXAMPLE.org", 28, edns))
538+ if r[3]&0x0F != 0 || binary.BigEndian.Uint16(r[6:]) != 0 || r[2]&0x80 == 0 {
539+ t.Fatalf("AAAA (edns %v): %x", edns, r)
540+ }
541+ if edns != (binary.BigEndian.Uint16(r[10:]) == 1) {
542+ t.Fatalf("AAAA (edns %v): arcount %d", edns, binary.BigEndian.Uint16(r[10:]))
543+ }
544+ }
545+}
546+
547+func TestOverrideForwardsEveryOtherName(t *testing.T) {
548+ var asked atomic.Int64
549+ up := fakeUpstream(t, func(q []byte) []byte { asked.Add(1); return answer(q) })
550+ _, addr := startOverride(t, "ermite.example.org", up)
551+ for _, name := range []string{"example.org", "other.example.org", "x.ermite.example.org", "ermite.example.org.evil.test", "ermite.example.or"} {
552+ r := udpExchange(t, addr, query(9, name))
553+ if string(r[len(r)-6:]) != "ANSWER" {
554+ t.Fatalf("%s was not forwarded: %x", name, r)
555+ }
556+ }
557+ if asked.Load() != 5 {
558+ t.Fatalf("%d upstream queries, want 5", asked.Load())
559+ }
560+}
561+
562+func TestOverrideOverTCPAndDisabledByDefault(t *testing.T) {
563+ up := fakeUpstream(t, answer)
564+ _, addr := startOverride(t, "ermite.example.org", up)
565+ tc, err := net.Dial("tcp", addr)
566+ if err != nil {
567+ t.Fatal(err)
568+ }
569+ defer func() { _ = tc.Close() }()
570+ _ = tc.SetDeadline(time.Now().Add(5 * time.Second))
571+ q := queryType(3, "ermite.example.org", 1, false)
572+ _, _ = tc.Write(append([]byte{byte(len(q) >> 8), byte(len(q))}, q...))
573+ var l [2]byte
574+ if _, err := io.ReadFull(tc, l[:]); err != nil {
575+ t.Fatal(err)
576+ }
577+ r := make([]byte, binary.BigEndian.Uint16(l[:]))
578+ if _, err := io.ReadFull(tc, r); err != nil {
579+ t.Fatal(err)
580+ }
581+ if binary.BigEndian.Uint16(r[6:]) != 1 || !strings.HasSuffix(string(r), "\x7f\x00\x00\x01") {
582+ t.Fatalf("tcp: %x", r)
583+ }
584+
585+ // Off by default: the name is forwarded like any other.
586+ _, addr2 := start(t, up)
587+ r2 := udpExchange(t, addr2, query(4, "ermite.example.org"))
588+ if string(r2[len(r2)-6:]) != "ANSWER" {
589+ t.Fatalf("not forwarded without Override: %x", r2)
590+ }
591+}
592+
593+func TestOverrideIgnoresMalformedAndNonQueries(t *testing.T) {
594+ f := New(func() []string { return nil }, 0)
595+ f.Override = "ermite.example.org"
596+ gw := netip.MustParseAddr("10.200.0.1")
597+ good := queryType(1, "ermite.example.org", 1, false)
598+ if f.overrideAnswer(gw, good) == nil {
599+ t.Fatal("control query not answered")
600+ }
601+ cut := good[:len(good)-3]
602+ chaos := queryType(1, "ermite.example.org", 1, false)
603+ binary.BigEndian.PutUint16(chaos[len(chaos)-2:], 3) // class CH
604+ resp := append([]byte(nil), good...)
605+ resp[2] |= 0x80 // QR set: not a query
606+ notify := append([]byte(nil), good...)
607+ notify[2] |= 4 << 3 // opcode 4
608+ ptr := append(append([]byte(nil), good[:hdrLen]...), 0xC0, 0x0C, 0, 1, 0, 1)
609+ for name, q := range map[string][]byte{"truncated": cut, "class CH": chaos, "response": resp, "opcode": notify, "pointer": ptr, "short": good[:5]} {
610+ if f.overrideAnswer(gw, q) != nil {
611+ t.Errorf("%s was answered locally", name)
612+ }
613+ }
614+}
A internal/dnsfwd/override.go +109 -0
@@ -0,0 +1,109 @@
1+// SPDX-License-Identifier: MIT
2+
3+package dnsfwd
4+
5+import (
6+ "encoding/binary"
7+ "net/netip"
8+ "strings"
9+)
10+
11+const (
12+ typeA = 1
13+ classIN = 1
14+ // overrideTTL is short: the sandbox lives for one job.
15+ overrideTTL = 30
16+)
17+
18+// overrideName returns Override normalised for comparison: lower case, without
19+// a trailing dot.
20+func (f *Forwarder) overrideName() string {
21+ return strings.TrimSuffix(strings.ToLower(f.Override), ".")
22+}
23+
24+// questionName reads the name of the question that starts at q[hdrLen]. It
25+// returns the lower-cased name without trailing dot and the offset just after
26+// the name. Compression pointers and over-long labels are refused (a question
27+// never uses them): ok is false and the query is forwarded as usual.
28+func questionName(q []byte) (name string, end int, ok bool) {
29+ var sb strings.Builder
30+ i := hdrLen
31+ for {
32+ if i >= len(q) {
33+ return "", 0, false
34+ }
35+ n := int(q[i])
36+ i++
37+ if n == 0 {
38+ return sb.String(), i, true
39+ }
40+ if n&0xC0 != 0 || i+n > len(q) {
41+ return "", 0, false
42+ }
43+ if sb.Len() > 0 {
44+ sb.WriteByte('.')
45+ }
46+ sb.WriteString(strings.ToLower(string(q[i : i+n])))
47+ i += n
48+ if sb.Len() > 253 {
49+ return "", 0, false
50+ }
51+ }
52+}
53+
54+// overrideAnswer builds the local answer to q when q asks for the overridden
55+// name in class IN, else nil. A gets addr; every other type gets NOERROR with
56+// no data (so AAAA is an empty answer: sandboxes have no IPv6, and an upstream
57+// NXDOMAIN or CNAME for the name must not leak in).
58+func (f *Forwarder) overrideAnswer(addr netip.Addr, q []byte) []byte {
59+ want := f.overrideName()
60+ if want == "" || len(q) < hdrLen {
61+ return nil
62+ }
63+ // A plain query: QR=0, opcode 0, one question, nothing else but additional
64+ // records (EDNS).
65+ if q[2]&0xF8 != 0 || binary.BigEndian.Uint16(q[4:]) != 1 ||
66+ binary.BigEndian.Uint16(q[6:]) != 0 || binary.BigEndian.Uint16(q[8:]) != 0 {
67+ return nil
68+ }
69+ name, end, ok := questionName(q)
70+ if !ok || name != want || end+4 > len(q) {
71+ return nil
72+ }
73+ qtype := binary.BigEndian.Uint16(q[end:])
74+ if binary.BigEndian.Uint16(q[end+2:]) != classIN {
75+ return nil
76+ }
77+ end += 4
78+ withA := qtype == typeA && addr.Is4()
79+
80+ r := make([]byte, 0, end+16+11)
81+ r = append(r, q[0], q[1])
82+ r = append(r, 0x80|(q[2]&0x01), 0x80) // QR, RD copied; RA; NOERROR
83+ r = append(r, 0, 1) // QDCOUNT
84+ if withA {
85+ r = append(r, 0, 1)
86+ } else {
87+ r = append(r, 0, 0)
88+ }
89+ r = append(r, 0, 0) // NSCOUNT
90+ // An EDNS query gets an OPT record back (RFC 6891), without options.
91+ edns := binary.BigEndian.Uint16(q[10:]) >= 1 && len(q) >= end+11 &&
92+ q[end] == 0 && binary.BigEndian.Uint16(q[end+1:]) == 41
93+ if edns {
94+ r = append(r, 0, 1)
95+ } else {
96+ r = append(r, 0, 0)
97+ }
98+ r = append(r, q[hdrLen:end]...)
99+ if withA {
100+ r = append(r, 0xC0, hdrLen, 0, typeA, 0, classIN)
101+ r = binary.BigEndian.AppendUint32(r, overrideTTL)
102+ a := addr.As4()
103+ r = append(r, 0, 4, a[0], a[1], a[2], a[3])
104+ }
105+ if edns {
106+ r = append(r, 0, 0, 41, 0x10, 0, 0, 0, 0, 0, 0, 0) // root, OPT, 4096 bytes, no options
107+ }
108+ return r
109+}
@@ -0,0 +1,109 @@
1+// SPDX-License-Identifier: MIT
2+
3+package dnsfwd
4+
5+import (
6+ "encoding/binary"
7+ "net/netip"
8+ "strings"
9+)
10+
11+const (
12+ typeA = 1
13+ classIN = 1
14+ // overrideTTL is short: the sandbox lives for one job.
15+ overrideTTL = 30
16+)
17+
18+// overrideName returns Override normalised for comparison: lower case, without
19+// a trailing dot.
20+func (f *Forwarder) overrideName() string {
21+ return strings.TrimSuffix(strings.ToLower(f.Override), ".")
22+}
23+
24+// questionName reads the name of the question that starts at q[hdrLen]. It
25+// returns the lower-cased name without trailing dot and the offset just after
26+// the name. Compression pointers and over-long labels are refused (a question
27+// never uses them): ok is false and the query is forwarded as usual.
28+func questionName(q []byte) (name string, end int, ok bool) {
29+ var sb strings.Builder
30+ i := hdrLen
31+ for {
32+ if i >= len(q) {
33+ return "", 0, false
34+ }
35+ n := int(q[i])
36+ i++
37+ if n == 0 {
38+ return sb.String(), i, true
39+ }
40+ if n&0xC0 != 0 || i+n > len(q) {
41+ return "", 0, false
42+ }
43+ if sb.Len() > 0 {
44+ sb.WriteByte('.')
45+ }
46+ sb.WriteString(strings.ToLower(string(q[i : i+n])))
47+ i += n
48+ if sb.Len() > 253 {
49+ return "", 0, false
50+ }
51+ }
52+}
53+
54+// overrideAnswer builds the local answer to q when q asks for the overridden
55+// name in class IN, else nil. A gets addr; every other type gets NOERROR with
56+// no data (so AAAA is an empty answer: sandboxes have no IPv6, and an upstream
57+// NXDOMAIN or CNAME for the name must not leak in).
58+func (f *Forwarder) overrideAnswer(addr netip.Addr, q []byte) []byte {
59+ want := f.overrideName()
60+ if want == "" || len(q) < hdrLen {
61+ return nil
62+ }
63+ // A plain query: QR=0, opcode 0, one question, nothing else but additional
64+ // records (EDNS).
65+ if q[2]&0xF8 != 0 || binary.BigEndian.Uint16(q[4:]) != 1 ||
66+ binary.BigEndian.Uint16(q[6:]) != 0 || binary.BigEndian.Uint16(q[8:]) != 0 {
67+ return nil
68+ }
69+ name, end, ok := questionName(q)
70+ if !ok || name != want || end+4 > len(q) {
71+ return nil
72+ }
73+ qtype := binary.BigEndian.Uint16(q[end:])
74+ if binary.BigEndian.Uint16(q[end+2:]) != classIN {
75+ return nil
76+ }
77+ end += 4
78+ withA := qtype == typeA && addr.Is4()
79+
80+ r := make([]byte, 0, end+16+11)
81+ r = append(r, q[0], q[1])
82+ r = append(r, 0x80|(q[2]&0x01), 0x80) // QR, RD copied; RA; NOERROR
83+ r = append(r, 0, 1) // QDCOUNT
84+ if withA {
85+ r = append(r, 0, 1)
86+ } else {
87+ r = append(r, 0, 0)
88+ }
89+ r = append(r, 0, 0) // NSCOUNT
90+ // An EDNS query gets an OPT record back (RFC 6891), without options.
91+ edns := binary.BigEndian.Uint16(q[10:]) >= 1 && len(q) >= end+11 &&
92+ q[end] == 0 && binary.BigEndian.Uint16(q[end+1:]) == 41
93+ if edns {
94+ r = append(r, 0, 1)
95+ } else {
96+ r = append(r, 0, 0)
97+ }
98+ r = append(r, q[hdrLen:end]...)
99+ if withA {
100+ r = append(r, 0xC0, hdrLen, 0, typeA, 0, classIN)
101+ r = binary.BigEndian.AppendUint32(r, overrideTTL)
102+ a := addr.As4()
103+ r = append(r, 0, 4, a[0], a[1], a[2], a[3])
104+ }
105+ if edns {
106+ r = append(r, 0, 0, 41, 0x10, 0, 0, 0, 0, 0, 0, 0) // root, OPT, 4096 bytes, no options
107+ }
108+ return r
109+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close