mirror of
https://github.com/Li4n0/revsuit.git
synced 2026-09-21 22:30:46 +08:00
feat: complete basic functions
Support http,dns and mysql connection. Support custom http, dns response. Support dns rebinding. Support mysql load local files and jdbc deserialize exploit.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
package cli
|
||||
@@ -0,0 +1,65 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/li4n0/revsuit/pkg/server"
|
||||
"github.com/urfave/cli/v2"
|
||||
"gopkg.in/yaml.v2"
|
||||
log "unknwon.dev/clog/v2"
|
||||
)
|
||||
|
||||
func Start() {
|
||||
app := &cli.App{
|
||||
Name: "RevSuit",
|
||||
Usage: "An Open-Sourced Reverse Platform Designed for Receive Various Kinds of Connection",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "addr",
|
||||
Usage: "platform listened address",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "token",
|
||||
Usage: "token used to manage platform",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "flags",
|
||||
Usage: "for http and dns connection, platform will only record the connection those match these regex flags. * meaning record all",
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "db",
|
||||
Usage: "database file path",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
conf := &server.Config{}
|
||||
if content, err := ioutil.ReadFile("config.yaml"); err == nil {
|
||||
if err := yaml.Unmarshal(content, conf); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
} else {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
if c.String("addr") != "" {
|
||||
conf.Addr = c.String("addr")
|
||||
}
|
||||
if c.String("token") != "" {
|
||||
conf.Addr = c.String("token")
|
||||
}
|
||||
if c.String("db") != "" {
|
||||
conf.Database = c.String("db")
|
||||
}
|
||||
server.New(conf).Run()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
sort.Sort(cli.FlagsByName(app.Flags))
|
||||
sort.Sort(cli.CommandsByName(app.Commands))
|
||||
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package cli
|
||||
|
||||
import "math/rand"
|
||||
|
||||
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()"
|
||||
|
||||
func genToken() string {
|
||||
b := make([]byte, 8)
|
||||
for i := range b {
|
||||
b[i] = letterBytes[rand.Intn(len(letterBytes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package database
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
func InitDB(driver, dsn string) (err error) {
|
||||
switch driver {
|
||||
case "sqlite":
|
||||
DB, err = NewSqlite3(dsn)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type MapField map[string]string
|
||||
|
||||
func (f MapField) Value() (driver.Value, error) {
|
||||
return json.Marshal(f)
|
||||
}
|
||||
|
||||
func (f *MapField) Scan(data interface{}) error {
|
||||
return json.Unmarshal(data.([]byte), f)
|
||||
}
|
||||
|
||||
|
||||
type ListField []string
|
||||
|
||||
func (f ListField) Value() (driver.Value, error) {
|
||||
return json.Marshal(f)
|
||||
}
|
||||
|
||||
func (f *ListField) Scan(data interface{}) error {
|
||||
return json.Unmarshal(data.([]byte), f)
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func NewSqlite3(dsn string) (*gorm.DB, error) {
|
||||
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db.Exec("PRAGMA foreign_keys=ON")
|
||||
return db, nil
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
@@ -0,0 +1,12 @@
|
||||
language: go
|
||||
go:
|
||||
- "1.12"
|
||||
- tip
|
||||
env:
|
||||
- GO111MODULE=on
|
||||
before_install:
|
||||
- go get github.com/mattn/goveralls
|
||||
install:
|
||||
- go get -t ./...
|
||||
script:
|
||||
- $HOME/gopath/bin/goveralls -service=travis-ci
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Joël Gähwiler
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,10 @@
|
||||
all: fmt vet lint
|
||||
|
||||
fmt:
|
||||
go fmt .
|
||||
|
||||
vet:
|
||||
go vet .
|
||||
|
||||
lint:
|
||||
golint .
|
||||
@@ -0,0 +1,99 @@
|
||||
# newdns
|
||||
|
||||
[](https://travis-ci.org/256dpi/newdns)
|
||||
[](https://coveralls.io/github/256dpi/newdns?branch=master)
|
||||
[](http://godoc.org/github.com/256dpi/newdns)
|
||||
[](https://github.com/256dpi/newdns/releases)
|
||||
[](https://goreportcard.com/report/github.com/256dpi/newdns)
|
||||
|
||||
**A library for building custom DNS servers in Go.**
|
||||
|
||||
The newdns library wraps the widely used, but low-level [github.com/miekg/dns](https://github.com/miekg/dns) package with a simple interface to quickly build custom DNS servers. The implemented server only supports a subset of record types (A, AAAA, CNAME, MX, TXT) and is intended to be used as a leaf authoritative name server only. It supports UDP and TCP as transport protocols and implements EDNS0. Conformance is tested by issuing a corpus of tests against a zone in AWS Route53 and comparing the response and behavior.
|
||||
|
||||
The intention of this project is not to build a feature-complete alternative to "managed zone" offerings by major cloud platforms. However, some projects may require frequent synchronization of many records between a custom database and a cloud-hosted "managed zone". In this scenario, a custom DNS server that queries the own database might be a lot simpler to manage and operate. Also, the distributed nature of the DNS system offers interesting qualities that could be leveraged by future applications.
|
||||
|
||||
## Example
|
||||
|
||||
```go
|
||||
// create zone
|
||||
zone := &newdns.Zone{
|
||||
Name: "example.com.",
|
||||
MasterNameServer: "ns1.hostmaster.com.",
|
||||
AllNameServers: []string{
|
||||
"ns1.hostmaster.com.",
|
||||
"ns2.hostmaster.com.",
|
||||
"ns3.hostmaster.com.",
|
||||
},
|
||||
Handler: func(name string) ([]newdns.Set, error) {
|
||||
// return apex records
|
||||
if name == "" {
|
||||
return []newdns.Set{
|
||||
{
|
||||
Name: "example.com.",
|
||||
Type: newdns.A,
|
||||
Records: []newdns.Record{
|
||||
{Address: "1.2.3.4"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "example.com.",
|
||||
Type: newdns.AAAA,
|
||||
Records: []newdns.Record{
|
||||
{Address: "1:2:3:4::"},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// return sub records
|
||||
if name == "foo" {
|
||||
return []newdns.Set{
|
||||
{
|
||||
Name: "foo.example.com.",
|
||||
Type: newdns.CNAME,
|
||||
Records: []newdns.Record{
|
||||
{Address: "bar.example.com."},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
// create server
|
||||
server := newdns.NewServer(newdns.Config{
|
||||
Handler: func(name string) (*newdns.Zone, error) {
|
||||
// check name
|
||||
if newdns.InZone("example.com.", name) {
|
||||
return zone, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
},
|
||||
Logger: func(e newdns.Event, msg *dns.Msg, err error, reason string) {
|
||||
fmt.Println(e, err, reason)
|
||||
},
|
||||
})
|
||||
|
||||
// run server
|
||||
go func() {
|
||||
err := server.Run(":1337")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
// print info
|
||||
fmt.Println("Query apex: dig example.com @0.0.0.0 -p 1337")
|
||||
fmt.Println("Query other: dig foo.example.com @0.0.0.0 -p 1337")
|
||||
|
||||
// wait forever
|
||||
select {}
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
- https://github.com/miekg/dns
|
||||
- https://github.com/coredns/coredns
|
||||
@@ -0,0 +1,85 @@
|
||||
package newdns
|
||||
|
||||
import "github.com/miekg/dns"
|
||||
|
||||
// Event denotes an event type emitted to the logger.
|
||||
type Event int
|
||||
|
||||
const (
|
||||
// Ignored are requests that haven been dropped by leaving the connection
|
||||
// hanging to mitigate attacks. Inspect the reason for more information.
|
||||
Ignored Event = iota
|
||||
|
||||
// Request is emitted for every accepted request. For every request event
|
||||
// a finish event fill follow. You can inspect the message to see the
|
||||
// complete request sent by the client.
|
||||
Request Event = iota
|
||||
|
||||
// Refused are requests that received an error due to some incompatibility.
|
||||
// Inspect the reason for more information.
|
||||
Refused Event = iota
|
||||
|
||||
// BackendError is emitted with errors returned by the callback and
|
||||
// validation functions. Inspect the error for more information.
|
||||
BackendError Event = iota
|
||||
|
||||
// NetworkError is emitted with errors returned by the connection. Inspect
|
||||
// the error for more information.
|
||||
NetworkError Event = iota
|
||||
|
||||
// Response is emitted with the final response to the client. You can inspect
|
||||
// the message to see the complete response to the client.
|
||||
Response Event = iota
|
||||
|
||||
// Finish is emitted when a request has been processed.
|
||||
Finish Event = iota
|
||||
|
||||
// ProxyRequest is emitted with every request forwarded to the fallback
|
||||
// DNS server.
|
||||
ProxyRequest Event = iota
|
||||
|
||||
// ProxyResponse is emitted with ever response received from the fallback
|
||||
// DNS server.
|
||||
ProxyResponse Event = iota
|
||||
|
||||
// ProxyError is emitted with errors returned by the fallback DNS server.
|
||||
// Inspect the error for more information.
|
||||
ProxyError Event = iota
|
||||
)
|
||||
|
||||
// String will return the name of the event.
|
||||
func (e Event) String() string {
|
||||
switch e {
|
||||
case Ignored:
|
||||
return "Ignored"
|
||||
case Request:
|
||||
return "Request"
|
||||
case Refused:
|
||||
return "Refused"
|
||||
case BackendError:
|
||||
return "BackendError"
|
||||
case NetworkError:
|
||||
return "NetworkError"
|
||||
case Response:
|
||||
return "Response"
|
||||
case Finish:
|
||||
return "Finish"
|
||||
case ProxyRequest:
|
||||
return "ProxyRequest"
|
||||
case ProxyResponse:
|
||||
return "ProxyResponse"
|
||||
case ProxyError:
|
||||
return "ProxyError"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// Logger is function that accepts logging events.
|
||||
type Logger func(e Event, msg *dns.Msg, err error, reason string)
|
||||
|
||||
func log(l Logger, e Event, msg *dns.Msg, err error, reason string) {
|
||||
if l != nil {
|
||||
l(e, msg, err, reason)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package newdns
|
||||
|
||||
import "github.com/miekg/dns"
|
||||
|
||||
// Proxy returns a handler that proxies requests to the provided DNS server. The
|
||||
// optional logger is called with events about the processing of requests.
|
||||
func Proxy(addr string, logger Logger) dns.Handler {
|
||||
return dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
// log request
|
||||
if logger != nil {
|
||||
logger(ProxyRequest, req, nil, "")
|
||||
}
|
||||
|
||||
// forward request to fallback
|
||||
rs, err := dns.Exchange(req, addr)
|
||||
if err != nil {
|
||||
if logger != nil {
|
||||
logger(ProxyError, nil, err, "")
|
||||
}
|
||||
_ = w.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// log response
|
||||
if logger != nil {
|
||||
logger(ProxyResponse, rs, nil, "")
|
||||
}
|
||||
|
||||
// write response
|
||||
err = w.WriteMsg(rs)
|
||||
if err != nil {
|
||||
if logger != nil {
|
||||
logger(NetworkError, nil, err, "")
|
||||
}
|
||||
_ = w.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package newdns
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Query can be used to query a DNS server over the provided protocol on its
|
||||
// address for the specified name and type. The supplied function can be set to
|
||||
// mutate the sent request.
|
||||
func Query(proto, addr, name, typ string, fn func(*dns.Msg)) (*dns.Msg, error) {
|
||||
// prepare request
|
||||
req := &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Id: dns.Id(),
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: name,
|
||||
Qtype: dns.StringToType[typ],
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// call function if available
|
||||
if fn != nil {
|
||||
fn(req)
|
||||
}
|
||||
|
||||
// prepare client
|
||||
client := dns.Client{
|
||||
Net: proto,
|
||||
Timeout: time.Second,
|
||||
}
|
||||
|
||||
// send request
|
||||
res, _, err := client.Exchange(req, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// reset id to allow direct comparison
|
||||
res.Id = 0
|
||||
|
||||
return res, nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package newdns
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Record holds a single DNS record.
|
||||
type Record struct {
|
||||
// The target address for A, AAAA, CNAME and MX records.
|
||||
Address string
|
||||
|
||||
// The priority for MX records.
|
||||
Priority int
|
||||
|
||||
// The data for TXT records.
|
||||
Data []string
|
||||
}
|
||||
|
||||
// Validate will validate the record.
|
||||
func (r *Record) Validate(typ Type) error {
|
||||
// validate A address
|
||||
if typ == A {
|
||||
ip := net.ParseIP(r.Address)
|
||||
if ip == nil || ip.To4() == nil {
|
||||
return errors.Errorf("invalid IPv4 address: %s", r.Address)
|
||||
}
|
||||
}
|
||||
|
||||
// validate AAAA address
|
||||
if typ == AAAA {
|
||||
ip := net.ParseIP(r.Address)
|
||||
if ip == nil || ip.To16() == nil {
|
||||
return errors.Errorf("invalid IPv6 address: %s", r.Address)
|
||||
}
|
||||
}
|
||||
|
||||
// validate CNAME and MX addresses
|
||||
if typ == CNAME || typ == MX {
|
||||
if !IsDomain(r.Address, true) {
|
||||
return errors.Errorf("invalid domain name: %s", r.Address)
|
||||
}
|
||||
}
|
||||
|
||||
// check TXT data
|
||||
if typ == TXT {
|
||||
if len(r.Data) == 0 {
|
||||
return errors.Errorf("missing data")
|
||||
}
|
||||
|
||||
for _, data := range r.Data {
|
||||
if len(data) > 255 {
|
||||
return errors.Errorf("data too long")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// validate NS addresses
|
||||
if typ == NS {
|
||||
if !IsDomain(r.Address, true) {
|
||||
return errors.Errorf("invalid ns name: %s", r.Address)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package newdns
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestRecordValidate(t *testing.T) {
|
||||
table := []struct {
|
||||
typ Type
|
||||
rec Record
|
||||
err string
|
||||
}{
|
||||
{
|
||||
typ: A,
|
||||
rec: Record{Address: "foo"},
|
||||
err: "invalid IPv4 address: foo",
|
||||
},
|
||||
{
|
||||
typ: AAAA,
|
||||
rec: Record{Address: "foo"},
|
||||
err: "invalid IPv6 address: foo",
|
||||
},
|
||||
{
|
||||
typ: A,
|
||||
rec: Record{Address: "1:2:3:4::"},
|
||||
err: "invalid IPv4 address: 1:2:3:4::",
|
||||
},
|
||||
{
|
||||
typ: A,
|
||||
rec: Record{Address: "1.2.3.4"},
|
||||
},
|
||||
{
|
||||
typ: AAAA,
|
||||
rec: Record{Address: "1:2:3:4::"},
|
||||
},
|
||||
{
|
||||
typ: CNAME,
|
||||
rec: Record{Address: "---"},
|
||||
err: "invalid domain name: ---",
|
||||
},
|
||||
{
|
||||
typ: CNAME,
|
||||
rec: Record{Address: "foo.com"},
|
||||
err: "invalid domain name: foo.com",
|
||||
},
|
||||
{
|
||||
typ: CNAME,
|
||||
rec: Record{Address: "foo.com."},
|
||||
},
|
||||
{
|
||||
typ: MX,
|
||||
rec: Record{Address: "foo.com"},
|
||||
err: "invalid domain name: foo.com",
|
||||
},
|
||||
{
|
||||
typ: MX,
|
||||
rec: Record{Address: "foo.com."},
|
||||
},
|
||||
{
|
||||
typ: TXT,
|
||||
rec: Record{Data: nil},
|
||||
err: "missing data",
|
||||
},
|
||||
{
|
||||
typ: TXT,
|
||||
rec: Record{Data: []string{"z4e6ycRMp6MP3WvWQMxIAOXglxANbj3oB0xD8BffktO4eo3VCR0s6TyGHKixvarOFJU0fqNkXeFOeI7sTXH5X0iXZukfLgnGTxLXNC7KkVFwtVFsh1P0IUNXtNBlOVWrVbxkS62ezbLpENNkiBwbkCvcTjwF2kyI0curAt9JhhJFb3AAq0q1iHWlJLn1KSrev9PIsY3alndDKjYTPxAojxzGKdK3A7rWLJ8Uzb3Z5OhLwP7jTKqbWVUocJRFLYpL"}},
|
||||
err: "data too long",
|
||||
},
|
||||
{
|
||||
typ: TXT,
|
||||
rec: Record{Data: []string{"foo"}},
|
||||
},
|
||||
{
|
||||
typ: NS,
|
||||
rec: Record{Address: "foo.com"},
|
||||
err: "invalid ns name: foo.com",
|
||||
},
|
||||
{
|
||||
typ: NS,
|
||||
rec: Record{Address: "foo.com."},
|
||||
},
|
||||
}
|
||||
|
||||
for i, item := range table {
|
||||
err := item.rec.Validate(item.typ)
|
||||
if err != nil {
|
||||
assert.Equal(t, item.err, err.Error(), i)
|
||||
} else {
|
||||
assert.Equal(t, item.err, "", item)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package newdns
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var fakeAddr = &net.TCPAddr{
|
||||
IP: net.IP{0, 0, 0, 0},
|
||||
Port: 0,
|
||||
}
|
||||
|
||||
type responseWriter struct {
|
||||
msg *dns.Msg
|
||||
}
|
||||
|
||||
func (w *responseWriter) LocalAddr() net.Addr {
|
||||
return fakeAddr
|
||||
}
|
||||
|
||||
func (w *responseWriter) RemoteAddr() net.Addr {
|
||||
return fakeAddr
|
||||
}
|
||||
|
||||
func (w *responseWriter) WriteMsg(msg *dns.Msg) error {
|
||||
// check message
|
||||
if w.msg != nil {
|
||||
panic("message already set")
|
||||
}
|
||||
|
||||
// set message
|
||||
w.msg = msg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *responseWriter) Write([]byte) (int, error) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (w *responseWriter) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *responseWriter) TsigStatus() error {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (w *responseWriter) TsigTimersOnly(bool) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (w *responseWriter) Hijack() {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// Resolver returns a very primitive recursive resolver that uses the provided
|
||||
// handler to resolve all names.
|
||||
func Resolver(handler dns.Handler) dns.Handler {
|
||||
return dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
// forward query if no recursion is desired
|
||||
if !req.RecursionDesired {
|
||||
handler.ServeDNS(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// prepare response
|
||||
res := new(dns.Msg)
|
||||
res.SetReply(req)
|
||||
res.RecursionAvailable = true
|
||||
|
||||
// query handler
|
||||
var wr responseWriter
|
||||
handler.ServeDNS(&wr, req)
|
||||
|
||||
// check response
|
||||
if wr.msg == nil {
|
||||
_ = w.WriteMsg(res)
|
||||
return
|
||||
}
|
||||
|
||||
// add resolved answers
|
||||
res.Answer = append(res.Answer, resolve(handler, wr.msg.Answer)...)
|
||||
|
||||
// write response
|
||||
err := w.WriteMsg(res)
|
||||
if err != nil {
|
||||
_ = w.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func resolve(handler dns.Handler, records []dns.RR) []dns.RR {
|
||||
// prepare result
|
||||
var res []dns.RR
|
||||
res = append(res, records...)
|
||||
|
||||
// handle records
|
||||
for _, record := range records {
|
||||
if cname, ok := record.(*dns.CNAME); ok {
|
||||
// query handler
|
||||
var wr responseWriter
|
||||
handler.ServeDNS(&wr, &dns.Msg{
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: cname.Target,
|
||||
Qtype: dns.TypeA,
|
||||
Qclass: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// add resolved answers
|
||||
res = append(res, resolve(handler, wr.msg.Answer)...)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package newdns
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestResolver(t *testing.T) {
|
||||
ret, err := Query("tcp", "1.1.1.1:53", "example.newdns.256dpi.com.", "A", func(msg *dns.Msg) {
|
||||
msg.RecursionDesired = true
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
equalJSON(t, &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Response: true,
|
||||
RecursionDesired: true,
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{Name: "example.newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.CNAME{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "example.newdns.256dpi.com.",
|
||||
Rrtype: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: ret.Answer[0].(*dns.CNAME).Hdr.Ttl,
|
||||
Rdlength: 10,
|
||||
},
|
||||
Target: "example.com.",
|
||||
},
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "example.com.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: ret.Answer[1].(*dns.A).Hdr.Ttl,
|
||||
Rdlength: 4,
|
||||
},
|
||||
A: ret.Answer[1].(*dns.A).A,
|
||||
},
|
||||
},
|
||||
}, ret)
|
||||
|
||||
addr := "0.0.0.0:53002"
|
||||
mux := dns.NewServeMux()
|
||||
mux.Handle("newdns.256dpi.com", Proxy(awsNS[0]+":53", nil))
|
||||
mux.Handle("example.com", Proxy("a.iana-servers.net:53", nil))
|
||||
handler := Resolver(mux)
|
||||
|
||||
serve(handler, addr, func() {
|
||||
ret, err := Query("udp", addr, "example.newdns.256dpi.com.", "A", func(msg *dns.Msg) {
|
||||
msg.RecursionDesired = true
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
equalJSON(t, &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Response: true,
|
||||
RecursionDesired: true,
|
||||
RecursionAvailable: true,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{Name: "example.newdns.256dpi.com.", Qtype: dns.TypeA, Qclass: dns.ClassINET},
|
||||
},
|
||||
Answer: []dns.RR{
|
||||
&dns.CNAME{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "example.newdns.256dpi.com.",
|
||||
Rrtype: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: ret.Answer[0].(*dns.CNAME).Hdr.Ttl,
|
||||
Rdlength: 13,
|
||||
},
|
||||
Target: "example.com.",
|
||||
},
|
||||
&dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: "example.com.",
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: ret.Answer[1].(*dns.A).Hdr.Ttl,
|
||||
Rdlength: 4,
|
||||
},
|
||||
A: ret.Answer[1].(*dns.A).A,
|
||||
},
|
||||
},
|
||||
}, ret)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package newdns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// Accept will return a dns.MsgAcceptFunc that only accepts normal queries.
|
||||
func Accept(logger Logger) dns.MsgAcceptFunc {
|
||||
return func(dh dns.Header) dns.MsgAcceptAction {
|
||||
// check if request
|
||||
if dh.Bits&(1<<15) != 0 {
|
||||
log(logger, Ignored, nil, nil, fmt.Sprintf("not a request"))
|
||||
return dns.MsgIgnore
|
||||
}
|
||||
|
||||
// check opcode
|
||||
if int(dh.Bits>>11)&0xF != dns.OpcodeQuery {
|
||||
log(logger, Ignored, nil, nil, fmt.Sprintf("not a query"))
|
||||
return dns.MsgIgnore
|
||||
}
|
||||
|
||||
// check question count
|
||||
if dh.Qdcount != 1 {
|
||||
log(logger, Ignored, nil, nil, fmt.Sprintf("invalid question count: %d", dh.Qdcount))
|
||||
return dns.MsgIgnore
|
||||
}
|
||||
|
||||
return dns.MsgAccept
|
||||
}
|
||||
}
|
||||
|
||||
// Run will start a UDP and TCP listener to serve the specified handler with the
|
||||
// specified accept function until the provided close channel is closed. It will
|
||||
// return the first error of a listener.
|
||||
func Run(addr string, handler dns.Handler, accept dns.MsgAcceptFunc, close <-chan struct{}) error {
|
||||
// prepare servers
|
||||
udp := &dns.Server{Addr: addr, Net: "udp", Handler: handler, MsgAcceptFunc: accept}
|
||||
tcp := &dns.Server{Addr: addr, Net: "tcp", Handler: handler, MsgAcceptFunc: accept}
|
||||
|
||||
// prepare errors
|
||||
errs := make(chan error, 2)
|
||||
|
||||
// run udp server
|
||||
go func() {
|
||||
errs <- udp.ListenAndServe()
|
||||
}()
|
||||
|
||||
// run tcp server
|
||||
go func() {
|
||||
errs <- tcp.ListenAndServe()
|
||||
}()
|
||||
|
||||
// await first error
|
||||
var err error
|
||||
select {
|
||||
case err = <-errs:
|
||||
case <-close:
|
||||
}
|
||||
|
||||
// shutdown servers
|
||||
_ = udp.Shutdown()
|
||||
_ = tcp.Shutdown()
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
package newdns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Config provides configuration for a DNS server.
|
||||
type Config struct {
|
||||
// The buffer size used if EDNS is enabled by a client.
|
||||
//
|
||||
// Default: 1220.
|
||||
BufferSize int
|
||||
|
||||
// The list of zones handled by this server.
|
||||
//
|
||||
// Default: ["."].
|
||||
Zones []string
|
||||
|
||||
// Handler is the callback that returns a zone for the specified name.
|
||||
// The returned zone must not be altered going forward.
|
||||
Handler func(name string) (*Zone, error)
|
||||
|
||||
// The fallback DNS server to be used if the zones is not matched. Exact
|
||||
// zones must be provided above for this to work.
|
||||
Fallback string
|
||||
|
||||
// Reporter is the callback called with request errors.
|
||||
Logger Logger
|
||||
}
|
||||
|
||||
// Server is a DNS server.
|
||||
type Server struct {
|
||||
config Config
|
||||
close chan struct{}
|
||||
}
|
||||
|
||||
// NewServer creates and returns a new DNS server.
|
||||
func NewServer(config Config) *Server {
|
||||
// set default buffer size
|
||||
if config.BufferSize <= 0 {
|
||||
config.BufferSize = 1220
|
||||
}
|
||||
|
||||
// set default zone
|
||||
if len(config.Zones) == 0 {
|
||||
config.Zones = []string{"."}
|
||||
}
|
||||
|
||||
// check zones if fallback
|
||||
if config.Fallback != "" {
|
||||
for _, zone := range config.Zones {
|
||||
if zone == "." {
|
||||
panic(`fallback conflicts with the match all pattern "." (default)`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &Server{
|
||||
config: config,
|
||||
close: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Run will run a udp and tcp server on the specified address. It will return
|
||||
// on the first accept error and close all servers.
|
||||
func (s *Server) Run(addr string) error {
|
||||
// prepare mux
|
||||
mux := dns.NewServeMux()
|
||||
|
||||
// register handler
|
||||
for _, zone := range s.config.Zones {
|
||||
mux.Handle(zone, s)
|
||||
}
|
||||
|
||||
// add fallback if available
|
||||
if s.config.Fallback != "" {
|
||||
mux.Handle(".", Proxy(s.config.Fallback, s.config.Logger))
|
||||
}
|
||||
|
||||
// run server
|
||||
err := Run(addr, mux, Accept(s.config.Logger), s.close)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ServeDNS implements the dns.Handler interface.
|
||||
func (s *Server) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
|
||||
// get question
|
||||
question := req.Question[0]
|
||||
|
||||
// check class
|
||||
if question.Qclass != dns.ClassINET {
|
||||
log(s.config.Logger, Ignored, nil, nil, fmt.Sprintf("unsupported class: %s", dns.ClassToString[question.Qclass]))
|
||||
return
|
||||
}
|
||||
|
||||
// log request and finish
|
||||
log(s.config.Logger, Request, req, nil, "")
|
||||
defer log(s.config.Logger, Finish, nil, nil, "")
|
||||
|
||||
// prepare response
|
||||
res := new(dns.Msg)
|
||||
res.SetReply(req)
|
||||
|
||||
// always compress responses
|
||||
res.Compress = true
|
||||
|
||||
// set flag
|
||||
res.Authoritative = true
|
||||
|
||||
// check edns
|
||||
if req.IsEdns0() != nil {
|
||||
// use edns in reply
|
||||
res.SetEdns0(uint16(s.config.BufferSize), false)
|
||||
|
||||
// check version
|
||||
if req.IsEdns0().Version() != 0 {
|
||||
log(s.config.Logger, Refused, nil, nil, fmt.Sprintf("unsupported EDNS version: %d", req.IsEdns0().Version()))
|
||||
s.writeError(w, req, res, nil, dns.RcodeBadVers)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// check any type
|
||||
if question.Qtype == dns.TypeANY {
|
||||
log(s.config.Logger, Refused, nil, nil, "unsupported type: ANY")
|
||||
s.writeError(w, req, res, nil, dns.RcodeNotImplemented)
|
||||
return
|
||||
}
|
||||
|
||||
// get name
|
||||
name := NormalizeDomain(question.Name, true, false, false)
|
||||
|
||||
// get zone
|
||||
zone, err := s.config.Handler(name)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "server handler error")
|
||||
log(s.config.Logger, BackendError, nil, err, "")
|
||||
s.writeError(w, req, res, nil, dns.RcodeServerFailure)
|
||||
return
|
||||
}
|
||||
|
||||
// check zone
|
||||
if zone == nil {
|
||||
log(s.config.Logger, Refused, nil, nil, "no zone")
|
||||
res.Authoritative = false
|
||||
s.writeError(w, req, res, nil, dns.RcodeRefused)
|
||||
return
|
||||
}
|
||||
|
||||
// validate zone
|
||||
err = zone.Validate()
|
||||
if err != nil {
|
||||
log(s.config.Logger, BackendError, nil, err, "")
|
||||
s.writeError(w, req, res, nil, dns.RcodeServerFailure)
|
||||
return
|
||||
}
|
||||
|
||||
// answer SOA directly
|
||||
if question.Qtype == dns.TypeSOA && name == zone.Name {
|
||||
s.writeSOAResponse(w, req, res, zone)
|
||||
return
|
||||
}
|
||||
|
||||
// answer NS directly
|
||||
if question.Qtype == dns.TypeNS && name == zone.Name {
|
||||
s.writeNSResponse(w, req, res, zone)
|
||||
return
|
||||
}
|
||||
|
||||
// check type
|
||||
typ := Type(question.Qtype)
|
||||
|
||||
// return error if type is not supported
|
||||
if !typ.valid() {
|
||||
log(s.config.Logger, Refused, nil, nil, fmt.Sprintf("unsupported type: %s", dns.TypeToString[question.Qtype]))
|
||||
s.writeError(w, req, res, zone, dns.RcodeNameError)
|
||||
return
|
||||
}
|
||||
|
||||
// lookup main answer
|
||||
answer, exists, err := zone.Lookup(name, w.RemoteAddr().String(), typ)
|
||||
if err != nil {
|
||||
log(s.config.Logger, BackendError, nil, err, "")
|
||||
s.writeError(w, req, res, nil, dns.RcodeServerFailure)
|
||||
return
|
||||
}
|
||||
|
||||
// check result
|
||||
if len(answer) == 0 {
|
||||
// write SOA with success code to indicate existence of other sets
|
||||
if exists {
|
||||
s.writeError(w, req, res, zone, dns.RcodeSuccess)
|
||||
return
|
||||
}
|
||||
|
||||
// otherwise return name error
|
||||
s.writeError(w, req, res, zone, dns.RcodeNameError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// prepare extra set
|
||||
var extra []Set
|
||||
|
||||
// TODO: Lookup glue records for NS records?
|
||||
|
||||
// lookup extra sets
|
||||
for _, set := range answer {
|
||||
for _, record := range set.Records {
|
||||
switch set.Type {
|
||||
case MX:
|
||||
// lookup internal MX target A and AAAA records
|
||||
if InZone(zone.Name, record.Address) {
|
||||
ret, _, err := zone.Lookup(record.Address, w.RemoteAddr().String(), A, AAAA)
|
||||
if err != nil {
|
||||
log(s.config.Logger, BackendError, nil, err, "")
|
||||
s.writeError(w, req, res, nil, dns.RcodeServerFailure)
|
||||
return
|
||||
}
|
||||
|
||||
// add to extra
|
||||
extra = append(extra, ret...)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set answer
|
||||
for _, set := range answer {
|
||||
res.Answer = append(res.Answer, s.convert(question.Name, zone, set)...)
|
||||
}
|
||||
|
||||
// set extra
|
||||
for _, set := range extra {
|
||||
res.Extra = append(res.Extra, s.convert(question.Name, zone, set)...)
|
||||
}
|
||||
|
||||
// add ns records
|
||||
for _, ns := range zone.AllNameServers {
|
||||
res.Ns = append(res.Ns, &dns.NS{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: TransferCase(question.Name, zone.Name),
|
||||
Rrtype: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: toSeconds(zone.NSTTL),
|
||||
},
|
||||
Ns: ns,
|
||||
})
|
||||
}
|
||||
|
||||
// check if NS query
|
||||
if typ == NS {
|
||||
// move answers
|
||||
res.Ns = res.Answer
|
||||
res.Answer = nil
|
||||
|
||||
// no authoritative response for other zone in NS queries
|
||||
res.Authoritative = false
|
||||
}
|
||||
|
||||
// write message
|
||||
s.writeMessage(w, req, res)
|
||||
}
|
||||
|
||||
// Close will close the server.
|
||||
func (s *Server) Close() {
|
||||
defer func() { recover() }()
|
||||
close(s.close)
|
||||
}
|
||||
|
||||
func (s *Server) writeSOAResponse(w dns.ResponseWriter, rq, rs *dns.Msg, zone *Zone) {
|
||||
// add soa record
|
||||
rs.Answer = append(rs.Answer, &dns.SOA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: zone.Name,
|
||||
Rrtype: dns.TypeSOA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: toSeconds(zone.SOATTL),
|
||||
},
|
||||
Ns: zone.MasterNameServer,
|
||||
Mbox: emailToDomain(zone.AdminEmail),
|
||||
Serial: 1,
|
||||
Refresh: toSeconds(zone.Refresh),
|
||||
Retry: toSeconds(zone.Retry),
|
||||
Expire: toSeconds(zone.Expire),
|
||||
Minttl: toSeconds(zone.MinTTL),
|
||||
})
|
||||
|
||||
// add ns records
|
||||
for _, ns := range zone.AllNameServers {
|
||||
rs.Ns = append(rs.Ns, &dns.NS{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: zone.Name,
|
||||
Rrtype: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: toSeconds(zone.NSTTL),
|
||||
},
|
||||
Ns: ns,
|
||||
})
|
||||
}
|
||||
|
||||
// write message
|
||||
s.writeMessage(w, rq, rs)
|
||||
}
|
||||
|
||||
func (s *Server) writeNSResponse(w dns.ResponseWriter, rq, rs *dns.Msg, zone *Zone) {
|
||||
// add ns records
|
||||
for _, ns := range zone.AllNameServers {
|
||||
rs.Answer = append(rs.Answer, &dns.NS{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: zone.Name,
|
||||
Rrtype: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: toSeconds(zone.NSTTL),
|
||||
},
|
||||
Ns: ns,
|
||||
})
|
||||
}
|
||||
|
||||
// write message
|
||||
s.writeMessage(w, rq, rs)
|
||||
}
|
||||
|
||||
func (s *Server) writeError(w dns.ResponseWriter, rq, rs *dns.Msg, zone *Zone, code int) {
|
||||
// set code
|
||||
rs.Rcode = code
|
||||
|
||||
// add soa record
|
||||
if zone != nil {
|
||||
rs.Ns = append(rs.Ns, &dns.SOA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: zone.Name,
|
||||
Rrtype: dns.TypeSOA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: toSeconds(zone.SOATTL),
|
||||
},
|
||||
Ns: zone.MasterNameServer,
|
||||
Mbox: emailToDomain(zone.AdminEmail),
|
||||
Serial: 1,
|
||||
Refresh: toSeconds(zone.Refresh),
|
||||
Retry: toSeconds(zone.Retry),
|
||||
Expire: toSeconds(zone.Expire),
|
||||
Minttl: toSeconds(zone.MinTTL),
|
||||
})
|
||||
}
|
||||
|
||||
// write message
|
||||
s.writeMessage(w, rq, rs)
|
||||
}
|
||||
|
||||
func (s *Server) writeMessage(w dns.ResponseWriter, rq, rs *dns.Msg) {
|
||||
// get buffer size
|
||||
var buffer = 512
|
||||
if rq.IsEdns0() != nil {
|
||||
buffer = int(rq.IsEdns0().UDPSize())
|
||||
}
|
||||
|
||||
// determine if client is using UDP
|
||||
isUDP := w.RemoteAddr().Network() == "udp"
|
||||
|
||||
// truncate message if client is using UDP and message is too long
|
||||
if isUDP && rs.Len() > buffer {
|
||||
rs.Truncated = true
|
||||
rs.Answer = nil
|
||||
rs.Ns = nil
|
||||
rs.Extra = nil
|
||||
}
|
||||
|
||||
// write message
|
||||
err := w.WriteMsg(rs)
|
||||
if err != nil {
|
||||
log(s.config.Logger, NetworkError, nil, err, "")
|
||||
_ = w.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// log response
|
||||
log(s.config.Logger, Response, rs, nil, "")
|
||||
}
|
||||
|
||||
func (s *Server) convert(query string, zone *Zone, set Set) []dns.RR {
|
||||
// prepare header
|
||||
header := dns.RR_Header{
|
||||
Name: TransferCase(query, set.Name),
|
||||
Rrtype: uint16(set.Type),
|
||||
Class: dns.ClassINET,
|
||||
Ttl: toSeconds(set.TTL),
|
||||
}
|
||||
|
||||
// ensure zone min TTL
|
||||
if set.TTL < zone.MinTTL {
|
||||
header.Ttl = toSeconds(zone.MinTTL)
|
||||
}
|
||||
|
||||
// prepare list
|
||||
var list []dns.RR
|
||||
|
||||
// add records
|
||||
for _, record := range set.Records {
|
||||
// construct record
|
||||
switch set.Type {
|
||||
case A:
|
||||
list = append(list, &dns.A{
|
||||
Hdr: header,
|
||||
A: net.ParseIP(record.Address),
|
||||
})
|
||||
case AAAA:
|
||||
list = append(list, &dns.AAAA{
|
||||
Hdr: header,
|
||||
AAAA: net.ParseIP(record.Address),
|
||||
})
|
||||
case CNAME:
|
||||
list = append(list, &dns.CNAME{
|
||||
Hdr: header,
|
||||
Target: dns.Fqdn(record.Address),
|
||||
})
|
||||
case MX:
|
||||
list = append(list, &dns.MX{
|
||||
Hdr: header,
|
||||
Preference: uint16(record.Priority),
|
||||
Mx: dns.Fqdn(record.Address),
|
||||
})
|
||||
case TXT:
|
||||
list = append(list, &dns.TXT{
|
||||
Hdr: header,
|
||||
Txt: record.Data,
|
||||
})
|
||||
case NS:
|
||||
list = append(list, &dns.NS{
|
||||
Hdr: header,
|
||||
Ns: dns.Fqdn(record.Address),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
package newdns
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Set is a set of records.
|
||||
type Set struct {
|
||||
// The FQDN of the set.
|
||||
Name string
|
||||
|
||||
// The type of the record.
|
||||
Type Type
|
||||
|
||||
// The records in the set.
|
||||
Records []Record
|
||||
|
||||
// The TTL of the set.
|
||||
//
|
||||
// Default: 5m.
|
||||
TTL time.Duration
|
||||
}
|
||||
|
||||
// Validate will validate the set and ensure defaults.
|
||||
func (s *Set) Validate() error {
|
||||
// check name
|
||||
if !IsDomain(s.Name, true) {
|
||||
return errors.Errorf("invalid name: %s", s.Name)
|
||||
}
|
||||
|
||||
// check type
|
||||
if !s.Type.valid() {
|
||||
return errors.Errorf("invalid type: %d", s.Type)
|
||||
}
|
||||
|
||||
// check records
|
||||
if len(s.Records) == 0 {
|
||||
return errors.Errorf("missing records")
|
||||
}
|
||||
|
||||
// check CNAME records
|
||||
if s.Type == CNAME && len(s.Records) > 1 {
|
||||
return errors.Errorf("multiple CNAME records")
|
||||
}
|
||||
|
||||
// validate records
|
||||
for _, record := range s.Records {
|
||||
err := record.Validate(s.Type)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid record")
|
||||
}
|
||||
}
|
||||
|
||||
// check for duplicate addresses if not TXT
|
||||
if len(s.Records) > 1 && s.Type != TXT {
|
||||
for i := 0; i < len(s.Records)-1; i++ {
|
||||
if s.Records[i].Address == s.Records[i+1].Address {
|
||||
return errors.Errorf("duplicate address: %s", s.Records[i].Address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set default ttl
|
||||
if s.TTL == 0 {
|
||||
s.TTL = 5 * time.Minute
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package newdns
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSetValidate(t *testing.T) {
|
||||
table := []struct {
|
||||
set Set
|
||||
err string
|
||||
}{
|
||||
{
|
||||
set: Set{
|
||||
Name: "foo",
|
||||
},
|
||||
err: "invalid name: foo",
|
||||
},
|
||||
{
|
||||
set: Set{
|
||||
Name: "example.com.",
|
||||
},
|
||||
err: "invalid type: 0",
|
||||
},
|
||||
{
|
||||
set: Set{
|
||||
Name: "example.com.",
|
||||
Type: A,
|
||||
},
|
||||
err: "missing records",
|
||||
},
|
||||
{
|
||||
set: Set{
|
||||
Name: "example.com.",
|
||||
Type: A,
|
||||
Records: []Record{
|
||||
{Address: "foo"},
|
||||
},
|
||||
},
|
||||
err: "invalid record: invalid IPv4 address: foo",
|
||||
},
|
||||
{
|
||||
set: Set{
|
||||
Name: "example.com.",
|
||||
Type: TXT,
|
||||
Records: []Record{
|
||||
{},
|
||||
},
|
||||
},
|
||||
err: "invalid record: missing data",
|
||||
},
|
||||
{
|
||||
set: Set{
|
||||
Name: "example.com.",
|
||||
Type: CNAME,
|
||||
Records: []Record{
|
||||
{},
|
||||
{},
|
||||
},
|
||||
},
|
||||
err: "multiple CNAME records",
|
||||
},
|
||||
{
|
||||
set: Set{
|
||||
Name: "example.com.",
|
||||
Type: A,
|
||||
Records: []Record{
|
||||
{Address: "1.2.3.4"},
|
||||
{Address: "1.2.3.4"},
|
||||
},
|
||||
},
|
||||
err: "duplicate address: 1.2.3.4",
|
||||
},
|
||||
}
|
||||
|
||||
for i, item := range table {
|
||||
err := item.set.Validate()
|
||||
if err != nil {
|
||||
assert.Equal(t, item.err, err.Error(), i)
|
||||
} else {
|
||||
assert.Equal(t, item.err, "", item)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package newdns
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// IsDomain returns whether the name is a valid domain and if requested also
|
||||
// fully qualified.
|
||||
func IsDomain(name string, fqdn bool) bool {
|
||||
_, ok := dns.IsDomainName(name)
|
||||
return ok && (!fqdn || fqdn && dns.IsFqdn(name))
|
||||
}
|
||||
|
||||
// InZone returns whether the provided name is part of the provided zone. Will
|
||||
// always return false if the provided domains are not valid.
|
||||
func InZone(zone, name string) bool {
|
||||
// check domains
|
||||
if !IsDomain(zone, false) || !IsDomain(name, false) {
|
||||
return false
|
||||
}
|
||||
|
||||
return dns.IsSubDomain(zone, name)
|
||||
}
|
||||
|
||||
// TrimZone will remove the zone from the specified name.
|
||||
func TrimZone(zone, name string) string {
|
||||
// return immediately if not in zone
|
||||
if !InZone(zone, name) {
|
||||
return name
|
||||
}
|
||||
|
||||
// count zone labels
|
||||
count := dns.CountLabel(zone)
|
||||
|
||||
// get segments
|
||||
labels := dns.SplitDomainName(name)
|
||||
|
||||
// get new labels
|
||||
newLabels := labels[0 : len(labels)-count]
|
||||
|
||||
// join name
|
||||
newName := strings.Join(newLabels, ".")
|
||||
|
||||
return newName
|
||||
}
|
||||
|
||||
// NormalizeDomain will normalize the provided domain name by removing space
|
||||
// around the name and lowercase it if request.
|
||||
func NormalizeDomain(name string, lower, makeFQDN, removeFQDN bool) string {
|
||||
// remove spaces
|
||||
name = strings.TrimSpace(name)
|
||||
|
||||
// lowercase if requested
|
||||
if lower {
|
||||
name = strings.ToLower(name)
|
||||
}
|
||||
|
||||
// make FQDN if requested
|
||||
if makeFQDN {
|
||||
name = dns.Fqdn(name)
|
||||
}
|
||||
|
||||
// remove FQDN if requested
|
||||
if removeFQDN && dns.IsFqdn(name) {
|
||||
name = name[:len(name)-1]
|
||||
}
|
||||
|
||||
return name
|
||||
}
|
||||
|
||||
// SplitDomain will split the provided domain either in separate labels or
|
||||
// hierarchical labels. The later allows walking a domain up to the root.
|
||||
func SplitDomain(name string, hierarchical bool) []string {
|
||||
// normalize name
|
||||
name = NormalizeDomain(name, false, false, true)
|
||||
|
||||
// return nil if empty
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// split in labels
|
||||
if !hierarchical {
|
||||
return dns.SplitDomainName(name)
|
||||
}
|
||||
|
||||
// prepare list
|
||||
var list []string
|
||||
|
||||
// walk domain
|
||||
for off, end := 0, false; !end; off, end = dns.NextLabel(name, off) {
|
||||
list = append(list, name[off:])
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
// TransferCase will transfer the case from the source name to the destination.
|
||||
// For the source "foo.AAA.com." and destination "aaa.com" the function will
|
||||
// return "AAA.com". The source must be either a child or the same as the
|
||||
// destination.
|
||||
func TransferCase(source, destination string) string {
|
||||
// get lower variants
|
||||
lowSource := strings.ToLower(source)
|
||||
lowDestination := strings.ToLower(destination)
|
||||
|
||||
// get index of destination in source
|
||||
index := strings.Index(lowSource, lowDestination)
|
||||
if index < 0 {
|
||||
return destination
|
||||
}
|
||||
|
||||
// take shared part from source
|
||||
return source[index:]
|
||||
}
|
||||
|
||||
func emailToDomain(email string) string {
|
||||
// split on at
|
||||
parts := strings.Split(email, "@")
|
||||
|
||||
// replace dots in username
|
||||
parts[0] = strings.ReplaceAll(parts[0], ".", "\\.")
|
||||
|
||||
// join domain
|
||||
name := parts[0] + "." + parts[1]
|
||||
|
||||
return dns.Fqdn(name)
|
||||
}
|
||||
|
||||
func toSeconds(d time.Duration) uint32 {
|
||||
return uint32(math.Ceil(d.Seconds()))
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package newdns
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsDomain(t *testing.T) {
|
||||
assert.True(t, IsDomain("example.com", false))
|
||||
assert.False(t, IsDomain("example.com", true))
|
||||
assert.True(t, IsDomain("example.com.", true))
|
||||
assert.True(t, IsDomain(" example.com.", true))
|
||||
assert.False(t, IsDomain("", false))
|
||||
assert.True(t, IsDomain("x", false))
|
||||
assert.True(t, IsDomain(".", false))
|
||||
}
|
||||
|
||||
func TestInZone(t *testing.T) {
|
||||
assert.True(t, InZone("example.com.", "foo.example.com."))
|
||||
assert.True(t, InZone("example.com", "foo.example.com"))
|
||||
assert.True(t, InZone("example.com", "example.com"))
|
||||
assert.True(t, InZone(".", "com"))
|
||||
assert.True(t, InZone(".", "."))
|
||||
assert.False(t, InZone("", "."))
|
||||
assert.False(t, InZone("", ""))
|
||||
assert.False(t, InZone("foo.example.com", "example.com"))
|
||||
}
|
||||
|
||||
func TestTrimZone(t *testing.T) {
|
||||
assert.Equal(t, "foo", TrimZone("example.com.", "foo.example.com."))
|
||||
assert.Equal(t, "foo", TrimZone("example.com", "foo.example.com"))
|
||||
assert.Equal(t, "", TrimZone("example.com", "example.com"))
|
||||
assert.Equal(t, "example.com", TrimZone("foo.example.com", "example.com"))
|
||||
}
|
||||
|
||||
func TestNormalizeDomain(t *testing.T) {
|
||||
assert.Equal(t, "", NormalizeDomain("", false, false, false))
|
||||
assert.Equal(t, ".", NormalizeDomain("", false, true, false))
|
||||
assert.Equal(t, "foo", NormalizeDomain(" foo", false, false, false))
|
||||
assert.Equal(t, "foo", NormalizeDomain("foo ", false, false, false))
|
||||
assert.Equal(t, "foo", NormalizeDomain(" fOO ", true, false, false))
|
||||
assert.Equal(t, "foo.", NormalizeDomain(" fOO ", true, true, false))
|
||||
assert.Equal(t, "foo", NormalizeDomain(" fOO. ", true, false, true))
|
||||
}
|
||||
|
||||
func TestSplitDomain(t *testing.T) {
|
||||
assert.Equal(t, []string(nil), SplitDomain("", false))
|
||||
assert.Equal(t, []string(nil), SplitDomain(".", false))
|
||||
assert.Equal(t, []string{"foo"}, SplitDomain("foo", false))
|
||||
assert.Equal(t, []string{"foo", "bar"}, SplitDomain("foo.bar", false))
|
||||
|
||||
assert.Equal(t, []string(nil), SplitDomain("", true))
|
||||
assert.Equal(t, []string(nil), SplitDomain(".", true))
|
||||
assert.Equal(t, []string{"foo"}, SplitDomain("foo", true))
|
||||
assert.Equal(t, []string{"foo.bar", "bar"}, SplitDomain("foo.bar", true))
|
||||
}
|
||||
|
||||
func TestTransferCase(t *testing.T) {
|
||||
table := []struct {
|
||||
src string
|
||||
dst string
|
||||
out string
|
||||
}{
|
||||
{
|
||||
src: "example.com",
|
||||
dst: "example.com",
|
||||
out: "example.com",
|
||||
},
|
||||
{
|
||||
src: "EXAmple.com",
|
||||
dst: "example.com",
|
||||
out: "EXAmple.com",
|
||||
},
|
||||
{
|
||||
src: "FOO.com",
|
||||
dst: "bar.com",
|
||||
out: "bar.com",
|
||||
},
|
||||
{
|
||||
src: "foo.EXAmple.com",
|
||||
dst: "example.com",
|
||||
out: "EXAmple.com",
|
||||
},
|
||||
{
|
||||
src: "foo.EXAmple.com",
|
||||
dst: "bar.example.com",
|
||||
out: "bar.example.com",
|
||||
},
|
||||
}
|
||||
|
||||
for i, item := range table {
|
||||
assert.Equal(t, item.out, TransferCase(item.src, item.dst), i)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package newdns
|
||||
|
||||
import "github.com/miekg/dns"
|
||||
|
||||
// Type denotes the DNS record type.
|
||||
type Type uint16
|
||||
|
||||
const (
|
||||
// A records return IPV4 addresses.
|
||||
A = Type(dns.TypeA)
|
||||
|
||||
// AAAA records return IPV6 addresses.
|
||||
AAAA = Type(dns.TypeAAAA)
|
||||
|
||||
// CNAME records return other DNS names.
|
||||
CNAME = Type(dns.TypeCNAME)
|
||||
|
||||
// MX records return mails servers with their priorities. The target mail
|
||||
// servers must itself be returned with an A or AAAA record.
|
||||
MX = Type(dns.TypeMX)
|
||||
|
||||
// TXT records return arbitrary text data.
|
||||
TXT = Type(dns.TypeTXT)
|
||||
|
||||
// NS records delegate names to other name servers.
|
||||
NS = Type(dns.TypeNS)
|
||||
|
||||
// REBINDING records return different ip when the same client requests twice.
|
||||
REBINDING = Type(99)
|
||||
)
|
||||
|
||||
func (t Type) valid() bool {
|
||||
switch t {
|
||||
case A, AAAA, CNAME, MX, TXT, NS, REBINDING:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func typeInList(list []Type, needle Type) bool {
|
||||
for _, t := range list {
|
||||
if t == needle {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package newdns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func run(s *Server, addr string, fn func()) {
|
||||
defer s.Close()
|
||||
|
||||
go func() {
|
||||
err := s.Run(addr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
fn()
|
||||
}
|
||||
|
||||
func serve(handler dns.Handler, addr string, fn func()) {
|
||||
closer := make(chan struct{})
|
||||
defer close(closer)
|
||||
|
||||
go func() {
|
||||
err := Run(addr, handler, Accept(nil), closer)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
fn()
|
||||
}
|
||||
|
||||
func equalJSON(t *testing.T, a, b interface{}) {
|
||||
buf := new(bytes.Buffer)
|
||||
|
||||
e := json.NewEncoder(buf)
|
||||
e.SetIndent("", " ")
|
||||
|
||||
_ = e.Encode(a)
|
||||
aa := buf.String()
|
||||
|
||||
buf.Reset()
|
||||
_ = e.Encode(b)
|
||||
bb := buf.String()
|
||||
|
||||
assert.JSONEq(t, aa, bb)
|
||||
}
|
||||
|
||||
func order(rrs []dns.RR) []dns.RR {
|
||||
cpy := make([]dns.RR, len(rrs))
|
||||
copy(cpy, rrs)
|
||||
|
||||
sort.Slice(cpy, func(i, j int) bool {
|
||||
var ai string
|
||||
switch rr := cpy[i].(type) {
|
||||
case *dns.NS:
|
||||
ai = rr.Ns
|
||||
rr.Hdr.Rdlength = 0
|
||||
}
|
||||
|
||||
var aj string
|
||||
switch rr := cpy[j].(type) {
|
||||
case *dns.NS:
|
||||
aj = rr.Ns
|
||||
rr.Hdr.Rdlength = 0
|
||||
}
|
||||
|
||||
return ai < aj
|
||||
})
|
||||
|
||||
return cpy
|
||||
}
|
||||
|
||||
func isIOError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), "i/o timeout") {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.Contains(err.Error(), "connection reset by peer") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package newdns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Zone describes a single authoritative DNS zone.
|
||||
type Zone struct {
|
||||
// The FQDN of the zone e.g. "example.com.".
|
||||
Name string
|
||||
|
||||
// The FQDN of the master mame server responsible for this zone. The FQDN
|
||||
// must be returned as A and AAAA record by the parent zone.
|
||||
MasterNameServer string
|
||||
|
||||
// A list of FQDNs to all authoritative name servers for this zone. The
|
||||
// FQDNs must be returned as A and AAAA records by the parent zone. It is
|
||||
// required to announce at least two distinct name servers per zone.
|
||||
AllNameServers []string
|
||||
|
||||
// The email address of the administrator e.g. "[email protected]".
|
||||
//
|
||||
// Default: "hostmaster@NAME".
|
||||
AdminEmail string
|
||||
|
||||
// The refresh interval.
|
||||
//
|
||||
// Default: 6h.
|
||||
Refresh time.Duration
|
||||
|
||||
// The retry interval for the zone.
|
||||
//
|
||||
// Default: 1h.
|
||||
Retry time.Duration
|
||||
|
||||
// The expiration interval of the zone.
|
||||
//
|
||||
// Default: 72h.
|
||||
Expire time.Duration
|
||||
|
||||
// The TTL for the SOA record.
|
||||
//
|
||||
// Default: 15m.
|
||||
SOATTL time.Duration
|
||||
|
||||
// The TTL for NS records.
|
||||
//
|
||||
// Default: 48h.
|
||||
NSTTL time.Duration
|
||||
|
||||
// The minimum TTL for all records. Either this value, or the SOATTL if lower,
|
||||
// is used to determine the "negative caching TTL" which is the duration
|
||||
// caches are allowed to cache missing records (NXDOMAIN).
|
||||
//
|
||||
// Default: 5min.
|
||||
MinTTL time.Duration
|
||||
|
||||
// The handler that responds to requests for this zone. The returned sets
|
||||
// must not be altered going forward.
|
||||
Handler func(name, remoteAddr string) ([]Set, error)
|
||||
}
|
||||
|
||||
// Validate will validate the zone and ensure the documented defaults.
|
||||
func (z *Zone) Validate() error {
|
||||
// check name
|
||||
if !IsDomain(z.Name, true) {
|
||||
return errors.Errorf("name not fully qualified: %s", z.Name)
|
||||
}
|
||||
|
||||
// check master name server
|
||||
if !IsDomain(z.MasterNameServer, true) {
|
||||
return errors.Errorf("master server not full qualified: %s", z.MasterNameServer)
|
||||
}
|
||||
|
||||
// check name server count
|
||||
if len(z.AllNameServers) < 1 {
|
||||
return errors.Errorf("missing name servers")
|
||||
}
|
||||
|
||||
// check name servers
|
||||
var includesMaster bool
|
||||
for _, ns := range z.AllNameServers {
|
||||
if !IsDomain(ns, true) {
|
||||
return errors.Errorf("name server not fully qualified: %s", ns)
|
||||
}
|
||||
|
||||
if ns == z.MasterNameServer {
|
||||
includesMaster = true
|
||||
}
|
||||
}
|
||||
|
||||
// check master inclusion
|
||||
if !includesMaster {
|
||||
return errors.Errorf("master name server not listed as name server: %s", z.MasterNameServer)
|
||||
}
|
||||
|
||||
// set default admin email
|
||||
if z.AdminEmail == "" {
|
||||
z.AdminEmail = fmt.Sprintf("hostmaster@%s", z.Name)
|
||||
}
|
||||
|
||||
// check admin email
|
||||
if !IsDomain(emailToDomain(z.AdminEmail), true) {
|
||||
return errors.Errorf("admin email cannot be converted to a domain name: %s", z.AdminEmail)
|
||||
}
|
||||
|
||||
// set default refresh
|
||||
if z.Refresh == 0 {
|
||||
z.Refresh = 6 * time.Hour
|
||||
}
|
||||
|
||||
// set default retry
|
||||
if z.Retry == 0 {
|
||||
z.Retry = time.Hour
|
||||
}
|
||||
|
||||
// set default expire
|
||||
if z.Expire == 0 {
|
||||
z.Expire = 72 * time.Hour
|
||||
}
|
||||
|
||||
// set default SOA TTL
|
||||
if z.SOATTL == 0 {
|
||||
z.SOATTL = 15 * time.Minute
|
||||
}
|
||||
|
||||
// set default NS TTL
|
||||
if z.NSTTL == 0 {
|
||||
z.NSTTL = 48 * time.Hour
|
||||
}
|
||||
|
||||
// set default min TTL
|
||||
if z.MinTTL == 0 {
|
||||
z.MinTTL = 5 * time.Minute
|
||||
}
|
||||
|
||||
// check retry
|
||||
if z.Retry >= z.Refresh {
|
||||
return errors.Errorf("retry must be less than refresh: %d", z.Retry)
|
||||
}
|
||||
|
||||
// check expire
|
||||
if z.Expire < z.Refresh+z.Retry {
|
||||
return errors.Errorf("expire must be bigger than the sum of refresh and retry: %d", z.Expire)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Lookup will lookup the specified name in the zone and return results for the
|
||||
// specified record types. If no results are returned, the second return value
|
||||
// indicates if there are other results for the specified name.
|
||||
func (z *Zone) Lookup(name, remoteAddr string, needle ...Type) ([]Set, bool, error) {
|
||||
// check name
|
||||
if !IsDomain(name, true) {
|
||||
return nil, false, errors.Errorf("invalid name: %s", name)
|
||||
}
|
||||
|
||||
// normalize name
|
||||
name = NormalizeDomain(name, true, false, false)
|
||||
|
||||
// check name
|
||||
if !InZone(z.Name, name) {
|
||||
return nil, false, errors.Errorf("name does not belong to zone: %s", name)
|
||||
}
|
||||
|
||||
// prepare result
|
||||
var result []Set
|
||||
|
||||
for i := 0; ; i++ {
|
||||
// get sets
|
||||
sets, err := z.Handler(TrimZone(z.Name, name),remoteAddr)
|
||||
if err != nil {
|
||||
return nil, false, errors.Wrap(err, "zone handler error")
|
||||
}
|
||||
|
||||
// return immediately if initial set is empty
|
||||
if i == 0 && len(sets) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// prepare counters
|
||||
counters := map[Type]int{
|
||||
A: 0,
|
||||
AAAA: 0,
|
||||
CNAME: 0,
|
||||
MX: 0,
|
||||
TXT: 0,
|
||||
}
|
||||
|
||||
// validate sets
|
||||
for _, set := range sets {
|
||||
// validate set
|
||||
err = set.Validate()
|
||||
if err != nil {
|
||||
return nil, false, errors.Wrap(err, "invalid set")
|
||||
}
|
||||
|
||||
// check relationship
|
||||
if !InZone(z.Name, set.Name) {
|
||||
return nil, false, errors.Errorf("set does not belong to zone: %s", set.Name)
|
||||
}
|
||||
|
||||
// increment counter
|
||||
counters[set.Type]++
|
||||
}
|
||||
|
||||
// check counters
|
||||
for _, counter := range counters {
|
||||
if counter > 1 {
|
||||
return nil, false, errors.New("multiple sets for same type")
|
||||
}
|
||||
}
|
||||
|
||||
// check apex CNAME
|
||||
if counters[CNAME] > 0 && name == z.Name {
|
||||
return nil, false, errors.Errorf("invalid CNAME set at apex: %s", name)
|
||||
}
|
||||
|
||||
// check CNAME is stand-alone
|
||||
if counters[CNAME] > 0 && (len(sets) > 1) {
|
||||
return nil, false, errors.Errorf("other sets with CNAME set: %s", name)
|
||||
}
|
||||
|
||||
// check if CNAME and query is not CNAME
|
||||
if counters[CNAME] > 0 && !typeInList(needle, CNAME) {
|
||||
// add set to result
|
||||
result = append(result, sets[0])
|
||||
|
||||
// get normalized address
|
||||
address := NormalizeDomain(sets[0].Records[0].Address, true, false, false)
|
||||
|
||||
// continue lookup with CNAME address if address is in zone
|
||||
if InZone(z.Name, address) {
|
||||
name = address
|
||||
continue
|
||||
}
|
||||
|
||||
return result, false, nil
|
||||
}
|
||||
|
||||
// add matching set
|
||||
for _, set := range sets {
|
||||
if typeInList(needle, set.Type) {
|
||||
// add set to result
|
||||
result = append(result, set)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// return if there are not matches, but indicate that there are sets
|
||||
// available for other types
|
||||
if len(result) == 0 {
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
return result, false, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package newdns
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestZoneValidate(t *testing.T) {
|
||||
table := []struct {
|
||||
zne Zone
|
||||
err string
|
||||
}{
|
||||
{
|
||||
zne: Zone{
|
||||
Name: "foo",
|
||||
},
|
||||
err: "name not fully qualified: foo",
|
||||
},
|
||||
{
|
||||
zne: Zone{
|
||||
Name: "example.com.",
|
||||
MasterNameServer: "foo",
|
||||
},
|
||||
err: "master server not full qualified: foo",
|
||||
},
|
||||
{
|
||||
zne: Zone{
|
||||
Name: "example.com.",
|
||||
MasterNameServer: "n1.example.com.",
|
||||
},
|
||||
err: "missing name servers",
|
||||
},
|
||||
{
|
||||
zne: Zone{
|
||||
Name: "example.com.",
|
||||
MasterNameServer: "n1.example.com.",
|
||||
AllNameServers: []string{
|
||||
"foo",
|
||||
},
|
||||
},
|
||||
err: "name server not fully qualified: foo",
|
||||
},
|
||||
{
|
||||
zne: Zone{
|
||||
Name: "example.com.",
|
||||
MasterNameServer: "n2.example.com.",
|
||||
AllNameServers: []string{
|
||||
"n1.example.com.",
|
||||
},
|
||||
},
|
||||
err: "master name server not listed as name server: n2.example.com.",
|
||||
},
|
||||
{
|
||||
zne: Zone{
|
||||
Name: "example.com.",
|
||||
MasterNameServer: "n1.example.com.",
|
||||
AllNameServers: []string{
|
||||
"n1.example.com.",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
zne: Zone{
|
||||
Name: "example.com.",
|
||||
MasterNameServer: "n1.example.com.",
|
||||
AllNameServers: []string{
|
||||
"n1.example.com.",
|
||||
},
|
||||
AdminEmail: "[email protected]",
|
||||
},
|
||||
err: "admin email cannot be converted to a domain name: [email protected]",
|
||||
},
|
||||
{
|
||||
zne: Zone{
|
||||
Name: "example.com.",
|
||||
MasterNameServer: "n1.example.com.",
|
||||
AllNameServers: []string{
|
||||
"n1.example.com.",
|
||||
},
|
||||
Refresh: 1,
|
||||
Retry: 2,
|
||||
},
|
||||
err: "retry must be less than refresh: 2",
|
||||
},
|
||||
{
|
||||
zne: Zone{
|
||||
Name: "example.com.",
|
||||
MasterNameServer: "n1.example.com.",
|
||||
AllNameServers: []string{
|
||||
"n1.example.com.",
|
||||
},
|
||||
Expire: 1,
|
||||
Retry: 2,
|
||||
},
|
||||
err: "expire must be bigger than the sum of refresh and retry: 1",
|
||||
},
|
||||
}
|
||||
|
||||
for i, item := range table {
|
||||
err := item.zne.Validate()
|
||||
if err != nil {
|
||||
assert.EqualValues(t, item.err, err.Error(), i)
|
||||
} else {
|
||||
assert.Equal(t, item.err, "", item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestZoneLookup(t *testing.T) {
|
||||
zone := Zone{
|
||||
Name: "example.com.",
|
||||
MasterNameServer: "ns1.example.com.",
|
||||
AllNameServers: []string{
|
||||
"ns1.example.com.",
|
||||
"ns2.example.com.",
|
||||
},
|
||||
Handler: func(name,remoteAddr string) ([]Set, error) {
|
||||
if name == "error" {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
if name == "invalid1" {
|
||||
return []Set{
|
||||
{Name: "foo"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
if name == "invalid2" {
|
||||
return []Set{
|
||||
{Name: "foo.", Type: A, Records: []Record{{Address: "1.2.3.4"}}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
if name == "multiple" {
|
||||
return []Set{
|
||||
{Name: "foo.example.com.", Type: A, Records: []Record{{Address: "1.2.3.4"}}},
|
||||
{Name: "foo.example.com.", Type: A, Records: []Record{{Address: "1.2.3.4"}}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
if name == "" {
|
||||
return []Set{
|
||||
{Name: "example.com.", Type: CNAME, Records: []Record{{Address: "cool.com."}}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
if name == "cname" {
|
||||
return []Set{
|
||||
{Name: "cname.example.com.", Type: A, Records: []Record{{Address: "1.2.3.4"}}},
|
||||
{Name: "cname.example.com.", Type: CNAME, Records: []Record{{Address: "cool.com."}}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
err := zone.Validate()
|
||||
assert.NoError(t, err)
|
||||
|
||||
table := []struct {
|
||||
name string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "foo",
|
||||
err: "invalid name: foo",
|
||||
},
|
||||
{
|
||||
name: "foo.",
|
||||
err: "name does not belong to zone: foo.",
|
||||
},
|
||||
{
|
||||
name: "error.example.com.",
|
||||
err: "zone handler error: EOF",
|
||||
},
|
||||
{
|
||||
name: "invalid1.example.com.",
|
||||
err: "invalid set: invalid name: foo",
|
||||
},
|
||||
{
|
||||
name: "invalid2.example.com.",
|
||||
err: "set does not belong to zone: foo.",
|
||||
},
|
||||
{
|
||||
name: "multiple.example.com.",
|
||||
err: "multiple sets for same type",
|
||||
},
|
||||
{
|
||||
name: "example.com.",
|
||||
err: "invalid CNAME set at apex: example.com.",
|
||||
},
|
||||
{
|
||||
name: "cname.example.com.",
|
||||
err: "other sets with CNAME set: cname.example.com.",
|
||||
},
|
||||
}
|
||||
|
||||
for i, item := range table {
|
||||
res, exists, err := zone.Lookup(item.name, "127.0.0.1", A)
|
||||
assert.Equal(t, item.err, err.Error(), i)
|
||||
assert.False(t, exists, i)
|
||||
assert.Nil(t, res, i)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package notice
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/li4n0/revsuit/internal/record"
|
||||
log "unknwon.dev/clog/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
lock sync.RWMutex
|
||||
announcer *Announcer
|
||||
)
|
||||
|
||||
// Bot is used to send notice
|
||||
type Bot interface {
|
||||
notice(record.Record) error
|
||||
buildPayload(record.Record) string
|
||||
}
|
||||
|
||||
// Announcer is used for storage and schedule bots
|
||||
type Announcer struct {
|
||||
Bots []Bot
|
||||
}
|
||||
|
||||
// New initializes a new Announcer
|
||||
func New() *Announcer {
|
||||
announcer = &Announcer{Bots: make([]Bot, 0)}
|
||||
return announcer
|
||||
}
|
||||
|
||||
// AddBot add a new bot to Announcer
|
||||
func (a *Announcer) AddBot(b Bot) *Announcer {
|
||||
lock.RLock()
|
||||
a.Bots = append(a.Bots, b)
|
||||
lock.RUnlock()
|
||||
return a
|
||||
}
|
||||
|
||||
// Notice let bots to send notice
|
||||
func Notice(r record.Record) {
|
||||
for _, bot := range announcer.Bots {
|
||||
if err := bot.notice(r); err != nil {
|
||||
log.Error(err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package notice
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/li4n0/revsuit/internal/record"
|
||||
)
|
||||
|
||||
var _ Bot = (*DingTalk)(nil)
|
||||
|
||||
type DingTalk struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type dingAt struct {
|
||||
AtMobiles []string `json:"atMobiles"`
|
||||
IsAtAll bool `json:"isAtAll"`
|
||||
}
|
||||
|
||||
type dingMarkdown struct {
|
||||
Title string `json:"title"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type dingPayload struct {
|
||||
MsgType string `json:"msgtype"`
|
||||
Markdown dingMarkdown `json:"markdown"`
|
||||
At []dingAt `json:"at"`
|
||||
}
|
||||
|
||||
func (d *DingTalk) buildPayload(r record.Record) string {
|
||||
payload := dingPayload{
|
||||
MsgType: "markdown",
|
||||
Markdown: dingMarkdown{
|
||||
Title: "New Connection",
|
||||
Text: "**<font color=\"#e96900\" face=\"Fira Code\" size=\"3\">New Connection</font>**\n" +
|
||||
formatRecordField(r, "> **<font color=\"#e96900\" face=\"Fira Code\">%s: </font>**<font color=\"#e96900\" face=\"Fira Code\">%v</font>\n"),
|
||||
},
|
||||
At: []dingAt{
|
||||
{
|
||||
IsAtAll: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
p, err := json.Marshal(&payload)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(p)
|
||||
}
|
||||
|
||||
func (d *DingTalk) notice(r record.Record) error {
|
||||
resp, err := http.DefaultClient.Post(d.URL, "application/json", strings.NewReader(d.buildPayload(r)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("HTTP request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode/100 != 2 {
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read HTTP response body: %v", err)
|
||||
}
|
||||
return fmt.Errorf("non-success response status code %d with body: %s", resp.StatusCode, data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package notice
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/li4n0/revsuit/internal/record"
|
||||
)
|
||||
|
||||
func formatRecordField(r record.Record,fieldFormat string) (content string) {
|
||||
structType := reflect.ValueOf(r)
|
||||
for i := 0; i < structType.NumField(); i++ {
|
||||
structField := structType.Type().Field(i)
|
||||
fieldName := structField.Name
|
||||
tag := structField.Tag
|
||||
label := tag.Get("notice")
|
||||
if label == "-" {
|
||||
continue
|
||||
} else if label != "" {
|
||||
fieldName = label
|
||||
}
|
||||
value := structType.Field(i).Interface()
|
||||
if value, ok := value.(record.Record); ok {
|
||||
content += formatRecordField(value, fieldFormat)
|
||||
continue
|
||||
}
|
||||
content += fmt.Sprintf(fieldFormat+"\n", strings.ToUpper(fieldName), value)
|
||||
}
|
||||
strings.TrimSuffix(content, "\n")
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package notice
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/li4n0/revsuit/internal/record"
|
||||
)
|
||||
|
||||
var _ Bot = (*Lark)(nil)
|
||||
|
||||
type Lark struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type larkText struct {
|
||||
Content string `json:"content"`
|
||||
Tag string `json:"tag"`
|
||||
}
|
||||
|
||||
type larkElement struct {
|
||||
Tag string `json:"tag"`
|
||||
Text larkText `json:"text"`
|
||||
}
|
||||
|
||||
type larkCard struct {
|
||||
Header larkHeader `json:"header"`
|
||||
Elements []larkElement `json:"elements"`
|
||||
}
|
||||
|
||||
type larkHeader struct {
|
||||
Title larkText `json:"title"`
|
||||
}
|
||||
|
||||
type larkPayload struct {
|
||||
MsgType string `json:"msg_type"`
|
||||
Card larkCard `json:"card"`
|
||||
}
|
||||
|
||||
func (d *Lark) buildPayload(r record.Record) string {
|
||||
payload := larkPayload{
|
||||
MsgType: "interactive",
|
||||
Card: larkCard{
|
||||
Header:larkHeader{
|
||||
Title: larkText{
|
||||
Tag: "plain_text",
|
||||
Content: "New Connection",
|
||||
},
|
||||
},
|
||||
Elements: []larkElement{
|
||||
{
|
||||
Tag: "div",
|
||||
Text: larkText{
|
||||
Tag: "lark_md",
|
||||
Content: formatRecordField(r,"**%s**: %v"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
p, err := json.Marshal(&payload)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(p)
|
||||
}
|
||||
|
||||
func (d *Lark) notice(r record.Record) error {
|
||||
resp, err := http.DefaultClient.Post(d.URL, "application/json", strings.NewReader(d.buildPayload(r)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("HTTP request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode/100 != 2 {
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read HTTP response body: %v", err)
|
||||
}
|
||||
return fmt.Errorf("non-success response status code %d with body: %s", resp.StatusCode, data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package notice
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/li4n0/revsuit/internal/record"
|
||||
)
|
||||
|
||||
var _ Bot = (*Slack)(nil)
|
||||
|
||||
type Slack struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type slackText struct {
|
||||
Type string `json:"type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type slackBlock struct {
|
||||
Type string `json:"type"`
|
||||
Text slackText `json:"text"`
|
||||
}
|
||||
|
||||
type slackAttachments struct {
|
||||
Color string `json:"color"`
|
||||
Blocks []slackBlock `json:"blocks"`
|
||||
}
|
||||
|
||||
type slackPayload struct {
|
||||
Attachments []slackAttachments `json:"attachments"`
|
||||
}
|
||||
|
||||
func (d *Slack) buildPayload(r record.Record) string {
|
||||
payload := slackPayload{
|
||||
Attachments: []slackAttachments{
|
||||
{
|
||||
Color: "#f2c744",
|
||||
Blocks: []slackBlock{
|
||||
{
|
||||
Type: "header",
|
||||
Text: slackText{
|
||||
Type: "plain_text",
|
||||
Text: "New Connection",
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "section",
|
||||
Text: slackText{
|
||||
Type: "mrkdwn",
|
||||
Text: formatRecordField(r, "- `%s: %v`"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
p, err := json.Marshal(&payload)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(p)
|
||||
}
|
||||
|
||||
func (d *Slack) notice(r record.Record) error {
|
||||
resp, err := http.DefaultClient.Post(d.URL, "application/json", strings.NewReader(d.buildPayload(r)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("HTTP request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode/100 != 2 {
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read HTTP response body: %v", err)
|
||||
}
|
||||
return fmt.Errorf("non-success response status code %d with body: %s", resp.StatusCode, data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package notice
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/li4n0/revsuit/internal/record"
|
||||
)
|
||||
|
||||
var _ Bot = (*Weixin)(nil)
|
||||
|
||||
type Weixin struct {
|
||||
URL string
|
||||
}
|
||||
|
||||
type weixinMarkdown struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type weixinPayload struct {
|
||||
ToUser string `json:"touser"`
|
||||
MsgType string `json:"msgtype"`
|
||||
Markdown weixinMarkdown `json:"markdown"`
|
||||
}
|
||||
|
||||
func (w *Weixin) buildPayload(r record.Record) string {
|
||||
payload := weixinPayload{
|
||||
ToUser: "@all",
|
||||
MsgType: "markdown",
|
||||
Markdown: weixinMarkdown{
|
||||
Content: "<font color=\"#e96900\" face=\"Fira Code\" size=3>New Connection</font>\n" +
|
||||
formatRecordField(r, `> **<font color="#e96900" face="Fira Code">%s: </font>**<font color="#e96900" face="Fira Code">%v</font>`),
|
||||
},
|
||||
}
|
||||
p, err := json.Marshal(&payload)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(p)
|
||||
}
|
||||
|
||||
func (w *Weixin) notice(r record.Record) error {
|
||||
resp, err := http.DefaultClient.Post(w.URL, "application/json", strings.NewReader(w.buildPayload(r)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("HTTP request: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode/100 != 2 {
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read HTTP response body: %v", err)
|
||||
}
|
||||
return fmt.Errorf("non-success response status code %d with body: %s", resp.StatusCode, data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package qqwry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "unknwon.dev/clog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
COPY_WRITE_URL = "https://qqwry.mirror.noc.one/copywrite.rar"
|
||||
QQWRY_URL = "https://qqwry.mirror.noc.one/qqwry.rar"
|
||||
)
|
||||
|
||||
func get(url string) (b []byte, err error) {
|
||||
client := http.Client{
|
||||
Timeout: 90 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // disable verify
|
||||
}}
|
||||
request, _ := http.NewRequest("GET", url, nil)
|
||||
request.Header.Add("User-Agent", "Nali/2.1.2 (Nali CLI, https://nali.skk.moe)")
|
||||
|
||||
resp, err := client.Do(request)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
b, err = ioutil.ReadAll(resp.Body)
|
||||
return
|
||||
}
|
||||
|
||||
func getKey(b []byte) (key uint32, err error) {
|
||||
if len(b) != 280 {
|
||||
return 0, errors.New("copywrite.rar is corrupt")
|
||||
}
|
||||
key = binary.LittleEndian.Uint32(b[20:])
|
||||
return
|
||||
}
|
||||
|
||||
func decrypt(b []byte, key uint32) (_ []byte, err error) {
|
||||
for i := 0; i < 0x200; i++ {
|
||||
key *= uint32(0x805)
|
||||
key++
|
||||
key &= uint32(0xff)
|
||||
b[i] = b[i] ^ byte(key)
|
||||
}
|
||||
rc, err := zlib.NewReader(bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer rc.Close()
|
||||
return ioutil.ReadAll(rc)
|
||||
}
|
||||
|
||||
func download() (err error) {
|
||||
var (
|
||||
copyWriteData, qqwryData []byte
|
||||
wg sync.WaitGroup
|
||||
)
|
||||
log.Info("Downloading qqwry.dat...")
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if copyWriteData, err = get(COPY_WRITE_URL); err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
if qqwryData, err = get(QQWRY_URL); err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var key uint32
|
||||
if key, err = getKey(copyWriteData); err != nil {
|
||||
return
|
||||
}
|
||||
b, err := decrypt(qqwryData, key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ = ioutil.WriteFile("qqwry.dat", b, 0644)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package qqwry
|
||||
|
||||
// Area return IpArea according to ip
|
||||
func Area(ip string) string {
|
||||
ipData := GetQQWry().SearchByIPv4(ip)
|
||||
if GetQQWry() == nil {
|
||||
return ""
|
||||
}
|
||||
if ipData.Area == " CZ88.NET" {
|
||||
return ipData.Country
|
||||
} else {
|
||||
return ipData.Country + " " + ipData.Area
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package qqwry
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sinlov/qqwry-golang/qqwry"
|
||||
log "unknwon.dev/clog/v2"
|
||||
)
|
||||
|
||||
var wry *qqwry.QQwry
|
||||
var once sync.Once
|
||||
|
||||
func init() {
|
||||
info, err := os.Stat("qqwry.dat")
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err := download()
|
||||
if err != nil {
|
||||
log.Error("Download qqwry.dat failed, caused by:%v, recommend to download it by yourself otherwise the `IpArea` will be null", err.Error())
|
||||
}
|
||||
}
|
||||
} else if info.ModTime().Sub(time.Now()) > 5*24*time.Hour {
|
||||
log.Info("Updating qqwry.dat...")
|
||||
err := download()
|
||||
if err != nil {
|
||||
log.Warn("Update qqwry.dat failed, please download qqwry.dat by yourself")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetQQWry() *qqwry.QQwry {
|
||||
once.Do(func() {
|
||||
qqwry.DatData.FilePath = "qqwry.dat"
|
||||
init := qqwry.DatData.InitDatFile()
|
||||
if v, ok := init.(error); ok {
|
||||
if v != nil {
|
||||
log.Error("qqwry init failed")
|
||||
wry = nil
|
||||
}
|
||||
}
|
||||
wry = qqwry.NewQQwry()
|
||||
})
|
||||
|
||||
return wry
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package record
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var recordChan = make(chan BaseRecord, 100)
|
||||
|
||||
type Record interface {
|
||||
GetFlag() string
|
||||
PushToClient()
|
||||
Notice()
|
||||
}
|
||||
|
||||
type BaseRecord struct {
|
||||
Record `gorm:"-" json:"-"`
|
||||
ID uint `gorm:"primarykey" form:"id" json:"id" notice:"-"`
|
||||
RuleName string `gorm:"index" form:"rule_name" json:"rule_name" notice:"rule"`
|
||||
Flag string `gorm:"index" form:"flag" json:"flag" `
|
||||
RemoteIP string `gorm:"index" form:"remote_ip" json:"remote_ip" notice:"remote_ip"`
|
||||
IpArea string `form:"ip_area" json:"ip_area" notice:"ip_area"`
|
||||
RequestTime time.Time `gorm:"index" form:"request_time" json:"request_time" notice:"-"`
|
||||
}
|
||||
|
||||
func (b BaseRecord) GetFlag() string {
|
||||
return b.Flag
|
||||
}
|
||||
|
||||
func (b BaseRecord) PushToClient() {
|
||||
recordChan <- b
|
||||
}
|
||||
|
||||
func (b BaseRecord) Notice() {
|
||||
}
|
||||
|
||||
func Channel() chan BaseRecord {
|
||||
return recordChan
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package rule
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
log "unknwon.dev/clog/v2"
|
||||
)
|
||||
|
||||
type Rule interface {
|
||||
CreateOrUpdate() error
|
||||
Delete() error
|
||||
}
|
||||
|
||||
type BaseRule struct {
|
||||
Rule `gorm:"-" json:"-"`
|
||||
ID uint `gorm:"primarykey" form:"id" json:"id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Name string `gorm:"index;unique;not null;" form:"name" json:"name"`
|
||||
FlagFormat string `gorm:"unique;not null;" form:"flag_format" json:"flag_format"`
|
||||
flagCatcher *regexp.Regexp `gorm:"-" json:"-"`
|
||||
Rank int `gorm:"default:0" json:"rank" form:"rank"`
|
||||
PushToClient bool `gorm:"default:false;not null;" form:"push_to_client" json:"push_to_client"`
|
||||
Notice bool `gorm:"default:false;not null;" form:"notice" json:"notice"`
|
||||
}
|
||||
|
||||
func compileCatcher(flagFormat string) (reg *regexp.Regexp, err error) {
|
||||
if reg, err := regexp.Compile(flagFormat); err == nil {
|
||||
return reg, nil
|
||||
} else {
|
||||
// * meaning record all connections.
|
||||
if flagFormat != "*" {
|
||||
return nil, err
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (br BaseRule) Match(s string) (flag, flagGroup string) {
|
||||
if br.flagCatcher == nil && br.FlagFormat != "*" {
|
||||
// compile rule flags
|
||||
catcher, err := compileCatcher(br.FlagFormat)
|
||||
if err != nil {
|
||||
log.Error("%s(rule:%s)", err.Error(), br.Name)
|
||||
}
|
||||
br.flagCatcher = catcher
|
||||
}
|
||||
|
||||
if br.flagCatcher == nil {
|
||||
// capture all connection.
|
||||
flag = "*"
|
||||
} else {
|
||||
matched := br.flagCatcher.FindStringSubmatch(s)
|
||||
if len(matched) == 0 {
|
||||
return
|
||||
}
|
||||
flag = matched[0]
|
||||
if len(matched) > 1 {
|
||||
flagGroup = matched[1]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user