Files
openwrt-ddns-namesilo/main.go

270 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"context"
"flag"
"fmt"
"log"
"net"
"os"
"strings"
"time"
)
// 判断 IPv6 地址类型
func classifyIPv6(ip net.IP) string {
if ip.IsLoopback() {
return "Loopback"
} else if ip.IsLinkLocalUnicast() {
return "Link-Local"
} else if ip.IsGlobalUnicast() {
// 排除 ULA (Unique Local Address, fc00::/7),通常以 fd 或 fc 开头
if len(ip) == 16 && (ip[0]&0xfe) == 0xfc {
return "ULA"
}
return "Global"
}
return "Unknown"
}
// AutoGetBestIPv6 自动寻找最佳的 IPv6 地址和对应的接口名称
// 优先级: 接口名含 "pppoe" > "wan" > 其他
func AutoGetBestIPv6() (string, string) {
interfaces, err := net.Interfaces()
if err != nil {
log.Printf("获取接口列表失败: %v", err)
return "", "NULL"
}
var fallbackIP string
var fallbackIface string
// 遍历所有接口
for _, iface := range interfaces {
// 跳过未启用或回环接口
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
// 必须是 IPv6 且不是 IPv4
if ip == nil || ip.To4() != nil {
continue
}
// 获取地址类型
addrType := classifyIPv6(ip)
// 只处理公网 IP (Global Unicast),忽略 Link-Local (fe80) 和 ULA (fdxx)
if addrType == "Global" {
ipStr := ip.String()
ifaceName := iface.Name
// 策略 1: 如果接口名包含 "pppoe",这就是最完美的 WAN 口,直接返回
if strings.Contains(ifaceName, "pppoe") {
log.Printf("自动检测: 命中高优先级接口 (PPPoE): %s -> %s\n", ifaceName, ipStr)
return ifaceName, ipStr
}
// 策略 2: 如果接口名包含 "wan" (如 wan6, eth0.2),暂存,如果没有 pppoe 就用它
if strings.Contains(ifaceName, "wan") {
log.Printf("自动检测: 命中次优先级接口 (WAN): %s -> %s\n", ifaceName, ipStr)
return ifaceName, ipStr
}
// 策略 3: 其他有公网 IP 的接口 (如 br-lan),作为保底
if fallbackIP == "" {
fallbackIP = ipStr
fallbackIface = ifaceName
}
}
}
}
// 如果没有找到明确的 WAN 口,返回保底的接口(例如 br-lan
if fallbackIP != "" {
log.Printf("自动检测: 未找到 WAN 接口,使用保底接口: %s -> %s\n", fallbackIface, fallbackIP)
return fallbackIface, fallbackIP
}
return "", "NULL"
}
// getIPv6ByInterface 获取指定接口名称的 IPv6
func getIPv6ByInterface(ifaceName string) string {
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
log.Printf("无法找到接口 %s: %v", ifaceName, err)
return "NULL"
}
addrs, err := iface.Addrs()
if err != nil {
return "NULL"
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip != nil && ip.To4() == nil && classifyIPv6(ip) == "Global" {
return ip.String()
}
}
return "NULL"
}
// 使用指定的 DNS 服务器解析域名的 IPv6 地址
func check_domain_ipv6(domain string) string {
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{
Timeout: time.Second * 5,
}
return d.DialContext(ctx, "udp", "8.8.8.8:53")
},
}
ips, err := resolver.LookupIP(context.Background(), "ip6", domain)
if err != nil {
return "NULL"
}
for _, ip := range ips {
if ip.To4() == nil {
return ip.String()
}
}
return "NULL"
}
// 循环逻辑
func Loop(info INFO) {
var Domain_ipv6_addr string
var Now_ipv6_addr string
var currentInterface string
// 判断是否自动模式
if *info._interface == "auto" {
currentInterface, Now_ipv6_addr = AutoGetBestIPv6()
} else {
currentInterface = *info._interface
Now_ipv6_addr = getIPv6ByInterface(currentInterface)
}
if Now_ipv6_addr == "NULL" {
log.Println("警告: 无法获取本机 IPv6 地址,请检查网络连接。")
return
}
Domain_ipv6_addr = check_domain_ipv6(*info._Subdomain)
if Now_ipv6_addr != Domain_ipv6_addr {
log.Printf("检测到变动! 接口[%s] IP: %s | 域名解析 IP: %s\n", currentInterface, Now_ipv6_addr, Domain_ipv6_addr)
RecordID := FetchSubdomainRecord(*info._Key, *info._Domain, *info._Subdomain)
if RecordID != "NULL" {
rrid := RecordID
rrhost := strings.Split(*info._Subdomain, ".")[0]
if strings.HasSuffix(*info._Subdomain, "."+*info._Domain) {
rrhost = strings.TrimSuffix(*info._Subdomain, "."+*info._Domain)
}
rrvalue := Now_ipv6_addr
ProcessDNSUpdateForDomain(*info._Key, *info._Domain, rrid, rrhost, rrvalue)
}
} else {
log.Printf("IP 未变动 (接口: %s, IP: %s)。无需更新。\n", currentInterface, Now_ipv6_addr)
}
}
// Sleep 函数
func Sleep(m int) {
time.Sleep(time.Duration(m) * time.Minute)
}
type INFO struct {
_interface *string
_Domain *string
_Subdomain *string
_Key *string
}
func GetDaemon(_Subdomain *string) *string {
parts := strings.Split(*_Subdomain, ".")
if len(parts) < 2 {
fmt.Println("无效的域名")
return nil
}
mainDomain := parts[len(parts)-2] + "." + parts[len(parts)-1]
return &mainDomain
}
func main() {
daemon := flag.Bool("d", false, "守护进程模式")
// 修改默认值为 "auto"
_interface := flag.String("i", "auto", "网卡接口名称 (默认 auto 自动检测)")
_Subdomain := flag.String("s", "v6.aixiao.me", "子域名")
_key := flag.String("k", "NULL", "NameSilo API 密钥")
flag.Parse()
var info INFO
info._interface = _interface
info._Subdomain = _Subdomain
info._Key = _key
info._Domain = GetDaemon(_Subdomain)
if *info._Key == "NULL" {
envKey := os.Getenv("NAMESILO")
if envKey != "" {
info._Key = &envKey
} else {
log.Fatalf("密钥未设置,请通过 -k 参数或 NAMESILO 环境变量提供密钥")
}
}
fmt.Println("--------------------------------")
fmt.Println("主域名:", *info._Domain)
fmt.Println("子域名:", *info._Subdomain)
fmt.Println("密钥:", *info._Key)
fmt.Println("接口模式:", *info._interface)
fmt.Println("--------------------------------")
// 守护进程模式占位
if *daemon {
if Daemon != nil {
Daemon()
}
}
// 首次运行立即检测
Loop(info)
for {
Sleep(30)
Loop(info)
}
}