Skip to content

Commit a88c019

Browse files
authored
Fix zone detection for subdomain DNS record management (#11)
Resolves a bug where DirectAdmin provider attempted to create DNS records in non-existent zones for subdomain requests. For example, when managing host.subdomain.domain.com, the provider incorrectly tried to use zone subdomain.domain.com instead of the actual managed zone domain.com. Key changes: - Add findManageableZone() with parent zone traversal algorithm - Add getDomainList() to retrieve available DirectAdmin domains - Add adjustRecordForZone() for proper subdomain record name handling - Update all CRUD operations to use detected manageable zones - Add comprehensive test coverage for zone detection scenarios - Add LIBDNS_DA_NON_ROOT_TEST_ZONE environment variable for testing This fix enables proper ACME challenge handling for subdomains in Caddy server deployments, resolving "Domain does not belong to you" errors.
1 parent 1b265a7 commit a88c019

File tree

5 files changed

+440
-17
lines changed

5 files changed

+440
-17
lines changed

.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
LIBDNS_DA_TEST_ZONE=domain.com.
1+
LIBDNS_DA_TEST_ZONE=domain.com
2+
LIBDNS_DA_NON_ROOT_TEST_ZONE=test.domain.com
23
LIBDNS_DA_TEST_SERVER_URL=https://da.domain.com:2222
34
LIBDNS_DA_TEST_INSECURE_SERVER_URL=https://1.1.1.1:2222
45
LIBDNS_DA_TEST_USER=admin

client.go

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,9 @@ func (p *Provider) getZoneRecords(ctx context.Context, zone string) ([]libdns.Re
8989
if err != nil {
9090
switch err {
9191
case ErrUnsupported:
92-
rr := libDnsRecord.RR()
9392
p.getLogger().Warn("Unsupported record conversion",
94-
zap.String("type", rr.Type),
95-
zap.String("name", rr.Name))
93+
zap.String("type", respData.Records[i].Type),
94+
zap.String("name", respData.Records[i].Name))
9695
continue
9796
default:
9897
return nil, err
@@ -140,7 +139,6 @@ func (p *Provider) appendZoneRecord(ctx context.Context, zone string, record lib
140139
return nil, err
141140
}
142141

143-
rr.Data = fmt.Sprintf("name=%v&value=%v", rr.Name, rr.Data)
144142
return &rr, nil
145143
}
146144

@@ -200,7 +198,6 @@ func (p *Provider) setZoneRecord(ctx context.Context, zone string, record libdns
200198
return nil, err
201199
}
202200

203-
rr.Data = fmt.Sprintf("name=%v&value=%v", rr.Name, rr.Data)
204201
return &rr, nil
205202
}
206203

@@ -298,3 +295,111 @@ func (p *Provider) executeRequest(ctx context.Context, method, url string) error
298295

299296
return nil
300297
}
298+
299+
func (p *Provider) getDomainList(ctx context.Context) ([]string, error) {
300+
reqURL, err := url.Parse(p.ServerURL)
301+
if err != nil {
302+
p.getLogger().Error("Failed to parse server URL", zap.Error(err))
303+
return nil, err
304+
}
305+
306+
reqURL.Path = "/CMD_API_SHOW_DOMAINS"
307+
308+
queryString := make(url.Values)
309+
queryString.Set("json", "yes")
310+
311+
reqURL.RawQuery = queryString.Encode()
312+
313+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL.String(), nil)
314+
if err != nil {
315+
p.getLogger().Error("Failed to build new request", zap.Error(err))
316+
return nil, err
317+
}
318+
319+
req.SetBasicAuth(p.User, p.LoginKey)
320+
321+
client := &http.Client{
322+
Transport: &http.Transport{
323+
TLSClientConfig: &tls.Config{
324+
InsecureSkipVerify: p.InsecureRequests,
325+
},
326+
}}
327+
328+
resp, err := client.Do(req)
329+
if err != nil {
330+
p.getLogger().Error("Failed to execute request", zap.Error(err))
331+
return nil, err
332+
}
333+
defer func(Body io.ReadCloser) {
334+
err := Body.Close()
335+
if err != nil {
336+
p.getLogger().Error("Failed to close response body", zap.Error(err))
337+
}
338+
}(resp.Body)
339+
340+
if resp.StatusCode != http.StatusOK {
341+
bodyBytes, err := io.ReadAll(resp.Body)
342+
if err != nil {
343+
p.getLogger().Error("Failed to read response body", zap.Error(err))
344+
return nil, err
345+
}
346+
347+
bodyString := string(bodyBytes)
348+
349+
p.getLogger().Error("API returned a non-200 status code",
350+
zap.Int("status_code", resp.StatusCode),
351+
zap.String("body", bodyString))
352+
353+
return nil, fmt.Errorf("api request failed with status code %d", resp.StatusCode)
354+
}
355+
356+
var respData daDomainList
357+
err = json.NewDecoder(resp.Body).Decode(&respData)
358+
if err != nil {
359+
p.getLogger().Error("Failed to decode JSON response", zap.Error(err))
360+
return nil, err
361+
}
362+
363+
return respData, nil
364+
}
365+
366+
func (p *Provider) findManageableZone(ctx context.Context, requestedZone string) (string, error) {
367+
p.getLogger().Debug("findManageableZone called", zap.String("zone", requestedZone))
368+
369+
// Remove trailing dot if present
370+
requestedZone = strings.TrimSuffix(requestedZone, ".")
371+
372+
// Get list of domains we can manage
373+
domains, err := p.getDomainList(ctx)
374+
if err != nil {
375+
return "", fmt.Errorf("failed to get domain list: %v", err)
376+
}
377+
378+
p.getLogger().Debug("Available domains", zap.Strings("domains", domains))
379+
380+
// Try the requested zone first (exact match)
381+
for _, domain := range domains {
382+
if strings.EqualFold(requestedZone, domain) {
383+
p.getLogger().Debug("Found exact match", zap.String("domain", domain))
384+
return domain, nil
385+
}
386+
}
387+
388+
// If no exact match, traverse backwards through the FQDN to find parent zones
389+
parts := strings.Split(requestedZone, ".")
390+
for i := 1; i < len(parts); i++ {
391+
candidateZone := strings.Join(parts[i:], ".")
392+
p.getLogger().Debug("Checking candidate zone", zap.String("candidate_zone", candidateZone))
393+
394+
for _, domain := range domains {
395+
if strings.EqualFold(candidateZone, domain) {
396+
p.getLogger().Debug("Found manageable parent zone",
397+
zap.String("parent_zone", domain),
398+
zap.String("requested_zone", requestedZone))
399+
return domain, nil
400+
}
401+
}
402+
}
403+
404+
return "", fmt.Errorf("no manageable zone found for %s in available domains: %v", requestedZone, domains)
405+
}

models.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,5 @@ type daResponse struct {
116116
Success string `json:"success,omitempty"`
117117
Result string `json:"result,omitempty"`
118118
}
119+
120+
type daDomainList []string

provider.go

Lines changed: 140 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,15 @@ func (p *Provider) caller() string {
6464

6565
// GetRecords lists all the records in the zone.
6666
func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record, error) {
67-
zone = strings.TrimSuffix(zone, ".")
67+
p.getLogger().Debug("GetRecords called",
68+
zap.String("zone", zone))
6869

69-
records, err := p.getZoneRecords(ctx, zone)
70+
managedZone, err := p.findManageableZone(ctx, zone)
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
records, err := p.getZoneRecords(ctx, managedZone)
7076
if err != nil {
7177
return nil, err
7278
}
@@ -76,11 +82,36 @@ func (p *Provider) GetRecords(ctx context.Context, zone string) ([]libdns.Record
7682

7783
// AppendRecords adds records to the zone. It returns the records that were added.
7884
func (p *Provider) AppendRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) {
79-
zone = strings.TrimSuffix(zone, ".")
85+
p.getLogger().Debug("AppendRecords called",
86+
zap.String("zone", zone),
87+
zap.Int("record_count", len(records)))
88+
89+
managedZone, err := p.findManageableZone(ctx, zone)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
if zone != managedZone {
95+
p.getLogger().Debug("Using managed zone",
96+
zap.String("managed_zone", managedZone),
97+
zap.String("requested_zone", zone))
98+
}
8099

81100
var created []libdns.Record
82101
for _, rec := range records {
83-
result, err := p.appendZoneRecord(ctx, zone, rec)
102+
// Adjust record name if managedZone differs from requested zone
103+
adjustedRecord := rec
104+
if managedZone != strings.TrimSuffix(zone, ".") {
105+
adjustedRecord = p.adjustRecordForZone(rec, zone, managedZone)
106+
}
107+
108+
adjustedRR := adjustedRecord.RR()
109+
p.getLogger().Debug("Creating record",
110+
zap.String("name", adjustedRR.Name),
111+
zap.String("type", adjustedRR.Type),
112+
zap.String("value", adjustedRR.Data))
113+
114+
result, err := p.appendZoneRecord(ctx, managedZone, adjustedRecord)
84115
if err != nil {
85116
return nil, err
86117
}
@@ -93,13 +124,38 @@ func (p *Provider) AppendRecords(ctx context.Context, zone string, records []lib
93124
// SetRecords sets the records in the zone, either by updating existing records or creating new ones.
94125
// It returns the updated records.
95126
func (p *Provider) SetRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) {
96-
zone = strings.TrimSuffix(zone, ".")
127+
p.getLogger().Debug("SetRecords called",
128+
zap.String("zone", zone),
129+
zap.Int("record_count", len(records)))
130+
131+
managedZone, err := p.findManageableZone(ctx, zone)
132+
if err != nil {
133+
return nil, err
134+
}
135+
136+
if zone != managedZone {
137+
p.getLogger().Debug("Using managed zone",
138+
zap.String("managed_zone", managedZone),
139+
zap.String("requested_zone", zone))
140+
}
97141

98142
var updated []libdns.Record
99143
var errors []error
100144

101145
for _, rec := range records {
102-
result, err := p.setZoneRecord(ctx, zone, rec)
146+
// Adjust record name if managedZone differs from requested zone
147+
adjustedRecord := rec
148+
if managedZone != strings.TrimSuffix(zone, ".") {
149+
adjustedRecord = p.adjustRecordForZone(rec, zone, managedZone)
150+
}
151+
152+
adjustedRR := adjustedRecord.RR()
153+
p.getLogger().Debug("Creating record",
154+
zap.String("name", adjustedRR.Name),
155+
zap.String("type", adjustedRR.Type),
156+
zap.String("value", adjustedRR.Data))
157+
158+
result, err := p.setZoneRecord(ctx, managedZone, adjustedRecord)
103159
if err != nil {
104160
errors = append(errors, err)
105161
continue
@@ -121,11 +177,36 @@ func (p *Provider) SetRecords(ctx context.Context, zone string, records []libdns
121177

122178
// DeleteRecords deletes the records from the zone. It returns the records that were deleted.
123179
func (p *Provider) DeleteRecords(ctx context.Context, zone string, records []libdns.Record) ([]libdns.Record, error) {
124-
zone = strings.TrimSuffix(zone, ".")
180+
p.getLogger().Debug("DeleteRecords called",
181+
zap.String("zone", zone),
182+
zap.Int("record_count", len(records)))
183+
184+
managedZone, err := p.findManageableZone(ctx, zone)
185+
if err != nil {
186+
return nil, err
187+
}
188+
189+
if zone != managedZone {
190+
p.getLogger().Debug("Using managed zone",
191+
zap.String("managed_zone", managedZone),
192+
zap.String("requested_zone", zone))
193+
}
125194

126195
var deleted []libdns.Record
127196
for _, rec := range records {
128-
result, err := p.deleteZoneRecord(ctx, zone, rec)
197+
// Adjust record name if managedZone differs from requested zone
198+
adjustedRecord := rec
199+
if managedZone != strings.TrimSuffix(zone, ".") {
200+
adjustedRecord = p.adjustRecordForZone(rec, zone, managedZone)
201+
}
202+
203+
adjustedRR := adjustedRecord.RR()
204+
p.getLogger().Debug("Deleting record",
205+
zap.String("name", adjustedRR.Name),
206+
zap.String("type", adjustedRR.Type),
207+
zap.String("value", adjustedRR.Data))
208+
209+
result, err := p.deleteZoneRecord(ctx, managedZone, adjustedRecord)
129210
if err != nil {
130211
return nil, err
131212
}
@@ -135,6 +216,57 @@ func (p *Provider) DeleteRecords(ctx context.Context, zone string, records []lib
135216
return deleted, nil
136217
}
137218

219+
// adjustRecordForZone adjusts the record name when the managed zone differs from the requested zone
220+
func (p *Provider) adjustRecordForZone(record libdns.Record, requestedZone, managedZone string) libdns.Record {
221+
requestedZone = strings.TrimSuffix(requestedZone, ".")
222+
managedZone = strings.TrimSuffix(managedZone, ".")
223+
224+
// Calculate the subdomain portion that was stripped during zone detection
225+
// Example: requestedZone="test.domain.com", managedZone="domain.com" -> subdomain="test"
226+
if !strings.HasSuffix(requestedZone, managedZone) {
227+
return record // Safety check - shouldn't happen with proper zone detection
228+
}
229+
230+
var subdomain string
231+
if requestedZone == managedZone {
232+
subdomain = ""
233+
} else {
234+
subdomain = strings.TrimSuffix(requestedZone, "."+managedZone)
235+
}
236+
237+
if subdomain == "" {
238+
return record
239+
}
240+
241+
rr := record.RR()
242+
243+
// Check if the record name has already been adjusted by seeing if it already ends with the subdomain
244+
if strings.HasSuffix(rr.Name, "."+subdomain) {
245+
p.getLogger().Debug("Record name already adjusted, skipping",
246+
zap.String("name", rr.Name),
247+
zap.String("subdomain", subdomain))
248+
return record
249+
}
250+
251+
// Adjust the record name to include the subdomain
252+
// Example: "_acme-challenge.libdns" -> "_acme-challenge.libdns.test"
253+
adjustedName := rr.Name + "." + subdomain
254+
255+
p.getLogger().Debug("Adjusting record name",
256+
zap.String("original_name", rr.Name),
257+
zap.String("adjusted_name", adjustedName),
258+
zap.String("subdomain", subdomain))
259+
260+
adjustedRR := &libdns.RR{
261+
Type: rr.Type,
262+
Name: adjustedName,
263+
Data: rr.Data,
264+
TTL: rr.TTL,
265+
}
266+
267+
return adjustedRR
268+
}
269+
138270
// Interface guards
139271
var (
140272
_ libdns.RecordGetter = (*Provider)(nil)

0 commit comments

Comments
 (0)