Handling Kerberos records (analyzer + editor + checker)

This commit is contained in:
nemunaire 2026-04-22 08:13:11 +07:00
commit 9dddbe75c9
4 changed files with 341 additions and 0 deletions

32
checkers/kerberos.go Normal file
View file

@ -0,0 +1,32 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package checkers
import (
kerberos "git.happydns.org/checker-kerberos/checker"
"git.happydns.org/happyDomain/internal/checker"
)
func init() {
checker.RegisterObservationProvider(kerberos.Provider())
checker.RegisterExternalizableChecker(kerberos.Definition())
}

View file

@ -0,0 +1,179 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package abstract
import (
"bytes"
"strconv"
"strings"
"github.com/miekg/dns"
"git.happydns.org/happyDomain/internal/helpers"
svc "git.happydns.org/happyDomain/internal/service"
"git.happydns.org/happyDomain/model"
)
// Kerberos groups the SRV records that advertise a Kerberos realm:
// KDC (TCP & UDP on 88), master KDC, admin server (kadmin) and
// kpasswd. Each slice is optional; the presence of at least one
// `_kerberos._tcp.` or `_kerberos._udp.` record is what advertises the
// realm to clients.
type Kerberos struct {
KDCTCP []*dns.SRV `json:"kdc_tcp,omitempty"`
KDCUDP []*dns.SRV `json:"kdc_udp,omitempty"`
Master []*dns.SRV `json:"master,omitempty"`
Admin []*dns.SRV `json:"admin,omitempty"`
KPasswdTCP []*dns.SRV `json:"kpasswd_tcp,omitempty"`
KPasswdUDP []*dns.SRV `json:"kpasswd_udp,omitempty"`
}
func (s *Kerberos) all() []*dns.SRV {
out := make([]*dns.SRV, 0,
len(s.KDCTCP)+len(s.KDCUDP)+len(s.Master)+len(s.Admin)+len(s.KPasswdTCP)+len(s.KPasswdUDP))
out = append(out, s.KDCTCP...)
out = append(out, s.KDCUDP...)
out = append(out, s.Master...)
out = append(out, s.Admin...)
out = append(out, s.KPasswdTCP...)
out = append(out, s.KPasswdUDP...)
return out
}
func (s *Kerberos) GetNbResources() int {
return len(s.all())
}
func (s *Kerberos) GenComment() string {
dest := map[string][]uint16{}
destloop:
for _, srv := range s.KDCTCP {
for _, port := range dest[srv.Target] {
if port == srv.Port {
continue destloop
}
}
dest[srv.Target] = append(dest[srv.Target], srv.Port)
}
for _, srv := range s.KDCUDP {
dest[srv.Target] = append(dest[srv.Target], srv.Port)
}
var buffer bytes.Buffer
first := true
for dn, ports := range dest {
if !first {
buffer.WriteString("; ")
} else {
first = false
}
buffer.WriteString(dn)
buffer.WriteString(" (")
firstport := true
for _, port := range ports {
if !firstport {
buffer.WriteString(", ")
} else {
firstport = false
}
buffer.WriteString(strconv.Itoa(int(port)))
}
buffer.WriteString(")")
}
return buffer.String()
}
func (s *Kerberos) GetRecords(domain string, ttl uint32, origin string) ([]happydns.Record, error) {
all := s.all()
rrs := make([]happydns.Record, len(all))
for i, srv := range all {
rrs[i] = srv
}
return rrs, nil
}
func kerberos_analyze(a *svc.Analyzer) error {
realms := map[string]*Kerberos{}
type bucket struct {
prefix string
append func(k *Kerberos, s *dns.SRV)
}
buckets := []bucket{
{"_kerberos._tcp.", func(k *Kerberos, s *dns.SRV) { k.KDCTCP = append(k.KDCTCP, s) }},
{"_kerberos._udp.", func(k *Kerberos, s *dns.SRV) { k.KDCUDP = append(k.KDCUDP, s) }},
{"_kerberos-master._tcp.", func(k *Kerberos, s *dns.SRV) { k.Master = append(k.Master, s) }},
{"_kerberos-adm._tcp.", func(k *Kerberos, s *dns.SRV) { k.Admin = append(k.Admin, s) }},
{"_kpasswd._tcp.", func(k *Kerberos, s *dns.SRV) { k.KPasswdTCP = append(k.KPasswdTCP, s) }},
{"_kpasswd._udp.", func(k *Kerberos, s *dns.SRV) { k.KPasswdUDP = append(k.KPasswdUDP, s) }},
}
for _, b := range buckets {
for _, record := range a.SearchRR(svc.AnalyzerRecordFilter{Prefix: b.prefix, Type: dns.TypeSRV}) {
domain := strings.TrimPrefix(record.Header().Name, b.prefix)
if _, ok := realms[domain]; !ok {
realms[domain] = &Kerberos{}
}
srv, ok := record.(*dns.SRV)
if !ok {
continue
}
rel := helpers.RRRelativeSubdomain(srv, a.GetOrigin(), domain).(*dns.SRV)
b.append(realms[domain], rel)
if err := a.UseRR(srv, domain, realms[domain]); err != nil {
return err
}
}
}
return nil
}
func init() {
svc.RegisterService(
func() happydns.ServiceBody {
return &Kerberos{}
},
kerberos_analyze,
happydns.ServiceInfos{
Name: "Kerberos",
Description: "Advertise a Kerberos realm (KDC, kadmin, kpasswd) through DNS.",
Family: happydns.SERVICE_FAMILY_ABSTRACT,
Categories: []string{
"service",
"authentication",
},
Restrictions: happydns.ServiceRestrictions{
NearAlone: true,
Single: true,
NeedTypes: []uint16{
dns.TypeSRV,
},
},
},
1,
)
}

