Skip to content

Commit 1c81009

Browse files
committed
Initial Commit
Signed-off-by: Khash Sajadi <[email protected]>
1 parent 5bc50fd commit 1c81009

File tree

8 files changed

+1719
-1
lines changed

8 files changed

+1719
-1
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@
1313

1414
# Dependency directories (remove the comment below to include it)
1515
# vendor/
16+
/.idea

README.md

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,78 @@
1-
# corednsmysql
1+
# mysql
22
MySQL backend for CoreDNS
3+
4+
## Supported Record Types
5+
6+
A, AAAA, CNAME, SOA, TXT, NS, MX, CAA and SRV. This backend doesn't support AXFR requests. It also doesn't support wildcard records yet.
7+
8+
## Setup
9+
10+
Add this as an external plugin in `plugin.cfg` file:
11+
12+
```
13+
mysql:github.com/cloud66-oss/coredns_mysql
14+
```
15+
16+
then run
17+
18+
```shell script
19+
$ go generate
20+
$ go build
21+
```
22+
23+
Add any required modules to CoreDNS code as prompted.
24+
25+
## Configuration
26+
27+
In the Corefile, `mysql` can be configured with the following parameters:
28+
29+
`dsn` DSN for MySQL as per https://github.com/go-sql-driver/mysql examples.
30+
`table_prefix` Prefix for the MySQL tables. Defaults to `coredns_`.
31+
`max_lifetime` Duration (in Golang format) for a SQL connection. Default is 1 minute.
32+
`max_open_connections` Maximum number of open connections to the database server. Default is 10.
33+
`max_idle_connections` Maximum number of idle connections in the database connection pool. Default is 10.
34+
`ttl` Default TTL for records without a specified TTL in seconds. Default is 360 (seconds)
35+
36+
## Database Setup
37+
This plugin doesn't create or migrate database schema for its use yet. To create the database and tables, use the following table structure (note the table name prefix):
38+
39+
```sql
40+
CREATE TABLE `coredns_records` (
41+
`id` INT NOT NULL AUTO_INCREMENT,
42+
`zone` VARCHAR(255) NOT NULL,
43+
`name` VARCHAR(255) NOT NULL,
44+
`ttl` INT DEFAULT NULL,
45+
`content` TEXT,
46+
`record_type` VARCHAR(255) NOT NULL,
47+
PRIMARY KEY (`id`)
48+
) ENGINE = INNODB AUTO_INCREMENT = 6 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
49+
```
50+
51+
## Record setup
52+
Each record served by this plugin, should belong to the zone it is allowed to server by CoreDNS. Here are some examples:
53+
54+
```sql
55+
-- Insert batch #1
56+
INSERT INTO coredns_records (zone, name, ttl, content, record_type) VALUES
57+
('test.', 'foo', 30, '{"ip": "1.1.1.1"}', 'A'),
58+
('test.', 'foo', '60', '{"ip": "1.1.1.0"}', 'A'),
59+
('test.', 'foo', 30, '{"text": "hello"}', 'TXT'),
60+
('test.', 'foo', 30, '{"host" : "foo.test.","priority" : 10}', 'MX');
61+
```
62+
63+
These can be queries using `dig` like this:
64+
65+
```shell script
66+
$ dig A MX foo.test
67+
```
68+
69+
### Acknowledgements and Credits
70+
This plugin, is inspired by https://github.com/wenerme/coredns-pdsql and https://github.com/arvancloud/redis
71+
72+
### Development
73+
To develop this plugin further, make sure you can compile CoreDNS locally and get this repo (`go get github.com/cloud66-oss/coredns_mysql`). You can switch the CoreDNS mod file to look for the plugin code locally while you're developing it:
74+
75+
Put `replace github.com/cloud66-oss/coredns_mysql => LOCAL_PATH_TO_THE_SOURCE_CODE` at the end of the `go.mod` file in CoreDNS code.
76+
77+
Pull requests and bug reports are welcome!
78+

go.mod

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/cloud66-oss/coredns_mysql
2+
3+
go 1.16
4+
5+
require (
6+
github.com/coredns/caddy v1.1.0
7+
github.com/coredns/coredns v1.8.4
8+
github.com/go-sql-driver/mysql v1.6.0
9+
github.com/miekg/dns v1.1.42
10+
golang.org/x/net v0.0.0-20210525063256-abc453219eb5
11+
)

go.sum

Lines changed: 915 additions & 0 deletions
Large diffs are not rendered by default.

handler.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package coredns_mysql
2+
3+
import (
4+
"time"
5+
6+
"github.com/coredns/coredns/plugin"
7+
"github.com/coredns/coredns/request"
8+
_ "github.com/go-sql-driver/mysql"
9+
"github.com/miekg/dns"
10+
"golang.org/x/net/context"
11+
)
12+
13+
type CoreDNSMySql struct {
14+
Next plugin.Handler
15+
Dsn string
16+
TablePrefix string
17+
MaxLifetime time.Duration
18+
MaxOpenConnections int
19+
MaxIdleConnections int
20+
Ttl uint32
21+
22+
tableName string
23+
lastZoneUpdate time.Time
24+
zoneUpdateTime time.Duration
25+
zones []string
26+
}
27+
28+
// ServeDNS implements the plugin.Handler interface.
29+
func (handler *CoreDNSMySql) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Msg) (int, error) {
30+
state := request.Request{W: w, Req: r}
31+
32+
qName := state.Name()
33+
qType := state.Type()
34+
35+
if time.Since(handler.lastZoneUpdate) > handler.zoneUpdateTime {
36+
err := handler.loadZones()
37+
if err != nil {
38+
return handler.errorResponse(state, dns.RcodeServerFailure, err)
39+
}
40+
}
41+
42+
qZone := plugin.Zones(handler.zones).Matches(qName)
43+
if qZone == "" {
44+
return plugin.NextOrFailure(handler.Name(), handler.Next, ctx, w, r)
45+
}
46+
47+
records, err := handler.findRecord(qZone, qName, qType)
48+
if err != nil {
49+
return handler.errorResponse(state, dns.RcodeServerFailure, err)
50+
}
51+
52+
if len(records) == 0 {
53+
// no record found but we are going to return a SOA
54+
rec := &Record{
55+
Name: qName,
56+
RecordType: "SOA",
57+
Content: "{}",
58+
}
59+
records = append(records, rec)
60+
}
61+
62+
if qType == "AXFR" {
63+
return handler.errorResponse(state, dns.RcodeNotImplemented, nil)
64+
}
65+
66+
answers := make([]dns.RR, 0, 10)
67+
extras := make([]dns.RR, 0, 10)
68+
69+
for _, record := range records {
70+
var answer dns.RR
71+
switch record.RecordType {
72+
case "A":
73+
answer, extras, err = record.AsARecord()
74+
case "AAAA":
75+
answer, extras, err = record.AsAAAARecord()
76+
case "CNAME":
77+
answer, extras, err = record.AsCNAMERecord()
78+
case "SOA":
79+
answer, extras, err = record.AsSOARecord()
80+
case "SRV":
81+
answer, extras, err = record.AsSRVRecord()
82+
case "NS":
83+
answer, extras, err = record.AsNSRecord()
84+
case "MX":
85+
answer, extras, err = record.AsMXRecord()
86+
case "TXT":
87+
answer, extras, err = record.AsTXTRecord()
88+
case "CAA":
89+
answer, extras, err = record.AsCAARecord()
90+
default:
91+
return handler.errorResponse(state, dns.RcodeNotImplemented, nil)
92+
}
93+
94+
if err != nil {
95+
return handler.errorResponse(state, dns.RcodeServerFailure, err)
96+
}
97+
if answer != nil {
98+
answers = append(answers, answer)
99+
}
100+
}
101+
102+
m := new(dns.Msg)
103+
m.SetReply(r)
104+
m.Authoritative = true
105+
m.RecursionAvailable = false
106+
m.Compress = true
107+
108+
m.Answer = append(m.Answer, answers...)
109+
m.Extra = append(m.Extra, extras...)
110+
111+
state.SizeAndDo(m)
112+
m = state.Scrub(m)
113+
_ = w.WriteMsg(m)
114+
return dns.RcodeSuccess, nil
115+
}
116+
117+
// Name implements the Handler interface.
118+
func (handler *CoreDNSMySql) Name() string { return "handler" }
119+
120+
func (handler *CoreDNSMySql) errorResponse(state request.Request, rCode int, err error) (int, error) {
121+
m := new(dns.Msg)
122+
m.SetRcode(state.Req, rCode)
123+
m.Authoritative, m.RecursionAvailable, m.Compress = true, false, true
124+
125+
state.SizeAndDo(m)
126+
_ = state.W.WriteMsg(m)
127+
// Return success as the rCode to signal we have written to the client.
128+
return dns.RcodeSuccess, err
129+
}

