From af0bb44e33da12d8a3c7dcd337fb1965f4673f85 Mon Sep 17 00:00:00 2001 From: odboy Date: Thu, 7 Nov 2024 23:54:55 +0800 Subject: [PATCH] release: version 1.0.0 --- README.md | 87 +++++++++++- RRType.md | 67 ++++++++++ config/app.go | 6 + config/database.go | 18 +++ config/dns_server.go | 10 ++ config/web_server.go | 15 +++ constant/rr_type.go | 19 +++ controller/resolve_record.go | 248 +++++++++++++++++++++++++++++++++++ core/handler.go | 156 ++++++++++++++++++++++ dao/database.go | 39 ++++++ dao/resolve_config.go | 42 ++++++ dao/resolve_record.go | 184 ++++++++++++++++++++++++++ dns.sqlite3 | Bin 0 -> 36864 bytes domain/resolve_record.go | 43 ++++++ go.mod | 42 +++++- go.sum | 231 ++++++++++++++++++++++++++++++++ handler.go | 80 ----------- main.go | 93 +++++++++---- util/strtool.go | 46 +++++++ 19 files changed, 1307 insertions(+), 119 deletions(-) create mode 100644 RRType.md create mode 100644 config/app.go create mode 100644 config/database.go create mode 100644 config/dns_server.go create mode 100644 config/web_server.go create mode 100644 constant/rr_type.go create mode 100644 controller/resolve_record.go create mode 100644 core/handler.go create mode 100644 dao/database.go create mode 100644 dao/resolve_config.go create mode 100644 dao/resolve_record.go create mode 100644 dns.sqlite3 create mode 100644 domain/resolve_record.go delete mode 100644 handler.go create mode 100644 util/strtool.go diff --git a/README.md b/README.md index 55af68c..7fb76c3 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,98 @@ # kenaito-dns -DNS服务器,可通过Web管理界面随意设置灵活的解析规则。为了纯血自研devops平台而生。 +## 背景 -# Go代理地址配置 +Bind9不能直接支持API的方式添加解析记录, 通过脚本修改Bind服务器配置这种事情实在是太冒险了,而且没有发现有开源的、性能嘎嘎好的DNS服务器项目。 + +## 简介 + +一个轻量级 DNS 服务器,让变更解析记录简单、优雅!为了纯血自研devops平台而生。 + +## 环境依赖 + +- gcc +- go version >= 1.20 + +## 接口文档 + +[在线阅读](https://oss.odboy.cn/blog/files/onlinedoc/kenaito-dns.html) + +## 项目结构 + +- constant 常量 +- controller api接口 +- core dns解析 +- dao 数据库交互 +- domain 各种领域模型 +- util 工具函数 + +## 项目耗时 + +`` +more than 7 hours +`` + +## 主要特性 + +- 纳秒级、毫秒级( <= 5 )响应时间 +- 支持API变更解析记录 +- 支持解析记录回滚 +- 支持A、AAAA、MX、TXT、CNAME记录解析 + +## 运行配置 + +#### Go代理地址配置 [去看看](https://blog.odboy.cn/go%E5%85%A8%E5%B1%80%E9%85%8D%E7%BD%AE%E5%9B%BD%E5%86%85%E6%BA%90-by-odboy/) -# nslookup 命令不存在解决 +#### window安装gcc(记得配置环境变量哦, 记得重启电脑哦) + +- [去看看](https://github.com/niXman/mingw-builds-binaries/releases) +- [去下载](https://github.com/niXman/mingw-builds-binaries/releases/download/14.2.0-rt_v12-rev0/x86_64-14.2.0-release-posix-seh-msvcrt-rt_v12-rev0.7z) +- [国内下载](https://oss.odboy.cn/blog/files/windows-gcc/x86_64-14.2.0-release-posix-seh-msvcrt-rt_v12-rev0.7z) + +#### window验证gcc + +```shell +gcc -v +``` + +## 问题解决 + +#### nslookup 命令不存在解决 ```shell yum install bind-utils -y ``` -# nslookup指定dns服务器查询 +#### nslookup指定dns服务器查询 ```shell # 这里dns服务器为 192.168.1.103 nslookup example.com 192.168.1.103 ``` -# 本程序所用依赖,感谢开源者的无私奉献 +## 特别鸣谢 -- [数据库操作](http://xorm.topgoer.com/) -- [DNS解析](https://github.com/miekg/dns) \ No newline at end of file +- [数据库操作 - xorm](http://xorm.topgoer.com/) +- [DNS解析 - miekg/dns](https://github.com/miekg/dns) +- [Web - gin](https://gin-gonic.com/zh-cn/docs/quickstart/) + +## 代码托管 + +- Gitea: [https://gitea.odboy.cn/odboy/kenaito-dns](https://gitea.odboy.cn/odboy/kenaito-dns) +- Github: [https://github.com/odboy-tianjun/kenaito-dns](https://github.com/odboy-tianjun/kenaito-dns) +- Gitee: [https://gitee.com/odboy/kenaito-dns](https://gitee.com/odboy/kenaito-dns) + +## 微信交流群 + +![wxcode](https://oss.odboy.cn/blog/files/userinfo/MyWxCode.png) + +(扫码添加微信,备注:kenaito-dns,邀您加入群聊) + +加入群聊的好处: + +- 第一时间收到项目更新通知。 +- 第一时间收到项目 bug 通知。 +- 第一时间收到新增开源案例通知。 +- 和众多大佬一起互相 (huá shuǐ) 交流 (mō yú)。 \ No newline at end of file diff --git a/RRType.md b/RRType.md new file mode 100644 index 0000000..8397c65 --- /dev/null +++ b/RRType.md @@ -0,0 +1,67 @@ +# 记录类型解析 + +- A记录 + +```text +将域名解析指向一个IPv4地址,通常为网站服务器的IPv4地址,例如:223.5.5.x 。 +如果您的域名要解析指向多个IPv4地址,可以通过添加多条”主机记录”相同但“记录值”不同的解析记录实现。 +``` + +- AAAA + +```text +将域名解析指向一个IPv6地址,通常为网站服务器的IPv6地址,例如ff03:0:0:0:0:0:0:c1 。 +如果您的域名要解析指向多个IPv6地址,可以通过添加多条“主机记录”相同但“记录值”不同的解析记录实现。 +``` + +- CNAME记 + +```text +将域名解析指向另一个域名, 由另一个域名提供 IP 地址解析结果。在使用CDN、企业邮箱、全局流量管理等产品时,通常需要通过配置CNAME记录解析到这类产品的CNAME接入域名 +如果您的域名要解析指向多个域名,可以通过添加多条“主机记录”相同但“记录值”不同的解析记录实现。 +``` + +- NS + +```text +如果需要把子域名交给其他DNS服务商解析,可以通过添加NS记录实现。通常其他DNS服务商地址会有多个,可以通过配置主机记录(子域名)相同但记录值不同的多条NS记录实现。 +``` + +- MX + +```text +MX全称为mail exchanger,用于电子邮件系统发邮件时根据收信人的地址后缀来定位邮件服务器地址。 +``` + +- SRV + +```text +SRV 记录用来标识某服务器提供了特定服务,通过SRV记录还可以获取对应服务的地址、端口、优先级、权重等信息。常见于微软系统的目录管理。 +SRV记录的主机记录格式通常为“服务的名字.协议的类型”,例如:_sip._tcp +``` + +- TXT + +```text +如果希望对域名进行标识和说明,可以使用 TXT 记录。 TXT 记录常用来做SSL数字证书签发验证、SPF 记录(反垃圾邮件) +``` + +# 主机记录,参数Name说明 + +```text +主机记录就是域名前缀,常用主机记录及含义如下: +www表示域名 www.odboy.cn +@表示主域名 odboy.cn +*泛解析,表示满足格式 *.odboy.cn 的所有域名(记录类型为“显性URL”时,不允许设置泛解析) +mail表示域名 mail.odboy.cn ,常用于邮箱业务的解析设置 +m表示域名 m.odboy.cn ,常用于手机网站 +二级域名如abc.odboy.cn,则写abc +多级域名如ab.cd.odboy.cn,则写ab.cd +``` + +# TTL,参数ttl说明 + +```text +TTL通常指全球运营商LocalDNS在获得权威DNS返回的解析结果后,将解析结果缓存的有效时间,也称为解析结果在LocalDNS的生存时间。通常TTL值越小,解析变更对终端用户生效更快。 +建议将TTL设置为默认10分钟,过小的TTL值会导致Localdns缓存解析结果时间较短,并影响终端用户访问域名的解析速度。日常运维时可以先将TTL调整为1分钟,待2小时后修改记录值并将TTL调整为10分钟,这有助于解析快速生效。 +``` diff --git a/config/app.go b/config/app.go new file mode 100644 index 0000000..4c65cba --- /dev/null +++ b/config/app.go @@ -0,0 +1,6 @@ +package config + +const ( + AppVersion = "1.0.0" + AppTimeFormat = "2006/01/02 15:04:05.999999" +) diff --git a/config/database.go b/config/database.go new file mode 100644 index 0000000..c8dc45f --- /dev/null +++ b/config/database.go @@ -0,0 +1,18 @@ +package config + +/* + * @Description 数据库配置 + * @Author www.odboy.cn + * @Date 20241108 + */ +import "xorm.io/core" + +const ( + DataSourceDriverName = "sqlite3" // 驱动名称 + DataSourceName = "dns.sqlite3" // 数据源名称 + DataSourceMaxOpenConnectionSize = 30 // 最大db连接数 + DataSourceMaxIdleConnectionSize = 10 // 最大db连接空闲数 + DataSourceConnMaxLifetime = 30 // 超过空闲数连接存活时间 + DataSourceShowLog = true // 是否显示SQL语句 + DataSourceLogLevel = core.LOG_DEBUG // 日志级别 +) diff --git a/config/dns_server.go b/config/dns_server.go new file mode 100644 index 0000000..f71ea0a --- /dev/null +++ b/config/dns_server.go @@ -0,0 +1,10 @@ +package config + +/* + * @Description DNS服务配置 + * @Author www.odboy.cn + * @Date 20241108 + */ +const ( + DnsServerPort = ":53" +) diff --git a/config/web_server.go b/config/web_server.go new file mode 100644 index 0000000..8725c0d --- /dev/null +++ b/config/web_server.go @@ -0,0 +1,15 @@ +package config + +/* + * @Description Web服务配置 + * @Author www.odboy.cn + * @Date 20241108 + */ +import "github.com/gin-gonic/gin" + +const ( + WebServerPort = ":18001" + WebMode = gin.ReleaseMode + WebReadTimeout = 10 + WebWriteTimeout = 10 +) diff --git a/constant/rr_type.go b/constant/rr_type.go new file mode 100644 index 0000000..8559bf5 --- /dev/null +++ b/constant/rr_type.go @@ -0,0 +1,19 @@ +package constant + +/* + * @Description 解析记录类型 常量 + * @Author www.odboy.cn + * @Date 20241107 + */ +const ( + // R_A A记录 + R_A = "A" + // R_AAAA AAAA记录 + R_AAAA = "AAAA" + // R_CNAME CNAME记录 + R_CNAME = "CNAME" + // R_MX MX记录 + R_MX = "MX" + // R_TXT TXT记录 + R_TXT = "TXT" +) diff --git a/controller/resolve_record.go b/controller/resolve_record.go new file mode 100644 index 0000000..bf39d4c --- /dev/null +++ b/controller/resolve_record.go @@ -0,0 +1,248 @@ +package controller + +/* + * @Description Web控制层 + * @Author www.odboy.cn + * @Date 20241108 + */ +import ( + "fmt" + "github.com/gin-gonic/gin" + "kenaito-dns/constant" + "kenaito-dns/dao" + "kenaito-dns/domain" + "kenaito-dns/util" + "net/http" + "strings" +) + +func InitRestFunc(r *gin.Engine) { + // 健康检查 + r.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "pong", + }) + }) + // 创建RR记录 + r.POST("/create", func(c *gin.Context) { + var jsonObj domain.CreateResolveRecord + err := c.ShouldBindJSON(&jsonObj) + newRecord, isErr := validRequestBody(c, err, jsonObj.Name, jsonObj.Type, jsonObj.Ttl, jsonObj.Value, false) + if isErr { + return + } + if dao.IsResolveRecordExist(newRecord) { + c.JSON(http.StatusBadRequest, gin.H{"message": "记录 " + newRecord.Name + " " + newRecord.RecordType + " " + newRecord.Value + " 已存在"}) + return + } + newRecord.Ttl = jsonObj.Ttl + executeResult, err, oldVersion, newVersion := dao.BackupResolveRecord(newRecord) + if !executeResult { + c.JSON(http.StatusBadRequest, gin.H{"message": fmt.Sprintf("添加"+newRecord.RecordType+"记录失败, %v", err)}) + return + } + executeResult, _ = dao.SaveResolveRecord(newRecord) + if !executeResult { + c.JSON(http.StatusBadRequest, gin.H{"message": fmt.Sprintf("添加"+newRecord.RecordType+"记录失败, %v", err)}) + return + } + body := make(map[string]interface{}) + body["oldVersion"] = oldVersion + body["newVersion"] = newVersion + c.JSON(http.StatusOK, gin.H{ + "message": "添加" + newRecord.RecordType + "记录成功", + "body": body, + }) + return + }) + // 删除RR记录 + r.POST("/remove", func(c *gin.Context) { + var jsonObj domain.RemoveResolveRecord + err := c.ShouldBindJSON(&jsonObj) + newRecord, isErr := validRequestBody(c, err, jsonObj.Name, jsonObj.Type, 0, jsonObj.Value, true) + if isErr { + return + } + if !dao.IsResolveRecordExist(newRecord) { + c.JSON(http.StatusBadRequest, gin.H{"message": "记录 " + newRecord.Name + " " + newRecord.RecordType + " " + newRecord.Value + " 不存在"}) + return + } + executeResult, err, oldVersion, newVersion := dao.BackupResolveRecord(newRecord) + if !executeResult { + c.JSON(http.StatusBadRequest, gin.H{"message": fmt.Sprintf("删除"+newRecord.RecordType+"记录失败, %v", err)}) + return + } + executeResult, err = dao.RemoveResolveRecord(newRecord) + if !executeResult { + c.JSON(http.StatusBadRequest, gin.H{"message": fmt.Sprintf("删除"+newRecord.RecordType+"记录失败, %v", err)}) + return + } + body := make(map[string]interface{}) + body["oldVersion"] = oldVersion + body["newVersion"] = newVersion + c.JSON(http.StatusOK, gin.H{ + "message": "删除" + newRecord.RecordType + "记录成功", + "body": body, + }) + return + }) + // 修改RR记录 + r.POST("/modify", func(c *gin.Context) { + var jsonObj domain.ModifyResolveRecord + err := c.ShouldBindJSON(&jsonObj) + if jsonObj.Id == 0 { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数ID(id)必填"}) + return + } + newRecord, isErr := validRequestBody(c, err, jsonObj.Name, jsonObj.Type, jsonObj.Ttl, jsonObj.Value, false) + if isErr { + return + } + if !dao.IsResolveRecordExistById(jsonObj.Id) { + c.JSON(http.StatusBadRequest, gin.H{"message": "记录 " + newRecord.Name + " " + newRecord.RecordType + " " + newRecord.Value + " 不存在"}) + return + } + if dao.IsUpdResolveRecordExist(jsonObj.Id, newRecord) { + c.JSON(http.StatusBadRequest, gin.H{"message": "记录 " + newRecord.Name + " " + newRecord.RecordType + " " + newRecord.Value + " 已存在"}) + return + } + executeResult, err, oldVersion, newVersion := dao.BackupResolveRecord(newRecord) + if !executeResult { + c.JSON(http.StatusBadRequest, gin.H{"message": fmt.Sprintf("修改"+newRecord.RecordType+"记录失败, %v", err)}) + return + } + executeResult, err = dao.ModifyResolveRecordById(jsonObj.Id, newRecord) + if !executeResult { + c.JSON(http.StatusBadRequest, gin.H{"message": fmt.Sprintf("更新"+newRecord.RecordType+"记录失败, %v", err)}) + return + } + body := make(map[string]interface{}) + body["oldVersion"] = oldVersion + body["newVersion"] = newVersion + c.JSON(http.StatusOK, gin.H{ + "message": "更新" + newRecord.RecordType + "记录成功", + "body": body, + }) + return + }) + // 分页查询RR记录 + r.POST("/queryPage", func(c *gin.Context) { + var jsonObj domain.QueryPageArgs + err := c.ShouldBindJSON(&jsonObj) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": fmt.Sprintf("校验失败, %v", err)}) + return + } + records := dao.FindResolveRecordPage(jsonObj.Page, jsonObj.PageSize, &jsonObj) + c.JSON(http.StatusOK, gin.H{"message": "分页查询RR记录成功", "body": records}) + return + }) + // 根据id查询RR记录明细 + r.POST("/queryById", func(c *gin.Context) { + var jsonObj domain.QueryByIdArgs + err := c.ShouldBindJSON(&jsonObj) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": fmt.Sprintf("校验失败, %v", err)}) + return + } + records := dao.FindResolveRecordById(jsonObj.Id) + c.JSON(http.StatusOK, gin.H{"message": "根据id查询RR记录明细成功", "body": records}) + return + }) + // 查询变更历史记录 + r.POST("/queryVersionList", func(c *gin.Context) { + records := dao.FindResolveVersion() + c.JSON(http.StatusOK, gin.H{"message": "查询变更历史记录列表成功", "body": records}) + return + }) + // 回滚到某一版本 + r.POST("/rollback", func(c *gin.Context) { + var jsonObj domain.RollbackVersionArgs + err := c.ShouldBindJSON(&jsonObj) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": fmt.Sprintf("校验失败, %v", err)}) + return + } + versions := dao.FindResolveRecordByVersion(jsonObj.Version) + if len(versions) == 0 { + c.JSON(http.StatusBadRequest, gin.H{"message": fmt.Sprintf("版本号 %d 不存在, 回滚失败", jsonObj.Version)}) + return + } + executeResult, err := dao.ModifyResolveVersion(jsonObj.Version) + if !executeResult { + c.JSON(http.StatusBadRequest, gin.H{"message": fmt.Sprintf("回滚失败, %v", err)}) + return + } + body := make(map[string]interface{}) + body["currentVersion"] = jsonObj.Version + c.JSON(http.StatusOK, gin.H{ + "message": "回滚成功", + "body": body, + }) + return + }) +} + +func validRequestBody(c *gin.Context, err error, name string, recordType string, ttl int, value string, isDelete bool) (*dao.ResolveRecord, bool) { + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"message": fmt.Sprintf("校验失败, %v", err)}) + return nil, true + } + if util.IsBlank(name) { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数主机记录(name)必填, 例如: www.odboy.cn 或 odboy.cn"}) + return nil, true + } + if util.IsBlank(recordType) { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数记录类型(type)必填, 目前支持 A、AAAA 记录"}) + return nil, true + } + if util.IsBlank(value) { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数记录值(value)必填"}) + return nil, true + } + if !isDelete { + if ttl < 10 { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数缓存有效时间(ttl)有误,必须大于等于10"}) + return nil, true + } + } + if !util.IsValidName(name) { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数主机记录(name)有误,无效的主机记录"}) + return nil, true + } + switch recordType { + case constant.R_A: + if !util.IsIPv4(value) { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数记录值(value)有误,无效的IPv4地址"}) + return nil, true + } + case constant.R_AAAA: + if !util.IsIPv6(value) { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数记录值(value)有误,无效的IPv6地址"}) + return nil, true + } + case constant.R_CNAME: + if !util.IsValidDomain(value) { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数记录值(value)有误,无效的主机记录"}) + return nil, true + } + case constant.R_MX: + if !util.IsValidDomain(value) { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数记录值(value)有误,无效的主机记录"}) + return nil, true + } + case constant.R_TXT: + if len(value) > 512 { + c.JSON(http.StatusBadRequest, gin.H{"message": "参数记录值(value)有误,长度必须 <= 512"}) + return nil, true + } + default: + c.JSON(http.StatusBadRequest, gin.H{"message": "参数记录值(type)有误,不支持的记录类型: " + recordType}) + return nil, true + } + newRecord := new(dao.ResolveRecord) + newRecord.Name = strings.TrimSpace(name) + newRecord.RecordType = strings.TrimSpace(recordType) + newRecord.Value = strings.TrimSpace(value) + return newRecord, false +} diff --git a/core/handler.go b/core/handler.go new file mode 100644 index 0000000..2745ec9 --- /dev/null +++ b/core/handler.go @@ -0,0 +1,156 @@ +package core + +/* + * @Description DNS解析处理入口 + * @Author www.odboy.cn + * @Date 20241107 + */ +import ( + "fmt" + "github.com/miekg/dns" + "kenaito-dns/constant" + "kenaito-dns/dao" + "net" +) + +func HandleDNSRequest(w dns.ResponseWriter, r *dns.Msg) { + msg := new(dns.Msg) + msg.SetReply(r) + // 将 DNS 响应标记为权威应答 + msg.Authoritative = true + // 将 DNS 响应标记为递归可用 + msg.RecursionAvailable = true + // 遍历请求中的问题部分,生成相应的回答 + for _, question := range r.Question { + switch question.Qtype { + case dns.TypeA: + handleARecord(question, msg) + case dns.TypeAAAA: + handleAAAARecord(question, msg) + case dns.TypeCNAME: + handleCNAMERecord(question, msg) + case dns.TypeMX: + handleMXRecord(question, msg) + case dns.TypeTXT: + handleTXTRecord(question, msg) + } + } + // 发送响应 + err := w.WriteMsg(msg) + if err != nil { + fmt.Printf("=== Handle Failed === %v \n", err) + } +} + +// 构建 A 记录 IPV4 +func handleARecord(q dns.Question, msg *dns.Msg) { + name := q.Name + queryName := name[0 : len(name)-1] + records := dao.FindResolveRecordByNameType(queryName, constant.R_A) + if len(records) > 0 { + for _, record := range records { + fmt.Printf("=== A记录 === 请求解析的域名:%s,解析的目标IP地址:%s\n", name, record.Value) + ip := net.ParseIP(record.Value) + rr := &dns.A{ + Hdr: dns.RR_Header{ + Name: name, + Rrtype: dns.TypeA, + Class: dns.ClassINET, + Ttl: uint32(record.Ttl) * 60, + }, + A: ip, + } + msg.Answer = append(msg.Answer, rr) + } + } +} + +// 构建 AAAA 记录 IPV6 +func handleAAAARecord(q dns.Question, msg *dns.Msg) { + name := q.Name + queryName := name[0 : len(name)-1] + records := dao.FindResolveRecordByNameType(queryName, constant.R_AAAA) + if len(records) > 0 { + for _, record := range records { + fmt.Printf("=== AAAA记录 === 请求解析的域名:%s,解析的目标IP地址:%s\n", name, record.Value) + ip := net.ParseIP(record.Value) + rr := &dns.AAAA{ + Hdr: dns.RR_Header{ + Name: name, + Rrtype: dns.TypeAAAA, + Class: dns.ClassINET, + Ttl: uint32(record.Ttl) * 60, + }, + AAAA: ip, + } + msg.Answer = append(msg.Answer, rr) + } + } +} + +// 构建 CNAME 记录 +func handleCNAMERecord(q dns.Question, msg *dns.Msg) { + name := q.Name + queryName := name[0 : len(name)-1] + records := dao.FindResolveRecordByNameType(queryName, constant.R_CNAME) + if len(records) > 0 { + for _, record := range records { + fmt.Printf("=== CNAME记录 === 请求解析的域名:%s,解析的目标域名:%s\n", name, record.Value) + rr := &dns.CNAME{ + Hdr: dns.RR_Header{ + Name: name, + Rrtype: dns.TypeCNAME, + Class: dns.ClassINET, + Ttl: uint32(record.Ttl) * 60, + }, + Target: record.Value + ".", + } + msg.Answer = append(msg.Answer, rr) + } + } +} + +// 构建 MX 记录 +func handleMXRecord(q dns.Question, msg *dns.Msg) { + name := q.Name + queryName := name[0 : len(name)-1] + records := dao.FindResolveRecordByNameType(queryName, constant.R_MX) + if len(records) > 0 { + for _, record := range records { + fmt.Printf("=== MX记录 === 请求解析的域名:%s,解析的目标域名:%s, MX优先级: 10\n", name, record.Value) + rr := &dns.MX{ + Hdr: dns.RR_Header{ + Name: name, + Rrtype: dns.TypeMX, + Class: dns.ClassINET, + Ttl: uint32(record.Ttl) * 60, + }, + Preference: 10, + Mx: record.Value + ".", + } + msg.Answer = append(msg.Answer, rr) + } + } +} + +// 构建 TXT 记录 +func handleTXTRecord(q dns.Question, msg *dns.Msg) { + name := q.Name + queryName := name[0 : len(name)-1] + records := dao.FindResolveRecordByNameType(queryName, constant.R_TXT) + if len(records) > 0 { + for _, record := range records { + fmt.Printf("=== TXT记录 === 请求解析的域名:%s,解析的目标值:%s\n", name, record.Value) + rr := &dns.TXT{ + Hdr: dns.RR_Header{ + Name: name, + Rrtype: dns.TypeTXT, + Class: dns.ClassINET, + Ttl: uint32(record.Ttl) * 60, + }, + Txt: []string{record.Value}, + } + msg.Answer = append(msg.Answer, rr) + } + } +} diff --git a/dao/database.go b/dao/database.go new file mode 100644 index 0000000..2c84caa --- /dev/null +++ b/dao/database.go @@ -0,0 +1,39 @@ +package dao + +/* + * @Description 连接数据库 + * @Author www.odboy.cn + * @Date 20241107 + */ +import ( + "fmt" + "github.com/go-xorm/xorm" + _ "github.com/mattn/go-sqlite3" + "kenaito-dns/config" + "time" +) + +var ( + Engine *xorm.Engine +) + +func init() { + var err error + Engine, err = xorm.NewEngine(config.DataSourceDriverName, config.DataSourceName) + if err != nil { + fmt.Println(err) + } + fmt.Println("[xorm] [info] " + time.Now().Format(config.AppTimeFormat) + " 数据库引擎创建成功(database engine create success)") + // 连接池配置 + Engine.SetMaxOpenConns(config.DataSourceMaxOpenConnectionSize) + Engine.SetMaxIdleConns(config.DataSourceMaxIdleConnectionSize) + Engine.SetConnMaxLifetime(config.DataSourceConnMaxLifetime * time.Minute) + // 日志相关 + Engine.ShowSQL(config.DataSourceShowLog) + Engine.Logger().SetLevel(config.DataSourceLogLevel) + err = Engine.Ping() + if err != nil { + fmt.Println(err) + } + fmt.Println("[xorm] [info] " + time.Now().Format(config.AppTimeFormat) + " 数据库连接成功(database engine connect success)") +} diff --git a/dao/resolve_config.go b/dao/resolve_config.go new file mode 100644 index 0000000..242c462 --- /dev/null +++ b/dao/resolve_config.go @@ -0,0 +1,42 @@ +package dao + +/* + * @Description 解析配置定义与操作 + * @Author www.odboy.cn + * @Date 20241107 + */ +import ( + "fmt" +) + +type ResolveVersion struct { + Id int `xorm:"pk not null integer 'id' autoincr"` + CurrentVersion int `xorm:"not null integer 'curr_version'"` +} + +func getResolveVersion() int { + var records []ResolveVersion + err := Engine.Table("resolve_config").Where("`id` = ?", 1).Find(&records) + if err != nil { + fmt.Println(err) + return 0 + } + if len(records) == 0 { + return 0 + } + return records[0].CurrentVersion +} + +func ModifyResolveVersion(currentVersion int) (bool, error) { + wrapper := new(ResolveVersion) + wrapper.Id = 1 + + updateRecord := new(ResolveVersion) + updateRecord.CurrentVersion = currentVersion + _, err := Engine.Table("resolve_config").Update(updateRecord, wrapper) + if err != nil { + fmt.Println(err) + return false, err + } + return true, nil +} diff --git a/dao/resolve_record.go b/dao/resolve_record.go new file mode 100644 index 0000000..a36ed64 --- /dev/null +++ b/dao/resolve_record.go @@ -0,0 +1,184 @@ +package dao + +/* + * @Description 解析记录定义与操作 + * @Author www.odboy.cn + * @Date 20241107 + */ +import ( + "fmt" + "kenaito-dns/domain" + "kenaito-dns/util" + "strings" +) + +type ResolveRecord struct { + Id int `xorm:"pk not null integer 'id' autoincr"` + Name string `xorm:"not null text 'name'"` + RecordType string `xorm:"not null text 'record_type'"` + Ttl int `xorm:"not null integer 'ttl'"` + Value string `xorm:"not null text 'value'"` + Version int `xorm:"not null integer 'version'"` +} + +func FindResolveRecordById(id int) []ResolveRecord { + var records []ResolveRecord + err := Engine.Table("resolve_record").Where("`id` = ?", id).Find(&records) + if err != nil { + fmt.Println(err) + } + return records +} + +func FindResolveRecordByVersion(version int) []ResolveRecord { + var records []ResolveRecord + err := Engine.Table("resolve_record").Where("`version` = ?", version).Find(&records) + if err != nil { + fmt.Println(err) + } + return records +} + +func FindResolveRecordByNameType(name string, recordType string) []ResolveRecord { + var records []ResolveRecord + err := Engine.Table("resolve_record").Where("`name` = ? and `record_type` = ? and `version` = ?", name, recordType, getResolveVersion()).Find(&records) + if err != nil { + fmt.Println(err) + } + return records +} + +func FindResolveRecordPage(pageNo int, pageSize int, args *domain.QueryPageArgs) []*ResolveRecord { + // 每页显示5条记录 + if pageSize <= 5 { + pageSize = 5 + } + // 要查询的页码 + if pageNo <= 0 { + pageNo = 1 + } + // 计算跳过的记录数 + offset := (pageNo - 1) * pageSize + records := make([]*ResolveRecord, 0) + session := Engine.Table("resolve_record").Where("") + if args != nil { + if !util.IsBlank(args.Name) { + qs := "%" + strings.TrimSpace(args.Name) + "%" + session.And("`name` LIKE ?", qs) + } + if !util.IsBlank(args.Type) { + qs := strings.TrimSpace(args.Type) + session.And("`record_type` = ?", qs) + } + if !util.IsBlank(args.Value) { + qs := strings.TrimSpace(args.Value) + session.And("`value` = ?", qs) + } + } + session.And("`version` = ?", getResolveVersion()) + err := session.Limit(pageSize, offset).Find(&records) + if err != nil { + fmt.Println(err) + } + return records +} + +func SaveResolveRecord(wrapper *ResolveRecord) (bool, error) { + _, err := Engine.Table("resolve_record").Insert(wrapper) + if err != nil { + fmt.Println(err) + return false, err + } + return true, nil +} + +func BackupResolveRecord(record *ResolveRecord) (bool, error, int, int) { + var backupRecords []*ResolveRecord + oldVersion := getResolveVersion() + newVersion := getResolveVersion() + 1 + oldRecords := FindResolveRecordByVersion(oldVersion) + for _, oldRecord := range oldRecords { + newRecord := new(ResolveRecord) + newRecord.Name = oldRecord.Name + newRecord.RecordType = oldRecord.RecordType + newRecord.Ttl = oldRecord.Ttl + newRecord.Value = oldRecord.Value + newRecord.Version = newVersion + backupRecords = append(backupRecords, newRecord) + } + record.Version = newVersion + if len(backupRecords) > 0 { + _, err := Engine.Table("resolve_record").Insert(backupRecords) + if err != nil { + return false, err, 0, 0 + } + } + updRecord := new(ResolveVersion) + updRecord.CurrentVersion = newVersion + condition := new(ResolveVersion) + condition.Id = 1 + _, err := Engine.Table("resolve_config").Update(updRecord, condition) + if err != nil { + return false, err, 0, 0 + } + return true, nil, oldVersion, newVersion +} + +func RemoveResolveRecord(wrapper *ResolveRecord) (bool, error) { + _, err := Engine.Table("resolve_record").Delete(wrapper) + if err != nil { + fmt.Println(err) + return false, err + } + return true, nil +} + +func IsResolveRecordExist(wrapper *ResolveRecord) bool { + wrapper.Version = getResolveVersion() + count, err := Engine.Table("resolve_record").Count(wrapper) + if err != nil { + fmt.Println(err) + return false + } + return count > 0 +} + +func IsResolveRecordExistById(id int) bool { + wrapper := new(ResolveRecord) + wrapper.Id = id + return IsResolveRecordExist(wrapper) +} + +func IsUpdResolveRecordExist(id int, wrapper *ResolveRecord) bool { + r := new(ResolveRecord) + r.Name = wrapper.Name + r.RecordType = wrapper.RecordType + r.Value = wrapper.Value + r.Version = getResolveVersion() + count, err := Engine.Table("resolve_record").Where("id != ?", id).Count(r) + if err != nil { + fmt.Println(err) + return false + } + return count > 0 +} + +func ModifyResolveRecordById(id int, updateRecord *ResolveRecord) (bool, error) { + wrapper := new(ResolveRecord) + wrapper.Id = id + _, err := Engine.Table("resolve_record").Update(updateRecord, wrapper) + if err != nil { + fmt.Println(err) + return false, err + } + return true, nil +} + +func FindResolveVersion() []int { + var records []int + err := Engine.Select("distinct version").Cols("version").Table("resolve_record").Find(&records) + if err != nil { + fmt.Println(err) + } + return records +} diff --git a/dns.sqlite3 b/dns.sqlite3 new file mode 100644 index 0000000000000000000000000000000000000000..5b88830a04bd59f4b89f2d16ce598bacedd343df GIT binary patch literal 36864 zcmeI*!E4%39Ki7xW3}34>2{Ib9$|-umR)w}VTa9@Fl5%YE`@m#XJ0#uHEWE*PNUs@ zf5!fW!S4HGgk5&sX)mUM>BJ7{4sE|L{)msq@5T4QU*JK+*Zb8@q~uW$p0*5^v#^}rg-w~r&atc%)&_A|6UkbUsv9x$DBNd5I_I{ z1Q0*~0WENEXgmjEVS#fepD($tasDxCz3r+{^@HwEHQPb&sB=8oTi&jFW#5y2d8_J4 zXL8VyYgSQ8r*r7YO3n9PdUaXb^=0j#T9teC%1*i7kgvRkEFbv0m71Pv$E*1paf)v%ubqj+t|zGO6%)J zV{Fks>*^hCv#-tus@GOiU2{TXYH%%H(uU5AZ$hKH@?DD88Sm@ky?(sU&*{it_VtN| z00IagfB*srAb