270 lines
6.3 KiB
Go
270 lines
6.3 KiB
Go
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)
|
||
}
|
||
}
|