From f21e7c468c4d4479919c4a483451c6672698ab0a Mon Sep 17 00:00:00 2001 From: Shulhan Date: Sun, 15 May 2022 22:57:26 +0700 Subject: cmd/resolver: implement command to add new record to zone The command has the following signature, zone.d rr add <"@" | subdomain> ...:: The domain name can be set to origin using "@" or empty string, subdomain (without ending with "."), or fully qualified domain name (end with "."). If ttl is set to 0, it will default to 604800 (7 days). List of valid type are A, NS, CNAME, PTR, MX, TXT, and AAAA. List of valid class are IN, CS, HS. --- .gitignore | 1 + _doc/resolver.adoc | 52 ++++++++++++++++ _sys/usr/share/man/man1/resolver.1.gz | Bin 2973 -> 3324 bytes client.go | 48 +++++++++++++++ cmd/resolver/main.go | 16 ++++- cmd/resolver/resolver.go | 110 ++++++++++++++++++++++++++++++++-- 6 files changed, 222 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 2e5d9f4..0fa8a44 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /_bin/linux_amd64 /_doc/*.html /_test/etc/rescached/hosts.d/hosts +/_test/etc/rescached/zone.d/ /_test/var/cache/rescached/ /cover.html /cover.out diff --git a/_doc/resolver.adoc b/_doc/resolver.adoc index b67a0e0..0ca00fc 100644 --- a/_doc/resolver.adoc +++ b/_doc/resolver.adoc @@ -213,6 +213,30 @@ zone.d delete :: Delete zone file inside the zone.d directory. +=== MANAGING RECORD IN ZONE.D + +zone.d rr add <"@" | subdomain> ...:: ++ +-- +Add new record into the zone file. + +The domain name can be set to origin using "@" or empty string, subdomain +(without ending with "."), or fully qualified domain name (end with "."). + +If ttl is set to 0, it will default to 604800 (7 days). + +List of valid type are A, NS, CNAME, PTR, MX, TXT, and AAAA. + +List of valid class are IN, CS, HS. + +The value parameter can be more than one, for example, the MX record +we pass two parameters: + + + +See the example below for more information. +-- + == EXIT STATUS Upon exit and success +resolver+ will return 0, or 1 otherwise. @@ -336,6 +360,34 @@ Delete record "my.hosts" from hosts file "hosts", "TTL": 604800 } + +=== MANAGING RECORD IN ZONE.D + +Assume that we have create zone "my.zone". + +Add IPv4 address "127.0.0.1" for domain my.zone, + + $ resolver zone.d rr add my.zone @ 0 A IN 127.0.0.1 + { + "Value": "127.0.0.1", + "Name": "my.zone", + "Type": 1, + "Class": 1, + "TTL": 604800 + } + +Add subdomain "www" with IPv4 address "192.168.1.2" to zone "my.zone", + + $ resolver zone.d rr add my.zone www 0 A IN 192.168.1.2 + { + "Value": "192.168.1.2", + "Name": "www.my.zone", + "Type": 1, + "Class": 1, + "TTL": 604800 + } + + == AUTHOR This software is developed by M. Shulhan (ms@kilabit.info). diff --git a/_sys/usr/share/man/man1/resolver.1.gz b/_sys/usr/share/man/man1/resolver.1.gz index 90dbf93..68e19f8 100644 Binary files a/_sys/usr/share/man/man1/resolver.1.gz and b/_sys/usr/share/man/man1/resolver.1.gz differ diff --git a/client.go b/client.go index bfec2fa..2833768 100644 --- a/client.go +++ b/client.go @@ -4,6 +4,7 @@ package rescached import ( + "encoding/base64" "encoding/json" "fmt" "net/http" @@ -473,3 +474,50 @@ func (cl *Client) ZonedDelete(name string) (zone *dns.Zone, err error) { } return zone, nil } + +// ZonedRecordAdd add new record to zone file. +func (cl *Client) ZonedRecordAdd(zone string, rreq dns.ResourceRecord) (rres *dns.ResourceRecord, err error) { + var ( + logp = "ZonedRecordAdd" + zrr = zoneRecordRequest{ + Zone: zone, + } + + res *libhttp.EndpointResponse + rawb []byte + ok bool + ) + + fmt.Printf("ZonedRecordAdd: %+v\n", rreq) + + zrr.Type, ok = dns.RecordTypeNames[rreq.Type] + if !ok { + return nil, fmt.Errorf("%s: unknown record type: %d", logp, rreq.Type) + } + + rawb, err = json.Marshal(rreq) + if err != nil { + return nil, fmt.Errorf("%s: %w", logp, err) + } + + zrr.Record = base64.StdEncoding.EncodeToString(rawb) + + _, rawb, err = cl.PostJSON(apiZonedRR, nil, zrr) + if err != nil { + return nil, fmt.Errorf("%s: %w", logp, err) + } + + rres = &dns.ResourceRecord{} + res = &libhttp.EndpointResponse{ + Data: rres, + } + err = json.Unmarshal(rawb, res) + if err != nil { + return nil, fmt.Errorf("%s: %w", logp, err) + } + if res.Code != http.StatusOK { + return nil, fmt.Errorf("%s: %d %s", logp, res.Code, res.Message) + } + + return rres, nil +} diff --git a/cmd/resolver/main.go b/cmd/resolver/main.go index 62af801..556cff1 100644 --- a/cmd/resolver/main.go +++ b/cmd/resolver/main.go @@ -148,7 +148,7 @@ query [type] [class] Valid type are either A, NS, CNAME, SOA, MB, MG, MR, NULL, WKS, PTR, HINFO, MINFO, MX, TXT, AAAA, or SRV. - Default value is A." + Default value is A. Valid class are either IN, CS, HS. Default value is IN. @@ -246,6 +246,20 @@ zone.d delete Delete zone file inside the zone.d directory. +=== MANAGING RECORD IN ZONE.D + +zone.d rr add ... + + Add new record into the zone file. + If domain ttl is set to 0, it will default to 604800 (7 days). + List of valid type are A, NS, CNAME, PTR, MX, TXT, and AAAA. + The value parameter can be more than one, for example, the MX record + we pass two parameters: + + + + See the example below for more information. + == Examples === QUERY diff --git a/cmd/resolver/resolver.go b/cmd/resolver/resolver.go index 75287db..36175a1 100644 --- a/cmd/resolver/resolver.go +++ b/cmd/resolver/resolver.go @@ -10,6 +10,7 @@ import ( "log" "math/rand" "os" + "strconv" "strings" "time" @@ -475,9 +476,9 @@ func (rsol *resolver) doCmdZoned(args []string) { var ( subCmd = strings.ToLower(args[0]) + resc = rsol.newRescachedClient() - resc *rescached.Client - err error + err error ) switch subCmd { @@ -487,7 +488,6 @@ func (rsol *resolver) doCmdZoned(args []string) { log.Fatalf("resolver: %s %s: missing parameter name", rsol.cmd, subCmd) } - resc = rsol.newRescachedClient() _, err = resc.ZonedCreate(args[0]) if err != nil { log.Fatalf("resolver: %s", err) @@ -500,18 +500,120 @@ func (rsol *resolver) doCmdZoned(args []string) { log.Fatalf("resolver: %s %s: missing parameter name", rsol.cmd, subCmd) } - resc = rsol.newRescachedClient() _, err = resc.ZonedDelete(args[0]) if err != nil { log.Fatalf("resolver: %s", err) } fmt.Println("OK") + case subCmdRR: + rsol.doCmdZonedRR(resc, args[1:]) + default: log.Fatalf("resolver: %s: unknown sub command: %s", rsol.cmd, subCmd) } } +func (rsol *resolver) doCmdZonedRR(resc *rescached.Client, args []string) { + var ( + cmdAction = strings.ToLower(args[0]) + ) + + args = args[1:] + + switch cmdAction { + case subCmdAdd: + rsol.doCmdZonedRRAdd(resc, args) + + default: + log.Fatalf("resolver: %s %s: unknown action: %q", rsol.cmd, subCmdRR, cmdAction) + } +} + +// doCmdZonedRRAdd add new record to the zone. +// This command accept the following arguments: +// +// 0 1 2 3 4 5 +// ... +// +// List of valid type are A, NS, CNAME, PTR, MX, TXT, and AAAA. +// For the MX record we pass two parameters: +// +// 5 6 +// +func (rsol *resolver) doCmdZonedRRAdd(resc *rescached.Client, args []string) { + if len(args) < 6 { + log.Fatalf("resolver: %s %s %s: missing arguments", rsol.cmd, subCmdRR, subCmdAdd) + } + + var ( + zone = strings.ToLower(args[0]) + rreq = dns.ResourceRecord{} + + rres *dns.ResourceRecord + vstr string + vuint uint64 + vbytes []byte + err error + ok bool + ) + + rreq.Name = strings.ToLower(args[1]) + if rreq.Name == "@" { + rreq.Name = "" + } + + vuint, err = strconv.ParseUint(args[2], 10, 64) + if err != nil { + log.Fatalf("resolver: invalid TTL: %q", args[2]) + } + + rreq.TTL = uint32(vuint) + + vstr = strings.ToUpper(args[3]) + rreq.Type, ok = dns.RecordTypes[vstr] + if !ok { + log.Fatalf("resolver: invalid record type: %q", vstr) + } + + vstr = strings.ToUpper(args[4]) + rreq.Class, ok = dns.RecordClasses[vstr] + if !ok { + log.Fatalf("resolver: invalid record class: %q", vstr) + } + + vstr = args[5] + + if rreq.Type == dns.RecordTypeMX { + if len(args) < 6 { + log.Fatalf("resolver: missing argument for MX record") + } + vuint, err = strconv.ParseUint(vstr, 10, 64) + if err != nil { + log.Fatalf("resolver: invalid MX preference: %q", vstr) + } + var rrMX = &dns.RDataMX{ + Preference: int16(vuint), + Exchange: args[6], + } + rreq.Value = rrMX + } else { + rreq.Value = args[5] + } + + rres, err = resc.ZonedRecordAdd(zone, rreq) + if err != nil { + log.Fatalf("resolver: %s", err) + } + + vbytes, err = json.MarshalIndent(rres, "", " ") + if err != nil { + log.Fatalf("resolver: %s", err) + } + + fmt.Println(string(vbytes)) +} + // initSystemResolver read the system resolv.conf to create fallback DNS // resolver. func (rsol *resolver) initSystemResolver() (err error) { -- cgit v1.3