Skip to content

NEW PROVIDER: Gidinet DNS provider and registrar#4004

Merged
tlimoncelli merged 5 commits intoStackExchange:mainfrom
zupolgec:provider-gidinet
Feb 2, 2026
Merged

NEW PROVIDER: Gidinet DNS provider and registrar#4004
tlimoncelli merged 5 commits intoStackExchange:mainfrom
zupolgec:provider-gidinet

Conversation

@zupolgec
Copy link
Copy Markdown
Collaborator

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
Copy Markdown
Contributor

@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
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
Contributor

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
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@zupolgec
Copy link
Copy Markdown
Collaborator 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
Copy Markdown
Collaborator 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.

@zupolgec
Copy link
Copy Markdown
Collaborator Author

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.

I got in contact with them. They could provide a test account that manages subdomains of "gdn.in" that could be automatically tested. I'll keep you updated.

The registrar functionality (managing nameservers) only works with API
reseller accounts. Regular customer credentials can manage DNS records
but not domain delegation settings. Added TODO to test and document the
specific error when using customer credentials.
@tlimoncelli
Copy link
Copy Markdown
Contributor

Thank you for contributing this new provider, @zupolgec !

Two notes:

  1. @fm: Faisal Misle is our “liaison to maintainers”. He'll reach out to you soon. He'll be requesting your email address so that we have a more direct way to contact you.
  2. By now you should have recieved a Github invite to have the "triage" role for this repo. Please accept the invite so we can assign bugs to you.

Thanks again!
Tom

@tlimoncelli tlimoncelli changed the title new provider: Add Gidinet DNS provider and registrar NEW PROVIDER: Gidinet DNS provider and registrar Feb 2, 2026
@tlimoncelli tlimoncelli merged commit fa57bf0 into StackExchange:main Feb 2, 2026
2 checks passed
@zupolgec
Copy link
Copy Markdown
Collaborator Author

zupolgec commented Feb 2, 2026

🥳 thank you @tlimoncelli

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants