Compare commits

..

2 Commits

Author SHA1 Message Date
骑着蜗牛追导弹 462e354964 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	README.md
#	dns.sqlite3
#	main.go
2024-11-08 21:57:18 +08:00
骑着蜗牛追导弹 af0bb44e33 release: version 1.0.0 2024-11-08 21:57:05 +08:00
17 changed files with 1022 additions and 142 deletions

View File

@ -1,47 +1,98 @@
# kenaito-dns # kenaito-dns
DNS服务器可通过Web管理界面随意设置灵活的解析规则。为了纯血自研devops平台而生。 ## 背景
# 环境依赖 Bind9不能直接支持API的方式添加解析记录 通过脚本修改Bind服务器配置这种事情实在是太冒险了而且没有发现有开源的、性能嘎嘎好的DNS服务器项目。
## 简介
一个轻量级 DNS 服务器让变更解析记录简单、优雅为了纯血自研devops平台而生。
## 环境依赖
- gcc - gcc
- go version >= 1.20 - go version >= 1.20
# Go代理地址配置 ## 接口文档
[在线阅读](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/) [去看看](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 ```shell
yum install bind-utils -y yum install bind-utils -y
``` ```
# nslookup指定dns服务器查询 #### nslookup指定dns服务器查询
```shell ```shell
# 这里dns服务器为 192.168.1.103 # 这里dns服务器为 192.168.1.103
nslookup example.com 192.168.1.103 nslookup example.com 192.168.1.103
``` ```
# 本程序所用依赖,感谢开源者的无私奉献 ## 特别鸣谢
- [数据库操作](http://xorm.topgoer.com/) - [数据库操作 - xorm](http://xorm.topgoer.com/)
- [DNS解析](https://github.com/miekg/dns) - [DNS解析 - miekg/dns](https://github.com/miekg/dns)
- [web](https://gin-gonic.com/zh-cn/docs/quickstart/) - [Web - gin](https://gin-gonic.com/zh-cn/docs/quickstart/)
# window安装gcc(记得配置环境变量哦, 记得重启电脑哦) ## 代码托管
- [去看看](https://github.com/niXman/mingw-builds-binaries/releases) - Gitea: [https://gitea.odboy.cn/odboy/kenaito-dns](https://gitea.odboy.cn/odboy/kenaito-dns)
- [去下载](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) - Github: [https://github.com/odboy-tianjun/kenaito-dns](https://github.com/odboy-tianjun/kenaito-dns)
- [国内下载](https://oss.odboy.cn/blog/files/windows-gcc/x86_64-14.2.0-release-posix-seh-msvcrt-rt_v12-rev0.7z) - Gitee: [https://gitee.com/odboy/kenaito-dns](https://gitee.com/odboy/kenaito-dns)
# window验证gcc ## 微信交流群
```shell ![wxcode](https://oss.odboy.cn/blog/files/userinfo/MyWxCode.png)
gcc -v
```
# sql转各种在线工具 (扫码添加微信备注kenaito-dns邀您加入群聊)
[去看看](https://gotool.top/) 加入群聊的好处:
- 第一时间收到项目更新通知。
- 第一时间收到项目 bug 通知。
- 第一时间收到新增开源案例通知。
- 和众多大佬一起互相 (huá shuǐ) 交流 (mō yú)。

67
RRType.md Normal file
View File

@ -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分钟这有助于解析快速生效。
```

6
config/app.go Normal file
View File

@ -0,0 +1,6 @@
package config
const (
AppVersion = "1.0.0"
AppTimeFormat = "2006/01/02 15:04:05.999999"
)

18
config/database.go Normal file
View File

@ -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 // 日志级别
)

10
config/dns_server.go Normal file
View File

@ -0,0 +1,10 @@
package config
/*
* @Description DNS服务配置
* @Author www.odboy.cn
* @Date 20241108
*/
const (
DnsServerPort = ":53"
)

15
config/web_server.go Normal file
View File

@ -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
)

19
constant/rr_type.go Normal file
View File

@ -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"
)

View File

@ -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
}

156
core/handler.go Normal file
View File

@ -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)
}
}
}

39
dao/database.go Normal file
View File

@ -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)")
}

42
dao/resolve_config.go Normal file
View File

@ -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
}

184
dao/resolve_record.go Normal file
View File

@ -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
}

Binary file not shown.

43
domain/resolve_record.go Normal file
View File

@ -0,0 +1,43 @@
package domain
/*
* @Description 领域模型定义
* @Author www.odboy.cn
* @Date 20241108
*/
type CreateResolveRecord struct {
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required"`
Ttl int `json:"ttl" binding:"required"`
Value string `json:"value" binding:"required"`
}
type RemoveResolveRecord struct {
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required"`
Value string `json:"value" binding:"required"`
}
type ModifyResolveRecord struct {
Id int `json:"id" binding:"required"`
Name string `json:"name" binding:"required"`
Type string `json:"type" binding:"required"`
Ttl int `json:"ttl" binding:"required"`
Value string `json:"value" binding:"required"`
}
type QueryPageArgs struct {
Page int `json:"page" binding:"required"`
PageSize int `json:"pageSize" binding:"required"`
Name string `json:"name"`
Type string `json:"type"`
Value string `json:"value"`
}
type QueryByIdArgs struct {
Id int `json:"id" binding:"required"`
}
type RollbackVersionArgs struct {
Version int `json:"version" binding:"required"`
}

View File

@ -1,80 +0,0 @@
package main
import (
"fmt"
"github.com/miekg/dns"
"net"
)
// 构建 A 记录的函数 IPV4
func handleARecord(q dns.Question, msg *dns.Msg) {
name := q.Name
targetIp := "192.235.111.111"
fmt.Printf("请求解析的域名:%s,解析的目标IP地址:%s\n", name, targetIp)
ip := net.ParseIP(targetIp)
rr := &dns.A{
Hdr: dns.RR_Header{
Name: name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: 60,
},
A: ip,
}
msg.Answer = append(msg.Answer, rr)
}
//// 构建 A 记录的函数 IPV6
//func handleAAAARecord(q dns.Question, msg *dns.Msg) {
// ip := net.ParseIP("rsdw::8888")
// rr := &dns.AAAA{
// Hdr: dns.RR_Header{
// Name: q.Name,
// Rrtype: dns.TypeAAAA,
// Class: dns.ClassINET,
// Ttl: 60,
// },
// AAAA: ip,
// }
// msg.Answer = append(msg.Answer, rr)
//}
//func handleCNAMERecord(q dns.Question, msg *dns.Msg) {
// rr := &dns.CNAME{
// Hdr: dns.RR_Header{
// Name: q.Name,
// Rrtype: dns.TypeCNAME,
// Class: dns.ClassINET,
// Ttl: 60,
// },
// Target: "example.com.",
// }
// msg.Answer = append(msg.Answer, rr)
//}
//
//func handleMXRecord(q dns.Question, msg *dns.Msg) {
// rr := &dns.MX{
// Hdr: dns.RR_Header{
// Name: q.Name,
// Rrtype: dns.TypeMX,
// Class: dns.ClassINET,
// Ttl: 60,
// },
// Preference: 10,
// Mx: "mail.example.com.",
// }
// msg.Answer = append(msg.Answer, rr)
//}
//
//func handleTXTRecord(q dns.Question, msg *dns.Msg) {
// rr := &dns.TXT{
// Hdr: dns.RR_Header{
// Name: q.Name,
// Rrtype: dns.TypeTXT,
// Class: dns.ClassINET,
// Ttl: 60,
// },
// Txt: []string{"v=spf1 include:_spf.example.com ~all"},
// }
// msg.Answer = append(msg.Answer, rr)
//}

102
main.go
View File

