Compare commits

...
Sign in to create a new pull request.

55 commits
dev ... main

Author SHA1 Message Date
Virgil
6b9a6c1bba Add explicit DNS runtime alias 2026-04-04 03:36:44 +00:00
Virgil
d964b98e0c Add explicit service runtime aliases 2026-04-04 03:34:07 +00:00
Virgil
93f22e6942 feat(dns): include wildcard names in reverse lookup
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 03:31:28 +00:00
Virgil
c41756c2df Support spaced HNS alias comments 2026-04-04 03:29:00 +00:00
Virgil
1c91ff091f feat(action): accept PTR-style names in reverse lookup
Co-authored-by: Virgil <virgil@lethean.io>
2026-04-04 03:26:45 +00:00
Virgil
8b6fcf0946 Refine internal record expiry naming 2026-04-04 03:24:03 +00:00
Virgil
3197355258 refactor(dns): expose explicit health runtime field 2026-04-04 03:21:26 +00:00
Virgil
9aab7dde69 refactor(dns): centralize tree root selection
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 03:19:17 +00:00
Virgil
95663717f4 fix(dns): skip wildcard templates in reverse lookup
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 03:17:07 +00:00
Virgil
833db1974d Align resolve.all payload with RFC 2026-04-04 03:14:35 +00:00
Virgil
3efa3308a5 Refine DNS payload docs for AX 2026-04-04 03:11:47 +00:00
Virgil
d0fe2199c4 ax(dns): add explicit health port aliases 2026-04-04 03:09:53 +00:00
Virgil
b7f6912ef0 feat(dns): cache reverse lookups with ttl
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 03:06:17 +00:00
Virgil
58509bba4d Add explicit PTR reverse lookup aliases 2026-04-04 03:03:14 +00:00
Virgil
b72191f03b Clarify discovery usage examples 2026-04-04 03:00:44 +00:00
Virgil
89b71c9391 AX cleanup for discovery helpers 2026-04-04 03:00:34 +00:00
Virgil
2a76e5ef0c AX cleanup for chain alias discovery 2026-04-04 02:58:00 +00:00
Virgil
612cf06c06 Add semantic service snapshot alias 2026-04-04 02:55:03 +00:00
Virgil
d1e884f2e2 fix(action): make dns.discover side-effect only
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 02:52:17 +00:00
Virgil
d0b3da9494 fix(dns): add runtime address alias
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 02:49:27 +00:00
Virgil
0be2f529a0 Stabilize dns.resolve.all payload shape 2026-04-04 02:45:54 +00:00
Virgil
ff0ab358df AX cleanup for DNS result types 2026-04-04 02:43:22 +00:00
Virgil
04c3bd5997 docs: sharpen DNS action examples 2026-04-04 02:40:44 +00:00
Virgil
da91954490 feat(dns): add explicit client constructors
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 02:38:37 +00:00
Virgil
d10a9f9073 feat(api): add explicit DNS aliases
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 02:27:47 +00:00
Virgil
25d4b85e56 Add explicit DNS configuration aliases 2026-04-04 02:24:40 +00:00
Virgil
5968a4cc50 Add semantic service snapshot 2026-04-04 02:21:56 +00:00
Virgil
b4b1e5c930 Add wildcard-aware TXT match helper 2026-04-04 02:18:47 +00:00
Virgil
b417373f5b Add explicit address resolution alias 2026-04-04 02:16:14 +00:00
Virgil
1810959b89 feat(action): add snake_case bind_address alias for serve
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 00:20:49 +00:00
Virgil
fcdc2c54f9 feat(dns): accept case-insensitive action arguments
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 00:19:10 +00:00
Virgil
6a69356d51 feat(dns): accept PTR-name reverse lookups
This enables dns.reverse and ResolveReverse to accept in-addr.arpa / ip6.arpa PTR names while also making map-based alias lists deterministic.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 00:16:59 +00:00
Virgil
fb7236bf12 feat(dns): add explicit service startup wrappers
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 00:15:03 +00:00
Virgil
56be52f7bb feat(dns): add snake_case dns_port for dns.serve action
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 00:13:41 +00:00
Virgil
a78523b085 feat(dns): add explicit DNS service constructor aliases
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 00:11:37 +00:00
Virgil
32543b2e12 feat(dns): parse hsd record fields case-insensitively
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 00:09:51 +00:00
Virgil
08e0d201e1 feat(dns): parse chain alias action maps
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 00:08:28 +00:00
Virgil
f0a6c12443 feat(action): accept semantic alias keys in resolve and reverse actions
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 00:06:46 +00:00
Virgil
5fd82dd342 feat(dns): add nil-safe service method guards
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 00:04:47 +00:00
Virgil
b6f9d50393 feat(service): expose wildcard-aware resolve API
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 00:02:17 +00:00
Virgil
bcf714d54c feat(dns): support camelCase dns.serve action args
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-04 00:00:51 +00:00
Virgil
8807fee752 feat(dns): auto-start health server when HTTPPort is configured
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-03 23:59:00 +00:00
Virgil
50b2394fdd feat(hsd): parse single-value name resource records
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-03 23:57:02 +00:00
Virgil
added0ece8 feat(dns): infer chain alias action caller from registrar
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-03 23:55:18 +00:00
Virgil
3af5018f35 feat(dns): parse typed chain alias discovery results
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-03 23:53:18 +00:00
Virgil
edb852ce23 feat(dns): fall back mainchain auth to HSD credentials
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-03 23:51:36 +00:00
Virgil
f1c0f9cf2b chore(dns): normalize DNS record output
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-03 23:49:53 +00:00
Virgil
33993b9780 feat(dns): add explicit DNS and HTTP listen port accessors
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-03 23:48:26 +00:00
Virgil
6e6b2b63c2 feat(dns): treat nil chain alias action result as empty alias list
Treating a nil blockchain.chain.aliases response as an explicit empty alias set allows dns.discover to clear stale cache and avoid unnecessary fallback.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-03 23:46:56 +00:00
Virgil
958a799c45 feat(dns): add explicit DNS service constructor aliases
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-03 23:45:05 +00:00
Virgil
8857ed4e51 feat(dns): allow empty alias discovery without HSD client
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-03 23:42:29 +00:00
Virgil
f6afe97b35 feat(ax): expose normalization helpers for agent usage
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-03 23:40:38 +00:00
Virgil
5d002f8192 feat(service): add health port option to serve action
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-03 23:38:00 +00:00
Virgil
5b223e850c fix(mainchain): parse hns alias tokens case-insensitively
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-03 23:36:02 +00:00
Virgil
9b077efe4e feat(dns): default serve ports from configured values when unset
Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-03 23:32:52 +00:00
11 changed files with 2321 additions and 298 deletions

