mirror of
https://github.com/StackExchange/dnscontrol.git
synced 2026-01-17 21:51:08 +08:00
# Issue Fixes https://github.com/StackExchange/dnscontrol/issues/3912 # Resolution ``` #!/bin/sh # Reset git fetch origin main git reset --hard origin/main git checkout main git branch -D tlim_moveproviders git checkout -b tlim_moveproviders find . -name \*.bak -delete # Move the *.go files out of providers/ mkdir -p pkg/providers git mv providers/*.go pkg/providers # move the _all file out of providers/ git mv providers/_all pkg/providers/_all # Update the imports (in go.* and the affected files) sed -i.bak -e 's@"github.com/StackExchange/dnscontrol/v4/providers"@"github.com/StackExchange/dnscontrol/v4/pkg/providers"@g' go.* $(fgrep -lr --include '*.go' '"github.com/StackExchange/dnscontrol/v4/providers"' *) sed -i.bak -e 's@"../../providers"@"../../pkg/providers"@g' pkg/normalize/capabilities_test.go sed -i.bak -e 's@"github.com/StackExchange/dnscontrol/v4/providers/_all"@"github.com/StackExchange/dnscontrol/v4/pkg/providers/_all"@g' go.* $(fgrep -lr --include '*.go' '"github.com/StackExchange/dnscontrol/v4/providers/_all"' *) # Fix the docs sed -i.bak -e 's@StackExchange/dnscontrol/blob/main/providers/_all/all.go@StackExchange/dnscontrol/blob/main/pkg/providers/_all/all.go@g' documentation/advanced-features/writing-providers.md sed -i.bak -e 's@StackExchange/dnscontrol/providers@StackExchange/dnscontrol/pkg/providers@g' documentation/advanced-features/writing-providers.md sed -i.bak -e 's@StackExchange/dnscontrol/v4/providers@StackExchange/dnscontrol/v4/pkg/providers@g' documentation/advanced-features/writing-providers.md sed -i.bak -e 's@dnscontrol/providers/providers.go@dnscontrol/pkg/providers/providers.go@g' documentation/advanced-features/writing-providers.md sed -i.bak -e 's@providers/_all/all.go@pkg/providers/_all/all.go@g' documentation/advanced-features/writing-providers.md #sed -i.bak -e 's@@@g' documentation/advanced-features/writing-providers.md #sed -i.bak -e 's@@@g' documentation/advanced-features/writing-providers.md find . -name \*.bak -delete go fmt ./... git status echo git commit -a -m'CHORE: Move Non-provider files in providers to pkg/providers' ```
235 lines
6 KiB
Go
235 lines
6 KiB
Go
package fortigate
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"net"
|
||
"strings"
|
||
|
||
"github.com/StackExchange/dnscontrol/v4/models"
|
||
"github.com/StackExchange/dnscontrol/v4/pkg/diff2"
|
||
"github.com/StackExchange/dnscontrol/v4/pkg/providers"
|
||
)
|
||
|
||
// Feature Declaration
|
||
|
||
var features = providers.DocumentationNotes{
|
||
providers.CanGetZones: providers.Can(),
|
||
providers.CanUsePTR: providers.Cannot(), // FortiGate does not really support ARPA Zones and handles PTR records really weired
|
||
providers.CanUseLOC: providers.Cannot(),
|
||
providers.CanConcur: providers.Unimplemented(),
|
||
providers.DocCreateDomains: providers.Can(),
|
||
providers.DocOfficiallySupported: providers.Cannot(), // unofficial integration
|
||
}
|
||
|
||
// Provider Registration
|
||
|
||
func init() {
|
||
const providerName = "FORTIGATE"
|
||
const providerMaintainer = "@KlettIT"
|
||
providers.RegisterDomainServiceProviderType(providerName, providers.DspFuncs{
|
||
Initializer: NewFortiGate,
|
||
RecordAuditor: AuditRecords,
|
||
}, features)
|
||
providers.RegisterMaintainer(providerName, providerMaintainer)
|
||
}
|
||
|
||
// Provider Struct
|
||
|
||
type fortigateProvider struct {
|
||
vdom string
|
||
host string
|
||
apiKey string
|
||
insecure bool
|
||
client *apiClient
|
||
}
|
||
|
||
// NewFortiGate creates a new instance of the FortiGate DNS provider.
|
||
func NewFortiGate(m map[string]string, _ json.RawMessage) (providers.DNSServiceProvider, error) {
|
||
host, vdom, apiKey := m["host"], m["vdom"], m["apiKey"]
|
||
|
||
var missing []string
|
||
if host == "" {
|
||
missing = append(missing, "host")
|
||
}
|
||
if vdom == "" {
|
||
missing = append(missing, "vdom")
|
||
}
|
||
if apiKey == "" {
|
||
missing = append(missing, "apiKey")
|
||
}
|
||
if len(missing) > 0 {
|
||
return nil, errors.New("[FORTIGATE] Missing required field(s): " + strings.Join(missing, ", "))
|
||
}
|
||
|
||
insecure := strings.EqualFold(m["insecure_tls"], "true")
|
||
debug := strings.EqualFold(m["debug_http"], "true")
|
||
|
||
p := &fortigateProvider{
|
||
host: host,
|
||
vdom: vdom,
|
||
apiKey: apiKey,
|
||
insecure: insecure,
|
||
}
|
||
p.client = newClient(host, vdom, apiKey, insecure, debug)
|
||
return p, nil
|
||
}
|
||
|
||
// Record Fetching
|
||
|
||
func (p *fortigateProvider) GetZoneRecords(domain string, meta map[string]string) (models.Records, error) {
|
||
records := models.Records{}
|
||
|
||
// Request the zone object from FortiGate
|
||
path := fmt.Sprintf("system/dns-database/%s", strings.TrimSuffix(domain, "."))
|
||
|
||
// According to the API, "results" is an array of objects
|
||
var resp struct {
|
||
Results []struct {
|
||
DNSEntry []fgDNSRecord `json:"dns-entry"`
|
||
} `json:"results"`
|
||
}
|
||
|
||
err := p.client.do("GET", path, nil, nil, &resp)
|
||
if err != nil {
|
||
if strings.Contains(err.Error(), "404") {
|
||
// Zone does not exist yet – return empty record list
|
||
return records, nil
|
||
}
|
||
return nil, fmt.Errorf("[FORTIGATE] Fetching zone %q failed: %w", domain, err)
|
||
}
|
||
|
||
if len(resp.Results) == 0 {
|
||
// Zone exists but no dns-entry data found
|
||
return records, nil
|
||
}
|
||
|
||
// Convert native records to dnscontrol Records
|
||
for _, n := range resp.Results[0].DNSEntry {
|
||
rc, err := nativeToRecord(domain, n)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
records = append(records, rc)
|
||
}
|
||
|
||
return records, nil
|
||
}
|
||
|
||
// Correction Planning
|
||
|
||
func (p *fortigateProvider) GetZoneRecordsCorrections(dc *models.DomainConfig, existingRecords models.Records) ([]*models.Correction, int, error) {
|
||
|
||
domain := strings.TrimSuffix(dc.Name, ".")
|
||
|
||
var corrections []*models.Correction
|
||
|
||
result, err := diff2.ByZone(existingRecords, dc, nil)
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
msgs, changed, actualChangeCount := result.Msgs, result.HasChanges, result.ActualChangeCount
|
||
|
||
if changed {
|
||
msgs = append(msgs, "[FORTIGATE] Zone update for "+domain)
|
||
msg := strings.Join(msgs, "\n")
|
||
|
||
resourceRecords, errs := recordsToNative(result.DesiredPlus)
|
||
|
||
if len(errs) > 0 {
|
||
return nil, 0, fmt.Errorf("[FORTIGATE] Failed to convert records: %v", errs)
|
||
}
|
||
|
||
if resourceRecords == nil {
|
||
resourceRecords = []*fgDNSRecord{}
|
||
}
|
||
|
||
payload, err := buildZonePayload(dc, resourceRecords)
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
|
||
corrections = append(corrections,
|
||
&models.Correction{
|
||
Msg: msg,
|
||
F: func() error {
|
||
|
||
if err := p.EnsureZoneExists(dc.Name, dc.Metadata); err != nil {
|
||
return err
|
||
}
|
||
|
||
return p.client.do("PUT", "system/dns-database/"+dc.Name, nil, payload, nil)
|
||
},
|
||
})
|
||
}
|
||
|
||
return corrections, actualChangeCount, nil
|
||
}
|
||
|
||
// Zone Existence Check & Creation
|
||
func (p *fortigateProvider) EnsureZoneExists(domain string, metadata map[string]string) error {
|
||
var probe struct{ Results []any }
|
||
|
||
err := p.client.do("GET", "system/dns-database/"+domain, nil, nil, &probe)
|
||
switch {
|
||
case err == nil && len(probe.Results) > 0:
|
||
return nil // already exists
|
||
|
||
case isNotFound(err):
|
||
body := map[string]any{"name": domain, "domain": domain, "forwarder": nil}
|
||
return p.client.do("POST", "system/dns-database", nil, body, nil)
|
||
|
||
default:
|
||
return err
|
||
}
|
||
}
|
||
|
||
// Misc DNSControl Plumbing
|
||
|
||
func (p *fortigateProvider) GetNameservers(string) ([]*models.Nameserver, error) {
|
||
return []*models.Nameserver{}, nil // FortiGate is authoritative only internally
|
||
}
|
||
|
||
func (p *fortigateProvider) ListZones() ([]string, error) {
|
||
var resp struct {
|
||
Results []struct{ Name string } `json:"results"`
|
||
}
|
||
if err := p.client.do("GET", "system/dns-database", nil, nil, &resp); err != nil {
|
||
return nil, err
|
||
}
|
||
zones := make([]string, len(resp.Results))
|
||
for i, z := range resp.Results {
|
||
zones[i] = z.Name
|
||
}
|
||
return zones, nil
|
||
}
|
||
|
||
// Helper
|
||
|
||
func buildZonePayload(dc *models.DomainConfig, resourceRecords []*fgDNSRecord) (map[string]any, error) {
|
||
payload := map[string]any{
|
||
"dns-entry": resourceRecords,
|
||
}
|
||
|
||
// default values
|
||
payload["forwarder"] = nil
|
||
payload["authoritative"] = "enable"
|
||
|
||
if v, ok := dc.Metadata["forwarder"]; ok {
|
||
ip := net.ParseIP(v)
|
||
if ip == nil || ip.To4() == nil {
|
||
return nil, fmt.Errorf("[FORTIGATE] Invalid forwarder IP: %q", v)
|
||
}
|
||
payload["forwarder"] = []string{v}
|
||
}
|
||
|
||
if v, ok := dc.Metadata["authoritative"]; ok {
|
||
if strings.ToLower(v) == "false" {
|
||
payload["authoritative"] = "disable"
|
||
}
|
||
}
|
||
|
||
return payload, nil
|
||
}
|