Documentation
¶
Overview ¶
* Copyright (c) 2024 Johan Stenstam, johani@johani.org
* Copyright (c) 2024 Johan Stenstam, johani@johani.org
* Copyright (c) 2025 Johan Stenstam, johan.stenstam@internetstiftelsen.se
* Copyright (c) 2026 Johan Stenstam, johan.stenstam@internetstiftelsen.se
* Copyright (c) Johan Stenstam, johani@johani.org * * API client configuration, role → clientKey registry, and the * shared InitApiClients helper used by each CLI binary's root.go.
* Copyright (c) 2024 Johan Stenstam, johani@johani.org
* Copyright (c) 2024 Johan Stenstam, johani@johani.org
* Copyright (c) 2024 Johan Stenstam, johan.stenstam@internetstiftelsen.se
* Copyright (c) Johan Stenstam, johani@johani.org
* Copyright (c) 2024 Johan Stenstam, johani@johani.org
* Copyright (c) Johan Stenstam, johani@johani.org
* Copyright (c) Johan Stenstam, johani@johani.org
* Copyright (c) 2024 Johan Stenstam, johani@johani.org
* Copyright (c) Johan Stenstam, johani@johani.org
* Copyright (c) Johan Stenstam, johani@johani.org
* Copyright (c) 2024 Johan Stenstam, johani@johani.org
* Johan Stenstam
* Copyright (c) 2026 Johan Stenstam, johani@johani.org
* Copyright (c) Johan Stenstam, johani@johani.org
* Copyright (c) Johan Stenstam, johan.stenstam@internetstiftelsen.se
* Copyright (c) 2024 Johan Stenstam, johani@johani.org
* Copyright (c) Johan Stenstam, johani@johani.org
* Copyright (c) 2024 Johan Stenstam, johani@johani.org
* Copyright (c) 2024 Johan Stenstam, johani@johani.org
* Copyright (c) Johan Stenstam, johan.stenstam@internetstiftelsen.se
* Johan Stenstam
* Copyright (c) Johan Stenstam, johani@johani.org
* Copyright (c) Johan Stenstam, johani@johani.org
Index ¶
- Constants
- Variables
- func AttachUpdateCreateFlags(cmd *cobra.Command)
- func CreateUpdate(role, updateType string)
- func GetApiClient(role string, dieOnError bool) (*tdns.ApiClient, error)
- func InitApiClients(c *CliConf) error
- func ListZones(cr tdns.ZoneResponse)
- func NewConfigCmd(role string) *cobra.Command
- func NewDaemonCmd(role string) *cobra.Command
- func NewDebugCmd(role string, extras ...*cobra.Command) *cobra.Command
- func NewKeysCmd(role string) *cobra.Command
- func NewKeystoreCmd(role string) *cobra.Command
- func NewPingCmd(role string) *cobra.Command
- func NewStopCmd(role string) *cobra.Command
- func NewTruststoreCmd(role string) *cobra.Command
- func NewZoneCmd(role string, extras ...*cobra.Command) *cobra.Command
- func PrepArgs(args ...interface{})
- func PrintCacheItem(item core.Tuple[string, cache.CachedRRset], suffix string)
- func PrintUpdateResult(ur tdns.UpdateResult)
- func ReadZoneData(zonename string) error
- func RegisterRole(role, clientKey string)
- func RunZoneBump(parent string, args []string)
- func RunZoneList(parent string, args []string)
- func RunZoneReload(parent string, args []string)
- func RunZoneWrite(parent string, args []string)
- func SendAgentMgmtCmd(req *tdns.AgentMgmtPost) (*tdns.AgentMgmtResponse, error)
- func SendCatalogCommand(api *tdns.ApiClient, data tdns.CatalogPost) (*tdns.CatalogResponse, error)
- func SendCommandNG(api *tdns.ApiClient, data tdns.CommandPost) (tdns.CommandResponse, error)
- func SendConfigCommand(api *tdns.ApiClient, data tdns.ConfigPost) (tdns.ConfigResponse, error)
- func SendDebug(api *tdns.ApiClient, data tdns.DebugPost) tdns.DebugResponse
- func SendDelegationCmd(api *tdns.ApiClient, data tdns.DelegationPost) (tdns.DelegationResponse, error)
- func SendDsyncCommand(api *tdns.ApiClient, data tdns.ZoneDsyncPost) (tdns.ZoneDsyncResponse, error)
- func SendKeystoreCmd(api *tdns.ApiClient, data tdns.KeystorePost) (tdns.KeystoreResponse, error)
- func SendNotify(zonename string, ntype string)
- func SendTruststore(api *tdns.ApiClient, data tdns.TruststorePost) (tdns.TruststoreResponse, error)
- func SendZoneCommand(api *tdns.ApiClient, data tdns.ZonePost) (tdns.ZoneResponse, error)
- func SetRootCommand(cmd *cobra.Command)
- func StartImrForCli(rootHints string) (context.Context, context.CancelFunc, *tdns.Imr, error)
- func StartInteractiveMode()
- func Terminate()
- func ValidateBySection(config *Config, configsections map[string]interface{}, cfgfile string) error
- func ValidateConfig(v *viper.Viper, cfgfile string) error
- func ValidateZoneConfig(v *viper.Viper, cfgfile string) error
- func VerboseListZone(cr tdns.ZoneResponse)
- type ApiDetails
- type CliConf
- type CommandNode
- type Config
Constants ¶
const DefaultDomainSuffix = "example.com."
Default domain suffix for CLI
Variables ¶
var AgentCmd = &cobra.Command{
Use: "agent",
Short: "TDNS Agent commands",
}
var AgentZoneCmd = &cobra.Command{
Use: "zone",
Short: "Agent zone management commands",
}
AgentZoneCmd is the agent-specific "zone" command group. It contains only the zone subcommands relevant to the agent, plus the new addrr/delrr commands for managing synced RRs.
var AuthCmd = &cobra.Command{
Use: "auth",
Short: "Interact with tdns-auth (authoritative) via API",
}
AuthCmd is the parent command for all auth-related commands
var Base32Cmd = &cobra.Command{
Use: "base32",
Short: "Convert data to/from base32 encoding and domain format",
Long: `This command converts data to or from base32 encoding and domain format.
It can read from standard input or from a file.
Examples:
echo '{"name":"example","value":123}' | tdns base32 encode --suffix=example.com.
cat domains.txt | tdns base32 decode`,
}
base32Cmd represents the base32 command
var Base32decodeCmd = &cobra.Command{ Use: "decode", Short: "Decode base32 domain data to JSON", Long: `Decode base32 domain data from stdin to JSON.`, Run: func(cmd *cobra.Command, args []string) { stat, _ := os.Stdin.Stat() if (stat.Mode() & os.ModeCharDevice) != 0 { fmt.Println("No stdin data provided. Please pipe domain data to this command.") fmt.Println("Example: cat domains.txt | tdns base32 decode") return } cookie, _ := cmd.Flags().GetString("cookie") reader := bufio.NewReader(os.Stdin) processBase32DomainsToJson(reader, cmd, cookie) }, }
decodeCmd represents the decode subcommand
var Base32encodeCmd = &cobra.Command{ Use: "encode", Short: "Encode JSON data to base32 domain format", Long: `Encode JSON data from stdin to base32 domain format.`, Run: func(cmd *cobra.Command, args []string) { stat, _ := os.Stdin.Stat() if (stat.Mode() & os.ModeCharDevice) != 0 { fmt.Println("No stdin data provided. Please pipe JSON data to this command.") fmt.Println("Example: echo '{\"name\":\"example\"}' | tdns base32 encode --suffix=example.com.") return } suffix, _ := cmd.Flags().GetString("suffix") cookie, _ := cmd.Flags().GetString("cookie") if suffix == "" { fmt.Fprintf(os.Stderr, "Error: domain suffix is required. Use --suffix flag.\n") return } if !strings.HasSuffix(suffix, ".") { suffix += "." fmt.Fprintf(os.Stderr, "Note: Added trailing dot to domain suffix: %s\n", suffix) } reader := bufio.NewReader(os.Stdin) processJsonToBase32Domains(reader, suffix, cookie) }, }
encodeCmd represents the encode subcommand
var CatalogCmd = &cobra.Command{
Use: "catalog",
Short: "Manage catalog zones (RFC 9432)",
Long: `Create and manage catalog zones, add/remove member zones and groups.`,
}
CatalogCmd is the root command for catalog zone management
var CatalogGroupCmd = &cobra.Command{
Use: "group",
Short: "Manage groups in catalog",
}
CatalogGroupCmd is the subcommand group for group operations
var CatalogNotifyCmd = &cobra.Command{
Use: "notify",
Short: "Manage notify addresses for catalog zones",
}
CatalogNotifyCmd is the subcommand group for notify address operations
var CatalogZoneCmd = &cobra.Command{
Use: "zone",
Short: "Manage member zones in catalog",
}
CatalogZoneCmd is the subcommand group for zone operations
var CatalogZoneGroupCmd = &cobra.Command{
Use: "group",
Short: "Manage group associations for zones",
}
CatalogZoneGroupCmd is the subcommand group for zone-group associations
var ChildCmd = &cobra.Command{
Use: "child",
Short: "Prefix, only useable via the 'child update create' subcommand",
}
var Conf tdns.Config
var DbCmd = &cobra.Command{
Use: "db",
Short: "TDNS DB commands",
}
var DdnsCmd = &cobra.Command{
Use: "ddns",
Short: "Send a DDNS update. Only usable via sub-commands.",
}
var DelCmd = &cobra.Command{
Use: "del",
Short: "Delegation prefix command. Only usable via sub-commands.",
}
var DeleteCmd = &cobra.Command{ Use: "delete [job-id]", Short: "Delete scan job(s)", Long: `Delete a specific scan job by job ID, or all jobs if --all is used`, Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { api, err := GetApiClient("scanner", true) if err != nil { log.Fatalf("Error getting API client for scanner: %v", err) } deleteAll, _ := cmd.Flags().GetBool("all") var endpoint string if deleteAll { endpoint = "/scanner/delete?all=true" } else if len(args) > 0 { endpoint = fmt.Sprintf("/scanner/delete?job_id=%s", args[0]) } else { log.Fatal("Error: either specify a job ID or use --all flag") } status, buf, err := api.RequestNG("DELETE", endpoint, nil, true) if err != nil { log.Fatalf("Error from scanner API: %v", err) } if status == http.StatusNotFound { log.Fatalf("Job not found") } if status == http.StatusBadRequest { log.Fatalf("Bad request: %s", string(buf)) } if status != http.StatusOK { log.Fatalf("Unexpected status code: %d", status) } var resp tdns.ScannerResponse err = json.Unmarshal(buf, &resp) if err != nil { log.Fatalf("Error unmarshaling response: %v", err) } if resp.Error { log.Fatalf("Error: %s", resp.ErrorMsg) } fmt.Printf("%s\n", resp.Msg) }, }
var DsyncDiscoveryCmd = &cobra.Command{ Use: "dsync-query", Short: "Send a DNS query for 'zone. DSYNC' and present the result.", Run: func(cmd *cobra.Command, args []string) { PrepArgs("zonename") tdns.Globals.Zonename = dns.Fqdn(tdns.Globals.Zonename) ctx, cancel, imr, err := StartImrForCli("") if err != nil { log.Fatalf("Error: %v", err) } defer cancel() dsync_res, err := imr.DsyncDiscovery(ctx, tdns.Globals.Zonename, tdns.Globals.Verbose) if err != nil { log.Fatalf("Error: %v", err) } fmt.Printf("Parent: %s\n", dsync_res.Parent) if len(dsync_res.Rdata) == 0 { fmt.Printf("No DSYNC record associated with '%s'\n", tdns.Globals.Zonename) } else { for _, nr := range dsync_res.Rdata { fmt.Printf("%s\tIN\tDSYNC\t%s\n", dsync_res.Qname, nr.String()) } } }, }
var ExitCmd = &cobra.Command{ Use: "exit", Short: "Exit the interactive shell", Run: func(cmd *cobra.Command, args []string) { Terminate() }, }
exitCmd represents the exit command
var (
GenerateCmd = &cobra.Command{
Use: "generate",
Short: "Generate DNS records or encodings",
}
)
var ImrCmd = &cobra.Command{
Use: "imr",
Short: "Interact with tdns-imr via API",
}
ImrCmd is the parent command for all IMR-related commands
var ImrDumpCmd = &cobra.Command{ Use: "dump", Short: "List records in the RRsetCache", Run: func(cmd *cobra.Command, args []string) { fmt.Printf("Listing records in the RRsetCache\n") if Conf.Internal.RRsetCache == nil { fmt.Println("RRsetCache is nil") return } items := []core.Tuple[string, cache.CachedRRset]{} for item := range Conf.Internal.RRsetCache.RRsets.IterBuffered() { items = append(items, item) } sort.Slice(items, func(i, j int) bool { return lessByReverseLabels(items[i].Val.Name, items[j].Val.Name) }) for _, it := range items { PrintCacheItem(it, ".") } }, }
var ImrFlushCmd = &cobra.Command{
Use: "flush",
Short: "Flush cached data",
}
var ImrQueryCmd = &cobra.Command{ Use: "query [name] [type]", Short: "Query DNS records", Long: `Query DNS records for a given name and type`, Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { if len(args) < 2 { fmt.Println("Error: both name and type are required.") _ = cmd.Usage() return } fmt.Printf("Querying %s for %s records (verbose mode: %t)\n", args[0], args[1], tdns.Globals.Verbose) qname := dns.Fqdn(args[0]) if _, ok := dns.IsDomainName(qname); !ok { fmt.Printf("Not a valid domain name: '%s'\n", qname) return } qtype, exist := dns.StringToType[strings.ToUpper(args[1])] if !exist { fmt.Printf("Not a valid DNS RR type: '%s'\n", args[1]) return } if Conf.Internal.RecursorCh == nil { fmt.Printf("No active channel to RecursorEngine. Terminating.\n") return } resp := make(chan tdns.ImrResponse, 1) Conf.Internal.RecursorCh <- tdns.ImrRequest{ Qname: qname, Qclass: dns.ClassINET, Qtype: qtype, ResponseCh: resp, } select { case r := <-resp: // Check cache entry to determine if this is a negative response var cached *cache.CachedRRset if Conf.Internal.RRsetCache != nil { cached = Conf.Internal.RRsetCache.Get(qname, qtype) } if cached != nil && (cached.Context == cache.ContextNXDOMAIN || cached.Context == cache.ContextNoErrNoAns) { vstate := cached.State stateStr := cache.ValidationStateToString[vstate] ctxStr := cache.CacheContextToString[cached.Context] fmt.Printf("%s %s (state: %s)\n", qname, ctxStr, stateStr) if tdns.Globals.Verbose { if vstate == cache.ValidationStateIndeterminate { fmt.Printf("Proof: not possible for zone in state=indeterminate\n") } else if len(cached.NegAuthority) > 0 { fmt.Printf("Proof:\n") for _, negRRset := range cached.NegAuthority { if negRRset != nil { for _, rr := range negRRset.RRs { fmt.Printf(" %s\n", rr.String()) } for _, rr := range negRRset.RRSIGs { fmt.Printf(" %s\n", rr.String()) } } } } else if cached.RRset != nil { for _, rr := range cached.RRset.RRs { if rr.Header().Rrtype == dns.TypeSOA { fmt.Printf(" %s\n", rr.String()) } } } } else { fmt.Printf("Proof only presented in verbose mode\n") } } else if r.RRset != nil { vstate := cache.ValidationStateNone if cached != nil { vstate = cached.State } suffix := fmt.Sprintf(" (state: %s)", cache.ValidationStateToString[vstate]) for _, rr := range r.RRset.RRs { switch rr.Header().Rrtype { case qtype, dns.TypeCNAME: fmt.Printf("%s%s\n", rr.String(), suffix) default: fmt.Printf("Not printing: %q\n", rr.String()) } } for _, rr := range r.RRset.RRSIGs { fmt.Printf("%s\n", rr.String()) } } else if r.Error { fmt.Printf("Error: %s\n", r.ErrorMsg) } else { fmt.Printf("No records found: %s\n", r.Msg) } case <-time.After(3 * time.Second): fmt.Println("Timeout waiting for response") return } }, }
Query command - takes name and type
var ImrSetCmd = &cobra.Command{
Use: "set",
Short: "Set IMR runtime parameters",
}
var ImrShowCmd = &cobra.Command{
Use: "show",
Short: "Show IMR state",
}
var ImrStatsCmd = &cobra.Command{ Use: "stats", Short: "Show statistics", Long: `Show DNS query statistics`, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { fmt.Println("Showing statistics") }, }
Stats command - no arguments
var ImrZoneCmd = &cobra.Command{
Use: "zone",
Short: "prefix command for zone operations",
Long: `prefix command for zone operations`,
}
Zone command - takes zone name
var JwtCmd = &cobra.Command{
Use: "jwt",
Short: "JWT inspection and manipulation commands",
}
var MaxWait int
MaxWait bound by --maxwait on "daemon start".
var NewState string
var NotifyCmd = &cobra.Command{
Use: "notify",
Short: "The 'notify' command is only usable via defined sub-commands",
}
var QuitCmd = &cobra.Command{ Use: "quit", Short: "Exit the interactive shell", Long: `Exit the interactive shell`, Hidden: true, Run: func(cmd *cobra.Command, args []string) { Terminate() }, }
quitCmd represents the quit command
var ReportCmd = &cobra.Command{ Use: "report <qname>", Short: "Send a report and (optionally) discover DSYNC via the internal resolver (imr)", Run: func(cmd *cobra.Command, args []string) { PrepArgs("zonename") dsyncLookup := true if dsyncTarget != "" && dsyncPort != 0 { dsyncLookup = false } ctx, cancel := context.WithCancel(context.Background()) defer cancel() tdns.Globals.App.Type = tdns.AppTypeCli if tdns.Globals.Debug { fmt.Printf("ReportCmd: Calling Conf.MainInit(%q)\n", tdns.DefaultCliCfgFile) } if err := Conf.MainInit(ctx, tdns.DefaultCliCfgFile); err != nil { tdns.Shutdowner(&Conf, fmt.Sprintf("Error initializing tdns-cli: %v", err)) } if reportSender == "" { fmt.Printf("Error: sender not specified\n") return } if dsyncLookup { _, cancel, imr, err := StartImrForCli("") if err != nil { log.Fatalf("Error initializing IMR: %v", err) } defer cancel() log.Printf("ReportCmd: Discovering DSYNC via IMR for %s", tdns.Globals.Zonename) dsyncRes, derr := imr.DsyncDiscovery(ctx, tdns.Globals.Zonename, tdns.Globals.Verbose) if derr != nil { log.Printf("ReportCmd: DSYNC discovery error: %v", derr) return } // Find DSYNC record with REPORT scheme var reportDSYNC *core.DSYNC for _, ds := range dsyncRes.Rdata { if ds.Scheme == core.SchemeReport { reportDSYNC = ds break } } if reportDSYNC == nil { log.Printf("ReportCmd: no DSYNC REPORT found for %s, aborting report", tdns.Globals.Zonename) return } targetIP = reportDSYNC.Target port = strconv.Itoa(int(reportDSYNC.Port)) } else { targetIP = dsyncTarget port = strconv.Itoa(dsyncPort) } if edeCode == 0 { edeCode = int(edns0.EDEMPZoneXfrFailure) } m := new(dns.Msg) m.SetNotify(tdns.Globals.Zonename) err := edns0.AddReportOptionToMessage(m, &edns0.ReportOption{ ZoneName: tdns.Globals.Zonename, EDECode: uint16(edeCode), Severity: 17, Sender: reportSender, Details: reportDetails, }) if err != nil { log.Printf("ReportCmd: failed to build report EDNS0 option: %v", err) return } log.Printf("ReportCmd: sending report to %s:%s (from DSYNC REPORT)", targetIP, port) c := core.NewDNSClient(core.TransportDo53, port, nil) if reportTsig { tsig := tdns.Globals.TsigKeys[reportSender+".key."] if tsig == nil { fmt.Printf("Error: tsig key not found for sender: %s\n", reportSender) return } // There is no built-in map or function in miekg/dns for this, so we use a switch. var alg string switch strings.ToLower(tsig.Algorithm) { case "hmac-sha1": alg = dns.HmacSHA1 case "hmac-sha256": alg = dns.HmacSHA256 case "hmac-sha384": alg = dns.HmacSHA384 case "hmac-sha512": alg = dns.HmacSHA512 default: alg = tsig.Algorithm } if tdns.Globals.Debug { fmt.Printf("TSIG signing the report with %s\n", tsig.Name) } tsigMap := map[string]string{tsig.Name: tsig.Secret} if c.DNSClientUDP != nil { c.DNSClientUDP.TsigSecret = tsigMap } if c.DNSClientTCP != nil { c.DNSClientTCP.TsigSecret = tsigMap } if c.DNSClientTLS != nil { c.DNSClientTLS.TsigSecret = tsigMap } m.SetTsig(tsig.Name, alg, 300, time.Now().Unix()) } if tdns.Globals.Debug { fmt.Printf("%s\n", m.String()) } resp, _, err := c.Exchange(m, targetIP, false) if err != nil { fmt.Printf("ReportCmd: error sending report: %v\n", err) os.Exit(1) } rcode := resp.Rcode if rcode == dns.RcodeSuccess { fmt.Printf("Report accepted (rcode: %s)\n", dns.RcodeToString[rcode]) } else { hasede, edecode, edemsg := edns0.ExtractEDEFromMsg(resp) if hasede { fmt.Printf("Error: rcode: %s, EDE Message: %s (EDE code: %d)\n", dns.RcodeToString[rcode], edemsg, edecode) } else { fmt.Printf("Error: rcode: %s\n", dns.RcodeToString[rcode]) fmt.Printf("Response msg:\n%s\n", resp.String()) } os.Exit(1) } }, }
var ResultsCmd = &cobra.Command{ Use: "results [job-id]", Short: "Get results of a completed scan job", Long: `Get detailed results of a completed scan job by job ID. Use --delete to delete the job after retrieving results.`, Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { api, err := GetApiClient("scanner", true) if err != nil { log.Fatalf("Error getting API client for scanner: %v", err) } jobID := args[0] endpoint := fmt.Sprintf("/scanner/status?job_id=%s", jobID) status, buf, err := api.RequestNG("GET", endpoint, nil, true) if err != nil { log.Fatalf("Error from scanner API: %v", err) } if status == http.StatusNotFound { log.Fatalf("Job not found: %s", jobID) } if status != http.StatusOK { log.Fatalf("Unexpected status code: %d", status) } var job tdns.ScanJobStatus err = json.Unmarshal(buf, &job) if err != nil { log.Fatalf("Error unmarshaling job status: %v", err) } if job.Status != "completed" { fmt.Printf("Job %s is not completed yet. Status: %s\n", jobID, job.Status) fmt.Printf("Progress: %d/%d tuples processed\n", job.ProcessedTuples, job.TotalTuples) return } if job.Error { fmt.Printf("Job %s completed with error: %s\n", jobID, job.ErrorMsg) return } deleteFlag, _ := cmd.Flags().GetBool("delete") if deleteFlag { deleteEndpoint := fmt.Sprintf("/scanner/delete?job_id=%s", jobID) delStatus, delBuf, err := api.RequestNG("DELETE", deleteEndpoint, nil, true) if err != nil { log.Printf("Warning: Error deleting job %s: %v", jobID, err) } else if delStatus == http.StatusOK { fmt.Printf("Job %s deleted successfully\n", jobID) } else { log.Printf("Warning: Failed to delete job %s: status %d, response: %s", jobID, delStatus, string(delBuf)) } } if tdns.Globals.Verbose { // Pretty print JSON var prettyJSON bytes.Buffer json.Indent(&prettyJSON, buf, "", " ") fmt.Println(prettyJSON.String()) } else { fmt.Printf("Job ID: %s\n", job.JobID) fmt.Printf("Status: %s\n", job.Status) fmt.Printf("Total Responses: %d\n\n", len(job.Responses)) for i, resp := range job.Responses { fmt.Printf("Response %d: %s (%s)\n", i+1, resp.Qname, tdns.ScanTypeToString[resp.ScanType]) if resp.DataChanged { fmt.Printf(" Data changed: Yes\n") } else { fmt.Printf(" Data changed: No\n") } if resp.AllNSInSync { fmt.Printf(" All NS in sync: Yes\n") } else if len(resp.Options) > 0 { for _, opt := range resp.Options { if opt == "all-ns" { fmt.Printf(" All NS in sync: No\n") break } } } if resp.Error { fmt.Printf(" Error: %s\n", resp.ErrorMsg) } fmt.Println() } } }, }
var RootKeysCmd = &cobra.Command{
Use: "keys",
Short: "Generate long-term keypairs for agent/combiner (JOSE)",
Long: `Generate JOSE keypairs used by tdns-agentv2 and tdns-combinerv2 for
authenticated NOTIFY(CHUNK) and API traffic. Use one keypair per party:
- Agent: agent.jose.private (+ optional agent.jose.pub for combiner config)
- Combiner: combiner.jose.private (+ optional combiner.jose.pub for agent config)
`,
}
RootKeysCmd is the root-level "keys" command (e.g. tdns-cli keys generate). It does not require tdns-cli config or API; use for generating JOSE keypairs on disk.
var ScanCdsCmd = &cobra.Command{ Use: "cds [zone...]", Short: "Send CDS scan request with ScanTuple data to tdns-scanner", Long: `Send CDS scan request for one or more zones. Zones can be specified as arguments or via --zone flag.`, Args: cobra.MinimumNArgs(0), Run: func(cmd *cobra.Command, args []string) { api, err := GetApiClient("scanner", true) if err != nil { log.Fatalf("Error getting API client for scanner: %v", err) } // Get zones from command arguments var zones []string if len(args) > 0 { for _, arg := range args { zone := strings.TrimSpace(arg) if zone != "" { zones = append(zones, zone) } } } if len(zones) == 0 { if tdns.Globals.Zonename == "" { log.Fatal("Error: specify zones as arguments or use --zone flag") } zones = []string{tdns.Globals.Zonename} } if tdns.Globals.Verbose { fmt.Printf("Scanning %d zones: %v\n", len(zones), zones) } scanTuples := make([]tdns.ScanTuple, 0, len(zones)) for _, zone := range zones { zone = dns.Fqdn(zone) scanTuple := tdns.ScanTuple{ Zone: zone, CurrentData: tdns.CurrentScanData{ CDS: nil, }, } scanTuples = append(scanTuples, scanTuple) } post := tdns.ScannerPost{ Command: "SCAN", ScanType: tdns.ScanCDS, ScanTuples: scanTuples, } status, buf, err := api.RequestNG("POST", "/scanner", post, true) if err != nil { log.Fatalf("Error from scanner API: %v", err) } if tdns.Globals.Verbose { fmt.Printf("Status: %d\n", status) } var resp tdns.ScannerResponse err = json.Unmarshal(buf, &resp) if err != nil { log.Fatalf("Error unmarshaling response: %v", err) } if resp.Error { log.Fatalf("Error from scanner: %s", resp.ErrorMsg) } fmt.Printf("Scanner response: %s\n", resp.Msg) if resp.Status != "" { fmt.Printf("Status: %s\n", resp.Status) } if resp.JobID != "" { fmt.Printf("Job ID: %s\n", resp.JobID) fmt.Printf("Use 'tdns-cli scanner status %s' to check job status\n", resp.JobID) } }, }
var ScanCmd = &cobra.Command{
Use: "scan",
Short: "Send scan requests to tdns-scanner",
}
var ScannerCmd = &cobra.Command{
Use: "scanner",
Short: "Interact with tdns-scanner via API",
}
var ServerName string = "PLACEHOLDER"
var StatusCmd = &cobra.Command{ Use: "status [job-id]", Short: "Get status of scan job(s)", Long: `Get status of a specific scan job by job ID, or list all jobs if no job ID is provided`, Args: cobra.MaximumNArgs(1), Run: func(cmd *cobra.Command, args []string) { api, err := GetApiClient("scanner", true) if err != nil { log.Fatalf("Error getting API client for scanner: %v", err) } var endpoint string if len(args) > 0 { endpoint = fmt.Sprintf("/scanner/status?job_id=%s", args[0]) } else { endpoint = "/scanner/status" } status, buf, err := api.RequestNG("GET", endpoint, nil, true) if err != nil { log.Fatalf("Error from scanner API: %v", err) } if status == http.StatusNotFound { log.Fatalf("Job not found") } if status == http.StatusBadRequest { log.Fatalf("Bad request: %s", string(buf)) } if status != http.StatusOK { log.Fatalf("Unexpected status code: %d", status) } if len(args) > 0 { // Single job - show detailed status var job tdns.ScanJobStatus err = json.Unmarshal(buf, &job) if err != nil { log.Fatalf("Error unmarshaling job status: %v", err) } fmt.Printf("Job ID: %s\n", job.JobID) fmt.Printf("Status: %s\n", job.Status) fmt.Printf("Created: %s\n", job.CreatedAt.Format("2006-01-02 15:04:05")) if job.StartedAt != nil { fmt.Printf("Started: %s\n", job.StartedAt.Format("2006-01-02 15:04:05")) } if job.CompletedAt != nil { fmt.Printf("Completed: %s\n", job.CompletedAt.Format("2006-01-02 15:04:05")) } fmt.Printf("Progress: %d/%d tuples processed\n", job.ProcessedTuples, job.TotalTuples) if job.Error { fmt.Printf("Error: %s\n", job.ErrorMsg) } if len(job.Responses) > 0 { fmt.Printf("\nResults (%d responses):\n", len(job.Responses)) for i, resp := range job.Responses { fmt.Printf("\n Response %d:\n", i+1) fmt.Printf(" Qname: %s\n", resp.Qname) fmt.Printf(" Scan Type: %s\n", tdns.ScanTypeToString[resp.ScanType]) fmt.Printf(" Data Changed: %t\n", resp.DataChanged) if resp.AllNSInSync { fmt.Printf(" All NS In Sync: true\n") } else if len(resp.Options) > 0 { for _, opt := range resp.Options { if opt == "all-ns" { fmt.Printf(" All NS In Sync: false\n") break } } } if resp.Error { fmt.Printf(" Error: %s\n", resp.ErrorMsg) } } } } else { // All jobs - show summary table var jobs []*tdns.ScanJobStatus err = json.Unmarshal(buf, &jobs) if err != nil { log.Fatalf("Error unmarshaling job list: %v", err) } if len(jobs) == 0 { fmt.Println("No jobs found") return } sort.Slice(jobs, func(i, j int) bool { return jobs[i].CreatedAt.After(jobs[j].CreatedAt) }) t := acidtab.New("JOB ID", "STATUS", "CREATED", "PROGRESS", "ERROR") for _, job := range jobs { progress := fmt.Sprintf("%d/%d", job.ProcessedTuples, job.TotalTuples) errorStr := "" if job.Error { errorStr = job.ErrorMsg if len(errorStr) > 30 { errorStr = errorStr[:27] + "..." } } created := job.CreatedAt.Format("2006-01-02 15:04:05") t.Row(job.JobID, job.Status, created, progress, errorStr) } fmt.Println(t.String()) } }, }
var SupportedDnssecAlgorithms = []string{
"RSASHA256",
"RSASHA512",
"ECDSAP256SHA256",
"ECDSAP384SHA384",
"ED25519",
}
SupportedDnssecAlgorithms lists the DNSSEC algorithm names tdns accepts for zone signing (RRSIG).
var SupportedSig0Algorithms = []string{
"RSASHA256",
"RSASHA512",
"ECDSAP256SHA256",
"ECDSAP384SHA384",
"ED25519",
}
SupportedSig0Algorithms lists the DNSSEC algorithm names tdns accepts for SIG(0) key generation and transaction signing.
var ToRFC3597Cmd = &cobra.Command{ Use: "rfc3597", Short: "Generate the RFC 3597 representation of a DNS record", Run: func(cmd *cobra.Command, args []string) { if rrstr == "" { log.Fatalf("Record to generate RFC 3597 representation for not specified.") } rr, err := dns.NewRR(rrstr) if err != nil { log.Fatalf("Could not parse record \"%s\": %v", rrstr, err) } fmt.Printf("Normal (len=%d): \"%s\"\n", dns.Len(rr), rr.String()) u := new(dns.RFC3597) u.ToRFC3597(rr) fmt.Printf("RFC 3597 (len=%d): \"%s\"\n", dns.Len(u), u.String()) }, }
var UpdateCmd = &cobra.Command{
Use: "update",
Short: "[OBE] Create and ultimately send a DNS UPDATE msg for zone auth data",
}
var VersionCmd = &cobra.Command{ Use: "version", Short: "Print the version of the app, more or less verbosely", Run: func(cmd *cobra.Command, args []string) { fmt.Printf("This is %s, version %s, compiled on %v\n", tdns.Globals.App.Name, tdns.Globals.App.Version, tdns.Globals.App.Date) }, }
Functions ¶
func AttachUpdateCreateFlags ¶
AttachUpdateCreateFlags adds the three flags common to every "update create" entry point — --signer, --server, --key — to cmd, bound to the package-level vars CreateUpdate reads. Use this from every leaf create command (including cross-package ones in tdns-mp/v2/cli) so the signer name, target server, and keyfile can always be overridden from the CLI.
func CreateUpdate ¶
func CreateUpdate(role, updateType string)
CreateUpdate starts an interactive CLI for composing, signing, and sending DNS UPDATEs.
It initializes the keystore and signing state, then enters a prompt loop that lets the user add or delete RRs, view the pending update, sign it with SIG(0) keys (from a keyfile or the keystore), select the target server, and send the update. The function updates package-level globals such as `zone`, `signer`, and `server` as needed. It may call os.Exit on fatal initialization or signing errors.
role is the api-client role used to discover the server's keystore path when the local "db.file" config setting is unset (e.g. "auth", "agent", "combiner"). The role's /config status response carries the DBFile field added by commit d03f360.
The updateType parameter selects the operational context used to initialize the interactive session (for example "child" or "zone") and does not affect the format of the DNS UPDATE messages produced.
func GetApiClient ¶
GetApiClient returns the configured ApiClient for the given role. The role is resolved to a clientKey via the RegisterRole registry and looked up in tdns.Globals.ApiClients. dieOnError controls whether unknown/missing roles terminate the process.
func InitApiClients ¶
InitApiClients creates a tdns.ApiClient for every entry in c.ApiServers and stashes them in tdns.Globals.ApiClients. Also parses TSIG keys from c.Keys and records c for later config lookups.
Unlike the previous per-binary implementations, this does *not* require a client named "tdns-auth". Callers resolve their ApiClient via GetApiClient(role, …) and get a use-time failure if the role they need isn't configured.
func ListZones ¶
func ListZones(cr tdns.ZoneResponse)
func NewConfigCmd ¶
NewConfigCmd returns a fresh "config" command tree bound to the given role. Each attachment point gets its own *cobra.Command.
func NewDaemonCmd ¶
NewDaemonCmd returns a fresh "daemon" command tree bound to the given role. Each attachment point (auth, agent, imr, scanner in tdns-cli; signer, combiner, agent in mpcli; plus root "server") gets its own *cobra.Command.
func NewDebugCmd ¶
NewDebugCmd returns a fresh "debug" command tree bound to the given role. Extras are attached as additional subcommands — used by tdns-mp to inject DebugAgentCmd under the agent tree only.
func NewKeysCmd ¶
NewKeysCmd returns a fresh "keys" command tree bound to the given role. Role must be "agent" or "combiner" — the tree is only meaningful under those two API clients (their configs point to the long_term_jose_priv_key).
func NewKeystoreCmd ¶
NewKeystoreCmd returns a fresh "keystore" command tree bound to the given role. The subtree (sig0 + dnssec branches with their children and flags) is built inline so every attachment point gets unique *cobra.Command instances.
func NewPingCmd ¶
NewPingCmd returns a fresh ping *cobra.Command bound to the given role. Each attachment site must create its own command (Cobra does not allow the same *cobra.Command under multiple parents). The role string is the registry key from RegisterRole.
func NewStopCmd ¶
NewStopCmd returns a fresh "stop" *cobra.Command bound to the given role. Each attachment site must create its own command (root "server" default in tdns-cli; signer / combiner / agent in mpcli).
func NewTruststoreCmd ¶
NewTruststoreCmd returns a fresh "truststore" command tree bound to the given role. The nested sig0 subtree and its children/flags are built inline so every attachment point gets unique *cobra.Command instances.
func NewZoneCmd ¶
NewZoneCmd returns a fresh "zone" command tree bound to the given role. Additional subcommands may be attached via extras — used by tdns-mp to inject signer-specific mplist under the signer's tree.
func PrepArgs ¶
func PrepArgs(args ...interface{})
PrepArgs validates and normalizes CLI parameters. It can work in two modes: 1. Legacy mode: reads from global variables (tdns.Globals.*) 2. Flag mode: reads from cobra.Command flags (pass cmd as first argument)
Usage:
PrepArgs("zonename") // reads from tdns.Globals.Zonename
PrepArgs(cmd, "zonename") // reads from --zone flag
PrepArgs(cmd, "zonename", "service") // reads from --zone and --service flags
func PrintCacheItem ¶
func PrintUpdateResult ¶
func PrintUpdateResult(ur tdns.UpdateResult)
func ReadZoneData ¶
func RegisterRole ¶
func RegisterRole(role, clientKey string)
RegisterRole associates role with clientKey. Later calls for the same role override earlier ones — this is how tdns-mp overrides the "agent" → "tdns-agent" default with "agent" → "tdns-mpagent".
Safe only from init() (map is not concurrency-safe; init ordering inside a package is deterministic, across imported packages it is topological and thus deterministic for override purposes).
func RunZoneBump ¶
func RunZoneList ¶
func RunZoneReload ¶
func RunZoneWrite ¶
func SendAgentMgmtCmd ¶
func SendAgentMgmtCmd(req *tdns.AgentMgmtPost) (*tdns.AgentMgmtResponse, error)
SendAgentMgmtCmd POSTs an AgentMgmtPost to the agent daemon's /agent endpoint. Every caller in this package talks to the agent, so the role is fixed rather than inferred from the Cobra tree.
func SendCatalogCommand ¶
func SendCatalogCommand(api *tdns.ApiClient, data tdns.CatalogPost) (*tdns.CatalogResponse, error)
SendCatalogCommand sends a catalog command to the API
func SendCommandNG ¶
func SendCommandNG(api *tdns.ApiClient, data tdns.CommandPost) (tdns.CommandResponse, error)
func SendConfigCommand ¶
func SendConfigCommand(api *tdns.ApiClient, data tdns.ConfigPost) (tdns.ConfigResponse, error)
func SendDelegationCmd ¶
func SendDelegationCmd(api *tdns.ApiClient, data tdns.DelegationPost) (tdns.DelegationResponse, error)
func SendDsyncCommand ¶
func SendDsyncCommand(api *tdns.ApiClient, data tdns.ZoneDsyncPost) (tdns.ZoneDsyncResponse, error)
func SendKeystoreCmd ¶
func SendKeystoreCmd(api *tdns.ApiClient, data tdns.KeystorePost) (tdns.KeystoreResponse, error)
func SendNotify ¶
func SendTruststore ¶
func SendTruststore(api *tdns.ApiClient, data tdns.TruststorePost) (tdns.TruststoreResponse, error)
func SendZoneCommand ¶
func SetRootCommand ¶
SetRootCommand allows the root package to provide the root command reference
func StartImrForCli ¶
StartImrForCli initializes and starts the internal IMR for CLI commands. It sets up the minimal config, starts RecursorEngine, and waits for initialization. Returns the context, cancel function, and the Imr instance, or an error if initialization fails.
func StartInteractiveMode ¶
func StartInteractiveMode()
func ValidateBySection ¶
func VerboseListZone ¶
func VerboseListZone(cr tdns.ZoneResponse)
Types ¶
type ApiDetails ¶
type ApiDetails struct {
Name string `validate:"required" yaml:"name"`
BaseURL string `validate:"required" yaml:"baseurl"`
ApiKey string `validate:"required" yaml:"apikey"`
AuthMethod string `validate:"required" yaml:"authmethod"`
RootCA string `yaml:"rootca"`
Command string `yaml:"command,omitempty"`
ConfigFile string `yaml:"config_file,omitempty"`
}
ApiDetails is one entry in the apiservers list of a CLI config file.
type CliConf ¶
type CliConf struct {
ApiServers []ApiDetails
Keys tdns.KeyConf
}
CliConf is the parsed CLI config. Each CLI binary's root.go populates one of these (currently via viper + Unmarshal) and hands it to InitApiClients.
type CommandNode ¶
type CommandNode struct {
Name string // Command name
Command *cobra.Command // Reference to original Cobra command
SubCommands map[string]*CommandNode // Child commands
Parent *CommandNode // Parent command (nil for root)
Args []string // Expected arguments from Use field
Guide string // Guide text for this command
}
CommandNode represents a node in our command tree
func BuildCommandTree ¶
func BuildCommandTree(cmd *cobra.Command, parent *CommandNode) *CommandNode
BuildCommandTree creates a tree structure from Cobra commands
func (*CommandNode) DebugPrint ¶
func (n *CommandNode) DebugPrint(indent string)
Debug function to print the tree structure
Source Files
¶
- agent_cmds.go
- agent_imr_cmds.go
- agent_zone_cmds.go
- algorithms.go
- apiclient.go
- auth_cmds.go
- base32_cmds.go
- catalog_cmds.go
- commands.go
- config.go
- config_cmds.go
- daemon_cmds.go
- db_cmds.go
- ddns_cmds.go
- debug_cmds.go
- dsync_cmds.go
- fakezone.go
- generate_cmds.go
- imr_cmds.go
- imr_dump_cmds.go
- imr_set_cmds.go
- interactive.go
- jose_keys_cmds.go
- jwt_cmds.go
- keys_generate_cmds.go
- keystore_cmds.go
- ksk_rollover_cli.go
- notify_cmds.go
- parentsync_cmds.go
- ping.go
- prepargs.go
- readline.go
- report_cmds.go
- rfc3597_cmds.go
- scanner_cmds.go
- truststore_cmds.go
- update.go
- version_cmd.go
- zone_cmds.go
- zone_dsync_cmds.go