View file

@ -0,0 +1,112 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-2026 happyDomain
Authors: Pierre-Olivier Mercier, et al.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at <contact@happydomain.org>.
For AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import TableRecords from "$lib/components/records/TableRecords.svelte";
import RawInput from "$lib/components/inputs/raw.svelte";
import type { Domain } from "$lib/model/domain";
import type { dnsResource, dnsTypeSRV } from "$lib/dns_rr";
interface Props {
dn: string;
origin: Domain;
readonly?: boolean;
value: dnsResource;
}
let { dn, origin, readonly = false, value = $bindable({}) }: Props = $props();
const type = "abstract.Kerberos";
// Each bucket maps to one field on the Go service body. The `key` here
// matches the JSON tag set on the Kerberos struct (see
// services/abstract/kerberos.go).
const buckets = [
{ key: "kdc_tcp", prefix: "_kerberos._tcp", label: "KDC (TCP)" },
{ key: "kdc_udp", prefix: "_kerberos._udp", label: "KDC (UDP)" },
{ key: "master", prefix: "_kerberos-master._tcp", label: "Master KDC" },
{ key: "admin", prefix: "_kerberos-adm._tcp", label: "Admin server (kadmin)" },
{ key: "kpasswd_tcp", prefix: "_kpasswd._tcp", label: "Password change (TCP)" },
{ key: "kpasswd_udp", prefix: "_kpasswd._udp", label: "Password change (UDP)" },
];
// Initialize empty arrays for buckets the server omitted.
for (const b of buckets) {
if (!(value as any)[b.key]) {
(value as any)[b.key] = [];
}
}
// Keep each record's Hdr.Name pinned to its bucket prefix so we don't
// accidentally write mis-labeled SRV records.
$effect(() => {
for (const b of buckets) {
const arr = (value as any)[b.key] as Array<dnsTypeSRV> | undefined;
if (!arr) continue;
for (const record of arr) {
if (record?.Hdr && record.Hdr.Name !== b.prefix) {
record.Hdr.Name = b.prefix;
}
}
}
});
</script>
{#each buckets as bucket}
<div class="mb-4">
<h5 class="pb-1 border-bottom border-1">{bucket.label}</h5>
<TableRecords
class="mt-3"
dn={bucket.prefix}
edit
{origin}
bind:rrs={(value as any)[bucket.key]}
rrtype="SRV"
>
{#snippet header(field: string)}
{#if field == "Priority"}
Priority
{:else if field == "Weight"}
Weight
{:else if field == "Port"}
Port
{:else if field == "Target"}
Target
{/if}
{/snippet}
{#snippet field(idx: number, field: string)}
{@const bucketArray = (value as any)[bucket.key] as Array<dnsTypeSRV>}
{#if bucketArray && bucketArray[idx]}
<RawInput
edit
index={bucket.key + idx.toString()}
specs={{
id: field,
type: field == "Target" ? "string" : "uint16",
}}
bind:value={bucketArray[idx][field as keyof dnsTypeSRV]}
/>
{/if}
{/snippet}
</TableRecords>
</div>
{/each}

View file

@ -78,6 +78,24 @@ export const servicesSpecs: Record<string, ServiceInfos> = {
"record_types": null,
"restrictions": {}
},
"abstract.Kerberos": {
"name": "Kerberos",
"_svctype": "abstract.Kerberos",
"description": "Advertise a Kerberos realm (KDC, kadmin, kpasswd) through DNS.",
"family": "abstract",
"categories": [
"service",
"authentication"
],
"record_types": null,
"restrictions": {
"nearAlone": true,
"needTypes": [
33
],
"single": true
}
},
"abstract.KeybaseVerif": {
"name": "Keybase Verification",
"_svctype": "abstract.KeybaseVerif",