├── .gitignore ├── cache └── .placeholder ├── composer.json ├── ddns.php ├── function.php ├── index.html ├── readme.md ├── routeros_script ├── ddns.rsc ├── ddnsv6-for-pc.rsc └── ddnsv6.rsc └── service ├── aliyun.php └── dnspod.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor/ 3 | cache/ 4 | -------------------------------------------------------------------------------- /cache/.placeholder: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vibbow/routeros-ddns-script/dcd93138531e67186c4c7c922ad1a4099a730d04/cache/.placeholder -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "alibabacloud/alidns": "^1.8", 4 | "tencentcloud/dnspod": "^3.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /ddns.php: -------------------------------------------------------------------------------- 1 | ddns($domain, $accessIP); 40 | break; 41 | case 'dnspod': 42 | $ddnsService = new Dnspod($accessID, $accessSecret); 43 | echo $ddnsService->ddns($domain, $accessIP); 44 | break; 45 | default: 46 | throw new Exception('Unknown service type'); 47 | } 48 | } 49 | catch (Exception $e) { 50 | echo $e->getMessage(); 51 | } 52 | -------------------------------------------------------------------------------- /function.php: -------------------------------------------------------------------------------- 1 | RouterOS DDNS Server -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 用于 RouterOS 的 DDNS 脚本 2 | 适用于 阿里云 / 腾讯云 3 | 4 | 该脚本无任何售后技术支持 5 | Use it wisely -------------------------------------------------------------------------------- /routeros_script/ddns.rsc: -------------------------------------------------------------------------------- 1 | ########################################## 2 | ## RouterOS DDNS 脚本 for 阿里云 / 腾讯云 3 | ## 4 | ## 该 DDNS 脚本可自动 获取/识别/更新 IP 地址 5 | ## 兼容 阿里云 / 腾讯云 DNS接口 6 | ## 7 | ## 作者: vibbow 8 | ## https://vsean.net/ 9 | ## 10 | ## 修改日期: 2021/12/01 11 | ## 12 | ## 该脚本无任何售后技术支持 13 | ## Use it wisely 14 | ########################################## 15 | 16 | # 域名 17 | :local domainName "sub.example.com"; 18 | # wan接口名称 19 | :local wanInterface "ether1"; 20 | # 要使用的服务 (aliyun/dnspod) 21 | :local service "aliyun"; 22 | # API接口 Access ID 23 | :local accessID ""; 24 | # API接口 Access Secret 25 | :local accessSecret ""; 26 | 27 | 28 | # 腾讯云 (dnspod) 设置 29 | # 30 | # 一般情况下无需设置此内容 31 | # 服务器会自动识别 domainID 和 recordID 32 | # 33 | # 如一直提示 "当前域名无权限,请返回域名列表。" 34 | # 则需要手动设置 35 | :local domainID ""; 36 | :local recordID ""; 37 | 38 | 39 | # ==== 以下内容无需修改 ==== 40 | # ========================= 41 | 42 | :local publicIP; 43 | :local dnsIP; 44 | :local epicFail false; 45 | 46 | # 获取当前外网IP 47 | :do { 48 | :local interfaceIP [ /ip address get [ find interface=$wanInterface ] address ]; 49 | :set $interfaceIP [ :pick $interfaceIP 0 [ :find $interfaceIP "/" ] ]; 50 | 51 | :if ($interfaceIP ~ "^(10|100|172|192)\\.") \ 52 | do={ 53 | :local fetchResult [/tool fetch url="http://ip.3322.net/" mode=http as-value output=user]; 54 | :set $publicIP ($fetchResult->"data") 55 | :set $publicIP [ :pick $publicIP 0 [ :find $publicIP "\n" ] ]; 56 | :set $publicIP [ :toip $publicIP ] 57 | } \ 58 | else={ \ 59 | :set $publicIP [ :toip $interfaceIP ]; 60 | } 61 | } \ 62 | on-error { 63 | :set $epicFail true; 64 | :log error ("DDNS: Get public IP failed."); 65 | } 66 | 67 | # 获取当前解析的IP 68 | :do { 69 | :set $dnsIP [ :resolve $domainName ]; 70 | } \ 71 | on-error { 72 | :set $epicFail true; 73 | :log error ("DDNS: Resolve domain " . $domainName . " failed."); 74 | } 75 | 76 | # 如IP有变动,则更新解析 77 | :if ($epicFail = false && $publicIP != $dnsIP) \ 78 | do={ 79 | :local callUrl ("https://ddns.vsean.net/ddns.php"); 80 | :local postData ("service=" . $service . "&domain=" . $domainName . "&access_id=" . $accessID . "&access_secret=" . $accessSecret . "&domain_id=" . $domainID . "&record_id=" . $recordID); 81 | :local fetchResult [/tool fetch url=$callUrl mode=https http-method=post http-data=$postData as-value output=user]; 82 | :log info ("DDNS: " . $fetchResult->"data"); 83 | } 84 | -------------------------------------------------------------------------------- /routeros_script/ddnsv6-for-pc.rsc: -------------------------------------------------------------------------------- 1 | ########################################## 2 | ## RouterOS DDNS 脚本 for 阿里云 / 腾讯云 IPv6版 3 | ## 4 | ## 该 DDNS 脚本可自动对指定 PC 做 IPv6 DDNS 5 | ## 兼容 阿里云 / 腾讯云 DNS接口 6 | ## 7 | ## 作者: vibbow 8 | ## https://vsean.net/ 9 | ## 10 | ## 修改日期: 2024/7/7 11 | ## 12 | ## 该脚本无任何售后技术支持 13 | ## Use it wisely 14 | ########################################## 15 | 16 | # 用来DDNS的域名 17 | :local domainName "sub.example.com"; 18 | 19 | # 要更新的计算机MAC地址 20 | :local macAddress "AA:BB:CC:DD:EE:FF"; 21 | 22 | # 用来查找计算机的端口 (通常是bridge) 23 | :local lanInterface "bridge"; 24 | 25 | # 要使用DDNS的服务 (aliyun/dnspod) 26 | :local service "aliyun"; 27 | 28 | # DDNS API接口 Access ID 29 | :local accessID ""; 30 | 31 | # DDNS API接口 Access Secret 32 | :local accessSecret ""; 33 | 34 | 35 | # 腾讯云 (dnspod) 设置 36 | # 37 | # 一般情况下无需设置此内容 38 | # 服务器会自动识别 domainID 和 recordID 39 | # 40 | # 如一直提示 "当前域名无权限,请返回域名列表。" 41 | # 则需要手动设置 42 | :local domainID ""; 43 | :local recordID ""; 44 | 45 | 46 | # ==== 以下内容无需修改 ==== 47 | # ========================= 48 | 49 | 50 | :local epicFail false; 51 | :local ipv6Address; 52 | :local ipv6AddressList 53 | :local dnsAddress; 54 | 55 | 56 | # 获取指定mac的所有ipv6地址 57 | :do { 58 | :set ipv6AddressList [ /ipv6 neighbor find mac-address=$macAddress interface=$lanInterface ]; 59 | 60 | :local addressListLength [ :len $ipv6AddressList ]; 61 | 62 | if ($addressListLength = 0) \ 63 | do={ 64 | :log error ("No ipv6 address found for " . $macAddress); 65 | :set $epicFail true; 66 | } 67 | } \ 68 | on-error { 69 | :set $epicFail true: 70 | } 71 | 72 | 73 | # 获取非本地的ipv6地址 74 | if ($epicFail = false) \ 75 | do={ 76 | :foreach id in=$ipv6AddressList \ 77 | do={ 78 | :local eachAddress [ /ipv6 neighbor get $id address ]; 79 | :local eachAddressStr [ :toip6 $eachAddress ]; 80 | :local isLinkLocal false; 81 | 82 | if ($eachAddress in fc00::/7) \ 83 | do={ 84 | :set $isLinkLocal true; 85 | } 86 | 87 | if ($eachAddress in fd00::/8) \ 88 | do={ 89 | :set $isLinkLocal true; 90 | } 91 | 92 | if ($eachAddress in fe80::/10) \ 93 | do={ 94 | :set $isLinkLocal true; 95 | } 96 | 97 | if (!$isLinkLocal) \ 98 | do={ 99 | :set ipv6Address $eachAddressStr; 100 | } 101 | } 102 | 103 | :local addressLength [ :len $ipv6Address ]; 104 | if ($addressLength = 0) \ 105 | do={ 106 | :log error ("No public ipv6 address for " . $macAddress); 107 | :set $epicFail true; 108 | } 109 | } 110 | 111 | 112 | # 获取当前解析的IP 113 | :do { 114 | set $dnsAddress [ :resolve $domainName ]; 115 | } \ 116 | on-error { 117 | :set $epicFail true; 118 | :log error ("Resolve domain " . $domainName . " failed."); 119 | } 120 | 121 | 122 | # 更新 IPv6 地址到 DDNS 123 | if ($epicFail = false && $ipv6Address != $dnsAddress) \ 124 | do={ 125 | :local callUrl ("https://ddns6.vsean.net/ddns.php"); 126 | :local postData ("service=" . $service . "&domain=" . $domainName . "&ip=" . $ipv6Address . "&access_id=" . $accessID . "&access_secret=" . $accessSecret . "&domain_id=" . $domainID . "&record_id=" . $recordID); 127 | :local fetchResult [/tool fetch url=$callUrl mode=https http-method=post http-data=$postData as-value output=user]; 128 | :log info ("DDNSv6: " . $fetchResult->"data"); 129 | } 130 | -------------------------------------------------------------------------------- /routeros_script/ddnsv6.rsc: -------------------------------------------------------------------------------- 1 | ########################################## 2 | ## RouterOS DDNS 脚本 for 阿里云 / 腾讯云 IPv6版 3 | ## 4 | ## 该 DDNS 脚本可自动 获取/识别/更新 IP 地址 5 | ## 兼容 阿里云 / 腾讯云 DNS接口 6 | ## 7 | ## 作者: vibbow 8 | ## https://vsean.net/ 9 | ## 10 | ## 修改日期: 2024/7/22 11 | ## 12 | ## 该脚本无任何售后技术支持 13 | ## Use it wisely 14 | ########################################## 15 | 16 | # 域名 17 | :local domainName "sub.example.com"; 18 | # wan接口名称 19 | :local wanInterface "ether1"; 20 | # 要使用的服务 (aliyun/dnspod) 21 | :local service "aliyun"; 22 | # API接口 Access ID 23 | :local accessID ""; 24 | # API接口 Access Secret 25 | :local accessSecret ""; 26 | 27 | 28 | # 腾讯云 (dnspod) 设置 29 | # 30 | # 一般情况下无需设置此内容 31 | # 服务器会自动识别 domainID 和 recordID 32 | # 33 | # 如一直提示 "当前域名无权限,请返回域名列表。" 34 | # 则需要手动设置 35 | :local domainID ""; 36 | :local recordID ""; 37 | 38 | 39 | # ==== 以下内容无需修改 ==== 40 | # ========================= 41 | 42 | :local publicIP; 43 | :local dnsIP; 44 | :local epicFail false; 45 | 46 | # 获取当前接口IPv6地址 47 | :do { 48 | :local interfaceIP; 49 | :local interfaceIPList [ /ipv6 address find interface=$wanInterface global ]; 50 | :local interfaceIPListSize [ :len $interfaceIPList ]; 51 | 52 | # 如果接口上只有一个IP,那么直接使用这个IP 53 | if ($interfaceIPListSize = 1) \ 54 | do={ 55 | :set $interfaceIP [ /ipv6 address get $interfaceIPList address ]; 56 | } 57 | 58 | # 如果接口上有多个IP,那么找到非内网地址的IP 59 | if ($interfaceIPListSize > 1) \ 60 | do={ 61 | :foreach id in $interfaceIPList \ 62 | do={ 63 | :local eachAddress [ /ipv6 address get $id address ]; 64 | :local isLinkLocal false; 65 | 66 | if ($eachAddress in fc00::/7) \ 67 | do={ 68 | :set $isLinkLocal true; 69 | } 70 | 71 | if ($eachAddress in fd00::/8) \ 72 | do={ 73 | :set $isLinkLocal true; 74 | } 75 | 76 | if ($eachAddress in fe80::/10) \ 77 | do={ 78 | :set $isLinkLocal true; 79 | } 80 | 81 | if (!$isLinkLocal) \ 82 | do={ 83 | :set interfaceIP $eachAddress; 84 | } 85 | } 86 | } 87 | 88 | :local interfaceIPLength [ :len $interfaceIP ]; 89 | 90 | if ($interfaceIPLength = 0) \ 91 | do={ 92 | set $epicFail true; 93 | :log error ("DDNSv6: No public IP on interface . " $wanInterface); 94 | } \ 95 | else={ 96 | :set $interfaceIP [ :pick $interfaceIP 0 [ :find $interfaceIP "/" ] ]; 97 | :set $publicIP [ :toip6 $interfaceIP ]; 98 | # :log info ("DDNSv6: Current interface IP is " . $publicIP); 99 | } 100 | } \ 101 | on-error { 102 | :set $epicFail true; 103 | :log error ("DDNSv6: Get public IP failed."); 104 | } 105 | 106 | # 获取当前解析的IP 107 | :do { 108 | :set $dnsIP [ :resolve $domainName ]; 109 | # :log info ("DDNSv6: Current resolved IP is " . $dnsIP); 110 | } \ 111 | on-error { 112 | :set $epicFail true; 113 | :log error ("DDNSv6: Resolve domain " . $domainName . " failed."); 114 | } 115 | 116 | # 如IP有变动,则更新解析 117 | :if ($epicFail = false && $publicIP != $dnsIP) \ 118 | do={ 119 | :local callUrl ("https://ddns6.vsean.net/ddns.php"); 120 | :local postData ("service=" . $service . "&domain=" . $domainName . "&access_id=" . $accessID . "&access_secret=" . $accessSecret . "&domain_id=" . $domainID . "&record_id=" . $recordID); 121 | :local fetchResult [/tool fetch url=$callUrl mode=https http-method=post http-data=$postData as-value output=user]; 122 | :log info ("DDNSv6: " . $fetchResult->"data"); 123 | } 124 | -------------------------------------------------------------------------------- /service/aliyun.php: -------------------------------------------------------------------------------- 1 | regionId('cn-hangzhou') 13 | ->asDefaultClient(); 14 | } 15 | 16 | public function ddns($domain, $accessIP) 17 | { 18 | $recordType = getRecordType($accessIP); 19 | $record = $this->getRecord($domain, $recordType); 20 | $recordIP = $record->Value; 21 | 22 | if ($recordIP === $accessIP) { 23 | return 'IP not changed'; 24 | } 25 | else { 26 | $this->updateRecord($record, $accessIP); 27 | return "IP update from {$recordIP} to {$accessIP}"; 28 | } 29 | } 30 | 31 | private function getRecord($domain, $recordType) 32 | { 33 | $response = Alidns::v20150109() 34 | ->DescribeSubDomainRecords() 35 | ->withSubDomain($domain) 36 | ->withType($recordType) 37 | ->request(); 38 | 39 | $record = $response->DomainRecords->Record[0] ?? null; 40 | 41 | if (empty($record)) { 42 | throw new Exception('No available records'); 43 | } 44 | 45 | return $record; 46 | } 47 | 48 | private function updateRecord($record, $ip) 49 | { 50 | Alidns::v20150109() 51 | ->UpdateDomainRecord() 52 | ->withRecordId($record->RecordId) 53 | ->withRR($record->RR) 54 | ->withType($record->Type) 55 | ->withValue($ip) 56 | ->request(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /service/dnspod.php: -------------------------------------------------------------------------------- 1 | domainID = intval($domainID); 23 | $this->recordID = intval($recordID); 24 | } 25 | 26 | $cred = new Credential($accessID, $accessSecret); 27 | $this->client = new DnspodClient($cred, ""); 28 | } 29 | 30 | public function ddns($domain, $accessIP) 31 | { 32 | $recordType = getRecordType($accessIP); 33 | 34 | if (empty($this->domainID) || empty($this->recordID)) { 35 | $this->getId($domain, $recordType); 36 | } 37 | 38 | $record = $this->getRecord(); 39 | $recordIP = $record->Value; 40 | 41 | if ($recordIP === $accessIP) { 42 | return 'IP not changed'; 43 | } 44 | else { 45 | $this->updateRecord($record, $accessIP); 46 | return "IP update from {$recordIP} to {$accessIP}"; 47 | } 48 | } 49 | 50 | private function getRecord() 51 | { 52 | $request = new DescribeRecordRequest(); 53 | $request->setDomain('example.com'); 54 | $request->setDomainId($this->domainID); 55 | $request->setRecordId($this->recordID); 56 | 57 | $response = $this->client->DescribeRecord($request); 58 | $recordInfo = $response->getRecordInfo(); 59 | 60 | return $recordInfo; 61 | } 62 | 63 | private function updateRecord($record, $ip) 64 | { 65 | $request = New ModifyDynamicDNSRequest(); 66 | $request->setDomain('example.com'); 67 | $request->setDomainId($this->domainID); 68 | $request->setRecordId($this->recordID); 69 | $request->setValue($ip); 70 | $request->setSubDomain($record->SubDomain); 71 | $request->setRecordLine($record->RecordLine); 72 | $this->client->ModifyDynamicDNS($request); 73 | } 74 | 75 | private function getId($domain, $recordType) 76 | { 77 | $domainID = null; 78 | $recordID = null; 79 | 80 | if (file_exists(CACHE_DIR . md5($domain . $recordType))) { 81 | $content = file_get_contents(CACHE_DIR . md5($domain . $recordType)); 82 | $record = json_decode($content, true); 83 | 84 | $this->domainID = $record['domain_id']; 85 | $this->recordID = $record['record_id']; 86 | return; 87 | } 88 | 89 | $request = new DescribeDomainListRequest(); 90 | $response = $this->client->DescribeDomainList($request); 91 | $domainList = $response->getDomainList(); 92 | 93 | foreach ($domainList as $eachDomain) { 94 | $thisDomainName = $eachDomain->Name; 95 | $thisdomainID = $eachDomain->DomainId; 96 | 97 | if (str_ends_with($domain, $thisDomainName)) { 98 | $domainID = $thisdomainID; 99 | $domainName = $thisDomainName; 100 | break; 101 | } 102 | } 103 | 104 | if (empty($domainID)) { 105 | throw new Exception("Domain not found"); 106 | } 107 | 108 | $request = new DescribeRecordListRequest(); 109 | $request->setDomain('example.com'); 110 | $request->setDomainId($domainID); 111 | $response = $this->client->DescribeRecordList($request); 112 | $recordList = $response->getRecordList(); 113 | 114 | foreach ($recordList as $eachRecord) { 115 | $thisRecordID = $eachRecord->RecordId; 116 | $thisRecordName = trim($eachRecord->Name, '.'); 117 | $thisRecordType = $eachRecord->Type; 118 | $thisRecordDomain = $thisRecordName . '.' . $domainName; 119 | 120 | if ($thisRecordType === $recordType && $thisRecordDomain === $domain) { 121 | $recordID = $thisRecordID; 122 | break; 123 | } 124 | } 125 | 126 | if (empty($recordID)) { 127 | throw new Exception("Record not found"); 128 | } 129 | 130 | $cacheData = [ 131 | 'domain' => $domain, 132 | 'domain_id' => $domainID, 133 | 'record_id' => $recordID, 134 | 'record_type' => $recordType 135 | ]; 136 | 137 | file_put_contents(CACHE_DIR . md5($domain . $recordType), json_encode($cacheData)); 138 | 139 | $this->domainID = $domainID; 140 | $this->recordID = $recordID; 141 | } 142 | } 143 | --------------------------------------------------------------------------------