internal recursive dns server #2
|
@ -11,4 +11,4 @@ RUN go build -o /app/hatecomputers
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
CMD ["/app/hatecomputers", "--server", "--migrate", "--port", "8080", "--template-path", "/app/templates", "--database-path", "/app/db/hatecomputers.db", "--static-path", "/app/static", "--scheduler"]
|
CMD ["/app/hatecomputers", "--server", "--migrate", "--port", "8080", "--template-path", "/app/templates", "--database-path", "/app/db/hatecomputers.db", "--static-path", "/app/static", "--scheduler", "--dns", "--dns-port", "8053", "--dns-recursion", "1.1.1.1:53,1.0.0.1:53"]
|
||||||
|
|
74
api/dns.go
74
api/dns.go
|
@ -1,6 +1,7 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -8,16 +9,31 @@ import (
|
||||||
|
|
||||||
"git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/cloudflare"
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/adapters/cloudflare"
|
||||||
"git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
|
||||||
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const MAX_USER_RECORDS = 20
|
const MAX_USER_RECORDS = 65
|
||||||
|
|
||||||
type FormError struct {
|
type FormError struct {
|
||||||
Errors []string
|
Errors []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func userCanFuckWithDNSRecord(user *database.User, record *database.DNSRecord) bool {
|
func userCanFuckWithDNSRecord(dbConn *sql.DB, user *database.User, record *database.DNSRecord) bool {
|
||||||
return user.ID == record.UserID && (record.Name == user.Username || strings.HasSuffix(record.Name, "."+user.Username))
|
ownedByUser := (user.ID == record.UserID)
|
||||||
|
|
||||||
|
if !record.Internal {
|
||||||
|
publicallyOwnedByUser := (record.Name == user.Username || strings.HasSuffix(record.Name, "."+user.Username))
|
||||||
|
return ownedByUser && publicallyOwnedByUser
|
||||||
|
}
|
||||||
|
|
||||||
|
owner, err := database.FindFirstDomainOwnerId(dbConn, record.Name)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
userIsOwnerOfDomain := owner == user.ID
|
||||||
|
return ownedByUser && userIsOwnerOfDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListDNSRecordsContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
|
func ListDNSRecordsContinuation(context *RequestContext, req *http.Request, resp http.ResponseWriter) ContinuationChain {
|
||||||
|
@ -40,8 +56,15 @@ func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, res
|
||||||
Errors: []string{},
|
Errors: []string{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal := req.FormValue("internal") == "on"
|
||||||
name := req.FormValue("name")
|
name := req.FormValue("name")
|
||||||
|
if internal && !strings.HasSuffix(name, ".") {
|
||||||
|
name += "."
|
||||||
|
}
|
||||||
|
|
||||||
recordType := req.FormValue("type")
|
recordType := req.FormValue("type")
|
||||||
|
recordType = strings.ToUpper(recordType)
|
||||||
|
|
||||||
recordContent := req.FormValue("content")
|
recordContent := req.FormValue("content")
|
||||||
ttl := req.FormValue("ttl")
|
ttl := req.FormValue("ttl")
|
||||||
ttlNum, err := strconv.Atoi(ttl)
|
ttlNum, err := strconv.Atoi(ttl)
|
||||||
|
@ -50,11 +73,12 @@ func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, res
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsRecord := &database.DNSRecord{
|
dnsRecord := &database.DNSRecord{
|
||||||
UserID: context.User.ID,
|
UserID: context.User.ID,
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: recordType,
|
Type: recordType,
|
||||||
Content: recordContent,
|
Content: recordContent,
|
||||||
TTL: ttlNum,
|
TTL: ttlNum,
|
||||||
|
Internal: internal,
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID)
|
dnsRecords, err := database.GetUserDNSRecords(context.DBConn, context.User.ID)
|
||||||
|
@ -67,18 +91,22 @@ func CreateDNSRecordContinuation(context *RequestContext, req *http.Request, res
|
||||||
formErrors.Errors = append(formErrors.Errors, "max records reached")
|
formErrors.Errors = append(formErrors.Errors, "max records reached")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !userCanFuckWithDNSRecord(context.User, dnsRecord) {
|
if !userCanFuckWithDNSRecord(context.DBConn, context.User, dnsRecord) {
|
||||||
formErrors.Errors = append(formErrors.Errors, "'name' must end with "+context.User.Username)
|
formErrors.Errors = append(formErrors.Errors, "'name' must end with "+context.User.Username+" or you must be a domain owner for internal domains")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(formErrors.Errors) == 0 {
|
if len(formErrors.Errors) == 0 {
|
||||||
cloudflareRecordId, err := cloudflare.CreateDNSRecord(context.Args.CloudflareZone, context.Args.CloudflareToken, dnsRecord)
|
if dnsRecord.Internal {
|
||||||
if err != nil {
|
dnsRecord.ID = utils.RandomId()
|
||||||
log.Println(err)
|
} else {
|
||||||
formErrors.Errors = append(formErrors.Errors, err.Error())
|
cloudflareRecordId, err := cloudflare.CreateDNSRecord(context.Args.CloudflareZone, context.Args.CloudflareToken, dnsRecord)
|
||||||
}
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
formErrors.Errors = append(formErrors.Errors, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
dnsRecord.ID = cloudflareRecordId
|
dnsRecord.ID = cloudflareRecordId
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(formErrors.Errors) == 0 {
|
if len(formErrors.Errors) == 0 {
|
||||||
|
@ -113,16 +141,18 @@ func DeleteDNSRecordContinuation(context *RequestContext, req *http.Request, res
|
||||||
return failure(context, req, resp)
|
return failure(context, req, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !userCanFuckWithDNSRecord(context.User, record) {
|
if !userCanFuckWithDNSRecord(context.DBConn, context.User, record) {
|
||||||
resp.WriteHeader(http.StatusUnauthorized)
|
resp.WriteHeader(http.StatusUnauthorized)
|
||||||
return failure(context, req, resp)
|
return failure(context, req, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = cloudflare.DeleteDNSRecord(context.Args.CloudflareZone, context.Args.CloudflareToken, recordId)
|
if !record.Internal {
|
||||||
if err != nil {
|
err = cloudflare.DeleteDNSRecord(context.Args.CloudflareZone, context.Args.CloudflareToken, recordId)
|
||||||
log.Println(err)
|
if err != nil {
|
||||||
resp.WriteHeader(http.StatusInternalServerError)
|
log.Println(err)
|
||||||
return failure(context, req, resp)
|
resp.WriteHeader(http.StatusInternalServerError)
|
||||||
|
return failure(context, req, resp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = database.DeleteDNSRecord(context.DBConn, recordId)
|
err = database.DeleteDNSRecord(context.DBConn, recordId)
|
||||||
|
|
25
args/args.go
25
args/args.go
|
@ -15,25 +15,35 @@ type Arguments struct {
|
||||||
StaticPath string
|
StaticPath string
|
||||||
CloudflareToken string
|
CloudflareToken string
|
||||||
CloudflareZone string
|
CloudflareZone string
|
||||||
Port int
|
|
||||||
Server bool
|
|
||||||
Migrate bool
|
|
||||||
Scheduler bool
|
|
||||||
|
|
||||||
|
Migrate bool
|
||||||
|
Scheduler bool
|
||||||
|
|
||||||
|
Port int
|
||||||
|
Server bool
|
||||||
OauthConfig *oauth2.Config
|
OauthConfig *oauth2.Config
|
||||||
OauthUserInfoURI string
|
OauthUserInfoURI string
|
||||||
|
|
||||||
|
Dns bool
|
||||||
|
DnsRecursion []string
|
||||||
|
DnsPort int
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetArgs() (*Arguments, error) {
|
func GetArgs() (*Arguments, error) {
|
||||||
port := flag.Int("port", 8080, "Port to listen on")
|
|
||||||
databasePath := flag.String("database-path", "./hatecomputers.db", "Path to the SQLite database")
|
databasePath := flag.String("database-path", "./hatecomputers.db", "Path to the SQLite database")
|
||||||
templatePath := flag.String("template-path", "./templates", "Path to the template directory")
|
templatePath := flag.String("template-path", "./templates", "Path to the template directory")
|
||||||
staticPath := flag.String("static-path", "./static", "Path to the static directory")
|
staticPath := flag.String("static-path", "./static", "Path to the static directory")
|
||||||
|
|
||||||
scheduler := flag.Bool("scheduler", false, "Run scheduled jobs via cron")
|
scheduler := flag.Bool("scheduler", false, "Run scheduled jobs via cron")
|
||||||
server := flag.Bool("server", false, "Run the server")
|
|
||||||
migrate := flag.Bool("migrate", false, "Run the migrations")
|
migrate := flag.Bool("migrate", false, "Run the migrations")
|
||||||
|
|
||||||
|
port := flag.Int("port", 8080, "Port to listen on")
|
||||||
|
server := flag.Bool("server", false, "Run the server")
|
||||||
|
|
||||||
|
dns := flag.Bool("dns", false, "Run DNS resolver")
|
||||||
|
dnsRecursion := flag.String("dns-recursion", "1.1.1.1:53,1.0.0.1:53", "Comma separated list of DNS resolvers")
|
||||||
|
dnsPort := flag.Int("dns-port", 8053, "Port to listen on for DNS resolver")
|
||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
cloudflareToken := os.Getenv("CLOUDFLARE_TOKEN")
|
cloudflareToken := os.Getenv("CLOUDFLARE_TOKEN")
|
||||||
|
@ -86,6 +96,9 @@ func GetArgs() (*Arguments, error) {
|
||||||
Server: *server,
|
Server: *server,
|
||||||
Migrate: *migrate,
|
Migrate: *migrate,
|
||||||
Scheduler: *scheduler,
|
Scheduler: *scheduler,
|
||||||
|
Dns: *dns,
|
||||||
|
DnsRecursion: strings.Split(*dnsRecursion, ","),
|
||||||
|
DnsPort: *dnsPort,
|
||||||
|
|
||||||
OauthConfig: oauthConfig,
|
OauthConfig: oauthConfig,
|
||||||
OauthUserInfoURI: oauthUserInfoURI,
|
OauthUserInfoURI: oauthUserInfoURI,
|
||||||
|
|
|
@ -2,8 +2,10 @@ package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -14,6 +16,7 @@ type DNSRecord struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
TTL int `json:"ttl"`
|
TTL int `json:"ttl"`
|
||||||
|
Internal bool `json:"internal"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +32,7 @@ func GetUserDNSRecords(db *sql.DB, userID string) ([]DNSRecord, error) {
|
||||||
var records []DNSRecord
|
var records []DNSRecord
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var record DNSRecord
|
var record DNSRecord
|
||||||
err := rows.Scan(&record.ID, &record.UserID, &record.Name, &record.Type, &record.Content, &record.TTL, &record.CreatedAt)
|
err := rows.Scan(&record.ID, &record.UserID, &record.Name, &record.Type, &record.Content, &record.TTL, &record.Internal, &record.CreatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -43,7 +46,7 @@ func SaveDNSRecord(db *sql.DB, record *DNSRecord) (*DNSRecord, error) {
|
||||||
log.Println("saving dns record", record)
|
log.Println("saving dns record", record)
|
||||||
|
|
||||||
record.CreatedAt = time.Now()
|
record.CreatedAt = time.Now()
|
||||||
_, err := db.Exec("INSERT OR REPLACE INTO dns_records (id, user_id, name, type, content, ttl, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", record.ID, record.UserID, record.Name, record.Type, record.Content, record.TTL, record.CreatedAt)
|
_, err := db.Exec("INSERT OR REPLACE INTO dns_records (id, user_id, name, type, content, ttl, internal, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", record.ID, record.UserID, record.Name, record.Type, record.Content, record.TTL, record.Internal, record.CreatedAt)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -56,7 +59,7 @@ func GetDNSRecord(db *sql.DB, recordID string) (*DNSRecord, error) {
|
||||||
|
|
||||||
row := db.QueryRow("SELECT * FROM dns_records WHERE id = ?", recordID)
|
row := db.QueryRow("SELECT * FROM dns_records WHERE id = ?", recordID)
|
||||||
var record DNSRecord
|
var record DNSRecord
|
||||||
err := row.Scan(&record.ID, &record.UserID, &record.Name, &record.Type, &record.Content, &record.TTL, &record.CreatedAt)
|
err := row.Scan(&record.ID, &record.UserID, &record.Name, &record.Type, &record.Content, &record.TTL, &record.Internal, &record.CreatedAt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -72,3 +75,53 @@ func DeleteDNSRecord(db *sql.DB, recordID string) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FindFirstDomainOwnerId(db *sql.DB, domain string) (string, error) {
|
||||||
|
log.Println("finding domain owner for", domain)
|
||||||
|
|
||||||
|
ownerID := ""
|
||||||
|
parts := strings.Split(domain, ".")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
return ownerID, fmt.Errorf("invalid domain; must have at least two parts")
|
||||||
|
}
|
||||||
|
|
||||||
|
for ownerID == "" {
|
||||||
|
row := db.QueryRow("SELECT user_id FROM domain_owners WHERE domain = ?", strings.Join(parts, "."))
|
||||||
|
err := row.Scan(&ownerID)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if len(parts) == 1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
parts = parts[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ownerID == "" {
|
||||||
|
return ownerID, fmt.Errorf("no owner found for domain")
|
||||||
|
}
|
||||||
|
return ownerID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FindDNSRecords(dbConn *sql.DB, name string, qtype string) ([]DNSRecord, error) {
|
||||||
|
log.Println("finding dns record(s) for", name, qtype)
|
||||||
|
|
||||||
|
rows, err := dbConn.Query("SELECT * FROM dns_records WHERE name = ? AND type = ?", name, qtype)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var records []DNSRecord
|
||||||
|
for rows.Next() {
|
||||||
|
var record DNSRecord
|
||||||
|
err := rows.Scan(&record.ID, &record.UserID, &record.Name, &record.Type, &record.Content, &record.TTL, &record.Internal, &record.CreatedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
|
@ -57,6 +57,7 @@ func MigrateDNSRecords(dbConn *sql.DB) (*sql.DB, error) {
|
||||||
type TEXT NOT NULL,
|
type TEXT NOT NULL,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
ttl INTEGER NOT NULL,
|
ttl INTEGER NOT NULL,
|
||||||
|
internal BOOLEAN NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE);`)
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE);`)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -65,6 +66,26 @@ func MigrateDNSRecords(dbConn *sql.DB) (*sql.DB, error) {
|
||||||
return dbConn, nil
|
return dbConn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MigrateDomainOwners(dbConn *sql.DB) (*sql.DB, error) {
|
||||||
|
log.Println("migrating domain_owners table")
|
||||||
|
|
||||||
|
_, err := dbConn.Exec(`CREATE TABLE IF NOT EXISTS domain_owners (
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
domain TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
|
||||||
|
);`)
|
||||||
|
if err != nil {
|
||||||
|
return dbConn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = dbConn.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_domain_owners_domain ON domain_owners (domain);`)
|
||||||
|
if err != nil {
|
||||||
|
return dbConn, err
|
||||||
|
}
|
||||||
|
return dbConn, nil
|
||||||
|
}
|
||||||
|
|
||||||
func MigrateUserSessions(dbConn *sql.DB) (*sql.DB, error) {
|
func MigrateUserSessions(dbConn *sql.DB) (*sql.DB, error) {
|
||||||
log.Println("migrating user_sessions table")
|
log.Println("migrating user_sessions table")
|
||||||
|
|
||||||
|
@ -88,6 +109,7 @@ func Migrate(dbConn *sql.DB) (*sql.DB, error) {
|
||||||
MigrateUsers,
|
MigrateUsers,
|
||||||
MigrateUserSessions,
|
MigrateUserSessions,
|
||||||
MigrateApiKeys,
|
MigrateApiKeys,
|
||||||
|
MigrateDomainOwners,
|
||||||
MigrateDNSRecords,
|
MigrateDNSRecords,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,110 @@
|
||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/args"
|
||||||
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const MAX_RECURSION = 10
|
||||||
|
|
||||||
|
func resolveRecursive(dbConn *sql.DB, dnsResolvers []string, domain string, qtype uint16, maxDepth int) ([]dns.RR, error) {
|
||||||
|
if maxDepth == 0 {
|
||||||
|
return nil, fmt.Errorf("too much recursion")
|
||||||
|
}
|
||||||
|
|
||||||
|
internalCnames, err := database.FindDNSRecords(dbConn, domain, "CNAME")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
answers := []dns.RR{}
|
||||||
|
for _, record := range internalCnames {
|
||||||
|
cnameRecursive, _ := resolveRecursive(dbConn, dnsResolvers, record.Content, qtype, maxDepth-1)
|
||||||
|
answers = append(answers, cnameRecursive...)
|
||||||
|
}
|
||||||
|
|
||||||
|
qtypeName := dns.TypeToString[qtype]
|
||||||
|
if qtypeName == "" {
|
||||||
|
return nil, fmt.Errorf("invalid query type %d", qtype)
|
||||||
|
}
|
||||||
|
|
||||||
|
typeDnsRecords, err := database.FindDNSRecords(dbConn, domain, qtypeName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, record := range typeDnsRecords {
|
||||||
|
answer, err := dns.NewRR(fmt.Sprintf("%s %d IN %s %s", record.Name, record.TTL, record.Type, record.Content))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
answers = append(answers, answer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(answers) > 0 {
|
||||||
|
// base case; we found the answer
|
||||||
|
return answers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
message := new(dns.Msg)
|
||||||
|
message.SetQuestion(dns.Fqdn(domain), qtype)
|
||||||
|
message.RecursionDesired = true
|
||||||
|
|
||||||
|
client := new(dns.Client)
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
in, _, err := client.Exchange(message, dnsResolvers[i])
|
||||||
|
for err != nil {
|
||||||
|
i += 1
|
||||||
|
if i == len(dnsResolvers) {
|
||||||
|
log.Println(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
in, _, err = client.Exchange(message, dnsResolvers[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
answers = append(answers, in.Answer...)
|
||||||
|
return answers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DnsHandler struct {
|
||||||
|
DnsResolvers []string
|
||||||
|
DbConn *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DnsHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
|
msg := new(dns.Msg)
|
||||||
|
msg.SetReply(r)
|
||||||
|
msg.Authoritative = true
|
||||||
|
|
||||||
|
for _, question := range r.Question {
|
||||||
|
answers, err := resolveRecursive(h.DbConn, h.DnsResolvers, question.Name, question.Qtype, MAX_RECURSION)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
msg.Answer = append(msg.Answer, answers...)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println(msg.Answer)
|
||||||
|
w.WriteMsg(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeServer(argv *args.Arguments, dbConn *sql.DB) *dns.Server {
|
||||||
|
handler := &DnsHandler{
|
||||||
|
DnsResolvers: argv.DnsRecursion,
|
||||||
|
DbConn: dbConn,
|
||||||
|
}
|
||||||
|
addr := fmt.Sprintf(":%d", argv.DnsPort)
|
||||||
|
|
||||||
|
return &dns.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Net: "udp",
|
||||||
|
Handler: handler,
|
||||||
|
UDPSize: 65535,
|
||||||
|
ReusePort: true,
|
||||||
|
}
|
||||||
|
}
|
4
go.mod
4
go.mod
|
@ -6,6 +6,7 @@ require (
|
||||||
github.com/go-co-op/gocron v1.37.0
|
github.com/go-co-op/gocron v1.37.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/mattn/go-sqlite3 v1.14.22
|
github.com/mattn/go-sqlite3 v1.14.22
|
||||||
|
github.com/miekg/dns v1.1.58
|
||||||
golang.org/x/oauth2 v0.18.0
|
golang.org/x/oauth2 v0.18.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -14,7 +15,10 @@ require (
|
||||||
github.com/google/uuid v1.4.0 // indirect
|
github.com/google/uuid v1.4.0 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
|
golang.org/x/mod v0.14.0 // indirect
|
||||||
golang.org/x/net v0.22.0 // indirect
|
golang.org/x/net v0.22.0 // indirect
|
||||||
|
golang.org/x/sys v0.18.0 // indirect
|
||||||
|
golang.org/x/tools v0.17.0 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // indirect
|
google.golang.org/appengine v1.6.7 // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
10
go.sum
10
go.sum
|
@ -23,6 +23,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
||||||
|
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
@ -41,15 +43,23 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||||
|
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||||
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
||||||
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||||
|
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||||
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
|
||||||
|
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||||
|
|
28
main.go
28
main.go
|
@ -6,6 +6,7 @@ import (
|
||||||
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api"
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/api"
|
||||||
"git.hatecomputers.club/hatecomputers/hatecomputers.club/args"
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/args"
|
||||||
"git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/database"
|
||||||
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/dns"
|
||||||
"git.hatecomputers.club/hatecomputers/hatecomputers.club/scheduler"
|
"git.hatecomputers.club/hatecomputers/hatecomputers.club/scheduler"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
)
|
)
|
||||||
|
@ -40,10 +41,27 @@ func main() {
|
||||||
|
|
||||||
if argv.Server {
|
if argv.Server {
|
||||||
server := api.MakeServer(argv, dbConn)
|
server := api.MakeServer(argv, dbConn)
|
||||||
err = server.ListenAndServe()
|
log.Println("🚀🚀 API listening on port", argv.Port)
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
go func() {
|
||||||
}
|
err = server.ListenAndServe()
|
||||||
log.Println("🚀🚀 server listening on port", argv.Port)
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if argv.Dns {
|
||||||
|
server := dns.MakeServer(argv, dbConn)
|
||||||
|
log.Println("🚀🚀 DNS resolver listening on port", argv.DnsPort)
|
||||||
|
go func() {
|
||||||
|
err = server.ListenAndServe()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
runForever := make(chan struct{})
|
||||||
|
<-runForever
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,13 +28,12 @@
|
||||||
<h2>Add An API Key</h2>
|
<h2>Add An API Key</h2>
|
||||||
<hr>
|
<hr>
|
||||||
<input type="submit" value="Generate" />
|
<input type="submit" value="Generate" />
|
||||||
</form>
|
{{ if .FormError }}
|
||||||
|
|
||||||
{{ if .FormError }}
|
|
||||||
{{ if (len .FormError.Errors) }}
|
{{ if (len .FormError.Errors) }}
|
||||||
{{ range $error := .FormError.Errors }}
|
{{ range $error := .FormError.Errors }}
|
||||||
<div class="error">{{ $error }}</div>
|
<div class="error">{{ $error }}</div>
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</form>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -5,11 +5,12 @@
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Content</th>
|
<th>Content</th>
|
||||||
<th>TTL</th>
|
<th>TTL</th>
|
||||||
|
<th>Internal</th>
|
||||||
<th>Delete</th>
|
<th>Delete</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{ if (eq (len .DNSRecords) 0) }}
|
{{ if (eq (len .DNSRecords) 0) }}
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5"><span class="blinky">No DNS records found</span></td>
|
<td colspan="6"><span class="blinky">No DNS records found</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ range $record := .DNSRecords }}
|
{{ range $record := .DNSRecords }}
|
||||||
|
@ -17,6 +18,7 @@
|
||||||
<td>{{ $record.Type }}</td>
|
<td>{{ $record.Type }}</td>
|
||||||
<td>{{ $record.Name }}</td>
|
<td>{{ $record.Name }}</td>
|
||||||
<td>{{ $record.Content }}</td>
|
<td>{{ $record.Content }}</td>
|
||||||
|
<td>{{ $record.Internal }}</td>
|
||||||
<td>{{ $record.TTL }}</td>
|
<td>{{ $record.TTL }}</td>
|
||||||
<td>
|
<td>
|
||||||
<form method="POST" action="/dns/delete">
|
<form method="POST" action="/dns/delete">
|
||||||
|
@ -64,14 +66,26 @@
|
||||||
value="{{ .RecordForm.TTL }}"
|
value="{{ .RecordForm.TTL }}"
|
||||||
{{ end }}
|
{{ end }}
|
||||||
required />
|
required />
|
||||||
|
<label for="internal">
|
||||||
|
Internal
|
||||||
|
<input style='display:inline;width:auto;' type="checkbox" name="internal"
|
||||||
|
{{ if .RecordForm.Internal }}
|
||||||
|
checked
|
||||||
|
{{ end }}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
|
||||||
<input type="submit" value="Add" />
|
<input type="submit" value="Add" />
|
||||||
|
|
||||||
|
{{ if .FormError }}
|
||||||
|
{{ if (len .FormError.Errors) }}
|
||||||
|
{{ range $error := .FormError.Errors }}
|
||||||
|
<div class="error">{{ $error }}</div>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{{ if .FormError }}
|
|
||||||
{{ if (len .FormError.Errors) }}
|
|
||||||
{{ range $error := .FormError.Errors }}
|
|
||||||
<div class="error">{{ $error }}</div>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
Loading…
Reference in New Issue