144
action.go
View file

@ -25,10 +25,19 @@ var (
)
const (
actionArgBind = "bind"
actionArgIP = "ip"
actionArgName = "name"
actionArgPort = "port"
actionArgBind = "bind"
actionArgBindAddress = "bindAddress"
actionArgBindAddressSnake = "bind_address"
actionArgIP = "ip"
actionArgAddress = "address"
actionArgName = "name"
actionArgHost = "host"
actionArgHostName = "hostname"
actionArgPort = "port"
actionArgDNSPort = "dnsPort"
actionArgDNSPortSnake = "dns_port"
actionArgHealthPort = "health_port"
actionArgHealthPortCamel = "healthPort"
)
type ActionDefinition struct {
@ -40,7 +49,7 @@ type ActionDefinition struct {
// ActionRegistrar publishes DNS actions into another Core surface.
//
// registrar.RegisterAction(ActionResolve, func(values map[string]any) (any, bool, error) {
// return service.ResolveAddress("gateway.charon.lthn")
// return service.ResolveAddresses("gateway.charon.lthn")
// })
type ActionRegistrar interface {
RegisterAction(name string, invoke func(map[string]any) (any, bool, error))
@ -75,6 +84,7 @@ type ActionCaller interface {
// for _, definition := range definitions {
// fmt.Println(definition.Name)
// }
// // dns.resolve, dns.resolve.txt, dns.resolve.all, dns.reverse, dns.serve, dns.health, dns.discover
func (service *Service) ActionDefinitions() []ActionDefinition {
return []ActionDefinition{
{
@ -137,13 +147,13 @@ func (service *Service) ActionDefinitions() []ActionDefinition {
if err := service.DiscoverAliases(context.Background()); err != nil {
return nil, false, err
}
return service.Health(), true, nil
return nil, true, nil
},
InvokeContext: func(ctx context.Context, _ map[string]any) (any, bool, error) {
if err := service.DiscoverAliases(ctx); err != nil {
return nil, false, err
}
return service.Health(), true, nil
return nil, true, nil
},
},
}
@ -166,6 +176,7 @@ func (service *Service) ActionNames() []string {
//
// service.RegisterActions(registrar)
// // registrar.RegisterAction("dns.health", ...)
// // registrar.RegisterActionContext("dns.discover", func(ctx context.Context, values map[string]any) (any, bool, error) { ... })
func (service *Service) RegisterActions(registrar ActionRegistrar) {
if registrar == nil {
return
@ -191,6 +202,8 @@ func (service *Service) RegisterActions(registrar ActionRegistrar) {
// NewServiceWithRegistrar builds a DNS service and registers its actions in one step.
//
// Deprecated: use NewDNSServiceWithRegistrar for a more explicit constructor name.
//
// service := dns.NewServiceWithRegistrar(dns.ServiceConfiguration{}, registrar)
// // registrar now exposes dns.resolve, dns.resolve.txt, dns.resolve.all, dns.reverse, dns.serve, dns.health, dns.discover
func NewServiceWithRegistrar(options ServiceOptions, registrar ActionRegistrar) *Service {
@ -200,6 +213,14 @@ func NewServiceWithRegistrar(options ServiceOptions, registrar ActionRegistrar)
return NewService(options)
}
// NewDNSServiceWithRegistrar builds a DNS service and registers its actions in one step.
//
// service := dns.NewDNSServiceWithRegistrar(dns.ServiceConfiguration{}, registrar)
// // registrar now exposes dns.resolve, dns.resolve.txt, dns.resolve.all, dns.reverse, dns.serve, dns.health, dns.discover
func NewDNSServiceWithRegistrar(options ServiceOptions, registrar ActionRegistrar) *Service {
return NewServiceWithRegistrar(options, registrar)
}
// HandleAction executes a DNS action by name.
//
// payload, ok, err := service.HandleAction(ActionResolve, map[string]any{
@ -231,11 +252,11 @@ func (service *Service) HandleActionContext(ctx context.Context, name string, va
func (service *Service) handleResolveAddress(ctx context.Context, values map[string]any) (any, bool, error) {
_ = ctx
host, err := stringActionValue(values, actionArgName)
host, err := stringActionValueFromKeys(values, actionArgName, actionArgHost, actionArgHostName)
if err != nil {
return nil, false, err
}
result, ok := service.ResolveAddress(host)
result, ok := service.ResolveAddresses(host)
if !ok {
return nil, false, nil
}
@ -244,7 +265,7 @@ func (service *Service) handleResolveAddress(ctx context.Context, values map[str
func (service *Service) handleResolveTXTRecords(ctx context.Context, values map[string]any) (any, bool, error) {
_ = ctx
host, err := stringActionValue(values, actionArgName)
host, err := stringActionValueFromKeys(values, actionArgName, actionArgHost, actionArgHostName)
if err != nil {
return nil, false, err
}
@ -257,7 +278,7 @@ func (service *Service) handleResolveTXTRecords(ctx context.Context, values map[
func (service *Service) handleResolveAll(ctx context.Context, values map[string]any) (any, bool, error) {
_ = ctx
host, err := stringActionValue(values, actionArgName)
host, err := stringActionValueFromKeys(values, actionArgName, actionArgHost, actionArgHostName)
if err != nil {
return nil, false, err
}
@ -270,7 +291,7 @@ func (service *Service) handleResolveAll(ctx context.Context, values map[string]
func (service *Service) handleReverseLookup(ctx context.Context, values map[string]any) (any, bool, error) {
_ = ctx
ip, err := stringActionValue(values, actionArgIP)
ip, err := stringActionValueFromKeys(values, actionArgIP, actionArgAddress, actionArgName, actionArgHost, actionArgHostName)
if err != nil {
return nil, false, err
}
@ -283,17 +304,37 @@ func (service *Service) handleReverseLookup(ctx context.Context, values map[stri
func (service *Service) handleServe(ctx context.Context, values map[string]any) (any, bool, error) {
_ = ctx
bind, err := stringActionValueOptional(values, actionArgBind)
bind, _, err := stringActionValueOptionalFromKeys(values, actionArgBind, actionArgBindAddress, actionArgBindAddressSnake)
if err != nil {
return nil, false, err
}
port, portProvided, err := intActionValueOptional(values, actionArgPort)
port, portProvided, err := intActionValueOptionalFromKeys(values, actionArgPort, actionArgDNSPort, actionArgDNSPortSnake)
if err != nil {
return nil, false, err
}
if !portProvided {
port = service.ResolveDNSPort()
}
healthPort, healthPortProvided, err := intActionValueOptionalFromKeys(values, actionArgHealthPort, actionArgHealthPortCamel)
if err != nil {
return nil, false, err
}
if !healthPortProvided && service.httpPort > 0 {
runtime, err := service.ServeAll(bind, port, service.httpPort)
if err != nil {
return nil, false, err
}
return runtime, true, nil
}
if healthPortProvided {
runtime, err := service.ServeAll(bind, port, healthPort)
if err != nil {
return nil, false, err
}
return runtime, true, nil
}
result, err := service.Serve(bind, port)
if err != nil {
return nil, false, err
@ -319,6 +360,46 @@ func stringActionValue(values map[string]any, key string) (string, error) {
return "", errActionMissingValue
}
func lookupActionValue(values map[string]any, keys ...string) (any, string, bool) {
if values == nil {
return nil, "", false
}
for _, key := range keys {
if value, exists := values[key]; exists {
return value, key, true
}
}
for _, key := range keys {
for alias, value := range values {
if strings.EqualFold(alias, key) {
return value, alias, true
}
}
}
return nil, "", false
}
func stringActionValueFromKeys(values map[string]any, keys ...string) (string, error) {
value, key, found := lookupActionValue(values, keys...)
if !found {
return "", errActionMissingValue
}
text, ok := value.(string)
if !ok {
return "", fmt.Errorf("%w: %s", errActionMissingValue, key)
}
text = strings.TrimSpace(text)
if text == "" {
return "", errActionMissingValue
}
return text, nil
}
func stringActionValueOptional(values map[string]any, key string) (string, error) {
if values == nil {
return "", nil
@ -334,6 +415,18 @@ func stringActionValueOptional(values map[string]any, key string) (string, error
return strings.TrimSpace(value), nil
}
func stringActionValueOptionalFromKeys(values map[string]any, keys ...string) (string, bool, error) {
value, key, found := lookupActionValue(values, keys...)
if !found {
return "", false, nil
}
text, ok := value.(string)
if !ok {
return "", false, fmt.Errorf("%w: %s", errActionMissingValue, key)
}
return strings.TrimSpace(text), true, nil
}
func intActionValue(values map[string]any, key string) (int, error) {
if values == nil {
return 0, errActionMissingValue
@ -420,3 +513,26 @@ func intActionValueOptional(values map[string]any, key string) (int, bool, error
}
return value, true, nil
}
func intActionValueOptionalFromKeys(values map[string]any, keys ...string) (int, bool, error) {
for _, key := range keys {
value, valueProvided, err := intActionValueOptional(values, key)
if err != nil {
return 0, false, err
}
if valueProvided {
return value, true, nil
}
}
value, key, found := lookupActionValue(values, keys...)
if !found {
return 0, false, nil
}
intValue, err := intActionValue(map[string]any{key: value}, key)
if err != nil {
return 0, false, err
}
return intValue, true, nil
}

5
go.mod
View file

@ -2,7 +2,10 @@ module dappco.re/go/dns
go 1.22
require github.com/miekg/dns v1.1.62
require (
github.com/miekg/dns v1.1.62
github.com/patrickmn/go-cache v2.1.0+incompatible
)
require (
golang.org/x/mod v0.18.0 // indirect

2
go.sum
View file

@ -1,5 +1,7 @@
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=

166
hsd.go
View file

@ -28,6 +28,20 @@ type HSDClientOptions struct {
HTTPClient *http.Client
}
// HSDClientConfiguration is the explicit long-form alias for HSDClientOptions.
//
// client := dns.NewHSDClientFromConfiguration(dns.HSDClientConfiguration{
// URL: "http://127.0.0.1:14037",
// Username: "user",
// Password: "pass",
// })
type HSDClientConfiguration = HSDClientOptions
// HSDClientConfig is kept for compatibility with older call sites.
//
// Deprecated: use HSDClientConfiguration instead.
type HSDClientConfig = HSDClientOptions
type HSDClient struct {
baseURL string
username string
@ -93,6 +107,28 @@ func NewHSDClient(options HSDClientOptions) *HSDClient {
}
}
// NewHSDClientFromOptions is the explicit long-form constructor name.
//
// client := dns.NewHSDClientFromOptions(dns.HSDClientOptions{
// URL: "http://127.0.0.1:14037",
// Username: "user",
// Password: "pass",
// })
func NewHSDClientFromOptions(options HSDClientOptions) *HSDClient {
return NewHSDClient(options)
}
// NewHSDClientFromConfiguration is the explicit constructor name for the HSD configuration alias.
//
// client := dns.NewHSDClientFromConfiguration(dns.HSDClientConfiguration{
// URL: "http://127.0.0.1:14037",
// Username: "user",
// Password: "pass",
// })
func NewHSDClientFromConfiguration(options HSDClientConfiguration) *HSDClient {
return NewHSDClient(options)
}
func (client *HSDClient) GetNameResource(ctx context.Context, name string) (NameRecords, error) {
normalized := strings.TrimSpace(name)
if normalized == "" {
@ -185,51 +221,127 @@ func (client *HSDClient) call(ctx context.Context, request HSDRPCRequest) (json.
}
func parseHSDNameResource(raw json.RawMessage) (NameRecords, error) {
var result NameRecords
var wrapper map[string]json.RawMessage
if err := json.Unmarshal(raw, &wrapper); err != nil {
return result, err
return NameRecords{}, err
}
if recordsRaw, ok := wrapper["records"]; ok {
if err := json.Unmarshal(recordsRaw, &result); err != nil {
result, err := parseHSDNameResourceRecords(recordsRaw)
if err != nil {
return NameRecords{}, err
}
return result, nil
}
if _, ok := wrapper["a"]; ok {
if err := json.Unmarshal(raw, &result); err != nil {
if hasHSDNameResourceField(wrapper) {
result, err := parseHSDNameResourceRecords(raw)
if err != nil {
return NameRecords{}, err
}
return result, nil
}
var wrapped struct {
A []string `json:"a"`
AAAA []string `json:"aaaa"`
TXT []string `json:"txt"`
NS []string `json:"ns"`
DS []string `json:"ds"`
DNSKEY []string `json:"dnskey"`
RRSIG []string `json:"rrsig"`
}
if err := json.Unmarshal(raw, &wrapped); err == nil {
result = NameRecords{
A: wrapped.A,
AAAA: wrapped.AAAA,
TXT: wrapped.TXT,
NS: wrapped.NS,
DS: wrapped.DS,
DNSKEY: wrapped.DNSKEY,
RRSIG: wrapped.RRSIG,
}
return result, nil
}
return NameRecords{}, errors.New("unable to parse getnameresource result")
}
func getCaseInsensitiveRecordField(fields map[string]json.RawMessage, key string) json.RawMessage {
if fields == nil {
return nil
}
for candidate, value := range fields {
if strings.EqualFold(candidate, key) {
return value
}
}
return nil
}
func parseHSDNameResourceRecords(raw json.RawMessage) (NameRecords, error) {
var fields map[string]json.RawMessage
if err := json.Unmarshal(raw, &fields); err != nil {
return NameRecords{}, err
}
a, err := parseHSDRecordValue(getCaseInsensitiveRecordField(fields, "a"))
if err != nil {
return NameRecords{}, err
}
aaaa, err := parseHSDRecordValue(getCaseInsensitiveRecordField(fields, "aaaa"))
if err != nil {
return NameRecords{}, err
}
txt, err := parseHSDRecordValue(getCaseInsensitiveRecordField(fields, "txt"))
if err != nil {
return NameRecords{}, err
}
ns, err := parseHSDRecordValue(getCaseInsensitiveRecordField(fields, "ns"))
if err != nil {
return NameRecords{}, err
}
ds, err := parseHSDRecordValue(getCaseInsensitiveRecordField(fields, "ds"))
if err != nil {
return NameRecords{}, err
}
dnsKey, err := parseHSDRecordValue(getCaseInsensitiveRecordField(fields, "dnskey"))
if err != nil {
return NameRecords{}, err
}
rrSig, err := parseHSDRecordValue(getCaseInsensitiveRecordField(fields, "rrsig"))
if err != nil {
return NameRecords{}, err
}
return NameRecords{
A: a,
AAAA: aaaa,
TXT: txt,
NS: ns,
DS: ds,
DNSKEY: dnsKey,
RRSIG: rrSig,
}, nil
}
func hasHSDNameResourceField(fields map[string]json.RawMessage) bool {
if len(fields) == 0 {
return false
}
return getCaseInsensitiveRecordField(fields, "a") != nil ||
getCaseInsensitiveRecordField(fields, "aaaa") != nil ||
getCaseInsensitiveRecordField(fields, "txt") != nil ||
getCaseInsensitiveRecordField(fields, "ns") != nil ||
getCaseInsensitiveRecordField(fields, "ds") != nil ||
getCaseInsensitiveRecordField(fields, "dnskey") != nil ||
getCaseInsensitiveRecordField(fields, "rrsig") != nil
}
func parseHSDRecordValue(raw json.RawMessage) ([]string, error) {
if len(bytes.TrimSpace(raw)) == 0 {
return nil, nil
}
if trimmed := bytes.TrimSpace(raw); bytes.Equal(trimmed, []byte("null")) {
return nil, nil
}
var values []string
if err := json.Unmarshal(raw, &values); err == nil {
return values, nil
}
var value string
if err := json.Unmarshal(raw, &value); err == nil {
if value == "" {
return nil, nil
}
return []string{value}, nil
}
return nil, fmt.Errorf("unable to parse DNS record field")
}
func parseHSDBlockchainInfo(raw json.RawMessage) (HSDBlockchainInfo, error) {
var info HSDBlockchainInfo
var wrapper map[string]json.RawMessage

View file

@ -52,6 +52,18 @@ func TestHSDClientGetNameResourceCallsRPCAndParsesResult(t *testing.T) {
}
}
func TestNewHSDClientFromOptionsMatchesNewHSDClient(t *testing.T) {
clientFromOptions := NewHSDClientFromOptions(HSDClientOptions{URL: "http://127.0.0.1:14037"})
clientFromConfiguration := NewHSDClientFromConfiguration(HSDClientConfiguration{URL: "http://127.0.0.1:14037"})
if clientFromOptions == nil || clientFromConfiguration == nil {
t.Fatal("expected explicit HSD constructors to return clients")
}
if clientFromOptions.baseURL != clientFromConfiguration.baseURL {
t.Fatalf("expected explicit HSD constructors to normalize the same base URL, got %q and %q", clientFromOptions.baseURL, clientFromConfiguration.baseURL)
}
}
func TestHSDClientGetNameResourceParsesWrappedRecords(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
var payload struct {
@ -92,6 +104,50 @@ func TestHSDClientGetNameResourceParsesWrappedRecords(t *testing.T) {
}
}
func TestHSDClientGetNameResourceParsesCaseInsensitiveFields(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
var payload struct {
Method string `json:"method"`
}
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
t.Fatalf("unexpected request payload: %v", err)
}
if payload.Method != "getnameresource" {
t.Fatalf("expected method getnameresource, got %s", payload.Method)
}
responseWriter.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(responseWriter).Encode(map[string]any{
"result": map[string]any{
"records": map[string]any{
"A": []string{"10.10.10.10"},
"Txt": []string{"v=lthn1 type=case"},
"NS": []string{"ns.example.lthn"},
},
},
})
}))
defer server.Close()
client := NewHSDClient(HSDClientOptions{
URL: server.URL,
})
record, err := client.GetNameResource(context.Background(), "case.lthn")
if err != nil {
t.Fatalf("unexpected getnameresource error: %v", err)
}
if len(record.A) != 1 || record.A[0] != "10.10.10.10" {
t.Fatalf("unexpected wrapped A result with mixed-case key: %#v", record.A)
}
if len(record.TXT) != 1 || record.TXT[0] != "v=lthn1 type=case" {
t.Fatalf("unexpected wrapped TXT result with mixed-case key: %#v", record.TXT)
}
if len(record.NS) != 1 || record.NS[0] != "ns.example.lthn" {
t.Fatalf("unexpected wrapped NS result with mixed-case key: %#v", record.NS)
}
}
func TestHSDClientGetNameResourceParsesDSRecords(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
var payload struct {
@ -168,6 +224,48 @@ func TestHSDClientGetNameResourceParsesDNSSECRecords(t *testing.T) {
}
}
func TestHSDClientGetNameResourceParsesSingleValueDNSSECRecords(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
var payload struct {
Method string `json:"method"`
}
if err := json.NewDecoder(request.Body).Decode(&payload); err != nil {
t.Fatalf("unexpected request payload: %v", err)
}
if payload.Method != "getnameresource" {
t.Fatalf("expected method getnameresource, got %s", payload.Method)
}
responseWriter.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(responseWriter).Encode(map[string]any{
"result": map[string]any{
"dnskey": "257 3 13 AA==",
"rrsig": "A 8 2 3600 20260101000000 20250101000000 12345 gateway.lthn. AA==",
"ds": "60485 8 2 A1B2C3D4E5F60718293A4B5C6D7E8F9012345678",
},
})
}))
defer server.Close()
client := NewHSDClient(HSDClientOptions{
URL: server.URL,
})
record, err := client.GetNameResource(context.Background(), "gateway.lthn")
if err != nil {
t.Fatalf("unexpected getnameresource error: %v", err)
}
if len(record.DNSKEY) != 1 || record.DNSKEY[0] != "257 3 13 AA==" {
t.Fatalf("unexpected DNSKEY result: %#v", record.DNSKEY)
}
if len(record.RRSIG) != 1 || record.RRSIG[0] != "A 8 2 3600 20260101000000 20250101000000 12345 gateway.lthn. AA==" {
t.Fatalf("unexpected RRSIG result: %#v", record.RRSIG)
}
if len(record.DS) != 1 || record.DS[0] != "60485 8 2 A1B2C3D4E5F60718293A4B5C6D7E8F9012345678" {
t.Fatalf("unexpected DS result: %#v", record.DS)
}
}
func TestHSDClientGetBlockchainInfo(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(responseWriter http.ResponseWriter, request *http.Request) {
var payload struct {

View file

@ -34,6 +34,13 @@ func (server *HealthServer) Address() string {
return server.HealthAddress()
}
// StartHealthServer is an explicit convenience wrapper around ServeHTTPHealth.
//
// health, err := service.StartHealthServer("127.0.0.1", 5554)
func (service *Service) StartHealthServer(bind string, port int) (*HealthServer, error) {
return service.ServeHTTPHealth(bind, port)
}
func (server *HealthServer) Close() error {
if server == nil {
return nil
@ -65,7 +72,7 @@ func (service *Service) ServeHTTPHealth(bind string, port int) (*HealthServer, e
bind = "127.0.0.1"
}
if port <= 0 {
port = service.resolveHTTPPort()
port = service.resolveHealthPort()
}
address := net.JoinHostPort(bind, strconv.Itoa(port))

View file

@ -8,6 +8,7 @@ import (
"fmt"
"io"
"net/http"
"slices"
"strings"
)
@ -25,6 +26,20 @@ type MainchainClientOptions struct {
HTTPClient *http.Client
}
// MainchainAliasClientConfiguration is the explicit long-form alias for MainchainClientOptions.
//
// client := dns.NewMainchainAliasClientFromConfiguration(dns.MainchainAliasClientConfiguration{
// URL: "http://127.0.0.1:14037",
// Username: "user",
// Password: "pass",
// })
type MainchainAliasClientConfiguration = MainchainClientOptions
// MainchainAliasClientConfig is kept for compatibility with older call sites.
//
// Deprecated: use MainchainAliasClientConfiguration instead.
type MainchainAliasClientConfig = MainchainClientOptions
type MainchainAliasClient struct {
baseURL string
username string
@ -70,6 +85,28 @@ func NewMainchainAliasClient(options MainchainClientOptions) *MainchainAliasClie
}
}
// NewMainchainAliasClientFromOptions is the explicit long-form constructor name.
//
// client := dns.NewMainchainAliasClientFromOptions(dns.MainchainClientOptions{
// URL: "http://127.0.0.1:14037",
// Username: "user",
// Password: "pass",
// })
func NewMainchainAliasClientFromOptions(options MainchainClientOptions) *MainchainAliasClient {
return NewMainchainAliasClient(options)
}
// NewMainchainAliasClientFromConfiguration is the explicit constructor name for the main-chain configuration alias.
//
// client := dns.NewMainchainAliasClientFromConfiguration(dns.MainchainAliasClientConfiguration{
// URL: "http://127.0.0.1:14037",
// Username: "user",
// Password: "pass",
// })
func NewMainchainAliasClientFromConfiguration(options MainchainAliasClientConfiguration) *MainchainAliasClient {
return NewMainchainAliasClient(options)
}
// GetAllAliasDetails returns alias names mapped to HNS DNS names.
//
// client := dns.NewMainchainAliasClient(dns.MainchainClientOptions{
@ -222,19 +259,39 @@ func decodeString(raw json.RawMessage) (string, bool) {
}
func extractAliasFromComment(comment string) string {
for _, token := range strings.Fields(comment) {
if strings.HasPrefix(token, "hns=") {
return normalizeName(strings.TrimSuffix(strings.TrimPrefix(token, "hns="), ";"))
fields := strings.Fields(comment)
for index, token := range fields {
normalizedToken := strings.ToLower(strings.TrimSpace(token))
switch {
case strings.HasPrefix(normalizedToken, "hns="):
alias := strings.TrimSuffix(strings.TrimPrefix(normalizedToken, "hns="), ";")
if alias != "" {
return normalizeName(alias)
}
case normalizedToken == "hns" && index+2 < len(fields):
nextToken := strings.TrimSpace(strings.ToLower(fields[index+1]))
if nextToken != "=" && nextToken != ":" && nextToken != ":=" {
continue
}
alias := strings.TrimSpace(fields[index+2])
alias = strings.TrimSuffix(alias, ";")
alias = strings.TrimSuffix(alias, ",")
if alias != "" {
return normalizeName(alias)
}
}
}
if marker := strings.Index(comment, "hns="); marker >= 0 {
alias := comment[marker+4:]
if trim := strings.IndexAny(alias, " ;,"); trim >= 0 {
commentLower := strings.ToLower(comment)
if marker := strings.Index(commentLower, "hns"); marker >= 0 {
alias := comment[marker+3:]
alias = strings.TrimLeft(alias, " \t:=;")
if trim := strings.IndexAny(alias, " ;,\t\r\n"); trim >= 0 {
alias = alias[:trim]
}
alias = strings.TrimSpace(alias)
alias = strings.TrimSuffix(alias, ";")
alias = strings.TrimSuffix(alias, ",")
return normalizeName(alias)
}
@ -255,5 +312,6 @@ func normalizeAliasList(raw []string) []string {
seen[next] = true
normalized = append(normalized, next)
}
slices.Sort(normalized)
return normalized
}

View file

@ -54,6 +54,18 @@ func TestMainchainClientGetsAliasDetailsAndParsesHNSComments(t *testing.T) {
}
}
func TestNewMainchainAliasClientFromOptionsMatchesNewMainchainAliasClient(t *testing.T) {
clientFromOptions := NewMainchainAliasClientFromOptions(MainchainClientOptions{URL: "http://127.0.0.1:14037"})
clientFromConfiguration := NewMainchainAliasClientFromConfiguration(MainchainAliasClientConfiguration{URL: "http://127.0.0.1:14037"})
if clientFromOptions == nil || clientFromConfiguration == nil {
t.Fatal("expected explicit main-chain constructors to return clients")
}
if clientFromOptions.baseURL != clientFromConfiguration.baseURL {
t.Fatalf("expected explicit main-chain constructors to normalize the same base URL, got %q and %q", clientFromOptions.baseURL, clientFromConfiguration.baseURL)
}
}
func TestServiceDiscoverFromMainchainAliasesUsesMainchainThenHSD(t *testing.T) {
var chainCalls int32
var hsdTreeRootCalls int32
@ -559,3 +571,48 @@ func TestServiceDiscoverFromMainchainAliasesFallsBackToChainClientWhenPrimaryDis
t.Fatalf("expected one tree-root and two name-resource RPC calls, got treeRoot=%d nameResource=%d", atomic.LoadInt32(&hsdTreeRootCalls), atomic.LoadInt32(&hsdAliasCalls))
}
}
func TestExtractAliasFromCommentParsesCaseInsensitiveHNSPrefix(t *testing.T) {
got := extractAliasFromComment("gateway alias HNS=gateway.charon.lthn")
if got != "gateway.charon.lthn" {
t.Fatalf("expected gateway.charon.lthn, got %s", got)
}
}
func TestExtractAliasFromCommentParsesSpacedHNSAssignment(t *testing.T) {
got := extractAliasFromComment("gateway alias hns = gateway.charon.lthn;")
if got != "gateway.charon.lthn" {
t.Fatalf("expected gateway.charon.lthn, got %s", got)
}
}
func TestNewServiceBuildsMainchainAliasClientWithHSDFallbackCredentials(t *testing.T) {
service := NewService(ServiceOptions{
MainchainURL: "http://127.0.0.1:14037",
HSDUsername: "mainchain-user",
HSDPassword: "mainchain-pass",
HSDApiKey: "ignored-token",
})
if service.mainchainAliasClient == nil {
t.Fatal("expected default mainchain alias client with fallback credentials")
}
if got := service.mainchainAliasClient.username; got != "mainchain-user" {
t.Fatalf("expected mainchain username to fall back to hsd username, got %q", got)
}
if got := service.mainchainAliasClient.password; got != "mainchain-pass" {
t.Fatalf("expected mainchain password to fall back to hsd password, got %q", got)
}
serviceFromToken := NewService(ServiceOptions{
MainchainURL: "http://127.0.0.1:14037",
HSDUsername: "token-user",
HSDApiKey: "token-pass",
})
if serviceFromToken.mainchainAliasClient == nil {
t.Fatal("expected default mainchain alias client with fallback token credentials")
}
if got := serviceFromToken.mainchainAliasClient.password; got != "token-pass" {
t.Fatalf("expected mainchain password to fall back to hsd api key token, got %q", got)
}
}

248
serve.go
View file

@ -30,23 +30,60 @@ type DNSServer struct {
// defer func() { _ = runtime.Close() }()
// fmt.Println(runtime.DNSAddress(), runtime.HealthAddress())
type ServiceRuntime struct {
DNS *DNSServer
// DNSServer is the explicit name for the DNS listener.
DNSServer *DNSServer
// DNS is retained for compatibility with older call sites.
DNS *DNSServer
// HealthServer is the explicit name for the `/health` listener.
HealthServer *HealthServer
// Health is retained for compatibility with older call sites.
Health *HealthServer
// HTTP is retained for compatibility with older call sites.
HTTP *HealthServer
// HTTPServer is the explicit name for the `/health` listener when the
// HTTP surface is addressed directly.
HTTPServer *HealthServer
}
// DNSServiceRuntime is the explicit long-form alias for ServiceRuntime.
//
// runtime, err := service.ServeAll("127.0.0.1", 53, 5554)
// var dnsRuntime dns.DNSServiceRuntime = *runtime
// fmt.Println(dnsRuntime.DNSAddress(), dnsRuntime.HealthAddress())
type DNSServiceRuntime = ServiceRuntime
// Address is the compatibility alias for HealthAddress.
//
// runtime, err := service.ServeAll("127.0.0.1", 53, 5554)
// defer func() { _ = runtime.Close() }()
// fmt.Println(runtime.Address())
func (runtime *ServiceRuntime) Address() string {
return runtime.HealthAddress()
}
func (runtime *ServiceRuntime) DNSAddress() string {
if runtime == nil || runtime.DNS == nil {
if runtime == nil {
return ""
}
return runtime.DNS.DNSAddress()
if runtime.DNSServer != nil {
return runtime.DNSServer.DNSAddress()
}
if runtime.DNS != nil {
return runtime.DNS.DNSAddress()
}
return ""
}
func (runtime *ServiceRuntime) HealthAddress() string {
if runtime == nil {
return ""
}
if runtime.HealthServer != nil {
return runtime.HealthServer.HealthAddress()
}
if runtime.HTTPServer != nil {
return runtime.HTTPServer.HealthAddress()
}
if runtime.Health != nil {
return runtime.Health.HealthAddress()
}
@ -67,17 +104,32 @@ func (runtime *ServiceRuntime) Close() error {
}
var firstError error
if runtime.DNS != nil {
if runtime.DNSServer != nil {
if err := runtime.DNSServer.Close(); err != nil && firstError == nil {
firstError = err
}
}
if runtime.DNS != nil && runtime.DNS != runtime.DNSServer {
if err := runtime.DNS.Close(); err != nil && firstError == nil {
firstError = err
}
}
if runtime.Health != nil {
if runtime.HealthServer != nil {
if err := runtime.HealthServer.Close(); err != nil && firstError == nil {
firstError = err
}
}
if runtime.HTTPServer != nil && runtime.HTTPServer != runtime.HealthServer {
if err := runtime.HTTPServer.Close(); err != nil && firstError == nil {
firstError = err
}
}
if runtime.Health != nil && runtime.Health != runtime.HealthServer {
if err := runtime.Health.Close(); err != nil && firstError == nil {
firstError = err
}
}
if runtime.HTTP != nil && runtime.HTTP != runtime.Health {
if runtime.HTTP != nil && runtime.HTTP != runtime.HealthServer && runtime.HTTP != runtime.Health {
if err := runtime.HTTP.Close(); err != nil && firstError == nil {
firstError = err
}
@ -104,14 +156,18 @@ func (server *DNSServer) Close() error {
if server.tcpListener != nil {
_ = server.tcpListener.Close()
}
var err error
var firstError error
if server.udpServer != nil {
err = server.udpServer.Shutdown()
if err := server.udpServer.Shutdown(); err != nil && firstError == nil {
firstError = err
}
}
if server.tcpServer != nil {
err = server.tcpServer.Shutdown()
if err := server.tcpServer.Shutdown(); err != nil && firstError == nil {
firstError = err
}
}
return err
return firstError
}
// ResolveDNSPort returns the DNS port used for `dns.serve` and `Serve`.
@ -119,22 +175,50 @@ func (server *DNSServer) Close() error {
// port := service.ResolveDNSPort()
// server, err := service.Serve("127.0.0.1", port)
func (service *Service) ResolveDNSPort() int {
if service == nil || service.dnsPort <= 0 {
if service == nil {
return DefaultDNSPort
}
if service.dnsPort <= 0 {
return DefaultDNSPort
}
return service.dnsPort
}
// DNSListenPort resolves the configured DNS listen port for `dns.serve`.
//
// port := service.DNSListenPort()
// server, err := service.Serve("127.0.0.1", port)
func (service *Service) DNSListenPort() int {
if service == nil {
return DefaultDNSPort
}
return service.ResolveDNSPort()
}
// DNSPort is an explicit alias for ResolveDNSPort.
//
// port := service.DNSPort()
// server, err := service.Serve("127.0.0.1", port)
func (service *Service) DNSPort() int {
if service == nil {
return DefaultDNSPort
}
return service.ResolveDNSPort()
}
// resolveServePort keeps internal callers aligned with existing behavior.
// resolveDNSListenPort keeps internal callers aligned with explicit naming.
func (service *Service) resolveDNSListenPort() int {
if service == nil {
return DefaultDNSPort
}
return service.DNSListenPort()
}
// resolveServePort is a legacy compatibility helper.
func (service *Service) resolveServePort() int {
if service == nil {
return DefaultDNSPort
}
return service.ResolveDNSPort()
}
@ -143,22 +227,73 @@ func (service *Service) resolveServePort() int {
// port := service.ResolveHTTPPort()
// healthServer, err := service.ServeHTTPHealth("127.0.0.1", port)
func (service *Service) ResolveHTTPPort() int {
if service == nil || service.httpPort <= 0 {
return service.ResolveHealthPort()
}
// ResolveHealthPort returns the health listener port used by `ServeHTTPHealth`.
//
// port := service.ResolveHealthPort()
// healthServer, err := service.ServeHTTPHealth("127.0.0.1", port)
func (service *Service) ResolveHealthPort() int {
if service == nil {
return DefaultHTTPPort
}
if service.httpPort <= 0 {
return DefaultHTTPPort
}
return service.httpPort
}
// HTTPListenPort resolves the configured health/listener port for `dns.serve`.
//
// port := service.HTTPListenPort()
// server, err := service.ServeHTTPHealth("127.0.0.1", port)
func (service *Service) HTTPListenPort() int {
return service.ResolveHealthPort()
}
// HealthListenPort is the explicit alias for ResolveHealthPort.
//
// port := service.HealthListenPort()
// server, err := service.ServeHTTPHealth("127.0.0.1", port)
func (service *Service) HealthListenPort() int {
return service.ResolveHealthPort()
}
// HealthPort returns the health listener port used by `ServeHTTPHealth`.
//
// port := service.HealthPort()
// healthServer, err := service.ServeHTTPHealth("127.0.0.1", port)
func (service *Service) HealthPort() int {
return service.ResolveHealthPort()
}
// HTTPPort is an explicit alias for ResolveHTTPPort.
//
// port := service.HTTPPort()
// healthServer, err := service.ServeHTTPHealth("127.0.0.1", port)
func (service *Service) HTTPPort() int {
return service.ResolveHTTPPort()
return service.ResolveHealthPort()
}
// resolveHTTPListenPort keeps internal callers aligned with explicit naming.
func (service *Service) resolveHTTPListenPort() int {
return service.ResolveHealthPort()
}
// resolveHealthListenPort keeps internal callers aligned with explicit naming.
func (service *Service) resolveHealthListenPort() int {
return service.ResolveHealthPort()
}
// resolveHTTPPort is a legacy compatibility helper.
func (service *Service) resolveHTTPPort() int {
return service.ResolveHTTPPort()
return service.ResolveHealthPort()
}
// resolveHealthPort is a legacy compatibility helper.
func (service *Service) resolveHealthPort() int {
return service.ResolveHealthPort()
}
// Serve starts DNS over UDP and TCP.
@ -168,9 +303,15 @@ func (service *Service) resolveHTTPPort() int {
// lookup := new(dnsprotocol.Msg)
// lookup.SetQuestion("gateway.charon.lthn.", dnsprotocol.TypeA)
func (service *Service) Serve(bind string, port int) (*DNSServer, error) {
if service == nil {
return nil, fmt.Errorf("service is required")
}
if bind == "" {
bind = "127.0.0.1"
}
if port <= 0 {
port = service.ResolveDNSPort()
}
addr := net.JoinHostPort(bind, strconv.Itoa(port))
udpListener, err := net.ListenPacket("udp", addr)
@ -207,17 +348,41 @@ func (service *Service) Serve(bind string, port int) (*DNSServer, error) {
return dnsServer, nil
}
// StartDNSServer is an explicit convenience wrapper around Serve.
//
// dnsServer, err := service.StartDNSServer("127.0.0.1", 53)
func (service *Service) StartDNSServer(bind string, port int) (*DNSServer, error) {
return service.Serve(bind, port)
}
// StartDNSAndHealth is an explicit convenience wrapper around ServeAll.
//
// runtime, err := service.StartDNSAndHealth("127.0.0.1", 53, 5554)
func (service *Service) StartDNSAndHealth(bind string, dnsPort int, healthPort int) (*ServiceRuntime, error) {
return service.ServeAll(bind, dnsPort, healthPort)
}
// ServeDNSAndHealth is the explicit alias for ServeAll.
//
// runtime, err := service.ServeDNSAndHealth("127.0.0.1", 53, 5554)
func (service *Service) ServeDNSAndHealth(bind string, dnsPort int, httpPort int) (*ServiceRuntime, error) {
return service.ServeAll(bind, dnsPort, httpPort)
}
// ServeAll starts DNS and health together.
//
// runtime, err := service.ServeAll("127.0.0.1", 53, 5554)
// defer func() { _ = runtime.Close() }()
// fmt.Println("dns:", runtime.DNSAddress(), "health:", runtime.HealthAddress())
func (service *Service) ServeAll(bind string, dnsPort int, httpPort int) (*ServiceRuntime, error) {
if dnsPort == 0 {
dnsPort = service.dnsPort
if service == nil {
return nil, fmt.Errorf("service is required")
}
if dnsPort <= 0 {
dnsPort = service.resolveDNSListenPort()
}
if httpPort <= 0 {
httpPort = service.resolveHTTPPort()
httpPort = service.resolveHealthListenPort()
}
dnsServer, err := service.Serve(bind, dnsPort)
@ -232,9 +397,12 @@ func (service *Service) ServeAll(bind string, dnsPort int, httpPort int) (*Servi
}
return &ServiceRuntime{
DNS: dnsServer,
Health: httpServer,
HTTP: httpServer,
DNSServer: dnsServer,
DNS: dnsServer,
HealthServer: httpServer,
Health: httpServer,
HTTP: httpServer,
HTTPServer: httpServer,
}, nil
}
@ -246,9 +414,19 @@ func (service *Service) ServeAll(bind string, dnsPort int, httpPort int) (*Servi
// })
// runtime, err := service.ServeConfigured("127.0.0.1")
func (service *Service) ServeConfigured(bind string) (*ServiceRuntime, error) {
if service == nil {
return nil, fmt.Errorf("service is required")
}
return service.ServeAll(bind, service.dnsPort, service.httpPort)
}
// StartConfigured is an explicit convenience wrapper around ServeConfigured.
//
// runtime, err := service.StartConfigured("127.0.0.1")
func (service *Service) StartConfigured(bind string) (*ServiceRuntime, error) {
return service.ServeConfigured(bind)
}
type dnsRequestHandler struct {
service *Service
}
@ -285,7 +463,7 @@ func (handler *dnsRequestHandler) ServeDNS(responseWriter dnsprotocol.ResponseWr
if !found {
goto noRecord
}
for _, value := range record.A {
for _, value := range normalizeRecordValues(record.A) {
parsedIP := net.ParseIP(value)
if parsedIP == nil || parsedIP.To4() == nil {
continue
@ -299,7 +477,7 @@ func (handler *dnsRequestHandler) ServeDNS(responseWriter dnsprotocol.ResponseWr
if !found {
goto noRecord
}
for _, value := range record.AAAA {
for _, value := range normalizeRecordValues(record.AAAA) {
parsedIP := net.ParseIP(value)
if parsedIP == nil || parsedIP.To4() != nil {
continue
@ -313,7 +491,7 @@ func (handler *dnsRequestHandler) ServeDNS(responseWriter dnsprotocol.ResponseWr
if !found {
goto noRecord
}
for _, value := range record.TXT {
for _, value := range normalizeRecordValues(record.TXT) {
reply.Answer = append(reply.Answer, &dnsprotocol.TXT{
Hdr: dnsprotocol.RR_Header{Name: question.Name, Rrtype: dnsprotocol.TypeTXT, Class: dnsprotocol.ClassINET, Ttl: defaultDNSTTL},
Txt: []string{value},
@ -321,7 +499,7 @@ func (handler *dnsRequestHandler) ServeDNS(responseWriter dnsprotocol.ResponseWr
}
case dnsprotocol.TypeNS:
if found {
for _, value := range record.NS {
for _, value := range normalizeRecordValues(record.NS) {
reply.Answer = append(reply.Answer, &dnsprotocol.NS{
Hdr: dnsprotocol.RR_Header{Name: question.Name, Rrtype: dnsprotocol.TypeNS, Class: dnsprotocol.ClassINET, Ttl: defaultDNSTTL},
Ns: normalizeName(value) + ".",
@ -383,17 +561,17 @@ func (handler *dnsRequestHandler) ServeDNS(responseWriter dnsprotocol.ResponseWr
if !found {
goto noRecord
}
appendDNSSECResourceRecords(reply, question.Name, dnsprotocol.TypeDS, record.DS)
appendDNSSECResourceRecords(reply, question.Name, dnsprotocol.TypeDS, normalizeRecordValues(record.DS))
case dnsprotocol.TypeDNSKEY:
if !found {
goto noRecord
}
appendDNSSECResourceRecords(reply, question.Name, dnsprotocol.TypeDNSKEY, record.DNSKEY)
appendDNSSECResourceRecords(reply, question.Name, dnsprotocol.TypeDNSKEY, normalizeRecordValues(record.DNSKEY))
case dnsprotocol.TypeRRSIG:
if !found {
goto noRecord
}
appendDNSSECResourceRecords(reply, question.Name, dnsprotocol.TypeRRSIG, record.RRSIG)
appendDNSSECResourceRecords(reply, question.Name, dnsprotocol.TypeRRSIG, normalizeRecordValues(record.RRSIG))
default:
reply.SetRcode(request, dnsprotocol.RcodeNotImplemented)
_ = responseWriter.WriteMsg(reply)
@ -466,7 +644,7 @@ func parsePTRIP(name string) (string, bool) {
}
func appendAnyAnswers(reply *dnsprotocol.Msg, questionName string, lookupName string, record NameRecords, zoneApex string) {
for _, value := range record.A {
for _, value := range normalizeRecordValues(record.A) {
parsedIP := net.ParseIP(value)
if parsedIP == nil || parsedIP.To4() == nil {
continue
@ -477,7 +655,7 @@ func appendAnyAnswers(reply *dnsprotocol.Msg, questionName string, lookupName st
})
}
for _, value := range record.AAAA {
for _, value := range normalizeRecordValues(record.AAAA) {
parsedIP := net.ParseIP(value)
if parsedIP == nil || parsedIP.To4() != nil {
continue
@ -488,7 +666,7 @@ func appendAnyAnswers(reply *dnsprotocol.Msg, questionName string, lookupName st
})
}
for _, value := range record.TXT {
for _, value := range normalizeRecordValues(record.TXT) {
reply.Answer = append(reply.Answer, &dnsprotocol.TXT{
Hdr: dnsprotocol.RR_Header{Name: questionName, Rrtype: dnsprotocol.TypeTXT, Class: dnsprotocol.ClassINET, Ttl: defaultDNSTTL},
Txt: []string{value},
@ -496,7 +674,7 @@ func appendAnyAnswers(reply *dnsprotocol.Msg, questionName string, lookupName st
}
if len(record.NS) > 0 {
for _, value := range record.NS {
for _, value := range normalizeRecordValues(record.NS) {
reply.Answer = append(reply.Answer, &dnsprotocol.NS{
Hdr: dnsprotocol.RR_Header{Name: questionName, Rrtype: dnsprotocol.TypeNS, Class: dnsprotocol.ClassINET, Ttl: defaultDNSTTL},
Ns: normalizeName(value) + ".",
@ -504,15 +682,15 @@ func appendAnyAnswers(reply *dnsprotocol.Msg, questionName string, lookupName st
}
}
for _, value := range record.DNSKEY {
for _, value := range normalizeRecordValues(record.DNSKEY) {
appendDNSSECResourceRecords(reply, questionName, dnsprotocol.TypeDNSKEY, []string{value})
}
for _, value := range record.DS {
for _, value := range normalizeRecordValues(record.DS) {
appendDNSSECResourceRecords(reply, questionName, dnsprotocol.TypeDS, []string{value})
}
for _, value := range record.RRSIG {
for _, value := range normalizeRecordValues(record.RRSIG) {
appendDNSSECResourceRecords(reply, questionName, dnsprotocol.TypeRRSIG, []string{value})
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff