Handling Kerberos records (analyzer + editor + checker)
This commit is contained in:
parent
f82a72de4d
commit
9dddbe75c9
4 changed files with 341 additions and 0 deletions
32
checkers/kerberos.go
Normal file
32
checkers/kerberos.go
Normal 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())
|
||||
}
|
||||
179
services/abstract/kerberos.go
Normal file
179
services/abstract/kerberos.go
Normal 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,
|
||||
)
|
||||
}
|
||||
112
web/src/lib/components/services/editors/abstract.Kerberos.svelte
Normal file
112
web/src/lib/components/services/editors/abstract.Kerberos.svelte
Normal 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}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue