Skip to content

Conversation

@zupolgec
Copy link

Summary

Hey! This adds support for Gidinet, an Italian domain registrar. I needed it for managing my domains so figured I'd contribute it back.

It works as both DNS provider and registrar:

  • DNS side handles the usual records (A, AAAA, CNAME, MX, NS, TXT, SRV)
  • Registrar side manages nameserver delegation

The API is SOAP-based which was fun to work with... Their docs are a bit sparse but I got it working after some trial and error.

Notes

  • Apex NS records can't be managed via DNS API (they're read-only), so I filter them out with a warning. Nameserver changes go through the registrar instead.
  • TTL values get rounded to what the API accepts
  • No CAA support unless you have their premium service

Tested with my own domains and it's been working fine. Happy to address any feedback!

Please create the GitHub label "provider-gidinet"

Add support for Gidinet (Italian domain registrar) as both DNS provider
and registrar.

Features:
- DNS Provider: Full CRUD operations for A, AAAA, CNAME, MX, NS, TXT, SRV records
- Registrar: Nameserver delegation management at registry level
- Zone listing via get-zones command
- Dual host support for migration scenarios

Technical details:
- Uses SOAP API at api.quickservicebox.com
- Record-based updates (diff2.ByRecord)
- Apex NS records filtered (managed by registrar only)
- TTL values automatically rounded to API-supported values

Documentation and CI/CD configuration included.
Copy link
Collaborator

@tlimoncelli tlimoncelli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good so far! Just cosmetic changes. Thanks for contributing this!

// The default for unlisted capabilities is 'Cannot'.
// See providers/capabilities.go for the entire list of capabilities.
providers.CanAutoDNSSEC: providers.Cannot(),
providers.CanConcur: providers.Cannot(), // SOAP API, safer to serialize
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All new providers must have providers.CanConcur: providers.Can(). The Cannot() setting is for legacy providers.

I don't see anything in the code that indicates Cannot() is justified. This setting means that the code in providers/gidinet/ can be used in a goroutine. This usually means that any caches in gidinetProvider{} are protected by mutexes. Since there aren't any caches, this should run fine.

Suggested change
providers.CanConcur: providers.Cannot(), // SOAP API, safer to serialize
providers.CanConcur: providers.Can(),

providers.CanConcur: providers.Cannot(), // SOAP API, safer to serialize
providers.CanGetZones: providers.Can(),
providers.CanUseAlias: providers.Cannot(),
providers.CanUseCAA: providers.Cannot(), // Only premium service
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
providers.CanUseCAA: providers.Cannot(), // Only premium service
providers.CanUseCAA: providers.Cannot("Only premium service"),

providers.CanUseSSHFP: providers.Cannot(),
providers.CanUseSVCB: providers.Cannot(),
providers.CanUseTLSA: providers.Cannot(),
providers.DocCreateDomains: providers.Cannot(), // Must be created via web UI
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
providers.DocCreateDomains: providers.Cannot(), // Must be created via web UI
providers.DocCreateDomains: providers.Cannot("Must be created via web UI"),

Comment on lines 47 to 52
soapEnvelope := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
%s
</soap:Body>
</soap:Envelope>`, string(bodyXML))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concatenation is faster in this situation:

Suggested change
soapEnvelope := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
%s
</soap:Body>
</soap:Envelope>`, string(bodyXML))
soapEnvelope := (`<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
' + string(bodyXML) + '
</soap:Body>
</soap:Envelope>`)

I did a small benchmark:

$ go test -bench=.
goos: darwin
goarch: arm64
pkg: github.com/StackExchange/dnscontrol/v4/bench
cpu: Apple M3 Max
BenchmarkSprintf-16                	 4205144	       267.2 ns/op
BenchmarkStringConcatenation-16    	12940010	        92.44 ns/op
BenchmarkStringsBuilder-16         	 6614196	       179.9 ns/op
PASS
ok  	github.com/StackExchange/dnscontrol/v4/bench	4.432s

)

// AllowedTTLValues lists the TTL values supported by the Gidinet API
var AllowedTTLValues = []uint32{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unexport AllowedTTLValues (i.e. rename it allowedTTLValues)

}

// parseSOAPResponse extracts the response from a SOAP envelope
func parseSOAPResponse(data []byte, response interface{}) error {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func parseSOAPResponse(data []byte, response interface{}) error {
func parseSOAPResponse(data []byte, response any) error {

// SOAPBody represents the SOAP body
type SOAPBody struct {
XMLName xml.Name `xml:"http://schemas.xmlsoap.org/soap/envelope/ Body"`
Content interface{}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Content interface{}
Content any

Comment on lines 300 to 302
if strings.HasSuffix(hostname, ".") {
return strings.TrimSuffix(hostname, ".")
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if strings.HasSuffix(hostname, ".") {
return strings.TrimSuffix(hostname, ".")
}
if before, ok := strings.CutSuffix(hostname, "."); ok {
return before
}

Comment on lines 311 to 314
suffix := "." + domain
if strings.HasSuffix(fqdn, suffix) {
return strings.TrimSuffix(fqdn, suffix)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
suffix := "." + domain
if strings.HasSuffix(fqdn, suffix) {
return strings.TrimSuffix(fqdn, suffix)
}
if before, ok := strings.CutSuffix(fqdn, "." + domain); ok {
return before
}

Comment on lines 14 to 20
a.Add("MX", rejectif.MxNull) // MX priority 0 is allowed (means highest priority)

a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Gidinet doesn't support quotes in TXT
a.Add("TXT", rejectif.TxtIsEmpty) // Empty TXT records not allowed
a.Add("TXT", rejectif.TxtHasBackticks) // Backticks not supported

a.Add("SRV", rejectif.SrvHasNullTarget) // SRV must have a target
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please include "last verified" dates for all these comments.

Suggested change
a.Add("MX", rejectif.MxNull) // MX priority 0 is allowed (means highest priority)
a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Gidinet doesn't support quotes in TXT
a.Add("TXT", rejectif.TxtIsEmpty) // Empty TXT records not allowed
a.Add("TXT", rejectif.TxtHasBackticks) // Backticks not supported
a.Add("SRV", rejectif.SrvHasNullTarget) // SRV must have a target
a.Add("MX", rejectif.MxNull) // Last verified 2026-01-24
a.Add("TXT", rejectif.TxtHasDoubleQuotes) // Last verified 2026-01-24
a.Add("TXT", rejectif.TxtIsEmpty) // Last verified 2026-01-24
a.Add("TXT", rejectif.TxtHasBackticks) // Last verified 2026-01-24
a.Add("SRV", rejectif.SrvHasNullTarget) // Last verified 2026-01-24

@tlimoncelli
Copy link
Collaborator

Let me introduce you to @fm, our "liaison to maintainers". He'll reach out to you with our "welcome kit".

}

// Find the smallest allowed value that is >= ttl
for _, v := range AllowedTTLValues {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(low priority) Consider using https://pkg.go.dev/slices#example-BinarySearch instead of a loop.

@zupolgec
Copy link
Author

Thank you @tlimoncelli 💪 Great review. I've addressed all your suggestions.

I'm going to reach out to the Gidinet owner to see if we can get a fix for long TXT records (more than 254 chars), SRV records and a test account with a test domain (that I can pay) to have a proper integration flow that can be tested automatically.

The GIDINET API rejects single TXT strings >250 chars but accepts
multiple quoted segments like: "chunk1" "chunk2"

- Add chunkTXT() to split long values into 250-char quoted chunks
- Add unchunkTXT() to parse chunked format back to single string
- Update toGidinetRecord to chunk TXT on create/update
- Update toRecordConfig to unchunk TXT when reading
- Remove TxtLongerThan(254) audit since long TXT now works
- Add comprehensive unit tests for chunking functions
@zupolgec
Copy link
Author

I found a way to support long TXT records (like DKIM keys >250 chars)!

The GIDINET API rejects single TXT strings longer than ~250 characters, but it accepts multiple quoted segments in the format: "chunk1" "chunk2"

I've added automatic chunking:

  • chunkTXT() splits long values into ≤250-char quoted chunks when creating/updating records
  • unchunkTXT() parses the chunked format back to a single string when reading records

This enables full DKIM support. Tested end-to-end with a 400+ char DKIM record - works perfectly with no drift on re-preview.

Unit tests included for the chunking functions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants