Compare commits
2 Commits
bd7d837a83
...
462e354964
Author | SHA1 | Date |
---|---|---|
骑着蜗牛追导弹 | 462e354964 | |
骑着蜗牛追导弹 | af0bb44e33 |
89
README.md
89
README.md
|
@ -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ú)。
|
|
@ -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分钟,这有助于解析快速生效。
|
||||||
|
```
|
|
@ -0,0 +1,6 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
const (
|
||||||
|
AppVersion = "1.0.0"
|
||||||
|
AppTimeFormat = "2006/01/02 15:04:05.999999"
|
||||||
|
)
|
|
@ -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 // 日志级别
|
||||||
|
)
|
|
@ -0,0 +1,10 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @Description DNS服务配置
|
||||||
|
* @Author www.odboy.cn
|
||||||
|
* @Date 20241108
|
||||||
|
*/
|
||||||
|
const (
|
||||||
|
DnsServerPort = ":53"
|
||||||
|
)
|
|
@ -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
|
||||||
|
)
|
|
@ -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"
|
||||||
|
)
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)")
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
BIN
dns.sqlite3
BIN
dns.sqlite3
Binary file not shown.
|
@ -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"`
|
||||||
|
}
|
80
handler.go
80
handler.go
|
@ -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
102
main.go
|
@ -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")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue