feat(dnsfwd): answer one overridden name with the listener address
- commit
- c747a0b650cfb08aed48148bb9467ce95dc366ec
- author
- brice · · 2026-10-13 07:19 UTC
- parent
- cbb8ad6
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +274 -5
M internal/dnsfwd/dnsfwd.go +19 -5
| @@ -12,6 +12,10 @@ | |||
| 12 | 12 | // connections, an absolute lifetime of TCPLifetime per TCP connection (a client | |
| 13 | 13 | // cannot hold a slot by trickling bytes), a deadline on every upstream | |
| 14 | 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 | 19 | package dnsfwd | |
| 16 | 20 | ||
| 17 | 21 | import ( | |
| @@ -62,12 +66,18 @@ type Forwarder struct { | |||
| 62 | 66 | // exists when the network is created and a missing one is an error to | |
| 63 | 67 | // report at once, not a listener on an address nobody owns. Set before Listen. | |
| 64 | 68 | 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 | 75 | mu sync.Mutex | |
| 67 | 76 | listeners map[netip.Addr]*listener | |
| 68 | 77 | } | |
| 69 | 78 | ||
| 70 | 79 | type listener struct { | |
| 80 | + | addr netip.Addr | |
| 71 | 81 | udp net.PacketConn | |
| 72 | 82 | tcp net.Listener | |
| 73 | 83 | cancel context.CancelFunc | |
| @@ -126,7 +136,7 @@ func (f *Forwarder) Listen(ctx context.Context, addr netip.Addr) error { | |||
| 126 | 136 | return fmt.Errorf("dnsfwd: listen tcp %s: %w", hp, err) | |
| 127 | 137 | } | |
| 128 | 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 | 140 | f.listeners[addr] = l | |
| 131 | 141 | log := zerowrap.FromCtx(ctx) | |
| 132 | 142 | l.done.Add(2) | |
| @@ -211,7 +221,7 @@ func (f *Forwarder) serveUDP(ctx context.Context, l *listener, log zerowrap.Logg | |||
| 211 | 221 | q := append([]byte(nil), buf[:n]...) | |
| 212 | 222 | go func() { | |
| 213 | 223 | defer func() { <-l.inflight }() | |
| 214 | - | resp := f.exchange(ctx, q, false) | |
| 224 | + | resp := f.exchange(ctx, l, q, false) | |
| 215 | 225 | if len(resp) > 0 { | |
| 216 | 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 | 287 | default: | |
| 278 | 288 | return // saturated: close, the client retries | |
| 279 | 289 | } | |
| 280 | - | resp := f.exchange(ctx, q, true) | |
| 290 | + | resp := f.exchange(ctx, l, q, true) | |
| 281 | 291 | <-l.inflight | |
| 282 | 292 | if len(resp) == 0 || len(resp) > 0xffff { | |
| 283 | 293 | return | |
| @@ -293,8 +303,12 @@ func (f *Forwarder) serveConn(ctx context.Context, l *listener, c net.Conn, dead | |||
| 293 | 303 | ||
| 294 | 304 | // exchange relays q to the upstreams in order and returns the first answer, or | |
| 295 | 305 | // 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 | + | } | |
| 298 | 312 | for _, up := range f.upstreams() { | |
| 299 | 313 | var resp []byte | |
| 300 | 314 | var err error | |
| @@ -12,6 +12,10 @@ | |||||
| 12 | // connections, an absolute lifetime of TCPLifetime per TCP connection (a client | 12 | // connections, an absolute lifetime of TCPLifetime per TCP connection (a client | ||
| 13 | // cannot hold a slot by trickling bytes), a deadline on every upstream | 13 | // 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 dnsfwd | 19 | 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 to | 66 | // 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 bool | 68 | 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.Mutex | 75 | mu sync.Mutex | ||
| 67 | listeners map[netip.Addr]*listener | 76 | listeners map[netip.Addr]*listener | ||
| 68 | } | 77 | } | ||
| 69 | 78 | ||||
| 70 | type listener struct { | 79 | type listener struct { | ||
| 80 | + | addr netip.Addr | |||
| 71 | udp net.PacketConn | 81 | udp net.PacketConn | ||
| 72 | tcp net.Listener | 82 | tcp net.Listener | ||
| 73 | cancel context.CancelFunc | 83 | 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] = l | 140 | 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 retries | 288 | 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.inflight | 291 | <-l.inflight | ||
| 282 | if len(resp) == 0 || len(resp) > 0xffff { | 292 | if len(resp) == 0 || len(resp) > 0xffff { | ||
| 283 | return | 293 | 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, or | 304 | // 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 the | 305 | // 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 []byte | 313 | var resp []byte | ||
| 300 | var err error | 314 | var err error | ||
M internal/dnsfwd/dnsfwd_test.go +146 -0
| @@ -466,3 +466,149 @@ func TestFreeBindListensOnAnAddressTheHostDoesNotOwnYet(t *testing.T) { | |||
| 466 | 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 | + | } | |
| @@ -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 | + | } | |||