mysql.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package coredns_mysql
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"time"
7+
8+
"github.com/miekg/dns"
9+
)
10+
11+
func (handler *CoreDNSMySql) findRecord(zone string, name string, types ...string) ([]*Record, error) {
12+
db, err := handler.db()
13+
if err != nil {
14+
return nil, err
15+
}
16+
defer db.Close()
17+
18+
query := strings.TrimSuffix(name, "." + zone)
19+
sqlQuery := fmt.Sprintf("SELECT name, zone, ttl, record_type, content FROM %s WHERE zone = ? AND name = ? AND record_type IN ('%s')",
20+
handler.tableName,
21+
strings.Join(types, "','"))
22+
result, err := db.Query(sqlQuery, zone, query)
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
var recordName string
28+
var recordZone string
29+
var recordType string
30+
var ttl uint32
31+
var content string
32+
records := make([]*Record, 0)
33+
for result.Next() {
34+
err = result.Scan(&recordName, &recordZone, &ttl, &recordType, &content)
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
records = append(records, &Record{
40+
Name: recordName,
41+
Zone: recordZone,
42+
RecordType: recordType,
43+
Ttl: ttl,
44+
Content: content,
45+
handler: handler,
46+
})
47+
}
48+
49+
return records, nil
50+
}
51+
52+
func (handler *CoreDNSMySql) loadZones() error {
53+
db, err := handler.db()
54+
if err != nil {
55+
return err
56+
}
57+
defer db.Close()
58+
59+
result, err := db.Query("SELECT DISTINCT zone FROM " + handler.tableName)
60+
if err != nil {
61+
return err
62+
}
63+
64+
var zone string
65+
zones := make([]string, 0)
66+
for result.Next() {
67+
err = result.Scan(&zone)
68+
if err != nil {
69+
return err
70+
}
71+
72+
zones = append(zones, zone)
73+
}
74+
75+
handler.lastZoneUpdate = time.Now()
76+
handler.zones = zones
77+
78+
return nil
79+
}
80+
81+
func (handler *CoreDNSMySql) hosts(zone string, name string) ([]dns.RR, error) {
82+
recs, err := handler.findRecord(zone, name, "A", "AAAA", "CNAME")
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
answers := make([]dns.RR, 0)
88+
89+
for _, rec := range recs {
90+
switch rec.RecordType {
91+
case "A":
92+
aRec, _, err := rec.AsARecord()
93+
if err != nil {
94+
return nil, err
95+
}
96+
answers = append(answers, aRec)
97+
case "AAAA":
98+
aRec, _, err := rec.AsAAAARecord()
99+
if err != nil {
100+
return nil, err
101+
}
102+
answers = append(answers, aRec)
103+
case "CNAME":
104+
aRec, _, err := rec.AsCNAMERecord()
105+
if err != nil {
106+
return nil, err
107+
}
108+
answers = append(answers, aRec)
109+
}
110+
}
111+
112+
return answers, nil
113+
}

0 commit comments

Comments
 (0)