@ -1,65 +1,81 @@
package main package main
/*
* @Description 服务入口
* @Author www.odboy.cn
* @Date 20241107
*/
import ( import (
"fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/miekg/dns" "github.com/miekg/dns"
"log" "kenaito-dns/config"
"kenaito-dns/controller"
"kenaito-dns/core"
"net/http" "net/http"
"time" "time"
) )
func main() { func main() {
fmt.Println("[app] [info] kenaito-dns version = " + config.AppVersion)
go initDNSServer()
initRestfulServer() initRestfulServer()
// 注册 DNS 请求处理函数
dns.HandleFunc(".", handleDNSRequest)
// 设置服务器地址和协议
server := &dns.Server{Addr: ":53", Net: "udp"}
// 开始监听
log.Printf("Starting DNS server on %s\n", server.Addr)
if err := server.ListenAndServe(); err != nil {
log.Fatalf("Failed to start server: %s\n", err.Error())
}
} }
func handleDNSRequest(w dns.ResponseWriter, r *dns.Msg) { func initDNSServer() {
msg := new(dns.Msg) // 注册 DNS 请求处理函数
msg.SetReply(r) dns.HandleFunc(".", core.HandleDNSRequest)
// 将 DNS 响应标记为权威应答 // 设置服务器地址和协议
msg.Authoritative = true server := &dns.Server{Addr: config.DnsServerPort, Net: "udp"}
// 将 DNS 响应标记为递归可用 // 开始监听
// msg.RecursionAvailable = true fmt.Printf("[dns] [info] Starting DNS server on %s\n", server.Addr)
// 遍历请求中的问题部分,生成相应的回答 if err := server.ListenAndServe(); err != nil {
for _, question := range r.Question { fmt.Printf("[dns] [error] Failed to start DNS server: %s\n", err.Error())
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)
} }
}
// 发送响应
w.WriteMsg(msg)
} }
func initRestfulServer() { func initRestfulServer() {
router := gin.Default() gin.SetMode(config.WebMode)
s := &http.Server{ // 创建一个新的 Gin 引擎实例,使用 gin.New() 方法来创建一个不包含任何默认中间件的实例
Addr: ":18001", router := gin.New()
// LoggerWithFormatter 中间件会写入日志到 gin.DefaultWriter
// 默认 gin.DefaultWriter = os.Stdout
// 自定义的日志格式化函数
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
// 自定义日志格式
return fmt.Sprintf("[gin] [info] %s [Request] [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
// 请求时间戳
param.TimeStamp.Format(config.AppTimeFormat),
// 客户端 IP 地址
param.ClientIP,
// 请求方法 (GET, POST 等)
param.Method,
// 请求路径
param.Path,
// 请求协议
param.Request.Proto,
// 响应状态码
param.StatusCode,
// 请求延迟时间
param.Latency,
// 用户代理
param.Request.UserAgent(),
// 错误信息(如果有的话)
param.ErrorMessage,
)
}))
// 使用 Recovery 中间件,处理任何出现的错误,并防止服务崩溃
router.Use(gin.Recovery())
server := &http.Server{
Addr: config.WebServerPort,
Handler: router, Handler: router,
ReadTimeout: 10 * time.Second, ReadTimeout: config.WebReadTimeout * time.Second,
WriteTimeout: 10 * time.Second, WriteTimeout: config.WebWriteTimeout * time.Second,
} }
initRestFunc(router) controller.InitRestFunc(router)
err := s.ListenAndServe() fmt.Printf("[gin] [info] Start Gin server: %s\n", config.WebServerPort)
err := server.ListenAndServe()
if err != nil { if err != nil {
log.Fatalln("restful服务监听失败") fmt.Printf("[gin] [error] Failed to start Gin server: %s\n", config.WebServerPort)
panic("restful服务监听失败")
} }
log.Println("restful服务监听ing")
} }

46
util/strtool.go Normal file
View File

@ -0,0 +1,46 @@
package util
/*
* @Description 工具类
* @Author www.odboy.cn
* @Date 20241107
*/
import (
"net"
"regexp"
"strings"
)
// IsBlank 检查字符串是否空
func IsBlank(s string) bool {
return strings.TrimSpace(s) == ""
}
// IsValidName 判断字符串是否是有效的域名
func IsValidName(s string) bool {
// 定义域名的正则表达式
domainRegex := `^([a-zA-Z0-9][a-zA-Z0-9\-]{1,61}[a-zA-Z0-9]\.)+[a-zA-Z0-9]{2,6}$`
re := regexp.MustCompile(domainRegex)
return re.MatchString(s)
}
// IsIPv4 判断字符串是否是ipv4地址
func IsIPv4(ipAddr string) bool {
ip := net.ParseIP(ipAddr)
return ip != nil && strings.Contains(ipAddr, ".")
}
// IsIPv6 判断字符串是否是ipv6地址
func IsIPv6(ipAddr string) bool {
ip := net.ParseIP(ipAddr)
return ip != nil && strings.Contains(ipAddr, ":")
}
// IsValidDomain 判断域名是否正常解析
func IsValidDomain(domain string) bool {
_, err := net.LookupHost(domain)
if err != nil {
return false
}
return true
}