Skip to content

Commit 1f8d80b

Browse files
Improve SRV record handling
1 parent 2a94f58 commit 1f8d80b

7 files changed

Lines changed: 305 additions & 7 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ vendorDontSpoof
182182
vendorSpoofFor
183183
vendorDontSpoofFor
184184
vendorSpoofTypes
185+
vendorSpoofSRV
185186
vendorIgnoreDHCPv6NoFQDN
186187
vendorIgnoreNonMicrosoftDHCP
187188
vendorDelegateIgnoredTo

cli.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ type Config struct {
4747
SpoofFor []*hostMatcher
4848
DontSpoofFor []*hostMatcher
4949
SpoofTypes *spoofTypes
50+
SpoofSRV srvMatchers
5051
IgnoreDHCPv6NoFQDN bool
5152
IgnoreNonMicrosoftDHCP bool
5253
DelegateIgnoredTo string
@@ -70,6 +71,7 @@ type Config struct {
7071
spoofFor []string
7172
dontSpoofFor []string
7273
spoofTypes []string
74+
spoofSRV []string
7375
spoofingTemporarilyDisabled bool
7476
}
7577

@@ -128,6 +130,10 @@ func (c Config) PrintSummary() {
128130
if len(c.spoofTypes) != 0 {
129131
fmt.Println("Answering only queries of type: " + strings.Join(toUpper(c.spoofTypes), ", "))
130132
}
133+
134+
if len(c.spoofSRV) > 0 {
135+
fmt.Println("Spoofing SRV records: " + c.SpoofSRV.String())
136+
}
131137
}
132138

133139
if c.DelegateIgnoredTo != "" {
@@ -225,7 +231,7 @@ func (c *Config) setRedundantOptions() {
225231
}
226232
}
227233

228-
//nolint:forbidigo,maintidx,gocognit
234+
//nolint:forbidigo,maintidx,gocognit,gocyclo
229235
func configFromCLI() (config *Config, logger *Logger, err error) {
230236
var (
231237
interfaceName string
@@ -274,6 +280,9 @@ func configFromCLI() (config *Config, logger *Logger, err error) {
274280
"and subdomains are included when the hostname\nstarts with a dot, supports * globbing (blocklist)")
275281
pflag.StringSliceVar(&config.spoofTypes, "spoof-types", defaultSpoofTypes,
276282
"Only spoof these query `types` (A, AAA, ANY, SOA, all types are spoofed\nif it is empty)")
283+
pflag.StringSliceVar(&config.spoofSRV, "spoof-srv", defaultSpoofSRV,
284+
"Spoof SRV records for these services (format is `service:port`, "+
285+
"the port can be omitted of Kerberos, HTTP, LDAP and LDAPS)")
277286
pflag.BoolVar(&config.IgnoreDHCPv6NoFQDN, "ignore-nofqdn", defaultIgnoreDHCPv6NoFQDN,
278287
"Ignore DHCPv6 messages where the client did not include its\nFQDN (useful with allowlist or blocklists)")
279288
pflag.BoolVar(&config.IgnoreNonMicrosoftDHCP, "ignore-non-microsoft-dhcp", defaultIgnoreNonMicrosoftDHCP,
@@ -450,6 +459,11 @@ func configFromCLI() (config *Config, logger *Logger, err error) {
450459
return config, logger, fmt.Errorf("parsing --spoof-types: %w", err)
451460
}
452461

462+
config.SpoofSRV, err = asSRVMatchers(config.spoofSRV)
463+
if err != nil {
464+
return config, logger, fmt.Errorf("parsing --spoof-srv: %w", err)
465+
}
466+
453467
config.PrintSummary()
454468

455469
return config, logger, nil

defaults.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ var (
3636
vendorSpoofFor = ""
3737
vendorDontSpoofFor = ""
3838
vendorSpoofTypes = ""
39+
vendorSpoofSRV = ""
3940
vendorIgnoreDHCPv6NoFQDN = ""
4041
vendorIgnoreNonMicrosoftDHCP = ""
4142
vendorDelegateIgnoredTo = ""
@@ -85,6 +86,7 @@ var (
8586
defaultSpoofFor = forceStrings(vendorSpoofFor)
8687
defaultDontSpoofFor = forceStrings(vendorDontSpoofFor)
8788
defaultSpoofTypes = forceStrings(vendorSpoofTypes)
89+
defaultSpoofSRV = forceStrings(vendorSpoofSRV)
8890
defaultIgnoreDHCPv6NoFQDN = forceBool(vendorIgnoreDHCPv6NoFQDN, false)
8991
defaultIgnoreNonMicrosoftDHCP = forceBool(vendorIgnoreNonMicrosoftDHCP, false)
9092
defaultDelegateIgnoredTo = vendorDelegateIgnoredTo

dns.go

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ const (
2323
// NIMLOC in the DNS spec. In this case we don't expect actual NIMLOC
2424
// messages, so we assume type 32 messages to be NetBIOS requests.
2525
typeNetBios = dns.TypeNIMLOC
26+
27+
srvWeight = 100
2628
)
2729

2830
// HandlerType specifies the type of name resolution query that is currently being handled.
@@ -145,15 +147,27 @@ func createDNSReplyFromRequest(
145147
Locator: encodeNetBIOSLocator(config.RelayIPv4.To4()),
146148
})
147149
case dns.TypeSRV:
148-
answer := dns.SRV{Hdr: rrHeader(answerName, dns.TypeSRV, config.TTL), Target: answerName}
149-
reply.Answer = append(reply.Answer, &answer)
150+
srv := config.SpoofSRV.Get(answerName)
151+
if srv == nil {
152+
logger.Errorf("could not get SRV record for %q", q.Name)
153+
154+
continue
155+
}
156+
157+
target := removeServiceAndPort(answerName)
158+
159+
reply.Answer = append(reply.Answer, &dns.SRV{
160+
Hdr: rrHeader(target, dns.TypeSRV, config.TTL),
161+
Target: target,
162+
Port: srv.Port, Weight: srvWeight,
163+
})
150164

151165
if config.RelayIPv4 != nil {
152-
reply.Extra = append(reply.Extra, rr(config.RelayIPv4, answerName, config.TTL))
166+
reply.Extra = append(reply.Extra, rr(config.RelayIPv4, target, config.TTL))
153167
}
154168

155169
if config.RelayIPv6 != nil {
156-
reply.Extra = append(reply.Extra, rr(config.RelayIPv6, answerName, config.TTL))
170+
reply.Extra = append(reply.Extra, rr(config.RelayIPv6, target, config.TTL))
157171
}
158172
default:
159173
answers := handleIgnored(logger, q, questionName, queryType(q, request.Opcode), peer, IgnoreReasonQueryTypeUnhandled,
@@ -472,3 +486,15 @@ func delegateToDNSServer(dnsServer string, timeout time.Duration) delegateQuesti
472486
return reply.Answer, nil
473487
}
474488
}
489+
490+
func removeServiceAndPort(host string) string {
491+
var parts []string
492+
493+
for _, part := range strings.Split(host, ".") {
494+
if !strings.HasPrefix(part, "_") {
495+
parts = append(parts, part)
496+
}
497+
}
498+
499+
return strings.Join(parts, ".")
500+
}

dns_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,98 @@ func TestDelegatedQueryTCP(t *testing.T) {
408408
}
409409
}
410410

411+
func TestSRVQuery(t *testing.T) {
412+
t.Run("default_port", func(t *testing.T) { testSRVQuery(t, "ldap", 389) })
413+
t.Run("custom_port", func(t *testing.T) { testSRVQuery(t, "ldap:123", 123) })
414+
}
415+
416+
func testSRVQuery(tb testing.TB, matcher string, port uint16) {
417+
tb.Helper()
418+
419+
serviceName := "_ldap._tcp.some.host"
420+
421+
srvQuery := &dns.Msg{}
422+
srvQuery.SetQuestion(serviceName, dns.TypeSRV)
423+
424+
relayIPv4 := mustParseIP(tb, "10.0.0.2")
425+
relayIPv6 := mustParseIP(tb, "fe80::1")
426+
mockRW := mockResonseWriter{Remote: &net.UDPAddr{IP: mustParseIP(tb, "10.0.0.1")}}
427+
428+
cfg := &Config{
429+
RelayIPv4: relayIPv4,
430+
RelayIPv6: relayIPv6,
431+
SpoofSRV: testSRVMatchers(tb, []string{matcher}),
432+
}
433+
434+
reply := createDNSReplyFromRequest(mockRW, srvQuery, nil, cfg, HandlerTypeDNS, nil)
435+
if reply == nil {
436+
tb.Fatalf("no reply")
437+
438+
return
439+
}
440+
441+
if len(reply.Answer) == 0 {
442+
tb.Fatalf("no answer in reply")
443+
}
444+
445+
srvRecord, ok := reply.Answer[0].(*dns.SRV)
446+
if !ok {
447+
tb.Fatalf("answer is not an A record but a %T", reply.Answer[0])
448+
}
449+
450+
if srvRecord.Port != port {
451+
tb.Fatalf("port is %d instead of 389", srvRecord.Port)
452+
}
453+
454+
if srvRecord.Hdr.Name != removeServiceAndPort(serviceName) {
455+
tb.Fatalf("reply name is %q instead of %q", srvRecord.Hdr.Name, removeServiceAndPort(serviceName))
456+
}
457+
458+
if srvRecord.Target != removeServiceAndPort(serviceName) {
459+
tb.Fatalf("target name is %q instead of %q", srvRecord.Target, removeServiceAndPort(serviceName))
460+
}
461+
462+
if len(reply.Extra) != 2 {
463+
tb.Fatalf("reply contains %d extra records instead of 2", len(reply.Extra))
464+
}
465+
466+
var checkedA, checkedAAAA bool
467+
468+
for _, extra := range reply.Extra {
469+
switch e := extra.(type) {
470+
case *dns.A:
471+
checkedA = true
472+
473+
if e.Hdr.Name != removeServiceAndPort(serviceName) {
474+
tb.Fatalf("A extra record name is %s instead of %s", e.Hdr.Name, removeServiceAndPort(serviceName))
475+
}
476+
477+
if !e.A.Equal(relayIPv4) {
478+
tb.Fatalf("A extra record contains %s instead of %s", e.A, relayIPv4)
479+
}
480+
case *dns.AAAA:
481+
checkedAAAA = true
482+
483+
if e.Hdr.Name != removeServiceAndPort(serviceName) {
484+
tb.Fatalf("AAAA extra record name is %s instead of %s", e.Hdr.Name, removeServiceAndPort(serviceName))
485+
}
486+
487+
if !e.AAAA.Equal(relayIPv6) {
488+
tb.Fatalf("AAAA extra record contains %s instead of %s", e.AAAA, relayIPv6)
489+
}
490+
default:
491+
tb.Fatalf("unexpected extra record: %#v", extra)
492+
}
493+
}
494+
495+
switch {
496+
case !checkedA:
497+
tb.Fatalf("A extra record missing")
498+
case !checkedAAAA:
499+
tb.Fatalf("AAAA extra record missing")
500+
}
501+
}
502+
411503
func testReply(tb testing.TB, requestFileName string, replyFileName string) {
412504
tb.Helper()
413505

filter.go

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"net"
77
"regexp"
8+
"strconv"
89
"strings"
910
"time"
1011

@@ -19,6 +20,13 @@ const (
1920
func shouldRespondToNameResolutionQuery(config *Config, host string, queryType uint16,
2021
from net.IP, fromHostnames []string, handlerType HandlerType,
2122
) (bool, string) {
23+
var hostWithService string
24+
25+
if queryType == dns.TypeSRV {
26+
hostWithService = host
27+
host = removeServiceAndPort(host)
28+
}
29+
2230
if config.spoofingTemporarilyDisabled {
2331
return false, "spoofing is temporarily disabled"
2432
}
@@ -63,7 +71,13 @@ func shouldRespondToNameResolutionQuery(config *Config, host string, queryType u
6371
}
6472

6573
if !config.SpoofTypes.ShouldSpoof(queryType) {
66-
return false, fmt.Sprintf("type %s is not in spoof-types", dnsQueryType(queryType))
74+
return false, fmt.Sprintf("type %s is not in spoof-types list", dnsQueryType(queryType))
75+
}
76+
77+
if queryType == dns.TypeSRV && !config.SpoofSRV.Contains(hostWithService) {
78+
service, _, _ := strings.Cut(hostWithService, ".")
79+
80+
return false, fmt.Sprintf("service %s is not in spoof-srv list", strings.TrimPrefix(service, "_"))
6781
}
6882

6983
switch {
@@ -369,3 +383,104 @@ func starToRegex(s string) (*regexp.Regexp, error) {
369383

370384
return regexp.Compile("^" + starReplacerRE.ReplaceAllString(regexp.QuoteMeta(s), ".*") + "$")
371385
}
386+
387+
type srvMatchers []*srvMatcher
388+
389+
func asSRVMatchers(matcherStrings []string) (srvMatchers, error) {
390+
matchers := make(srvMatchers, 0, len(matcherStrings))
391+
392+
for _, m := range matcherStrings {
393+
matcher := &srvMatcher{
394+
Service: strings.ToLower(m),
395+
}
396+
397+
switch strings.Count(matcher.Service, ":") {
398+
case 0:
399+
matcher.isDefaultPort = true
400+
401+
switch matcher.Service {
402+
case "ldap":
403+
matcher.Port = 389
404+
case "ldaps":
405+
matcher.Port = 636
406+
case "http":
407+
matcher.Port = 80
408+
case "https":
409+
matcher.Port = 443
410+
case "kerberos":
411+
matcher.Port = 88
412+
default:
413+
return nil, fmt.Errorf("missing port in service: %q", m)
414+
}
415+
416+
matchers = append(matchers, matcher)
417+
case 1:
418+
service, portStr, found := strings.Cut(m, ":")
419+
if !found {
420+
return nil, fmt.Errorf("cannot parse service: %q", m)
421+
}
422+
423+
port, err := strconv.Atoi(portStr)
424+
if err != nil {
425+
return nil, fmt.Errorf("parse port %q in service %q", portStr, m)
426+
}
427+
428+
matchers = append(matchers, &srvMatcher{Service: strings.ToLower(service), Port: uint16(port)}) //nolint:gosec
429+
default:
430+
return nil, fmt.Errorf("SRV matcher contains more than one colon: %q", m)
431+
}
432+
}
433+
434+
return matchers, nil
435+
}
436+
437+
func (matchers srvMatchers) Contains(service string) bool {
438+
service, _, _ = strings.Cut(service, ".")
439+
440+
for _, m := range matchers {
441+
if m.Matches(service) {
442+
return true
443+
}
444+
}
445+
446+
return false
447+
}
448+
449+
func (matchers srvMatchers) String() string {
450+
elements := make([]string, 0, len(matchers))
451+
452+
for _, m := range matchers {
453+
switch {
454+
case m.isDefaultPort:
455+
elements = append(elements, m.Service)
456+
default:
457+
elements = append(elements, fmt.Sprintf("%s:%d", m.Service, m.Port))
458+
}
459+
}
460+
461+
return strings.Join(elements, ", ")
462+
}
463+
464+
func (matchers srvMatchers) Get(service string) *srvMatcher {
465+
service, _, _ = strings.Cut(service, ".")
466+
467+
for _, m := range matchers {
468+
if m.Matches(service) {
469+
return m
470+
}
471+
}
472+
473+
return nil
474+
}
475+
476+
type srvMatcher struct {
477+
Service string
478+
Port uint16
479+
isDefaultPort bool
480+
}
481+
482+
func (sm *srvMatcher) Matches(service string) bool {
483+
service, _, _ = strings.Cut(service, ".")
484+
485+
return strings.EqualFold(strings.TrimPrefix(sm.Service, "_"), strings.TrimPrefix(service, "_"))
486+
}

0 commit comments

Comments
 (0)