├── .gitignore ├── .gitmodules ├── src ├── public_ip │ ├── public_ip_getter_iface.h │ ├── public_ip_getter_ipify.h │ ├── public_ip_getter_porkbun.h │ ├── public_ip_getter.h │ ├── public_ip_getter_iface.cpp │ ├── public_ip_getter_ipify.cpp │ ├── public_ip_getter.cpp │ └── public_ip_getter_porkbun.cpp ├── dns_service │ ├── dns_service_porkbun.h │ ├── dns_service_cloudflare.h │ ├── dns_service_dnspod.h │ ├── dns_service.h │ ├── dns_service.cpp │ ├── dns_service_porkbun.cpp │ ├── dns_service_dnspod.cpp │ └── dns_service_cloudflare.cpp ├── pve │ ├── pve_pct_wrapper.h │ ├── pve_pct_wrapper.cpp │ ├── pve_api_client.h │ └── pve_api_client.cpp ├── config.h ├── utils.h ├── config.cpp ├── utils.cpp └── main.cpp ├── res └── pve-ddns-client.yml ├── LICENSE ├── 3rdparty ├── build.bat ├── build_mips.sh ├── build_debug.bat └── build.sh ├── .github └── workflows │ ├── cmake-mips.yml │ ├── cmake.yml │ └── codeql.yml ├── CMakeLists.txt └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | CMakeLists.txt.user 2 | CMakeCache.txt 3 | CMakeFiles 4 | CMakeScripts 5 | Testing 6 | Makefile 7 | cmake_install.cmake 8 | install_manifest.txt 9 | compile_commands.json 10 | CTestTestfile.cmake 11 | _deps 12 | 13 | # CMake output 14 | build 15 | cmake-build-* 16 | /3rdparty/prebuilt 17 | 18 | .idea 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "3rdparty/glog"] 2 | path = 3rdparty/glog 3 | url = https://github.com/google/glog.git 4 | [submodule "3rdparty/yaml-cpp"] 5 | path = 3rdparty/yaml-cpp 6 | url = https://github.com/jbeder/yaml-cpp.git 7 | [submodule "3rdparty/cmdline"] 8 | path = 3rdparty/cmdline 9 | url = https://github.com/MihailJP/cmdline.git 10 | [submodule "3rdparty/curl"] 11 | path = 3rdparty/curl 12 | url = https://github.com/curl/curl.git 13 | [submodule "3rdparty/openssl"] 14 | path = 3rdparty/openssl 15 | url = https://github.com/openssl/openssl.git 16 | [submodule "3rdparty/rapidjson"] 17 | path = 3rdparty/rapidjson 18 | url = https://github.com/Tencent/rapidjson.git 19 | [submodule "3rdparty/fmt"] 20 | path = 3rdparty/fmt 21 | url = https://github.com/fmtlib/fmt.git 22 | -------------------------------------------------------------------------------- /src/public_ip/public_ip_getter_iface.h: -------------------------------------------------------------------------------- 1 | #ifndef PUBLIC_IP_GETTER_IFACE_H 2 | #define PUBLIC_IP_GETTER_IFACE_H 3 | 4 | #include "public_ip_getter.h" 5 | 6 | /// Public IP getter using local network interface 7 | class PublicIpGetterIface : public IPublicIpGetter 8 | { 9 | public: 10 | const std::string & getServiceName() override; 11 | bool setCredentials(const std::string & cred_str) override; 12 | std::string getIpv4() override; 13 | std::string getIpv6() override; 14 | 15 | protected: 16 | bool getIp(std::string & v4_ip, std::string & v6_ip); 17 | 18 | private: 19 | /// Service name 20 | std::string _service_name = PUBLIC_IP_GETTER_IFACE; 21 | /// Network interface 22 | std::string _interface; 23 | }; 24 | 25 | #endif //PUBLIC_IP_GETTER_IFACE_H 26 | -------------------------------------------------------------------------------- /src/public_ip/public_ip_getter_ipify.h: -------------------------------------------------------------------------------- 1 | #ifndef PVE_DDNS_CLIENT_SRC_PUBLIC_IP_PUBLIC_IP_GETTER_IPIFY_H 2 | #define PVE_DDNS_CLIENT_SRC_PUBLIC_IP_PUBLIC_IP_GETTER_IPIFY_H 3 | 4 | #include "public_ip_getter.h" 5 | 6 | /// Public IP getter using ipify APIs 7 | class PublicIpGetterIpify : public IPublicIpGetter 8 | { 9 | public: 10 | const std::string & getServiceName() override; 11 | bool setCredentials(const std::string & cred_str) override; 12 | std::string getIpv4() override; 13 | std::string getIpv6() override; 14 | 15 | protected: 16 | static std::string getIp(const std::string & api_host); 17 | 18 | private: 19 | /// Service name 20 | std::string _service_name = PUBLIC_IP_GETTER_IPIFY; 21 | }; 22 | 23 | #endif //PVE_DDNS_CLIENT_SRC_PUBLIC_IP_PUBLIC_IP_GETTER_IPIFY_H 24 | -------------------------------------------------------------------------------- /src/public_ip/public_ip_getter_porkbun.h: -------------------------------------------------------------------------------- 1 | #ifndef PVE_DDNS_CLIENT_SRC_PUBLIC_IP_PUBLIC_IP_GETTER_PORKBUN_H 2 | #define PVE_DDNS_CLIENT_SRC_PUBLIC_IP_PUBLIC_IP_GETTER_PORKBUN_H 3 | 4 | #include "public_ip_getter.h" 5 | 6 | /// Public IP getter using Porkbun APIs 7 | class PublicIpGetterPorkbun : public IPublicIpGetter 8 | { 9 | public: 10 | const std::string & getServiceName() override; 11 | bool setCredentials(const std::string & cred_str) override; 12 | std::string getIpv4() override; 13 | std::string getIpv6() override; 14 | 15 | protected: 16 | std::string getIp(const std::string & api_host) const; 17 | 18 | private: 19 | /// Service name 20 | std::string _service_name = PUBLIC_IP_GETTER_PORKBUN; 21 | /// Porkbun API key 22 | std::string _api_key; 23 | /// Porkbun secret key 24 | std::string _api_secret; 25 | }; 26 | 27 | #endif //PVE_DDNS_CLIENT_SRC_PUBLIC_IP_PUBLIC_IP_GETTER_PORKBUN_H 28 | -------------------------------------------------------------------------------- /src/dns_service/dns_service_porkbun.h: -------------------------------------------------------------------------------- 1 | #ifndef PVE_DDNS_CLIENT_SRC_DNS_SERVICE_DNS_SERVICE_PORKBUN_H 2 | #define PVE_DDNS_CLIENT_SRC_DNS_SERVICE_DNS_SERVICE_PORKBUN_H 3 | 4 | #include "dns_service.h" 5 | 6 | /// Porkbun DNS service implementation 7 | class DnsServicePorkbun : public IDnsService 8 | { 9 | public: 10 | const std::string & getServiceName() override; 11 | bool setCredentials(const std::string & cred_str) override; 12 | std::string getIpv4(const std::string & domain) override; 13 | std::string getIpv6(const std::string & domain) override; 14 | bool setIpv4(const std::string & domain, const std::string & ip) override; 15 | bool setIpv6(const std::string & domain, const std::string & ip) override; 16 | 17 | protected: 18 | std::string getIp(const std::string & domain, bool is_v4); 19 | bool setIp(const std::string & domain, const std::string & ip, bool is_v4); 20 | 21 | private: 22 | /// Service name 23 | std::string _service_name = DNS_SERVICE_PORKBUN; 24 | /// Porkbun API key 25 | std::string _api_key; 26 | /// Porkbun secret key 27 | std::string _api_secret; 28 | }; 29 | 30 | #endif //PVE_DDNS_CLIENT_SRC_DNS_SERVICE_DNS_SERVICE_PORKBUN_H 31 | -------------------------------------------------------------------------------- /res/pve-ddns-client.yml: -------------------------------------------------------------------------------- 1 | general: 2 | update-interval-ms: 300000 3 | log-overdue-days: 3 4 | log-buf-secs: 2 5 | max-log-size-mb: 2 6 | service-mode: true 7 | public-ip: 8 | service: porkbun 9 | credentials: api_key,secret_key 10 | pve-api: 11 | host: https://pve.domain.com:8006 12 | user: root 13 | realm: pam 14 | token-id: ddns 15 | token-uuid: uuid 16 | sync_host_static_v6_address: false 17 | client: 18 | dns: dnspod 19 | credentials: token_id,token 20 | ipv4: ["v4sub1.domain.com", "v4sub2.domain.com"] 21 | ipv6: ["v6sub1.domain.com", "v6sub2.domain.com"] 22 | host: 23 | node: node 24 | iface: vmbr0 25 | dns: porkbun 26 | credentials: api_key,secret_key 27 | ipv4: ["v4sub1.domain.com", "v4sub2.domain.com"] 28 | ipv6: ["v6sub1.domain.com", "v6sub2.domain.com"] 29 | guests: 30 | - node: node 31 | vmid: 100 32 | iface: ens18 33 | dns: porkbun 34 | credentials: api_key,secret_key 35 | ipv4: ["v4sub1.domain.com", "v4sub2.domain.com"] 36 | ipv6: ["v6sub1.domain.com", "v6sub2.domain.com"] 37 | - node: node 38 | vmid: 101 39 | iface: ens18 40 | dns: porkbun 41 | credentials: api_key,secret_key 42 | ipv4: ["v4sub1.domain.com", "v4sub2.domain.com"] 43 | ipv6: ["v6sub1.domain.com", "v6sub2.domain.com"] 44 | -------------------------------------------------------------------------------- /src/pve/pve_pct_wrapper.h: -------------------------------------------------------------------------------- 1 | #ifndef PVE_DDNS_CLIENT_SRC_PVE_PVE_PCT_WRAPPER_H 2 | #define PVE_DDNS_CLIENT_SRC_PVE_PVE_PCT_WRAPPER_H 3 | 4 | #include 5 | #include 6 | 7 | /// Proxmox VE pct (Proxmox Container Toolkit) wrapper 8 | class PvePctWrapper 9 | { 10 | public: 11 | /// Init by executing 'pct list' and update LXC guest VM id list 12 | /// \return Operation result 13 | bool init(); 14 | 15 | /// Check if given VM id is in LXC guest VM id list 16 | /// \param vmid VM id 17 | /// \return Result 18 | bool isLxcGuest(int vmid) const; 19 | 20 | /// Get LXC guest VM IPv4 and IPv6 address of specific interface 21 | /// \param vmid VM id 22 | /// \param iface Interface name 23 | /// \return A pair of strings, first is IPv4 address, second is IPv6 address (empty string if failed to get) 24 | std::pair getGuestIp(int vmid, const std::string & iface) const; 25 | 26 | protected: 27 | static bool execute(const std::string & cmd, std::string & result); 28 | void parseListResult(const std::string & result); 29 | 30 | private: 31 | /// If pct is available 32 | bool _available = false; 33 | /// LCX VMID list (from pct list) 34 | std::vector _lxc_vmids; 35 | }; 36 | 37 | #endif //PVE_DDNS_CLIENT_SRC_PVE_PVE_PCT_WRAPPER_H 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2023, wzkres 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /src/dns_service/dns_service_cloudflare.h: -------------------------------------------------------------------------------- 1 | #ifndef PVE_DDNS_CLIENT_SRC_DNS_SERVICE_DNS_SERVICE_CLOUDFLARE_H 2 | #define PVE_DDNS_CLIENT_SRC_DNS_SERVICE_DNS_SERVICE_CLOUDFLARE_H 3 | 4 | #include 5 | 6 | #include "dns_service.h" 7 | 8 | class DnsServiceCloudflare : public IDnsService 9 | { 10 | public: 11 | const std::string & getServiceName() override; 12 | bool setCredentials(const std::string & cred_str) override; 13 | std::string getIpv4(const std::string & domain) override; 14 | std::string getIpv6(const std::string & domain) override; 15 | bool setIpv4(const std::string & domain, const std::string & ip) override; 16 | bool setIpv6(const std::string & domain, const std::string & ip) override; 17 | 18 | protected: 19 | bool verifyToken(); 20 | bool getZoneId(const std::string & domain_name, std::string & out_zone_id); 21 | bool getRecordId(const std::string & domain_name, const std::string & zone_id, const std::string & type, 22 | std::string & out_record_id, std::string & out_record_content); 23 | std::string getIp(const std::string & domain, bool is_v4); 24 | bool setIp(const std::string & domain, const std::string & ip, bool is_v4); 25 | 26 | private: 27 | /// Service name 28 | std::string _service_name = DNS_SERVICE_CLOUDFLARE; 29 | /// API token 30 | std::string _token; 31 | /// Zone ID map 32 | std::unordered_map _zones; 33 | /// DNS record ID map 34 | std::unordered_map _records; 35 | }; 36 | 37 | #endif //PVE_DDNS_CLIENT_SRC_DNS_SERVICE_DNS_SERVICE_CLOUDFLARE_H 38 | -------------------------------------------------------------------------------- /3rdparty/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | echo Remove prebuilt folder... 4 | rd /S /Q prebuilt 5 | echo Create prebuild folder... 6 | md prebuilt 7 | 8 | goto comment 9 | echo Building openssl... 10 | cd openssl-cmake 11 | rd /S /Q cmake_build 12 | cmake -B ./cmake_build -DBUILD_SHARED_LIBS=NO -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=../../prebuilt 13 | cmake --build ./cmake_build --config Release 14 | cmake --install ./cmake_build 15 | cd .. 16 | rd /S /Q cmake_build 17 | cd .. 18 | echo openssl built! 19 | :comment 20 | 21 | echo Building curl... 22 | cd curl 23 | rd /S /Q cmake_build 24 | cmake -B ./cmake_build -DBUILD_SHARED_LIBS=NO -DCMAKE_BUILD_TYPE=Release -DCMAKE_USE_SCHANNEL=YES ^ 25 | -DCMAKE_INSTALL_PREFIX:PATH=../prebuilt 26 | cmake --build ./cmake_build --config Release 27 | cmake --install ./cmake_build 28 | rd /S /Q cmake_build 29 | cd .. 30 | echo curl built! 31 | 32 | echo Building glog... 33 | cd glog 34 | rd /S /Q cmake_build 35 | cmake -B ./cmake_build -DWITH_GFLAGS=NO -DBUILD_SHARED_LIBS=NO -DCMAKE_BUILD_TYPE=Release ^ 36 | -DCMAKE_INSTALL_PREFIX:PATH=../prebuilt 37 | cmake --build ./cmake_build --config Release 38 | cmake --install ./cmake_build 39 | rd /S /Q cmake_build 40 | cd .. 41 | echo glog built! 42 | 43 | echo Building yaml-cpp... 44 | cd yaml-cpp 45 | rd /S /Q cmake_build 46 | cmake -B ./cmake_build -DBUILD_SHARED_LIBS=NO -DCMAKE_BUILD_TYPE=Release ^ 47 | -DCMAKE_INSTALL_PREFIX:PATH=../prebuilt 48 | cmake --build ./cmake_build --config Release 49 | cmake --install ./cmake_build 50 | rd /S /Q cmake_build 51 | cd .. 52 | echo yaml-cpp built! 53 | 54 | echo All 3rdparty libs built! 55 | -------------------------------------------------------------------------------- /3rdparty/build_mips.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | script_path="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" 3 | 4 | echo "Removing prebuilt dir..." 5 | rm -rf ${script_path}/prebuilt 6 | echo "Creating prebuilt dir..." 7 | mkdir -p ${script_path}/prebuilt 8 | 9 | echo "Building openssl..." 10 | cd ${script_path}/openssl 11 | ./Configure linux-mips32 no-async no-shared --prefix=${script_path}/prebuilt --openssldir=${script_path}/prebuilt 12 | make -j4 13 | make install_sw 14 | make clean 15 | echo "openssl built!" 16 | 17 | echo "Building curl..." 18 | cd ${script_path}/curl 19 | rm -rf cmake_build 20 | mkdir cmake_build 21 | cd cmake_build 22 | cmake -DCURL_DISABLE_LDAP=YES -DCURL_DISABLE_LDAPS=YES -DCMAKE_USE_LIBSSH2=NO -DBUILD_SHARED_LIBS=NO -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=${script_path}/prebuilt ../ 23 | make -j4 24 | make install 25 | make clean 26 | cd .. 27 | rm -rf cmake_build 28 | echo "curl built!" 29 | 30 | echo "Building glog..." 31 | cd ${script_path}/glog 32 | rm -rf cmake_build 33 | mkdir cmake_build 34 | cd cmake_build 35 | cmake -DWITH_GFLAGS=NO -DWITH_UNWIND=NO -DBUILD_SHARED_LIBS=NO -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=${script_path}/prebuilt ../ 36 | make -j4 37 | make install 38 | make clean 39 | cd .. 40 | rm -rf cmake_build 41 | echo "glog built!" 42 | 43 | echo "Building yaml-cpp..." 44 | cd ${script_path}/yaml-cpp 45 | rm -rf cmake_build 46 | mkdir cmake_build 47 | cd cmake_build 48 | cmake -DBUILD_SHARED_LIBS=NO -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=${script_path}/prebuilt ../ 49 | make -j4 50 | make install 51 | make clean 52 | cd .. 53 | rm -rf cmake_build 54 | echo "yaml-cpp built!" 55 | 56 | echo "All 3rdparty libs built!" 57 | -------------------------------------------------------------------------------- /3rdparty/build_debug.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | echo Remove prebuilt folder... 4 | rd /S /Q prebuilt 5 | echo Create prebuild folder... 6 | md prebuilt 7 | 8 | goto comment 9 | echo Building openssl... 10 | cd openssl-cmake 11 | rd /S /Q cmake_build 12 | cmake -B ./cmake_build -G Ninja -DBUILD_SHARED_LIBS=NO -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=../../prebuilt 13 | cmake --build ./cmake_build --config Release 14 | cmake --install ./cmake_build 15 | cd .. 16 | rd /S /Q cmake_build 17 | cd .. 18 | echo openssl built! 19 | :comment 20 | 21 | echo Building curl... 22 | cd curl 23 | rd /S /Q cmake_build 24 | cmake -B ./cmake_build -G Ninja -DBUILD_SHARED_LIBS=NO -DCMAKE_BUILD_TYPE=Debug -DCMAKE_USE_SCHANNEL=YES ^ 25 | -DCMAKE_INSTALL_PREFIX:PATH=../prebuilt 26 | cmake --build ./cmake_build --config Release 27 | cmake --install ./cmake_build 28 | rd /S /Q cmake_build 29 | cd .. 30 | echo curl built! 31 | 32 | echo Building glog... 33 | cd glog 34 | rd /S /Q cmake_build 35 | cmake -B ./cmake_build -G Ninja -DWITH_GFLAGS=NO -DBUILD_SHARED_LIBS=NO -DCMAKE_BUILD_TYPE=Debug ^ 36 | -DCMAKE_INSTALL_PREFIX:PATH=../prebuilt 37 | cmake --build ./cmake_build --config Release 38 | cmake --install ./cmake_build 39 | rd /S /Q cmake_build 40 | cd .. 41 | echo glog built! 42 | 43 | echo Building yaml-cpp... 44 | cd yaml-cpp 45 | rd /S /Q cmake_build 46 | cmake -B ./cmake_build -G Ninja -DBUILD_SHARED_LIBS=NO -DCMAKE_BUILD_TYPE=Debug ^ 47 | -DCMAKE_INSTALL_PREFIX:PATH=../prebuilt 48 | cmake --build ./cmake_build --config Release 49 | cmake --install ./cmake_build 50 | rd /S /Q cmake_build 51 | cd .. 52 | echo yaml-cpp built! 53 | 54 | echo All 3rdparty libs built! 55 | -------------------------------------------------------------------------------- /src/public_ip/public_ip_getter.h: -------------------------------------------------------------------------------- 1 | #ifndef PVE_DDNS_CLIENT_SRC_PUBLIC_IP_PUBLIC_IP_GETTER_H 2 | #define PVE_DDNS_CLIENT_SRC_PUBLIC_IP_PUBLIC_IP_GETTER_H 3 | 4 | #include 5 | 6 | /// Public IP getter implementations 7 | constexpr const char * PUBLIC_IP_GETTER_IFACE = "iface"; 8 | constexpr const char * PUBLIC_IP_GETTER_PORKBUN = "porkbun"; 9 | constexpr const char * PUBLIC_IP_GETTER_IPIFY = "ipify"; 10 | 11 | /// Public IP getter interface 12 | class IPublicIpGetter 13 | { 14 | public: 15 | /// Get service name 16 | /// \return Service name string 17 | virtual const std::string & getServiceName() = 0; 18 | 19 | /// Set credentials string (format is implementation dependent) 20 | /// \param cred_str Credentials string 21 | /// \return Operation result 22 | virtual bool setCredentials(const std::string & cred_str) = 0; 23 | 24 | /// Get public IPv4 address 25 | /// \return IPv4 address or empty string if failed 26 | virtual std::string getIpv4() = 0; 27 | 28 | /// Get public IPv6 address 29 | /// \return IPv6 address or empty string if failed 30 | virtual std::string getIpv6() = 0; 31 | }; 32 | 33 | /// Public IP getter factory 34 | class PublicIpGetterFactory 35 | { 36 | public: 37 | /// Create public IP getter instance 38 | /// \param service_name Service name 39 | /// \return Instance pointer or nullptr if failed 40 | static IPublicIpGetter * create(const std::string & service_name); 41 | 42 | /// Destroy public IP getter instance 43 | /// \param ip_getter Instance pointer 44 | static void destroy(IPublicIpGetter * ip_getter); 45 | }; 46 | 47 | #endif //PVE_DDNS_CLIENT_SRC_PUBLIC_IP_PUBLIC_IP_GETTER_H 48 | -------------------------------------------------------------------------------- /src/dns_service/dns_service_dnspod.h: -------------------------------------------------------------------------------- 1 | #ifndef PVE_DDNS_CLIENT_SRC_DNS_SERVICE_DNS_SERVICE_DNSPOD_H 2 | #define PVE_DDNS_CLIENT_SRC_DNS_SERVICE_DNS_SERVICE_DNSPOD_H 3 | 4 | #include 5 | 6 | #include "dns_service.h" 7 | 8 | /// DNSPod domain record cache 9 | typedef struct dnspod_record_cache_ 10 | { 11 | std::string domain; 12 | bool is_v4; 13 | std::string record_id; 14 | std::string line_id; 15 | } dnspod_record_cache; 16 | 17 | /// DNSPod tencent DNS service implementation 18 | class DnsServiceDnspod : public IDnsService 19 | { 20 | public: 21 | const std::string & getServiceName() override; 22 | bool setCredentials(const std::string & cred_str) override; 23 | std::string getIpv4(const std::string & domain) override; 24 | std::string getIpv6(const std::string & domain) override; 25 | bool setIpv4(const std::string & domain, const std::string & ip) override; 26 | bool setIpv6(const std::string & domain, const std::string & ip) override; 27 | 28 | protected: 29 | bool getVersion(std::string & version); 30 | std::string getIp(const std::string & domain, bool is_v4); 31 | bool setIp(const std::string & domain, const std::string & ip, bool is_v4); 32 | bool updateRecordCache(const std::string & domain, bool is_v4, 33 | const std::string & record_id, const std::string & line_id); 34 | const dnspod_record_cache * getRecordCache(const std::string & domain, bool is_v4) const; 35 | 36 | private: 37 | /// Service name 38 | std::string _service_name = DNS_SERVICE_DNSPOD; 39 | /// DNSPod token (id,token) 40 | std::string _token; 41 | /// Domain records cache 42 | std::vector _records_cache; 43 | }; 44 | 45 | #endif //PVE_DDNS_CLIENT_SRC_DNS_SERVICE_DNS_SERVICE_DNSPOD_H 46 | -------------------------------------------------------------------------------- /3rdparty/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | script_path="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" 3 | 4 | echo "Removing prebuilt dir..." 5 | rm -rf ${script_path}/prebuilt 6 | echo "Creating prebuilt dir..." 7 | mkdir -p ${script_path}/prebuilt 8 | 9 | echo "Building openssl..." 10 | cd ${script_path}/openssl 11 | ./config no-shared --prefix=${script_path}/prebuilt --openssldir=${script_path}/prebuilt 12 | make -j4 13 | make install_sw 14 | make clean 15 | echo "openssl built!" 16 | 17 | echo "Building curl..." 18 | cd ${script_path}/curl 19 | rm -rf cmake_build 20 | mkdir cmake_build 21 | cd cmake_build 22 | cmake -DCURL_DISABLE_LDAP=YES -DCURL_DISABLE_LDAPS=YES -DBUILD_CURL_EXE=NO -DOPENSSL_ROOT_DIR=${script_path}/prebuilt -DOPENSSL_INCLUDE_DIR=${script_path}/prebuilt/include -DOPENSSL_LIBRARIES=${script_path}/prebuilt/lib -DCMAKE_USE_LIBSSH2=NO -DBUILD_SHARED_LIBS=NO -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=${script_path}/prebuilt ../ 23 | make -j4 24 | make install 25 | make clean 26 | cd .. 27 | rm -rf cmake_build 28 | echo "curl built!" 29 | 30 | echo "Building glog..." 31 | cd ${script_path}/glog 32 | rm -rf cmake_build 33 | mkdir cmake_build 34 | cd cmake_build 35 | cmake -DWITH_GFLAGS=NO -DWITH_UNWIND=NO -DBUILD_SHARED_LIBS=NO -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=${script_path}/prebuilt ../ 36 | make -j4 37 | make install 38 | make clean 39 | cd .. 40 | rm -rf cmake_build 41 | echo "glog built!" 42 | 43 | echo "Building yaml-cpp..." 44 | cd ${script_path}/yaml-cpp 45 | rm -rf cmake_build 46 | mkdir cmake_build 47 | cd cmake_build 48 | cmake -DBUILD_SHARED_LIBS=NO -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX:PATH=${script_path}/prebuilt ../ 49 | make -j4 50 | make install 51 | make clean 52 | cd .. 53 | rm -rf cmake_build 54 | echo "yaml-cpp built!" 55 | 56 | echo "All 3rdparty libs built!" 57 | -------------------------------------------------------------------------------- /src/public_ip/public_ip_getter_iface.cpp: -------------------------------------------------------------------------------- 1 | #include "public_ip_getter_iface.h" 2 | 3 | #include "glog/logging.h" 4 | 5 | #include "../utils.h" 6 | 7 | 8 | const std::string& PublicIpGetterIface::getServiceName() 9 | { 10 | return _service_name; 11 | } 12 | 13 | bool PublicIpGetterIface::setCredentials(const std::string & cred_str) 14 | { 15 | if (cred_str.empty()) 16 | { 17 | LOG(WARNING) << "Credentials string is empty!"; 18 | return false; 19 | } 20 | _interface = cred_str; 21 | 22 | return true; 23 | } 24 | 25 | std::string PublicIpGetterIface::getIpv4() 26 | { 27 | std::string ipv4, ipv6; 28 | if (!getIp(ipv4, ipv6)) 29 | { 30 | LOG(WARNING) << "Failed to getIp from iface " << _interface << "!"; 31 | return {}; 32 | } 33 | return ipv4; 34 | } 35 | 36 | std::string PublicIpGetterIface::getIpv6() 37 | { 38 | std::string ipv4, ipv6; 39 | if (!getIp(ipv4, ipv6)) 40 | { 41 | LOG(WARNING) << "Failed to getIp from iface " << _interface << "!"; 42 | return {}; 43 | } 44 | return ipv6; 45 | } 46 | 47 | bool PublicIpGetterIface::getIp(std::string& v4_ip, std::string& v6_ip) 48 | { 49 | std::string result; 50 | #if WIN32 51 | if (!shell_execute("ipconfig", result)) 52 | { 53 | LOG(WARNING) << "Failed to shell_execute ipconfig!"; 54 | return false; 55 | } 56 | if (!get_ip_from_ipconfig_result(result, _interface, v4_ip, v6_ip)) 57 | { 58 | LOG(WARNING) << "Failed to get_ip_from_ipconfig_result interface '" << _interface << "' result '" 59 | << result << "'!"; 60 | return false; 61 | } 62 | #else 63 | if (!shell_execute("ip addr", result)) 64 | { 65 | LOG(WARNING) << "Failed to shell_execute ip addr!"; 66 | return false; 67 | } 68 | if (!get_ip_from_ip_addr_result(result, _interface, v4_ip, v6_ip)) 69 | { 70 | LOG(WARNING) << "Failed to get_ip_from_ip_addr_result interface '" << _interface << "' result '" 71 | << result << "'!"; 72 | return false; 73 | } 74 | #endif 75 | return true; 76 | } 77 | -------------------------------------------------------------------------------- /src/public_ip/public_ip_getter_ipify.cpp: -------------------------------------------------------------------------------- 1 | #include "public_ip_getter_ipify.h" 2 | 3 | #include "glog/logging.h" 4 | #include "rapidjson/document.h" 5 | #include "rapidjson/error/en.h" 6 | 7 | #include "../utils.h" 8 | #include "../config.h" 9 | 10 | static const char * API_HOST = "https://api6.ipify.org/?format=json"; 11 | static const char * API_HOST_V4 = "https://api.ipify.org/?format=json"; 12 | 13 | const std::string & PublicIpGetterIpify::getServiceName() 14 | { 15 | return _service_name; 16 | } 17 | 18 | bool PublicIpGetterIpify::setCredentials(const std::string & cred_str) 19 | { 20 | if (!cred_str.empty()) 21 | LOG(WARNING) << "Credential is not needed for ipify public IP getter!"; 22 | return true; 23 | } 24 | 25 | std::string PublicIpGetterIpify::getIpv4() 26 | { 27 | return getIp(API_HOST_V4); 28 | } 29 | 30 | std::string PublicIpGetterIpify::getIpv6() 31 | { 32 | std::string v6_ip = getIp(API_HOST); 33 | if (!is_ipv6(v6_ip)) 34 | { 35 | LOG(WARNING) << "'" << v6_ip << "' is not valid IPv6 ip!"; 36 | return ""; 37 | } 38 | return v6_ip; 39 | } 40 | 41 | std::string PublicIpGetterIpify::getIp(const std::string & api_host) 42 | { 43 | int resp_code = 0; 44 | std::string resp_data; 45 | const bool ret = http_req(api_host, "", Config::getInstance()._http_timeout_ms, {}, resp_code, resp_data); 46 | if (!ret || 200 != resp_code) 47 | { 48 | LOG(WARNING) << "Failed to request '" << api_host << "', response code is " << resp_code << ", response is " 49 | << resp_data << "!"; 50 | return ""; 51 | } 52 | rapidjson::Document d; 53 | rapidjson::ParseResult ok = d.Parse(resp_data.c_str()); 54 | if (!ok) 55 | { 56 | LOG(WARNING) << "Failed to parse response json, error '" << rapidjson::GetParseError_En(ok.Code()) 57 | << "' (" << ok.Offset() << ")"; 58 | return ""; 59 | } 60 | 61 | if (d.HasMember("ip") && d["ip"].IsString()) 62 | { 63 | LOG(INFO) << "Successfully got my ip: " << d["ip"].GetString() << " from '" << api_host << "'."; 64 | return d["ip"].GetString(); 65 | } 66 | 67 | return ""; 68 | } 69 | -------------------------------------------------------------------------------- /src/dns_service/dns_service.h: -------------------------------------------------------------------------------- 1 | #ifndef PVE_DDNS_CLIENT_SRC_DNS_SERVICE_DNS_SERVICE_H 2 | #define PVE_DDNS_CLIENT_SRC_DNS_SERVICE_DNS_SERVICE_H 3 | 4 | #include 5 | 6 | /// DNS service implementations 7 | constexpr const char * DNS_SERVICE_PORKBUN = "porkbun"; 8 | constexpr const char * DNS_SERVICE_DNSPOD = "dnspod"; 9 | constexpr const char * DNS_SERVICE_CLOUDFLARE = "cloudflare"; 10 | 11 | /// DNS service interface 12 | class IDnsService 13 | { 14 | public: 15 | /// Get service name 16 | /// \return Service name string 17 | virtual const std::string & getServiceName() = 0; 18 | 19 | /// Set credentials string (format is implementation dependent) 20 | /// \param cred_str Credentials string 21 | /// \return Operation result 22 | virtual bool setCredentials(const std::string & cred_str) = 0; 23 | 24 | /// Get IPv4 address of domain (A record) 25 | /// \param domain Domain name (e.g. sub.site.com) 26 | /// \return IPv4 address or empty string if failed 27 | virtual std::string getIpv4(const std::string & domain) = 0; 28 | 29 | /// Get IPv6 address of domain (AAAA record) 30 | /// \param domain Doamin name (e.g. sub.site.com) 31 | /// \return IPv6 address or empty string if failed 32 | virtual std::string getIpv6(const std::string & domain) = 0; 33 | 34 | /// Set IPv4 address of domain (A record) 35 | /// \param domain Domain name 36 | /// \param ip IPv4 address string 37 | /// \return Operation result 38 | virtual bool setIpv4(const std::string & domain, const std::string & ip) = 0; 39 | 40 | /// Set IPv6 address of dmain (AAAA record) 41 | /// \param domain Domain name 42 | /// \param ip IPv6 address string 43 | /// \return Operation result 44 | virtual bool setIpv6(const std::string & domain, const std::string & ip) = 0; 45 | }; 46 | 47 | /// DNS service factory 48 | class DnsServiceFactory 49 | { 50 | public: 51 | /// Create DNS service instance 52 | /// \param service_name Service name 53 | /// \return Instance pointer or nullptr if failed 54 | static IDnsService * create(const std::string & service_name); 55 | 56 | /// Destroy DNS service instance 57 | /// \param dns_service Instance pointer 58 | static void destroy(IDnsService * dns_service); 59 | }; 60 | 61 | #endif //PVE_DDNS_CLIENT_SRC_DNS_SERVICE_DNS_SERVICE_H 62 | -------------------------------------------------------------------------------- /src/pve/pve_pct_wrapper.cpp: -------------------------------------------------------------------------------- 1 | #include "pve_pct_wrapper.h" 2 | 3 | #include "fmt/format.h" 4 | #include "glog/logging.h" 5 | 6 | #include "../utils.h" 7 | 8 | static const char * pct_cmd = "pct"; 9 | 10 | bool PvePctWrapper::init() 11 | { 12 | const std::string pct_list = fmt::format("{} list", pct_cmd); 13 | std::string result; 14 | if (!shell_execute(pct_list, result)) 15 | { 16 | LOG(WARNING) << "Failed to get LCX vmid list, result is '" << result << "'!"; 17 | _available = false; 18 | return false; 19 | } 20 | parseListResult(result); 21 | _available = true; 22 | return true; 23 | } 24 | 25 | bool PvePctWrapper::isLxcGuest(const int vmid) const 26 | { 27 | if (!_available) 28 | return false; 29 | return std::find(_lxc_vmids.begin(), _lxc_vmids.end(), vmid) != _lxc_vmids.end(); 30 | } 31 | 32 | std::pair PvePctWrapper::getGuestIp(const int vmid, const std::string & iface) const 33 | { 34 | if (!_available) 35 | return { "", "" }; 36 | 37 | const std::string pct_ip_addr = fmt::format("{} exec {} ip addr", pct_cmd, vmid); 38 | std::string result; 39 | if (!shell_execute(pct_ip_addr, result)) 40 | { 41 | LOG(WARNING) << "Failed to get ip of LXC guest vmid '" << vmid << "', result is '" << result << "'!"; 42 | return { "", "" }; 43 | } 44 | std::string v4_ip, v6_ip; 45 | if (!get_ip_from_ip_addr_result(result, iface, v4_ip, v6_ip)) 46 | { 47 | LOG(WARNING) << "Failed to get_ip_from_ip_addr_result '" << result 48 | << "' with specified iface: " << iface << "!"; 49 | return { "", "" }; 50 | } 51 | return { v4_ip, v6_ip }; 52 | } 53 | 54 | void PvePctWrapper::parseListResult(const std::string & result) 55 | { 56 | _lxc_vmids.clear(); 57 | 58 | std::istringstream f(result); 59 | std::string line; 60 | while (std::getline(f, line)) 61 | { 62 | const char * nptr = line.c_str(); 63 | char * endptr = nullptr; 64 | const int vmid = static_cast(strtol(line.c_str(), &endptr, 10)); 65 | if (nptr == endptr) 66 | continue; 67 | LOG(INFO) << "Got LXC VMID '" << vmid << "' from result line '" << line << "'."; 68 | _lxc_vmids.emplace_back(vmid); 69 | } 70 | LOG(INFO) << "Total " << _lxc_vmids.size() << " LXC VM id(s) parsed from pct list result."; 71 | } 72 | -------------------------------------------------------------------------------- /src/pve/pve_api_client.h: -------------------------------------------------------------------------------- 1 | #ifndef PVE_DDNS_CLIENT_SRC_PVEAPICLIENT_H 2 | #define PVE_DDNS_CLIENT_SRC_PVEAPICLIENT_H 3 | 4 | #include 5 | 6 | /// Proxmox VE API client 7 | class PveApiClient 8 | { 9 | public: 10 | /// Init using infos from global config 11 | /// \return Init result 12 | bool init(); 13 | 14 | /// Get host node IPv4 and IPv6 address of specific interface 15 | /// \param node PVE node name 16 | /// \param iface Interface name 17 | /// \return A pair of strings, first is IPv4 address, second is IPv6 address (empty string if failed to get) 18 | std::pair getHostIp(const std::string & node, const std::string & iface); 19 | 20 | /// Get KVM guest VM IPv4 and IPv6 address of specific interface 21 | /// \param node PVE node name 22 | /// \param vmid VM id 23 | /// \param iface Interface name 24 | /// \return A pair of strings, first is IPv4 address, second is IPv6 address (empty string if failed to get) 25 | std::pair getGuestIp(const std::string & node, 26 | int vmid, 27 | const std::string & iface); 28 | 29 | /// Set IPv4, IPv6 address of a specific host interface (will only generate a temp modified config file) 30 | /// \param node PVE node name 31 | /// \param iface Interface name 32 | /// \param v4_ip IPv4 address 33 | /// \param v6_ip IPv6 address 34 | /// \return Operation result 35 | bool setHostNetworkAddress(const std::string & node, const std::string & iface, 36 | const std::string & v4_ip, const std::string & v6_ip); 37 | 38 | /// Apply temp config file generated by set operation 39 | /// \param node PVE node name 40 | /// \return Operation result 41 | bool applyHostNetworkChange(const std::string & node); 42 | 43 | /// Revert (discard) temp modified config file 44 | /// \param node PVE node name 45 | /// \return Operation result 46 | bool revertHostNetworkChange(const std::string & node); 47 | 48 | protected: 49 | bool req(const std::string & api_url, const std::string & req_data, int & resp_code, std::string & resp_data) const; 50 | bool reqHostNetwork(const std::string & method, const std::string & node) const; 51 | bool checkApiHost() const; 52 | 53 | private: 54 | /// PVE API host (root url) 55 | std::string _api_host; 56 | /// PVE API access token 57 | std::string _api_token; 58 | }; 59 | 60 | #endif //PVE_DDNS_CLIENT_SRC_PVEAPICLIENT_H 61 | -------------------------------------------------------------------------------- /src/dns_service/dns_service.cpp: -------------------------------------------------------------------------------- 1 | #include "dns_service.h" 2 | 3 | #include "glog/logging.h" 4 | 5 | #include "../utils.h" 6 | #include "dns_service_porkbun.h" 7 | #include "dns_service_dnspod.h" 8 | #include "dns_service_cloudflare.h" 9 | 10 | IDnsService * DnsServiceFactory::create(const std::string & service_name) 11 | { 12 | if (service_name.empty()) 13 | { 14 | LOG(WARNING) << "Invalid service_name!"; 15 | return nullptr; 16 | } 17 | 18 | if (str_iequals(service_name, DNS_SERVICE_PORKBUN)) 19 | { 20 | auto * service = new(std::nothrow) DnsServicePorkbun(); 21 | if (nullptr == service) 22 | { 23 | LOG(ERROR) << "Failed to instantiate DnsServicePorkbun!"; 24 | return nullptr; 25 | } 26 | return service; 27 | } 28 | 29 | if (str_iequals(service_name, DNS_SERVICE_DNSPOD)) 30 | { 31 | auto * service = new(std::nothrow) DnsServiceDnspod(); 32 | if (nullptr == service) 33 | { 34 | LOG(ERROR) << "Failed to instantiate DnsServiceDnspod!"; 35 | return nullptr; 36 | } 37 | return service; 38 | } 39 | 40 | if (str_iequals(service_name, DNS_SERVICE_CLOUDFLARE)) 41 | { 42 | auto * service = new(std::nothrow) DnsServiceCloudflare(); 43 | if (nullptr == service) 44 | { 45 | LOG(ERROR) << "Failed to instantiate DnsServiceCloudflare!"; 46 | return nullptr; 47 | } 48 | return service; 49 | } 50 | 51 | LOG(WARNING) << "Unsupported dns service '" << service_name << "'!"; 52 | 53 | return nullptr; 54 | } 55 | 56 | void DnsServiceFactory::destroy(IDnsService * dns_service) 57 | { 58 | if (nullptr == dns_service) 59 | { 60 | LOG(WARNING) << "Invalid param!"; 61 | return; 62 | } 63 | 64 | const std::string & name = dns_service->getServiceName(); 65 | if (str_iequals(name, DNS_SERVICE_PORKBUN)) 66 | { 67 | auto * g = dynamic_cast(dns_service); 68 | if (nullptr == g) 69 | LOG(WARNING) << "dns_service is not instance of DnsServicePorkbun!"; 70 | delete g; 71 | } 72 | else if (str_iequals(name, DNS_SERVICE_DNSPOD)) 73 | { 74 | auto * g = dynamic_cast(dns_service); 75 | if (nullptr == g) 76 | LOG(WARNING) << "dns_service is not instance of DnsServiceDnspod!"; 77 | delete g; 78 | } 79 | else if (str_iequals(name, DNS_SERVICE_CLOUDFLARE)) 80 | { 81 | auto * g = dynamic_cast(dns_service); 82 | if (nullptr == g) 83 | LOG(WARNING) << "dns_service is not instance of DnsServiceCloudflare"; 84 | delete g; 85 | } 86 | else 87 | LOG(WARNING) << "Unsupported dns service '" << name << "'!"; 88 | } 89 | -------------------------------------------------------------------------------- /src/public_ip/public_ip_getter.cpp: -------------------------------------------------------------------------------- 1 | #include "public_ip_getter.h" 2 | 3 | #include "glog/logging.h" 4 | 5 | #include "../utils.h" 6 | #include "public_ip_getter_iface.h" 7 | #include "public_ip_getter_porkbun.h" 8 | #include "public_ip_getter_ipify.h" 9 | 10 | IPublicIpGetter * PublicIpGetterFactory::create(const std::string & service_name) 11 | { 12 | if (service_name.empty()) 13 | { 14 | LOG(WARNING) << "Invalid service_name!"; 15 | return nullptr; 16 | } 17 | 18 | if (str_iequals(service_name, PUBLIC_IP_GETTER_IFACE)) 19 | { 20 | auto * getter = new(std::nothrow) PublicIpGetterIface(); 21 | if (nullptr == getter) 22 | { 23 | LOG(ERROR) << "Failed to instantiate PublicIpGetterIface!"; 24 | return nullptr; 25 | } 26 | return getter; 27 | } 28 | 29 | if (str_iequals(service_name, PUBLIC_IP_GETTER_PORKBUN)) 30 | { 31 | auto * getter = new(std::nothrow) PublicIpGetterPorkbun(); 32 | if (nullptr == getter) 33 | { 34 | LOG(ERROR) << "Failed to instantiate PublicIpGetterPorkbun!"; 35 | return nullptr; 36 | } 37 | return getter; 38 | } 39 | 40 | if (str_iequals(service_name, PUBLIC_IP_GETTER_IPIFY)) 41 | { 42 | auto * getter = new(std::nothrow) PublicIpGetterIpify(); 43 | if (nullptr == getter) 44 | { 45 | LOG(ERROR) << "Failed to instantiate PublicIpGetterIpify!"; 46 | return nullptr; 47 | } 48 | return getter; 49 | } 50 | 51 | LOG(WARNING) << "Unsupported public ip getter '" << service_name << "'!"; 52 | 53 | return nullptr; 54 | } 55 | 56 | void PublicIpGetterFactory::destroy(IPublicIpGetter * ip_getter) 57 | { 58 | if (nullptr == ip_getter) 59 | { 60 | LOG(WARNING) << "Invalid param!"; 61 | return; 62 | } 63 | 64 | const std::string & name = ip_getter->getServiceName(); 65 | if (str_iequals(name, PUBLIC_IP_GETTER_IFACE)) 66 | { 67 | auto * g = dynamic_cast(ip_getter); 68 | if (nullptr == g) 69 | LOG(WARNING) << "ip_getter is not instance of PublicIpGetterIface!"; 70 | delete g; 71 | } 72 | else if (str_iequals(name, PUBLIC_IP_GETTER_PORKBUN)) 73 | { 74 | auto * g = dynamic_cast(ip_getter); 75 | if (nullptr == g) 76 | LOG(WARNING) << "ip_getter is not instance of PublicIpGetterPorkbun!"; 77 | delete g; 78 | } 79 | else if (str_iequals(name, PUBLIC_IP_GETTER_IPIFY)) 80 | { 81 | auto * g = dynamic_cast(ip_getter); 82 | if (nullptr == g) 83 | LOG(WARNING) << "ip_getter is not instance of PublicIpGetterIpify!"; 84 | delete g; 85 | } 86 | else 87 | LOG(WARNING) << "Unsupported public ip getter '" << name << "'!"; 88 | } 89 | -------------------------------------------------------------------------------- /.github/workflows/cmake-mips.yml: -------------------------------------------------------------------------------- 1 | name: CMake-MIPS 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | model: 7 | description: 'OpenWRT SDK' 8 | required: true 9 | default: 'mt7620a' 10 | 11 | env: 12 | BUILD_TYPE: Release 13 | 14 | jobs: 15 | build: 16 | runs-on: [self-hosted, linux, x64] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | with: 21 | submodules: recursive 22 | 23 | - name: Setting up OpenWRT SDK env... 24 | run: | 25 | echo '/home/admin/openwrt/OpenWrt-SDK-ramips-for-linux-x86_64-gcc-4.8-linaro_uClibc-0.9.33.2/staging_dir/toolchain-mipsel_24kec+dsp_gcc-4.8-linaro_uClibc-0.9.33.2/mipsel-openwrt-linux-uclibc:/home/admin/openwrt/OpenWrt-SDK-ramips-for-linux-x86_64-gcc-4.8-linaro_uClibc-0.9.33.2/staging_dir/toolchain-mipsel_24kec+dsp_gcc-4.8-linaro_uClibc-0.9.33.2/bin' >> $GITHUB_PATH 26 | echo 'AR=mipsel-openwrt-linux-uclibc-ar' >> $GITHUB_ENV 27 | echo 'AS=mipsel-openwrt-linux-uclibc-as' >> $GITHUB_ENV 28 | echo 'LD=mipsel-openwrt-linux-uclibc-ld' >> $GITHUB_ENV 29 | echo 'NM=mipsel-openwrt-linux-uclibc-nm' >> $GITHUB_ENV 30 | echo 'CC=mipsel-openwrt-linux-uclibc-gcc' >> $GITHUB_ENV 31 | echo 'CPP=mipsel-openwrt-linux-uclibc-cpp' >> $GITHUB_ENV 32 | echo 'GCC=mipsel-openwrt-linux-uclibc-gcc' >> $GITHUB_ENV 33 | echo 'CXX=mipsel-openwrt-linux-uclibc-g++' >> $GITHUB_ENV 34 | echo 'RANLIB=mipsel-openwrt-linux-uclibc-ranlib' >> $GITHUB_ENV 35 | echo 'STAGING_DIR=/home/admin/openwrt/OpenWrt-SDK-ramips-for-linux-x86_64-gcc-4.8-linaro_uClibc-0.9.33.2/staging_dir' >> $GITHUB_ENV 36 | echo 'CFLAGS=-Os -s' >> $GITHUB_ENV 37 | echo 'CXXFLAGS=-Os -s' >> $GITHUB_ENV 38 | if: github.event.inputs.model == 'mt7620a' 39 | 40 | - name: Check 3rdparty prebuilt cache 41 | id: cache-nix 42 | uses: actions/cache@v2 43 | with: 44 | path: 3rdparty/prebuilt 45 | key: ${{ runner.os }}-${{ github.event.inputs.model }}-build-${{ hashFiles('3rdparty/build_mips.sh') }} 46 | 47 | - name: Prebuild 3rdparty libs 48 | run: | 49 | # make file runnable, might not be necessary 50 | chmod +x "${GITHUB_WORKSPACE}/3rdparty/build_mips.sh" 51 | # run script 52 | "${GITHUB_WORKSPACE}/3rdparty/build_mips.sh" 53 | if: steps.cache-nix.outputs.cache-hit != 'true' 54 | 55 | - name: Configure CMake 56 | run: cmake -B ${{github.workspace}}/build -DMIPS_TARGET=YES -DPVE_DDNS_CLIENT_VER="0.0.4" -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} 57 | 58 | - name: Build 59 | run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} 60 | 61 | - name: Upload artifact 62 | uses: actions/upload-artifact@v2 63 | with: 64 | name: ${{ github.event.inputs.model }}-artifact 65 | path: | 66 | ${{github.workspace}}/build/pve-ddns-client 67 | ${{github.workspace}}/build/*.yml 68 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.8) 2 | 3 | set(PROJECT_NAME pve-ddns-client) 4 | project(${PROJECT_NAME}) 5 | 6 | message("CMake version: " ${CMAKE_VERSION}) 7 | 8 | set(CMAKE_CXX_STANDARD 14) 9 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 10 | 11 | option(MIPS_TARGET "Building for mips target" OFF) 12 | 13 | add_definitions(-DFMT_HEADER_ONLY) 14 | add_definitions(-DCURL_STATICLIB) 15 | if(WIN32) 16 | add_definitions(-DHAVE_SNPRINTF) 17 | endif() 18 | if(DEFINED PVE_DDNS_CLIENT_VER) 19 | add_definitions(-DPVE_DDNS_CLIENT_VER="${PVE_DDNS_CLIENT_VER}") 20 | endif() 21 | 22 | if(APPLE) 23 | find_library(LIB_FOUNDATION Foundation) 24 | if (NOT LIB_FOUNDATION) 25 | message(FATAL_ERROR "Lib Foundation not found!") 26 | endif() 27 | 28 | find_library(LIB_SYSTEMCONFIGURATION SystemConfiguration) 29 | if (NOT LIB_SYSTEMCONFIGURATION) 30 | message(FATAL_ERROR "Lib SystemConfiguration not found") 31 | endif() 32 | endif() 33 | 34 | file(GLOB_RECURSE SOURCES "src/*.cpp" "src/*.cc") 35 | 36 | add_executable(${PROJECT_NAME} ${SOURCES}) 37 | 38 | target_include_directories(${PROJECT_NAME} PRIVATE 39 | "${CMAKE_SOURCE_DIR}/3rdparty/cmdline" 40 | "${CMAKE_SOURCE_DIR}/3rdparty/fmt/include" 41 | "${CMAKE_SOURCE_DIR}/3rdparty/rapidjson/include" 42 | "${CMAKE_SOURCE_DIR}/3rdparty/prebuilt/include") 43 | 44 | target_link_directories(${PROJECT_NAME} PRIVATE "${CMAKE_SOURCE_DIR}/3rdparty/prebuilt/lib") 45 | if(EXISTS ${CMAKE_SOURCE_DIR}/3rdparty/prebuilt/lib64) 46 | target_link_directories(${PROJECT_NAME} PRIVATE "${CMAKE_SOURCE_DIR}/3rdparty/prebuilt/lib64") 47 | endif() 48 | 49 | if(WIN32) 50 | if(CMAKE_BUILD_TYPE STREQUAL "Debug") 51 | target_link_libraries(${PROJECT_NAME} PRIVATE libcurl-d glogd yaml-cppd) 52 | else() 53 | target_link_libraries(${PROJECT_NAME} PRIVATE libcurl glog yaml-cpp) 54 | endif() 55 | target_link_libraries(${PROJECT_NAME} PRIVATE wldap32 crypt32 Ws2_32) 56 | set_property(TARGET ${PROJECT_NAME} PROPERTY 57 | MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") 58 | elseif(APPLE) 59 | target_link_libraries(${PROJECT_NAME} PRIVATE curl glog yaml-cpp ssl crypto z pthread dl 60 | ${LIB_FOUNDATION} ${LIB_SYSTEMCONFIGURATION}) 61 | else() 62 | if(MIPS_TARGET) 63 | target_link_libraries(${PROJECT_NAME} PRIVATE curl glog yaml-cpp ssl crypto pthread dl) 64 | else() 65 | target_link_libraries(${PROJECT_NAME} PRIVATE curl glog yaml-cpp ssl crypto z pthread dl) 66 | endif() 67 | endif() 68 | 69 | add_custom_command( 70 | TARGET ${PROJECT_NAME} 71 | POST_BUILD 72 | COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_SOURCE_DIR}/res/pve-ddns-client.yml 73 | ${CMAKE_CURRENT_BINARY_DIR}/pve-ddns-client.yml 74 | COMMENT "Copy config yaml file to ${CMAKE_CURRENT_BINARY_DIR} directory" VERBATIM 75 | ) 76 | 77 | # Add tests and install targets if needed. 78 | install(TARGETS ${PROJECT_NAME} DESTINATION bin) 79 | 80 | # uninstall xargs rm < install_manifest.txt 81 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | #ifndef PVE_DDNS_CLIENT_SRC_CONFIG_H 2 | #define PVE_DDNS_CLIENT_SRC_CONFIG_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | // Config node 10 | typedef struct config_node_ 11 | { 12 | // PVE node 13 | std::string node; 14 | // Interface name 15 | std::string iface; 16 | // DNS service type 17 | std::string dns_type; 18 | // Credentials 19 | std::string credentials; 20 | // IPv4 domain names to update 21 | std::vector ipv4_domains; 22 | // IPv6 domain names to update 23 | std::vector ipv6_domains; 24 | } config_node; 25 | 26 | // DNS record node 27 | typedef struct dns_record_node_ 28 | { 29 | // Last get time (resolve) 30 | std::chrono::milliseconds last_get_time; 31 | // Last IP 32 | std::string last_ip; 33 | } dns_record_node; 34 | 35 | // Global config singleton 36 | class Config 37 | { 38 | public: 39 | static Config & getInstance() 40 | { 41 | static Config instance; 42 | return instance; 43 | } 44 | 45 | // Load config yaml 46 | bool loadConfig(const std::string & config_file); 47 | 48 | // Config yaml file path 49 | std::string _yml_path; 50 | // Log file saving path 51 | std::string _log_path; 52 | 53 | // Default http request timeout 30s 54 | long _http_timeout_ms = 30000; 55 | 56 | // Update interval 57 | std::chrono::milliseconds _update_interval = std::chrono::milliseconds(300000); 58 | // Log cleaner keep days 59 | int _log_overdue_days = 3; 60 | // Default realtime logging output 61 | int _log_buf_secs = 0; 62 | // Max size in MB per log file 63 | int _max_log_size_mb = 2; 64 | // Long-running service mode 65 | bool _service_mode = true; 66 | 67 | // Public IP service related 68 | std::string _public_ip_service; 69 | std::string _public_ip_credentials; 70 | 71 | // PVE API related stuff 72 | std::string _pve_api_host; 73 | std::string _pve_api_user; 74 | std::string _pve_api_realm; 75 | std::string _pve_api_token_id; 76 | std::string _pve_api_token_uuid; 77 | 78 | bool _sync_host_static_v6_address = false; 79 | 80 | // Client config 81 | config_node _client_config; 82 | // Host config 83 | config_node _host_config; 84 | // Guest configs 85 | std::unordered_map _guest_configs; 86 | 87 | std::string _my_public_ipv4; 88 | std::string _my_public_ipv6; 89 | 90 | std::unordered_map _ipv4_records; 91 | std::unordered_map _ipv6_records; 92 | 93 | // Last update time 94 | std::chrono::milliseconds _last_update_time = std::chrono::milliseconds(0); 95 | 96 | private: 97 | // ctor is hidden 98 | Config() = default; 99 | // copy ctor is hidden 100 | Config(Config const &) = default; 101 | // assign op is hidden 102 | Config & operator=(Config const &) = default; 103 | }; 104 | 105 | #endif //PVE_DDNS_CLIENT_SRC_CONFIG_H 106 | -------------------------------------------------------------------------------- /src/public_ip/public_ip_getter_porkbun.cpp: -------------------------------------------------------------------------------- 1 | #include "public_ip_getter_porkbun.h" 2 | 3 | #include "glog/logging.h" 4 | #include "fmt/format.h" 5 | #include "rapidjson/document.h" 6 | #include "rapidjson/error/en.h" 7 | 8 | #include "../utils.h" 9 | #include "../config.h" 10 | 11 | static const char * API_HOST = "https://porkbun.com/api/json/v3/"; 12 | static const char * API_HOST_V4 = "https://api-ipv4.porkbun.com/api/json/v3/"; 13 | static const char * API_PING = "ping"; 14 | 15 | const std::string & PublicIpGetterPorkbun::getServiceName() 16 | { 17 | return _service_name; 18 | } 19 | 20 | bool PublicIpGetterPorkbun::setCredentials(const std::string & cred_str) 21 | { 22 | if (cred_str.empty()) 23 | { 24 | LOG(WARNING) << "Credentials string is empty!"; 25 | return false; 26 | } 27 | std::string::size_type comma_pos = cred_str.find(','); 28 | if (std::string::npos == comma_pos) 29 | { 30 | LOG(WARNING) << "Invalid credentials string '" << cred_str << "', should be in format 'API_KEY,API_SECRET'!"; 31 | return false; 32 | } 33 | _api_key = cred_str.substr(0, comma_pos); 34 | _api_secret = cred_str.substr(comma_pos + 1); 35 | 36 | return true; 37 | } 38 | 39 | std::string PublicIpGetterPorkbun::getIpv4() 40 | { 41 | return getIp(API_HOST_V4); 42 | } 43 | 44 | std::string PublicIpGetterPorkbun::getIpv6() 45 | { 46 | std::string v6_ip = getIp(API_HOST); 47 | if (!is_ipv6(v6_ip)) 48 | { 49 | LOG(WARNING) << "'" << v6_ip << "' is not valid IPv6 ip!"; 50 | return ""; 51 | } 52 | return v6_ip; 53 | } 54 | 55 | std::string PublicIpGetterPorkbun::getIp(const std::string & api_host) const 56 | { 57 | const std::string req_url = fmt::format("{}{}", api_host, API_PING); 58 | const std::string req_body = fmt::format(R"({{"secretapikey":"{}","apikey":"{}"}})", _api_secret, _api_key); 59 | int resp_code = 0; 60 | std::string resp_data; 61 | const bool ret = http_req(req_url, req_body, Config::getInstance()._http_timeout_ms, {}, resp_code, resp_data); 62 | if (!ret || 200 != resp_code) 63 | { 64 | LOG(WARNING) << "Failed to request '" << req_url << "', response code is " << resp_code << ", response is " 65 | << resp_data << "!"; 66 | return ""; 67 | } 68 | rapidjson::Document d; 69 | rapidjson::ParseResult ok = d.Parse(resp_data.c_str()); 70 | if (!ok) 71 | { 72 | LOG(WARNING) << "Failed to parse response json, error '" << rapidjson::GetParseError_En(ok.Code()) 73 | << "' (" << ok.Offset() << ")"; 74 | return ""; 75 | } 76 | 77 | if (d.HasMember("status") && d["status"].IsString()) 78 | { 79 | const std::string status_str = d["status"].GetString(); 80 | if ("SUCCESS" == status_str) 81 | { 82 | if (d.HasMember("yourIp") && d["yourIp"].IsString()) 83 | { 84 | LOG(INFO) << "Successfully got my ip: " << d["yourIp"].GetString() << " from '" << api_host << "'."; 85 | return d["yourIp"].GetString(); 86 | } 87 | } 88 | } 89 | 90 | return ""; 91 | } 92 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef PVE_DDNS_CLIENT_SRC_UTILS_H 2 | #define PVE_DDNS_CLIENT_SRC_UTILS_H 3 | 4 | #include 5 | #include 6 | 7 | /// \brief Get app version string 8 | /// \return Version string 9 | std::string get_version_string(); 10 | 11 | /// \brief Case-insensitive string comparison 12 | /// \param l First string 13 | /// \param r Second string 14 | /// \return Result 15 | bool str_iequals(const std::string & l, const std::string & r); 16 | 17 | /// \brief Get root and sub-domain from given domain name (www.domain.com => domain.com, www) 18 | /// \param domain Domain name string 19 | /// \return A pair of strings, first is root, second is sub, may be empty 20 | std::pair get_sub_domain(const std::string & domain); 21 | 22 | /// \brief Get DNS service key from type, api key and secret 23 | /// \param dns_type DNS service type 24 | /// \param credentials DNS service credentials 25 | /// \return Hashed key 26 | size_t get_dns_service_key(const std::string & dns_type, const std::string & credentials); 27 | 28 | /// \brief Check if given IP is v4 address 29 | /// \param s IP address string 30 | /// \return Result 31 | bool is_ipv4(const std::string & s); 32 | 33 | /// \brief Check if given string is hex string 34 | /// \param s String to test 35 | /// \return Result 36 | bool check_hex(const std::string & s); 37 | 38 | /// \brief Check if given IP is v6 address 39 | /// \param s IP address string 40 | /// \return Result 41 | bool is_ipv6(const std::string & s); 42 | 43 | /// \brief Try to get IPv4, IPv6 address by parsing output from ip addr command 44 | /// \param result Output from ip addr 45 | /// \param iface Network interface 46 | /// \param ipv4 IPv4 address 47 | /// \param ipv6 IPv6 address 48 | /// \return Result 49 | bool get_ip_from_ip_addr_result(const std::string & result, const std::string & iface, 50 | std::string & ipv4, std::string & ipv6); 51 | 52 | /// \brief Try to get IPv4, IPv6 address by parsing output from ipconfig command 53 | /// \param result Output from ipconfig 54 | /// \param iface Network interface 55 | /// \param ipv4 IPv4 address 56 | /// \param ipv6 IPv6 address 57 | /// \return Result 58 | bool get_ip_from_ipconfig_result(const std::string & result, const std::string & iface, 59 | std::string & ipv4, std::string & ipv6); 60 | 61 | /// \brief HTTP request 62 | /// \param url URL 63 | /// \param req_data Request body 64 | /// \param timeout_ms Timeout 65 | /// \param custom_headers Custom headers 66 | /// \param resp_code Response code 67 | /// \param resp_data Response data 68 | /// \return If request succeeded 69 | bool http_req(const std::string & url, const std::string & req_data, long timeout_ms, 70 | const std::vector & custom_headers, 71 | int & resp_code, std::string & resp_data); 72 | 73 | /// \brief HTTP request with customizable method, e.g. PUT, DELETE... 74 | /// \param url URL 75 | /// \param req_data Request body 76 | /// \param timeout_ms Timeout 77 | /// \param custom_headers Custom headers 78 | /// \param method Method name 79 | /// \param resp_code Response code 80 | /// \param resp_data Response data 81 | /// \return If request succeeded 82 | bool http_req(const std::string & url, const std::string & req_data, long timeout_ms, 83 | const std::vector & custom_headers, const std::string & method, 84 | int & resp_code, std::string & resp_data); 85 | 86 | /// \brief Execute shell command with output stored in result 87 | /// \param cmd Shell command 88 | /// \param result Result 89 | /// \return If execution succeeded 90 | bool shell_execute(const std::string & cmd, std::string & result); 91 | 92 | #endif //PVE_DDNS_CLIENT_SRC_UTILS_H 93 | -------------------------------------------------------------------------------- /.github/workflows/cmake.yml: -------------------------------------------------------------------------------- 1 | name: CMake 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | env: 11 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 12 | BUILD_TYPE: Release 13 | 14 | jobs: 15 | build: 16 | # The CMake configure and build commands are platform agnostic and should work equally 17 | # well on Windows or Mac. You can convert this to a matrix build if you need 18 | # cross-platform coverage. 19 | # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix 20 | strategy: 21 | matrix: 22 | platform: [ubuntu-20.04, macos-11, windows-latest] 23 | runs-on: ${{ matrix.platform }} 24 | 25 | steps: 26 | - uses: actions/checkout@v4 27 | with: 28 | submodules: recursive 29 | 30 | - name: Set short git commit SHA 31 | id: vars 32 | run: echo "COMMIT_SHORT_SHA=$(git rev-parse --short ${{ github.sha }})" >> $GITHUB_ENV 33 | 34 | - name: Check 3rdparty prebuilt cache (Linux & macOS) 35 | id: cache-nix 36 | uses: actions/cache@v4 37 | with: 38 | path: 3rdparty/prebuilt 39 | key: ${{ runner.os }}-${{ matrix.platform }}-build-${{ hashFiles('3rdparty/build.sh') }} 40 | if: runner.os == 'Linux' || runner.os == 'macOS' 41 | 42 | - name: Check 3rdparty prebuilt cache (Windows) 43 | id: cache-win 44 | uses: actions/cache@v4 45 | with: 46 | path: 3rdparty/prebuilt 47 | key: ${{ runner.os }}-${{ matrix.platform }}-build-${{ hashFiles('3rdparty/build.bat') }} 48 | if: runner.os == 'Windows' 49 | 50 | - name: Prebuild 3rdparty libs (Linux & macOS) 51 | run: | 52 | # make file runnable, might not be necessary 53 | chmod +x "${GITHUB_WORKSPACE}/3rdparty/build.sh" 54 | # run script 55 | "${GITHUB_WORKSPACE}/3rdparty/build.sh" 56 | if: (runner.os == 'Linux' || runner.os == 'macOS') && steps.cache-nix.outputs.cache-hit != 'true' 57 | 58 | - name: Prebuild 3rdparty libs (Windows) 59 | run: | 60 | cmd /k "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvars64.bat" 61 | .\build.bat 62 | working-directory: .\3rdparty 63 | if: runner.os == 'Windows' && steps.cache-win.outputs.cache-hit != 'true' 64 | 65 | - name: Configure CMake 66 | # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. 67 | # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type 68 | run: cmake -B ${{github.workspace}}/build -DPVE_DDNS_CLIENT_VER="0.0.4_${{env.COMMIT_SHORT_SHA}}" -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} 69 | 70 | - name: Build 71 | # Build your program with the given configuration 72 | run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} 73 | 74 | #- name: Test 75 | # working-directory: ${{github.workspace}}/build 76 | # Execute tests defined by the CMake configuration. 77 | # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail 78 | #run: ctest -C ${{env.BUILD_TYPE}} 79 | # run: pve-ddns-client 80 | 81 | - name: Upload artifact (Linux & macOS) 82 | uses: actions/upload-artifact@v4 83 | with: 84 | name: ${{ runner.os }}-artifact 85 | path: | 86 | ${{github.workspace}}/build/pve-ddns-client 87 | ${{github.workspace}}/build/*.yml 88 | if: runner.os == 'Linux' || runner.os == 'macOS' 89 | 90 | - name: Upload artifact (Windows) 91 | uses: actions/upload-artifact@v4 92 | with: 93 | name: ${{ runner.os }}-artifact 94 | path: | 95 | ${{github.workspace}}\build\${{env.BUILD_TYPE}}\pve-ddns-client.exe 96 | ${{github.workspace}}\build\*.yml 97 | if: runner.os == 'Windows' 98 | -------------------------------------------------------------------------------- /src/dns_service/dns_service_porkbun.cpp: -------------------------------------------------------------------------------- 1 | #include "dns_service_porkbun.h" 2 | 3 | #include "fmt/format.h" 4 | #include "glog/logging.h" 5 | #include "rapidjson/document.h" 6 | #include "rapidjson/error/en.h" 7 | 8 | #include "../utils.h" 9 | #include "../config.h" 10 | 11 | static const char * API_HOST = "https://porkbun.com/api/json/v3/"; 12 | static const char * API_RETRIEVE = "dns/retrieveByNameType/{}/{}/{}"; 13 | static const char * API_EDIT = "dns/editByNameType/{}/{}/{}"; 14 | 15 | const std::string & DnsServicePorkbun::getServiceName() 16 | { 17 | return _service_name; 18 | } 19 | 20 | bool DnsServicePorkbun::setCredentials(const std::string & cred_str) 21 | { 22 | if (cred_str.empty()) 23 | { 24 | LOG(WARNING) << "Credentials string is empty!"; 25 | return false; 26 | } 27 | std::string::size_type comma_pos = cred_str.find(','); 28 | if (std::string::npos == comma_pos) 29 | { 30 | LOG(WARNING) << "Invalid credentials string '" << cred_str << "', should be in format 'API_KEY,API_SECRET'!"; 31 | return false; 32 | } 33 | _api_key = cred_str.substr(0, comma_pos); 34 | _api_secret = cred_str.substr(comma_pos + 1); 35 | 36 | return true; 37 | } 38 | 39 | std::string DnsServicePorkbun::getIpv4(const std::string & domain) 40 | { 41 | return getIp(domain, true); 42 | } 43 | 44 | std::string DnsServicePorkbun::getIpv6(const std::string & domain) 45 | { 46 | return getIp(domain, false); 47 | } 48 | 49 | bool DnsServicePorkbun::setIpv4(const std::string & domain, const std::string & ip) 50 | { 51 | return setIp(domain, ip, true); 52 | } 53 | 54 | bool DnsServicePorkbun::setIpv6(const std::string & domain, const std::string & ip) 55 | { 56 | return setIp(domain, ip, false); 57 | } 58 | 59 | std::string DnsServicePorkbun::getIp(const std::string & domain, bool is_v4) 60 | { 61 | if (domain.empty()) 62 | { 63 | LOG(WARNING) << "Invalid param!"; 64 | return ""; 65 | } 66 | 67 | const auto sub_domain = get_sub_domain(domain); 68 | const std::string api_part = fmt::format(API_RETRIEVE, sub_domain.first, is_v4 ? "A" : "AAAA", sub_domain.second); 69 | const std::string req_url = fmt::format("{}{}", API_HOST, api_part); 70 | const std::string req_body = fmt::format(R"({{"secretapikey":"{}","apikey":"{}"}})", _api_secret, _api_key); 71 | 72 | int resp_code = 0; 73 | std::string resp_data; 74 | const bool ret = http_req(req_url, req_body, Config::getInstance()._http_timeout_ms, {}, resp_code, resp_data); 75 | if (!ret || 200 != resp_code) 76 | { 77 | LOG(WARNING) << "Failed to request '" << req_url << "', response code is " << resp_code << ", response is " 78 | << resp_data << "!"; 79 | return ""; 80 | } 81 | 82 | rapidjson::Document d; 83 | rapidjson::ParseResult ok = d.Parse(resp_data.c_str()); 84 | if (!ok) 85 | { 86 | LOG(WARNING) << "Failed to parse response json, error '" << rapidjson::GetParseError_En(ok.Code()) 87 | << "' (" << ok.Offset() << ")"; 88 | return ""; 89 | } 90 | 91 | if (d.HasMember("status") && d["status"].IsString()) 92 | { 93 | const std::string status_str = d["status"].GetString(); 94 | if ("SUCCESS" == status_str) 95 | { 96 | auto it = d["records"].Begin(); 97 | while (it != d["records"].End()) 98 | return (*it)["content"].GetString(); 99 | } 100 | } 101 | 102 | return ""; 103 | } 104 | 105 | bool DnsServicePorkbun::setIp(const std::string & domain, const std::string & ip, bool is_v4) 106 | { 107 | if (domain.empty() || ip.empty()) 108 | { 109 | LOG(WARNING) << "Invalid params, domain '" << domain << "', ip '" << ip << "'!"; 110 | return false; 111 | } 112 | 113 | const auto sub_domain = get_sub_domain(domain); 114 | const std::string api_part = fmt::format(API_EDIT, sub_domain.first, is_v4 ? "A" : "AAAA", sub_domain.second); 115 | const std::string req_url = fmt::format("{}{}", API_HOST, api_part); 116 | const std::string req_body = fmt::format(R"({{"secretapikey":"{}","apikey":"{}","content":"{}"}})", 117 | _api_secret, _api_key, ip); 118 | 119 | int resp_code = 0; 120 | std::string resp_data; 121 | const bool ret = http_req(req_url, req_body, Config::getInstance()._http_timeout_ms, {}, resp_code, resp_data); 122 | if (!ret || 200 != resp_code) 123 | { 124 | LOG(WARNING) << "Failed to request '" << req_url << "', response code is " << resp_code << ", response is " 125 | << resp_data << "!"; 126 | return false; 127 | } 128 | 129 | rapidjson::Document d; 130 | rapidjson::ParseResult ok = d.Parse(resp_data.c_str()); 131 | if (!ok) 132 | { 133 | LOG(WARNING) << "Failed to parse response json, error '" << rapidjson::GetParseError_En(ok.Code()) 134 | << "' (" << ok.Offset() << ")"; 135 | return false; 136 | } 137 | 138 | if (d.HasMember("status") && d["status"].IsString()) 139 | { 140 | const std::string status_str = d["status"].GetString(); 141 | if ("SUCCESS" == status_str) 142 | return true; 143 | } 144 | 145 | return false; 146 | } 147 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | branches: [ "main" ] 19 | # schedule: 20 | # - cron: '25 17 * * 6' 21 | 22 | env: 23 | # Customize the CMake build type here (Release, Debug, RelWithDebInfo, etc.) 24 | BUILD_TYPE: Release 25 | 26 | jobs: 27 | analyze: 28 | name: Analyze 29 | # Runner size impacts CodeQL analysis time. To learn more, please see: 30 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 31 | # - https://gh.io/supported-runners-and-hardware-resources 32 | # - https://gh.io/using-larger-runners 33 | # Consider using larger runners for possible analysis time improvements. 34 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-20.04' }} 35 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 36 | permissions: 37 | actions: read 38 | contents: read 39 | security-events: write 40 | 41 | strategy: 42 | fail-fast: false 43 | matrix: 44 | language: [ 'c-cpp' ] 45 | # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] 46 | # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both 47 | # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 48 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 49 | 50 | steps: 51 | - name: Checkout repository 52 | uses: actions/checkout@v4 53 | with: 54 | submodules: recursive 55 | 56 | # Initializes the CodeQL tools for scanning. 57 | - name: Initialize CodeQL 58 | uses: github/codeql-action/init@v3 59 | with: 60 | languages: ${{ matrix.language }} 61 | # If you wish to specify custom queries, you can do so here or in a config file. 62 | # By default, queries listed here will override any specified in a config file. 63 | # Prefix the list here with "+" to use these queries and those in the config file. 64 | 65 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 66 | # queries: security-extended,security-and-quality 67 | 68 | - name: Check 3rdparty prebuilt cache (Linux & macOS) 69 | id: cache-nix 70 | uses: actions/cache@v4 71 | with: 72 | path: 3rdparty/prebuilt 73 | key: ${{ runner.os }}-${{ matrix.platform }}-build-${{ hashFiles('3rdparty/build.sh') }} 74 | if: runner.os == 'Linux' || runner.os == 'macOS' 75 | 76 | - name: Prebuild 3rdparty libs (Linux & macOS) 77 | run: | 78 | # make file runnable, might not be necessary 79 | chmod +x "${GITHUB_WORKSPACE}/3rdparty/build.sh" 80 | # run script 81 | "${GITHUB_WORKSPACE}/3rdparty/build.sh" 82 | if: (runner.os == 'Linux' || runner.os == 'macOS') && steps.cache-nix.outputs.cache-hit != 'true' 83 | 84 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 85 | # If this step fails, then you should remove it and run the build manually (see below) 86 | # - name: Autobuild 87 | # uses: github/codeql-action/autobuild@v3 88 | 89 | # ℹ️ Command-line programs to run using the OS shell. 90 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 91 | 92 | # If the Autobuild fails above, remove it and uncomment the following three lines. 93 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 94 | 95 | - name: Configure CMake 96 | # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. 97 | # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type 98 | run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} 99 | 100 | - name: Build 101 | # Build your program with the given configuration 102 | run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} 103 | 104 | - name: Perform CodeQL Analysis 105 | uses: github/codeql-action/analyze@v3 106 | with: 107 | category: "/language:${{matrix.language}}" 108 | upload: False 109 | output: sarif-results 110 | 111 | - name: filter-sarif 112 | uses: advanced-security/filter-sarif@v1 113 | with: 114 | patterns: | 115 | -**/* 116 | src/**/* 117 | input: sarif-results/cpp.sarif 118 | output: sarif-results/cpp.sarif 119 | 120 | - name: Upload SARIF 121 | uses: github/codeql-action/upload-sarif@v3 122 | with: 123 | sarif_file: sarif-results/cpp.sarif 124 | category: "/language:${{matrix.language}}" 125 | -------------------------------------------------------------------------------- /src/config.cpp: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | 3 | #include 4 | 5 | #include "yaml-cpp/yaml.h" 6 | 7 | // Parse general config from yaml node 8 | static void parse_general_config(const YAML::Node & yaml_node, Config & config) 9 | { 10 | if (yaml_node["update-interval-ms"]) 11 | { 12 | const auto interval_ms = yaml_node["update-interval-ms"].as(); 13 | config._update_interval = std::chrono::milliseconds(interval_ms); 14 | } 15 | if (yaml_node["log-overdue-days"]) 16 | config._log_overdue_days = yaml_node["log-overdue-days"].as(); 17 | if (yaml_node["log-buf-secs"]) 18 | config._log_buf_secs = yaml_node["log-buf-secs"].as(); 19 | if (yaml_node["max-log-size-mb"]) 20 | config._max_log_size_mb = yaml_node["max-log-size-mb"].as(); 21 | if (yaml_node["service-mode"]) 22 | { 23 | const auto val = yaml_node["service-mode"].as(); 24 | config._service_mode = val == "true"; 25 | } 26 | if (yaml_node["public-ip"]) 27 | { 28 | const auto & pi = yaml_node["public-ip"]; 29 | if (pi["service"]) 30 | config._public_ip_service = pi["service"].as(); 31 | if (pi["credentials"]) 32 | config._public_ip_credentials = pi["credentials"].as(); 33 | } 34 | if (yaml_node["pve-api"]) 35 | { 36 | const auto & pa = yaml_node["pve-api"]; 37 | if (pa["host"]) 38 | { 39 | config._pve_api_host = pa["host"].as(); 40 | // Append trailing slash if needed 41 | if (config._pve_api_host[config._pve_api_host.length() - 1] != '/') 42 | config._pve_api_host.append("/"); 43 | } 44 | if (pa["user"]) 45 | config._pve_api_user = pa["user"].as(); 46 | if (pa["realm"]) 47 | config._pve_api_realm = pa["realm"].as(); 48 | if (pa["token-id"]) 49 | config._pve_api_token_id = pa["token-id"].as(); 50 | if (pa["token-uuid"]) 51 | config._pve_api_token_uuid = pa["token-uuid"].as(); 52 | } 53 | if (yaml_node["sync_host_static_v6_address"]) 54 | { 55 | const auto val = yaml_node["sync_host_static_v6_address"].as(); 56 | config._sync_host_static_v6_address = val == "true"; 57 | } 58 | } 59 | 60 | // Parse ddns config from yaml node 61 | static void parse_ddns_config(const YAML::Node & yaml_node, config_node & cfg_node) 62 | { 63 | if (yaml_node["node"]) 64 | cfg_node.node = yaml_node["node"].as(); 65 | if (yaml_node["iface"]) 66 | cfg_node.iface = yaml_node["iface"].as(); 67 | if (yaml_node["dns"]) 68 | cfg_node.dns_type = yaml_node["dns"].as(); 69 | if (yaml_node["credentials"]) 70 | cfg_node.credentials = yaml_node["credentials"].as(); 71 | if (yaml_node["ipv4"] && yaml_node["ipv4"].IsSequence()) 72 | { 73 | const auto & ipv4_domains = yaml_node["ipv4"]; 74 | for (auto it = ipv4_domains.begin(); it != ipv4_domains.end(); ++it) 75 | cfg_node.ipv4_domains.emplace_back(it->as()); 76 | } 77 | if (yaml_node["ipv6"] && yaml_node["ipv6"].IsSequence()) 78 | { 79 | const auto & ipv4_domains = yaml_node["ipv6"]; 80 | for (auto it = ipv4_domains.begin(); it != ipv4_domains.end(); ++it) 81 | cfg_node.ipv6_domains.emplace_back(it->as()); 82 | } 83 | } 84 | 85 | bool Config::loadConfig(const std::string & config_file) 86 | { 87 | bool conf_valid = false; 88 | try 89 | { 90 | YAML::Node conf = YAML::LoadFile(config_file); 91 | // Reset current loaded configs first 92 | _client_config = {}; 93 | _host_config = {}; 94 | _guest_configs.clear(); 95 | // Mandatory general node 96 | if (conf["general"]) 97 | { 98 | std::cout << "Found general config!" << std::endl; 99 | parse_general_config(conf["general"], *this); 100 | } 101 | else 102 | { 103 | std::cerr << "General config not found!" << std::endl; 104 | return conf_valid; 105 | } 106 | // Load client config if specified 107 | if (conf["client"]) 108 | { 109 | std::cout << "Found client config!" << std::endl; 110 | parse_ddns_config(conf["client"], _client_config); 111 | std::cout << "Client config loaded, " << _client_config.ipv4_domains.size() << " ipv4 domain(s), " 112 | << _client_config.ipv6_domains.size() << " ipv6 domain(s)!" << std::endl; 113 | } 114 | // Load host config if specified 115 | if (conf["host"]) 116 | { 117 | std::cout << "Found host config!" << std::endl; 118 | parse_ddns_config(conf["host"], _host_config); 119 | std::cout << "Host config loaded, " << _host_config.ipv4_domains.size() << " ipv4 domain(s), " 120 | << _host_config.ipv6_domains.size() << " ipv6 domain(s)!" << std::endl; 121 | } 122 | // Load each guest config if any 123 | if (conf["guests"] && conf["guests"].IsSequence()) 124 | { 125 | std::cout << "Found guests config!" << std::endl; 126 | const auto & guests = conf["guests"]; 127 | for (auto it = guests.begin(); it != guests.end(); ++it) 128 | { 129 | const auto & guest_node = it->as(); 130 | if (guest_node["vmid"]) 131 | { 132 | config_node temp = {}; 133 | parse_ddns_config(guest_node, temp); 134 | _guest_configs.emplace(guest_node["vmid"].as(), temp); 135 | } 136 | } 137 | std::cout << _guest_configs.size() << " guest config(s) loaded!" << std::endl; 138 | } 139 | 140 | conf_valid = true; 141 | } 142 | catch (...) 143 | { 144 | std::cerr << "Failed to load config yaml file '" << config_file << "'!" << std::endl; 145 | } 146 | 147 | return conf_valid; 148 | } 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pve-ddns-client 2 | [![License](https://img.shields.io/badge/License-BSD%202--Clause-orange.svg)](https://opensource.org/licenses/BSD-2-Clause) 3 | [![CMake](https://github.com/wzkres/pve-ddns-client/actions/workflows/cmake.yml/badge.svg)](https://github.com/wzkres/pve-ddns-client/actions/workflows/cmake.yml) 4 | [![CodeQL](https://github.com/wzkres/pve-ddns-client/actions/workflows/codeql.yml/badge.svg)](https://github.com/wzkres/pve-ddns-client/actions/workflows/codeql.yml) 5 | 6 | ## EN 7 | A Proxmox VE (PVE) dedicated DDNS updater written in C++ 8 | ### Detailed description 9 | **pve-ddns-client** is a DDNS updater designed specifically for the Proxmox VE (PVE) virtualization management platform. Generally deployed in the PVE host system, it is used to update DDNS domain records for host and all guests (including KVM and LXC guests). Also it can be deployed on any device that can access the PVE host API (in this case, DDNS updating of LXC guests will not work, since pct command line tool can only be accessed from the PVE host system). It can even be deployed as a normal DDNS updater (when only the client section is specified in the configuration file). 10 | ### Usage 11 | - pve-ddns-client.yml yaml config file 12 | ```yaml 13 | # General configuration 14 | general: 15 | # Update interval in milliseconds, only for service mode 16 | update-interval-ms: 300000 17 | # Log overdue days 18 | log-overdue-days: 3 19 | # Log buffer seconds 20 | log-buf-secs: 2 21 | # Max size per log file in megabytes 22 | max-log-size-mb: 2 23 | # Long-running service mode 24 | service-mode: true 25 | # Public IP getter 26 | public-ip: 27 | # Service type: porkbun, ipify 28 | service: porkbun 29 | # Credentials 30 | # porkbun: api_key,secret_key 31 | # ipify: leave empty 32 | credentials: api_key,secret_key 33 | # PVE API access 34 | pve-api: 35 | # API root url 36 | host: https://pve.domain.com:8006 37 | # Username 38 | user: root 39 | # Realm 40 | realm: pam 41 | # Token ID 42 | token-id: ddns 43 | # Token UUID 44 | token-uuid: uuid 45 | # Special feature for syncing host static IPv6 address with guest DHCP IPv6 address 46 | sync_host_static_v6_address: false 47 | # Client DDNS configuration (when working as a normal DDNS updater) 48 | client: 49 | # DNS service type: porkbun, dnspod, cloudflare 50 | dns: dnspod 51 | # Credentials 52 | # porkbun: api_key,secret_key 53 | # dnspod: token_id,token 54 | # cloudflare: api_token 55 | credentials: token_id,token 56 | # IPv4 (A record) domains 57 | ipv4: ["v4sub1.domain.com", "v4sub2.domain.com"] 58 | # IPv6 (AAAA record) domains 59 | ipv6: ["v6sub1.domain.com", "v6sub2.domain.com"] 60 | # PVE host DDNS configuration 61 | host: 62 | # Node name 63 | node: node 64 | # Network interface name 65 | iface: vmbr0 66 | # DNS service type, refer to client section 67 | dns: porkbun 68 | # Credentials, refer to client section 69 | credentials: api_key,secret_key 70 | # IPv4 (A record) domains 71 | ipv4: ["v4sub1.domain.com", "v4sub2.domain.com"] 72 | # IPv6 (AAAA record) domains 73 | ipv6: ["v6sub1.domain.com", "v6sub2.domain.com"] 74 | # PVE guest DDNS configuration, basically same as host section with an additional VM id 75 | guests: 76 | # KVM guest example 77 | - node: node 78 | vmid: 100 79 | iface: ens18 80 | dns: porkbun 81 | credentials: api_key,secret_key 82 | ipv4: ["v4sub1.domain.com", "v4sub2.domain.com"] 83 | ipv6: ["v6sub1.domain.com", "v6sub2.domain.com"] 84 | # LXC guest example (must run inside PVE host system for this to work) 85 | - node: node 86 | vmid: 101 87 | iface: eth0 88 | dns: porkbun 89 | credentials: api_key,secret_key 90 | ipv4: ["v4sub1.domain.com", "v4sub2.domain.com"] 91 | ipv6: ["v6sub1.domain.com", "v6sub2.domain.com"] 92 | ``` 93 | - Command line parameters 94 | ``` 95 | usage: ./pve-ddns-client [options] ... 96 | options: 97 | -v, --version Print version 98 | -h, --help Show usage 99 | -c, --config Config yaml file to load (string [=./pve-ddns-client.yml]) 100 | -l, --log Log file path (string [=./]) 101 | ``` 102 | ### Build 103 | Please refer to GitHub Actions workflow:https://github.com/wzkres/pve-ddns-client/blob/main/.github/workflows/cmake.yml 104 | 105 | ## CN 106 | 一款C++开发的针对Proxmox VE环境的DDNS自动更新程序 107 | ### 详细介绍 108 | 本程序用于配合Proxmox VE(以下简称PVE)虚拟化环境下的宿主机和客户机动态IP(DHCP)变化,自动更新相关域名记录。一般部署于PVE宿主系统中(可同时支持宿主、KVM客户、LXC客户系统的动态IP域名更新),也可部署在任意可访问到PVE宿主API的设备上(此时由于无法调用宿主系统上的pct命令行工具,所有LXC客户系统将无法正常更新DDNS域名),甚至可以作为普通DDNS更新程序部署在任何设备上(配置文件中仅指定client配置)。 109 | ### 使用说明 110 | - pve-ddns-client.yml 配置文件说明: 111 | ```yaml 112 | # 通用配置 113 | general: 114 | # 更新间隔时间,单位毫秒,仅服务模式时有效 115 | update-interval-ms: 300000 116 | # 日志保留时间,单位天 117 | log-overdue-days: 3 118 | # 日志缓冲时间,单位秒 119 | log-buf-secs: 2 120 | # 日志文件滚动大小,单位兆 121 | max-log-size-mb: 2 122 | # 是否作为服务模式启动 123 | service-mode: true 124 | # 公网IP获取方式 125 | public-ip: 126 | # 服务类型,可选值为 porkbun, ipify 127 | service: porkbun 128 | # 服务鉴权信息 129 | # porkbun为 api_key,secret_key 的格式 130 | # ipify不需要鉴权 131 | credentials: api_key,secret_key 132 | # Proxmox VE API访问相关配置 133 | pve-api: 134 | # API访问地址 135 | host: https://pve.domain.com:8006 136 | # 用户名 137 | user: root 138 | # realm 139 | realm: pam 140 | # Token ID 141 | token-id: ddns 142 | # Token UUID 143 | token-uuid: uuid 144 | # 特殊功能,根据VM的动态IPv6地址,更新宿主系统的静态IPv6地址 145 | sync_host_static_v6_address: false 146 | # 客户端DDNS配置(运行本程序的系统,不一定是PVE的宿主,此时本程序工作方式与一般DDNS更新程序类似) 147 | client: 148 | # 服务类型,可选值为 porkbun, dnspod, cloudflare 149 | dns: dnspod 150 | # 鉴权信息 151 | # porkbun为 api_key,secret_key 的格式 152 | # dnspod为 token_id,token 的格式 153 | # cloudflare诶 api_token 的格式 154 | credentials: token_id,token 155 | # 所有需要更新IPv4 A记录的域名 156 | ipv4: ["v4sub1.domain.com", "v4sub2.domain.com"] 157 | # 所有需要更新IPv6 AAAA记录的域名 158 | ipv6: ["v6sub1.domain.com", "v6sub2.domain.com"] 159 | # PVE宿主DDNS配置 160 | host: 161 | # node名 162 | node: node 163 | # 网卡名 164 | iface: vmbr0 165 | # 服务类型,参考client部分说明 166 | dns: porkbun 167 | # 鉴权信息,参考client部分说明 168 | credentials: api_key,secret_key 169 | # 所有需要更新IPv4 A记录的域名 170 | ipv4: ["v4sub1.domain.com", "v4sub2.domain.com"] 171 | # 所有需要更新IPv6 AAAA记录的域名 172 | ipv6: ["v6sub1.domain.com", "v6sub2.domain.com"] 173 | # PVE客户虚拟机DDNS配置,除需指定vmid外,其它配置项与host一致 174 | guests: 175 | # KVM客户系统节点示例 176 | - node: node 177 | vmid: 100 178 | iface: ens18 179 | dns: porkbun 180 | credentials: api_key,secret_key 181 | ipv4: ["v4sub1.domain.com", "v4sub2.domain.com"] 182 | ipv6: ["v6sub1.domain.com", "v6sub2.domain.com"] 183 | # LXC客户系统节点示例(此时程序需运行在PVE宿主系统上) 184 | - node: node 185 | vmid: 101 186 | iface: eth0 187 | dns: porkbun 188 | credentials: api_key,secret_key 189 | ipv4: ["v4sub1.domain.com", "v4sub2.domain.com"] 190 | ipv6: ["v6sub1.domain.com", "v6sub2.domain.com"] 191 | ``` 192 | - 程序参数说明 193 | ``` 194 | usage: ./pve-ddns-client [options] ... 195 | options: 196 | -v, --version 显示版本号 197 | -h, --help 显示使用说明 198 | -c, --config 指定配置文件(默认 ./pve-ddns-client.yml) 199 | -l, --log 指定日志保存位置(默认 ./) 200 | ``` 201 | ### 构建 202 | 请参考GitHub Actions workflow:https://github.com/wzkres/pve-ddns-client/blob/main/.github/workflows/cmake.yml 203 | -------------------------------------------------------------------------------- /src/dns_service/dns_service_dnspod.cpp: -------------------------------------------------------------------------------- 1 | #include "dns_service_dnspod.h" 2 | 3 | #include "fmt/format.h" 4 | #include "glog/logging.h" 5 | #include "rapidjson/document.h" 6 | #include "rapidjson/error/en.h" 7 | 8 | #include "../utils.h" 9 | #include "../config.h" 10 | 11 | static const char * API_HOST = "https://dnsapi.cn/"; 12 | static const char * API_VERSION = "Info.Version"; 13 | static const char * API_RECORD_LIST = "Record.List"; 14 | static const char * API_RECORD_DDNS = "Record.Ddns"; 15 | 16 | const std::string & DnsServiceDnspod::getServiceName() 17 | { 18 | return _service_name; 19 | } 20 | 21 | bool DnsServiceDnspod::setCredentials(const std::string & cred_str) 22 | { 23 | if (cred_str.empty()) 24 | { 25 | LOG(WARNING) << "Credentials string is empty!"; 26 | return false; 27 | } 28 | std::string::size_type comma_pos = cred_str.find(','); 29 | if (std::string::npos == comma_pos) 30 | { 31 | LOG(WARNING) << "Invalid credentials string '" << cred_str << "', should be in format 'TOKEN_ID,TOKEN'!"; 32 | return false; 33 | } 34 | _token = cred_str; 35 | 36 | std::string api_version; 37 | if (!getVersion(api_version)) 38 | { 39 | LOG(WARNING) << "Failed to get API version, maybe wrong token!"; 40 | return false; 41 | } 42 | 43 | LOG(INFO) << "Successfully got API version '" << api_version << "'."; 44 | return true; 45 | } 46 | 47 | std::string DnsServiceDnspod::getIpv4(const std::string & domain) 48 | { 49 | return getIp(domain, true); 50 | } 51 | 52 | std::string DnsServiceDnspod::getIpv6(const std::string & domain) 53 | { 54 | return getIp(domain, false); 55 | } 56 | 57 | bool DnsServiceDnspod::setIpv4(const std::string & domain, const std::string & ip) 58 | { 59 | return setIp(domain, ip, true); 60 | } 61 | 62 | bool DnsServiceDnspod::setIpv6(const std::string & domain, const std::string & ip) 63 | { 64 | return setIp(domain, ip, false); 65 | } 66 | 67 | bool DnsServiceDnspod::getVersion(std::string & version) 68 | { 69 | const auto & config = Config::getInstance(); 70 | 71 | const std::string req_url = fmt::format("{}{}", API_HOST, API_VERSION); 72 | const std::string req_body = fmt::format(R"(login_token={}&format=json)", _token); 73 | 74 | int resp_code = 0; 75 | std::string resp_data; 76 | const bool ret = http_req(req_url, req_body, config._http_timeout_ms, {}, resp_code, resp_data); 77 | if (!ret || 200 != resp_code) 78 | { 79 | LOG(WARNING) << "Failed to request '" << req_url << "', response code is " << resp_code << ", response is " 80 | << resp_data << "!"; 81 | return false; 82 | } 83 | 84 | rapidjson::Document d; 85 | rapidjson::ParseResult ok = d.Parse(resp_data.c_str()); 86 | if (!ok) 87 | { 88 | LOG(WARNING) << "Failed to parse response json, error '" << rapidjson::GetParseError_En(ok.Code()) 89 | << "' (" << ok.Offset() << ")"; 90 | return false; 91 | } 92 | 93 | if (d.HasMember("status") && d["status"].IsObject()) 94 | { 95 | const auto & status = d["status"]; 96 | if (status.HasMember("code") && status["code"].IsString() && str_iequals(status["code"].GetString(), "1")) 97 | { 98 | if (status.HasMember("message") && status["message"].IsString()) 99 | { 100 | version = status["message"].GetString(); 101 | return true; 102 | } 103 | } 104 | } 105 | 106 | LOG(WARNING) << "Invalid response '" << resp_data << "'!"; 107 | return false; 108 | } 109 | 110 | std::string DnsServiceDnspod::getIp(const std::string & domain, bool is_v4) 111 | { 112 | if (domain.empty()) 113 | { 114 | LOG(WARNING) << "Invalid param!"; 115 | return ""; 116 | } 117 | 118 | const auto & config = Config::getInstance(); 119 | 120 | const auto sub_domain = get_sub_domain(domain); 121 | const std::string req_url = fmt::format("{}{}", API_HOST, API_RECORD_LIST); 122 | const std::string req_body = fmt::format( 123 | R"(login_token={}&domain={}&sub_domain={}&record_type={}&format=json&lang=en)", 124 | _token, sub_domain.first, sub_domain.second, is_v4 ? "A" : "AAAA" 125 | ); 126 | 127 | int resp_code = 0; 128 | std::string resp_data; 129 | const bool ret = http_req(req_url, req_body, config._http_timeout_ms, {}, resp_code, resp_data); 130 | if (!ret || 200 != resp_code) 131 | { 132 | LOG(WARNING) << "Failed to request '" << req_url << "', response code is " << resp_code << ", response is " 133 | << resp_data << "!"; 134 | return ""; 135 | } 136 | 137 | rapidjson::Document d; 138 | rapidjson::ParseResult ok = d.Parse(resp_data.c_str()); 139 | if (!ok) 140 | { 141 | LOG(WARNING) << "Failed to parse response json, error '" << rapidjson::GetParseError_En(ok.Code()) 142 | << "' (" << ok.Offset() << ")"; 143 | return ""; 144 | } 145 | 146 | if (d.HasMember("status") && d["status"].IsObject()) 147 | { 148 | const auto & status = d["status"]; 149 | if (status.HasMember("code") && status["code"].IsString() && str_iequals(status["code"].GetString(), "1")) 150 | { 151 | if (d.HasMember("records") && d["records"].IsArray()) 152 | { 153 | const auto & result = d["records"].GetArray(); 154 | for (const auto & r : result) 155 | { 156 | std::string record_id, line_id; 157 | if (r.HasMember("id") && r["id"].IsString()) 158 | record_id = r["id"].GetString(); 159 | if (r.HasMember("line_id") && r["line_id"].IsString()) 160 | line_id = r["line_id"].GetString(); 161 | if (!updateRecordCache(domain, is_v4, record_id, line_id)) 162 | { 163 | LOG(WARNING) << "Failed to update record cache for IP" << (is_v4 ? "v4" : "v6") 164 | << " domain '" << domain << "'!"; 165 | return ""; 166 | } 167 | if (r.HasMember("value") && r["value"].IsString()) 168 | return r["value"].GetString(); 169 | } 170 | } 171 | } 172 | } 173 | 174 | LOG(WARNING) << "Invalid response '" << resp_data << "'!"; 175 | return ""; 176 | } 177 | 178 | bool DnsServiceDnspod::setIp(const std::string & domain, const std::string & ip, bool is_v4) 179 | { 180 | if (domain.empty() || ip.empty()) 181 | { 182 | LOG(WARNING) << "Invalid params, domain '" << domain << "', ip '" << ip << "'!"; 183 | return false; 184 | } 185 | 186 | const auto & record_cache = getRecordCache(domain, is_v4); 187 | if (nullptr == record_cache) 188 | { 189 | LOG(WARNING) << "No record cache found for IP" << (is_v4 ? "v4" : "v6") << " domain '" << domain << "'!"; 190 | return false; 191 | } 192 | 193 | const auto & config = Config::getInstance(); 194 | 195 | const auto sub_domain = get_sub_domain(domain); 196 | const std::string req_url = fmt::format("{}{}", API_HOST, API_RECORD_DDNS); 197 | const std::string req_body = fmt::format( 198 | R"(login_token={}&domain={}&sub_domain={}&record_id={}&record_line_id={}&format=json&lang=en)", 199 | _token, sub_domain.first, sub_domain.second, record_cache->record_id, record_cache->line_id 200 | ); 201 | 202 | int resp_code = 0; 203 | std::string resp_data; 204 | const bool ret = http_req(req_url, req_body, config._http_timeout_ms, {}, resp_code, resp_data); 205 | if (!ret || 200 != resp_code) 206 | { 207 | LOG(WARNING) << "Failed to request '" << req_url << "', response code is " << resp_code << ", response is " 208 | << resp_data << "!"; 209 | return ""; 210 | } 211 | 212 | rapidjson::Document d; 213 | rapidjson::ParseResult ok = d.Parse(resp_data.c_str()); 214 | if (!ok) 215 | { 216 | LOG(WARNING) << "Failed to parse response json, error '" << rapidjson::GetParseError_En(ok.Code()) 217 | << "' (" << ok.Offset() << ")"; 218 | return ""; 219 | } 220 | 221 | if (d.HasMember("status") && d["status"].IsObject()) 222 | { 223 | const auto & status = d["status"]; 224 | if (status.HasMember("code") && status["code"].IsString() && str_iequals(status["code"].GetString(), "1")) 225 | return true; 226 | } 227 | 228 | LOG(WARNING) << "Invalid response '" << resp_data << "'!"; 229 | 230 | return false; 231 | } 232 | 233 | bool DnsServiceDnspod::updateRecordCache(const std::string & domain, bool is_v4, 234 | const std::string & record_id, const std::string & line_id) 235 | { 236 | if (domain.empty() || record_id.empty() || line_id.empty()) 237 | { 238 | LOG(WARNING) << "Invalid params, domain '" << domain << "', record id '" << record_id << "', line id '" 239 | << line_id << "'"; 240 | return false; 241 | } 242 | 243 | auto found = std::find_if(_records_cache.begin(), _records_cache.end(), 244 | [&domain, is_v4](const dnspod_record_cache & rc) 245 | { 246 | return rc.domain == domain && rc.is_v4 == is_v4; 247 | }); 248 | 249 | if (found != _records_cache.end()) 250 | { 251 | // Update exist record cache 252 | found->record_id = record_id; 253 | found->line_id = line_id; 254 | } 255 | else 256 | { 257 | // Build up new record cache 258 | _records_cache.emplace_back(dnspod_record_cache{ domain, is_v4, record_id, line_id }); 259 | } 260 | 261 | return true; 262 | } 263 | 264 | const dnspod_record_cache * DnsServiceDnspod::getRecordCache(const std::string & domain, bool is_v4) const 265 | { 266 | if (domain.empty()) 267 | { 268 | LOG(WARNING) << "Invalid param!"; 269 | return nullptr; 270 | } 271 | 272 | auto found = std::find_if(_records_cache.begin(), _records_cache.end(), 273 | [&domain, is_v4](const dnspod_record_cache & rc) 274 | { 275 | return rc.domain == domain && rc.is_v4 == is_v4; 276 | }); 277 | 278 | if (found != _records_cache.end()) 279 | return &(*found); 280 | 281 | return nullptr; 282 | } 283 | -------------------------------------------------------------------------------- /src/pve/pve_api_client.cpp: -------------------------------------------------------------------------------- 1 | #include "pve_api_client.h" 2 | 3 | #include "glog/logging.h" 4 | #include "fmt/format.h" 5 | #include "rapidjson/document.h" 6 | #include "rapidjson/error/en.h" 7 | 8 | #include "../config.h" 9 | #include "../utils.h" 10 | 11 | static const char * API_VERSION = "api2/json/version"; 12 | static const char * API_HOST_NETWORK = "api2/json/nodes/{}/network/{}"; 13 | static const char * API_HOST_NETWORK_APPLY = "api2/json/nodes/{}/network"; 14 | static const char * API_GUEST_NETWORK = "api2/json//nodes/{}/qemu/{}/agent/network-get-interfaces"; 15 | 16 | static std::string get_pve_api_http_auth_header() 17 | { 18 | const auto & config = Config::getInstance(); 19 | // Authorization: PVEAPIToken=USER@REALM!TOKENID=UUID 20 | return fmt::format("Authorization: PVEAPIToken={}@{}!{}={}", 21 | config._pve_api_user, config._pve_api_realm, 22 | config._pve_api_token_id, config._pve_api_token_uuid); 23 | } 24 | 25 | bool PveApiClient::init() 26 | { 27 | const bool ret = checkApiHost(); 28 | if (!ret) 29 | LOG(WARNING) << "Failed to checkApiHost!"; 30 | return ret; 31 | } 32 | 33 | std::pair PveApiClient::getHostIp(const std::string & node, const std::string & iface) 34 | { 35 | const auto & config = Config::getInstance(); 36 | 37 | const std::string api_part = fmt::format(API_HOST_NETWORK, node, iface); 38 | const std::string req_url = fmt::format("{}{}", config._pve_api_host, api_part); 39 | 40 | int resp_code = 0; 41 | std::string resp_data; 42 | const bool ret = req(req_url, "", resp_code, resp_data); 43 | if (!ret || 200 != resp_code) 44 | { 45 | LOG(WARNING) << "Failed to request '" << req_url << "', response code is " << resp_code << ", response is " 46 | << resp_data << "!"; 47 | return { "", "" }; 48 | } 49 | 50 | rapidjson::Document d; 51 | rapidjson::ParseResult ok = d.Parse(resp_data.c_str()); 52 | if (!ok) 53 | { 54 | LOG(WARNING) << "Failed to parse response json, error '" << rapidjson::GetParseError_En(ok.Code()) 55 | << "' (" << ok.Offset() << ")"; 56 | return { "", "" }; 57 | } 58 | 59 | if (d.HasMember("data") && d["data"].IsObject()) 60 | { 61 | const auto & data = d["data"]; 62 | std::string v4_ip, v6_ip; 63 | if (data.HasMember("address") && data["address"].IsString()) 64 | v4_ip = data["address"].GetString(); 65 | if (data.HasMember("address6") && data["address6"].IsString()) 66 | v6_ip = data["address6"].GetString(); 67 | 68 | return { v4_ip, v6_ip }; 69 | } 70 | 71 | return { "", "" }; 72 | } 73 | 74 | std::pair PveApiClient::getGuestIp(const std::string & node, 75 | const int vmid, 76 | const std::string & iface) 77 | { 78 | const auto & config = Config::getInstance(); 79 | 80 | const std::string api_part = fmt::format(API_GUEST_NETWORK, node, vmid); 81 | const std::string req_url = fmt::format("{}{}", config._pve_api_host, api_part); 82 | 83 | int resp_code = 0; 84 | std::string resp_data; 85 | const bool ret = req(req_url, "", resp_code, resp_data); 86 | if (!ret || 200 != resp_code) 87 | { 88 | LOG(WARNING) << "Failed to request '" << req_url << "', response code is " << resp_code << ", response is " 89 | << resp_data << "!"; 90 | return { "", "" }; 91 | } 92 | 93 | rapidjson::Document d; 94 | rapidjson::ParseResult ok = d.Parse(resp_data.c_str()); 95 | if (!ok) 96 | { 97 | LOG(WARNING) << "Failed to parse response json, error '" << rapidjson::GetParseError_En(ok.Code()) 98 | << "' (" << ok.Offset() << ")"; 99 | return { "", "" }; 100 | } 101 | 102 | if (d.HasMember("data") && d["data"].IsObject()) 103 | { 104 | const auto & data = d["data"]; 105 | std::string v4_ip, v6_ip; 106 | if (data.HasMember("result") && data["result"].IsArray()) 107 | { 108 | const auto & result = data["result"].GetArray(); 109 | for (const auto & r : result) 110 | { 111 | if (r.HasMember("name") && r["name"].IsString() && r["name"].GetString() == iface) 112 | { 113 | if (r.HasMember("ip-addresses") && r["ip-addresses"].IsArray()) 114 | { 115 | const auto & ips = r["ip-addresses"].GetArray(); 116 | for (const auto & ip : ips) 117 | { 118 | if (ip.HasMember("ip-address-type") && ip["ip-address-type"].IsString()) 119 | { 120 | const std::string type = ip["ip-address-type"].GetString(); 121 | if ("ipv4" == type) 122 | v4_ip = ip["ip-address"].GetString(); 123 | else if ("ipv6" == type && v6_ip.empty()) 124 | { 125 | v6_ip = ip["ip-address"].GetString(); 126 | if (v6_ip.compare(0, 4, "fe80") == 0) 127 | v6_ip.clear(); 128 | } 129 | } 130 | } 131 | break; 132 | } 133 | } 134 | } 135 | } 136 | 137 | return { v4_ip, v6_ip }; 138 | } 139 | 140 | return { "", "" }; 141 | } 142 | 143 | bool PveApiClient::setHostNetworkAddress(const std::string & node, const std::string & iface, 144 | const std::string & v4_ip, const std::string & v6_ip) 145 | { 146 | const auto & config = Config::getInstance(); 147 | 148 | const std::string api_part = fmt::format(API_HOST_NETWORK, node, iface); 149 | const std::string req_url = fmt::format("{}{}", config._pve_api_host, api_part); 150 | const std::string req_body = fmt::format(R"(type=bridge&address6={}&netmask6=128&address={}&netmask=255.255.255.0)", 151 | v6_ip, v4_ip); 152 | 153 | int resp_code = 0; 154 | std::string resp_data; 155 | std::vector headers = { get_pve_api_http_auth_header() }; 156 | const bool ret = http_req(req_url, req_body, Config::getInstance()._http_timeout_ms, headers, "put", 157 | resp_code, resp_data); 158 | if (!ret || 200 != resp_code) 159 | { 160 | LOG(WARNING) << "Failed to request '" << req_url << "', response code is " << resp_code << ", response is " 161 | << resp_data << "!"; 162 | return false; 163 | } 164 | 165 | rapidjson::Document d; 166 | rapidjson::ParseResult ok = d.Parse(resp_data.c_str()); 167 | if (!ok) 168 | { 169 | LOG(WARNING) << "Failed to parse response json, error '" << rapidjson::GetParseError_En(ok.Code()) 170 | << "' (" << ok.Offset() << ")"; 171 | return false; 172 | } 173 | 174 | return true; 175 | } 176 | 177 | bool PveApiClient::applyHostNetworkChange(const std::string & node) 178 | { 179 | return reqHostNetwork("put", node); 180 | } 181 | 182 | bool PveApiClient::revertHostNetworkChange(const std::string & node) 183 | { 184 | return reqHostNetwork("delete", node); 185 | } 186 | 187 | bool PveApiClient::req(const std::string & api_url, const std::string & req_data, 188 | int & resp_code, std::string & resp_data) const 189 | { 190 | std::vector headers = { get_pve_api_http_auth_header() }; 191 | return http_req(api_url, req_data, Config::getInstance()._http_timeout_ms, headers, resp_code, resp_data); 192 | } 193 | 194 | bool PveApiClient::reqHostNetwork(const std::string & method, const std::string & node) const 195 | { 196 | const auto & config = Config::getInstance(); 197 | 198 | const std::string api_part = fmt::format(API_HOST_NETWORK_APPLY, node); 199 | const std::string req_url = fmt::format("{}{}", config._pve_api_host, api_part); 200 | 201 | int resp_code = 0; 202 | std::string resp_data; 203 | std::vector headers = { get_pve_api_http_auth_header() }; 204 | const bool ret = http_req(req_url, "", Config::getInstance()._http_timeout_ms, headers, method, 205 | resp_code, resp_data); 206 | if (!ret || 200 != resp_code) 207 | { 208 | LOG(WARNING) << "Failed to request '" << req_url << "', response code is " << resp_code << ", response is " 209 | << resp_data << "!"; 210 | return false; 211 | } 212 | 213 | rapidjson::Document d; 214 | rapidjson::ParseResult ok = d.Parse(resp_data.c_str()); 215 | if (!ok) 216 | { 217 | LOG(WARNING) << "Failed to parse response json, error '" << rapidjson::GetParseError_En(ok.Code()) 218 | << "' (" << ok.Offset() << ")"; 219 | return false; 220 | } 221 | 222 | return true; 223 | } 224 | 225 | bool PveApiClient::checkApiHost() const 226 | { 227 | const auto & config = Config::getInstance(); 228 | 229 | const std::string req_url = fmt::format("{}{}", config._pve_api_host, API_VERSION); 230 | int resp_code = 0; 231 | std::string resp_data; 232 | const bool ret = req(req_url, "", resp_code, resp_data); 233 | if (!ret || 200 != resp_code) 234 | { 235 | LOG(WARNING) << "Failed to request '" << req_url << "', response code is " << resp_code << ", response is " 236 | << resp_data << "!"; 237 | return ret; 238 | } 239 | 240 | rapidjson::Document d; 241 | rapidjson::ParseResult ok = d.Parse(resp_data.c_str()); 242 | if (!ok) 243 | { 244 | LOG(WARNING) << "Failed to parse response json, error '" << rapidjson::GetParseError_En(ok.Code()) 245 | << "' (" << ok.Offset() << ")"; 246 | return false; 247 | } 248 | 249 | if (d.HasMember("data") && d["data"].IsObject()) 250 | { 251 | const auto & data = d["data"]; 252 | if (data.HasMember("version") && data["version"].IsString()) 253 | { 254 | LOG(INFO) << "Successfully got API version: " << data["version"].GetString(); 255 | return true; 256 | } 257 | } 258 | 259 | return false; 260 | } 261 | -------------------------------------------------------------------------------- /src/dns_service/dns_service_cloudflare.cpp: -------------------------------------------------------------------------------- 1 | #include "dns_service_cloudflare.h" 2 | 3 | #include "fmt/format.h" 4 | #include "glog/logging.h" 5 | #include "rapidjson/document.h" 6 | #include "rapidjson/error/en.h" 7 | 8 | #include "../utils.h" 9 | #include "../config.h" 10 | 11 | static const char * API_HOST = "https://api.cloudflare.com/client/v4/"; 12 | static const char * API_VERIFY_TOKEN = "user/tokens/verify"; 13 | static const char * API_LIST_ZONES = "zones"; 14 | static const char * API_LIST_RECORDS = "zones/{}/dns_records"; 15 | static const char * API_PATCH_RECORD = "zones/{}/dns_records/{}"; 16 | 17 | const std::string & DnsServiceCloudflare::getServiceName() 18 | { 19 | return _service_name; 20 | } 21 | 22 | bool DnsServiceCloudflare::setCredentials(const std::string & cred_str) 23 | { 24 | if (cred_str.empty()) 25 | { 26 | LOG(WARNING) << "Credentials string is empty!"; 27 | return false; 28 | } 29 | _token = cred_str; 30 | if (!verifyToken()) 31 | { 32 | LOG(WARNING) << "Invalid cloudflare API token '" << cred_str << "'!"; 33 | return false; 34 | } 35 | 36 | return true; 37 | } 38 | 39 | std::string DnsServiceCloudflare::getIpv4(const std::string & domain) 40 | { 41 | return getIp(domain, true); 42 | } 43 | 44 | std::string DnsServiceCloudflare::getIpv6(const std::string & domain) 45 | { 46 | return getIp(domain, false); 47 | } 48 | 49 | bool DnsServiceCloudflare::setIpv4(const std::string & domain, const std::string & ip) 50 | { 51 | return setIp(domain, ip, true); 52 | } 53 | 54 | bool DnsServiceCloudflare::setIpv6(const std::string & domain, const std::string & ip) 55 | { 56 | return setIp(domain, ip, false); 57 | } 58 | 59 | bool DnsServiceCloudflare::verifyToken() 60 | { 61 | if (_token.empty()) 62 | { 63 | LOG(WARNING) << "Empty token string!"; 64 | return false; 65 | } 66 | 67 | const auto & config = Config::getInstance(); 68 | 69 | const std::string req_url = fmt::format("{}{}", API_HOST, API_VERIFY_TOKEN); 70 | 71 | int resp_code = 0; 72 | std::string resp_data; 73 | std::vector headers = { fmt::format("Authorization: Bearer {}", _token) }; 74 | const bool ret = http_req(req_url, "", Config::getInstance()._http_timeout_ms, headers, "", 75 | resp_code, resp_data); 76 | if (!ret || 200 != resp_code) 77 | { 78 | LOG(WARNING) << "Failed to request '" << req_url << "', response code is " << resp_code << ", response is " 79 | << resp_data << "!"; 80 | return false; 81 | } 82 | 83 | rapidjson::Document d; 84 | rapidjson::ParseResult ok = d.Parse(resp_data.c_str()); 85 | if (!ok) 86 | { 87 | LOG(WARNING) << "Failed to parse response json, error '" << rapidjson::GetParseError_En(ok.Code()) 88 | << "' (" << ok.Offset() << ")"; 89 | return false; 90 | } 91 | if (d.HasMember("success") && d["success"].IsBool()) 92 | { 93 | const bool success = d["success"].GetBool(); 94 | if (success) 95 | return true; 96 | } 97 | 98 | LOG(WARNING) << "Invalid response '" << resp_data << "'!"; 99 | return false; 100 | } 101 | 102 | bool DnsServiceCloudflare::getZoneId(const std::string & domain_name, std::string & out_zone_id) 103 | { 104 | const auto & config = Config::getInstance(); 105 | 106 | const std::string req_url = fmt::format("{}{}?name={}", API_HOST, API_LIST_ZONES, domain_name); 107 | 108 | int resp_code = 0; 109 | std::string resp_data; 110 | std::vector headers = { fmt::format("Authorization: Bearer {}", _token) }; 111 | const bool ret = http_req(req_url, "", Config::getInstance()._http_timeout_ms, headers, "", 112 | resp_code, resp_data); 113 | if (!ret || 200 != resp_code) 114 | { 115 | LOG(WARNING) << "Failed to request '" << req_url << "', response code is " << resp_code << ", response is " 116 | << resp_data << "!"; 117 | return false; 118 | } 119 | 120 | rapidjson::Document d; 121 | rapidjson::ParseResult ok = d.Parse(resp_data.c_str()); 122 | if (!ok) 123 | { 124 | LOG(WARNING) << "Failed to parse response json, error '" << rapidjson::GetParseError_En(ok.Code()) 125 | << "' (" << ok.Offset() << ")"; 126 | return false; 127 | } 128 | if (d.HasMember("success") && d["success"].IsBool()) 129 | { 130 | const bool success = d["success"].GetBool(); 131 | if (success) 132 | { 133 | if (d.HasMember("result") && d["result"].IsArray() && !d["result"].GetArray().Empty()) 134 | { 135 | const auto & result = *d["result"].GetArray().begin(); 136 | if (result.HasMember("id") && result["id"].IsString()) 137 | { 138 | out_zone_id = result["id"].GetString(); 139 | return true; 140 | } 141 | } 142 | } 143 | } 144 | 145 | LOG(WARNING) << "Invalid response '" << resp_data << "'!"; 146 | return false; 147 | } 148 | 149 | bool DnsServiceCloudflare::getRecordId(const std::string & domain_name, const std::string & zone_id, 150 | const std::string & type, 151 | std::string & out_record_id, std::string & out_record_content) 152 | { 153 | const auto & config = Config::getInstance(); 154 | 155 | const std::string api_part = fmt::format(API_LIST_RECORDS, zone_id); 156 | const std::string req_url = fmt::format("{}{}?type={}&name={}", API_HOST, api_part, type, domain_name); 157 | 158 | int resp_code = 0; 159 | std::string resp_data; 160 | std::vector headers = { fmt::format("Authorization: Bearer {}", _token) }; 161 | const bool ret = http_req(req_url, "", Config::getInstance()._http_timeout_ms, headers, "", 162 | resp_code, resp_data); 163 | if (!ret || 200 != resp_code) 164 | { 165 | LOG(WARNING) << "Failed to request '" << req_url << "', response code is " << resp_code << ", response is " 166 | << resp_data << "!"; 167 | return false; 168 | } 169 | 170 | rapidjson::Document d; 171 | rapidjson::ParseResult ok = d.Parse(resp_data.c_str()); 172 | if (!ok) 173 | { 174 | LOG(WARNING) << "Failed to parse response json, error '" << rapidjson::GetParseError_En(ok.Code()) 175 | << "' (" << ok.Offset() << ")"; 176 | return false; 177 | } 178 | if (d.HasMember("success") && d["success"].IsBool()) 179 | { 180 | const bool success = d["success"].GetBool(); 181 | if (success) 182 | { 183 | if (d.HasMember("result") && d["result"].IsArray() && !d["result"].GetArray().Empty()) 184 | { 185 | const auto & result = *d["result"].GetArray().begin(); 186 | if (result.HasMember("id") && result["id"].IsString()) 187 | { 188 | out_record_id = result["id"].GetString(); 189 | if (result.HasMember("content") && result["content"].IsString()) 190 | { 191 | out_record_content = result["content"].GetString(); 192 | return true; 193 | } 194 | } 195 | } 196 | } 197 | } 198 | 199 | LOG(WARNING) << "Invalid response '" << resp_data << "'!"; 200 | return false; 201 | } 202 | 203 | std::string DnsServiceCloudflare::getIp(const std::string & domain, bool is_v4) 204 | { 205 | const auto sub_domain = get_sub_domain(domain); 206 | const std::string rec_type = is_v4 ? "A" : "AAAA"; 207 | std::string rec_id_key = domain; 208 | rec_id_key.append("_"); 209 | rec_id_key.append(rec_type); 210 | 211 | std::string zone_id, record_id, record_content; 212 | if (_zones.find(sub_domain.first) == _zones.end()) 213 | { 214 | if (!getZoneId(sub_domain.first, zone_id)) 215 | { 216 | LOG(WARNING) << "Failed to retrieve zone id of '" << sub_domain.first << "'!"; 217 | return ""; 218 | } 219 | _zones[sub_domain.first] = zone_id; 220 | } 221 | else 222 | zone_id = _zones[sub_domain.first]; 223 | 224 | if (!getRecordId(domain, zone_id, rec_type, record_id, record_content)) 225 | { 226 | LOG(WARNING) << "Failed to retrieve DNS record id and/or content of '" << rec_id_key << "'!"; 227 | return ""; 228 | } 229 | _records[rec_id_key] = record_id; 230 | 231 | return record_content; 232 | } 233 | 234 | bool DnsServiceCloudflare::setIp(const std::string & domain, const std::string & ip, bool is_v4) 235 | { 236 | const auto sub_domain = get_sub_domain(domain); 237 | const std::string rec_type = is_v4 ? "A" : "AAAA"; 238 | std::string rec_id_key = domain; 239 | rec_id_key.append("_"); 240 | rec_id_key.append(rec_type); 241 | 242 | std::string zone_id, record_id, record_content; 243 | if (_zones.find(sub_domain.first) == _zones.end()) 244 | { 245 | LOG(WARNING) << "Missing zone ID of '" << sub_domain.first << "'!"; 246 | return false; 247 | } 248 | else 249 | zone_id = _zones[sub_domain.first]; 250 | 251 | if (_records.find(rec_id_key) == _records.end()) 252 | { 253 | LOG(WARNING) << "Missing DNS record ID of '" << rec_id_key << "'!"; 254 | return false; 255 | } 256 | 257 | const auto & config = Config::getInstance(); 258 | 259 | const std::string api_part = fmt::format(API_PATCH_RECORD, zone_id, record_id); 260 | const std::string req_url = fmt::format("{}{}", API_HOST, api_part); 261 | const std::string req_body = fmt::format(R"({{"type":"{}","name":"{}","content":"{}"}})", 262 | rec_type, domain, ip); 263 | 264 | int resp_code = 0; 265 | std::string resp_data; 266 | std::vector headers = { fmt::format("Authorization: Bearer {}", _token) }; 267 | const bool ret = http_req(req_url, req_body, Config::getInstance()._http_timeout_ms, headers, "patch", 268 | resp_code, resp_data); 269 | if (!ret || 200 != resp_code) 270 | { 271 | LOG(WARNING) << "Failed to request '" << req_url << "', response code is " << resp_code << ", response is " 272 | << resp_data << "!"; 273 | return false; 274 | } 275 | 276 | rapidjson::Document d; 277 | rapidjson::ParseResult ok = d.Parse(resp_data.c_str()); 278 | if (!ok) 279 | { 280 | LOG(WARNING) << "Failed to parse response json, error '" << rapidjson::GetParseError_En(ok.Code()) 281 | << "' (" << ok.Offset() << ")"; 282 | return false; 283 | } 284 | if (d.HasMember("success") && d["success"].IsBool()) 285 | { 286 | const bool success = d["success"].GetBool(); 287 | if (success) 288 | return true; 289 | } 290 | 291 | LOG(WARNING) << "Invalid response '" << resp_data << "'!"; 292 | return false; 293 | } 294 | -------------------------------------------------------------------------------- /src/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "fmt/format.h" 7 | #include "glog/logging.h" 8 | #include "curl/curl.h" 9 | 10 | #if WIN32 11 | #define pve_popen _popen 12 | #define pve_pclose _pclose 13 | #else 14 | #define pve_popen popen 15 | #define pve_pclose pclose 16 | #endif 17 | 18 | //typedef struct curl_read_userdata_ 19 | //{ 20 | // const std::string & req_data; 21 | // size_t pos; 22 | //} curl_read_userdata; 23 | 24 | static size_t write_string_callback(const void * bufptr, size_t size, size_t nitems, void * userp) 25 | { 26 | if (nullptr == bufptr || nullptr == userp) 27 | { 28 | LOG(WARNING) << "Invalid curl write callback function params bufptr and/or userp!"; 29 | return nitems; 30 | } 31 | if (size < 1 || nitems < 1) 32 | { 33 | LOG(WARNING) << "Invalid curl write callback function params, size is '" << size 34 | << "', nitems is '" << nitems << "'!"; 35 | return nitems; 36 | } 37 | 38 | auto * str = reinterpret_cast(userp); 39 | str->append(reinterpret_cast(bufptr), size * nitems); 40 | return nitems; 41 | } 42 | 43 | //static size_t read_string_callback(char * bufptr, size_t size, size_t nitems, void * userp) 44 | //{ 45 | // if (nullptr == bufptr || nullptr == userp) 46 | // { 47 | // LOG(WARNING) << "Invalid curl read callback function params bufptr and/pr userp!"; 48 | // return nitems; 49 | // } 50 | // if (size < 1 || nitems < 1) 51 | // { 52 | // LOG(WARNING) << "Invalid curl read callback function params, size is '" << size 53 | // << "', nitems is '" << nitems << "'!"; 54 | // return nitems; 55 | // } 56 | // 57 | // auto * userdata = reinterpret_cast(userp); 58 | // if (userdata->pos >= userdata->req_data.length()) 59 | // return 0; 60 | // auto bytes_left = static_cast(userdata->req_data.length() - userdata->pos); 61 | // auto bytes_to_copy = bytes_left < (size * nitems) ? bytes_left : (size * nitems); 62 | // memcpy(bufptr, userdata->req_data.data() + userdata->pos, bytes_to_copy); 63 | // userdata->pos += bytes_to_copy; 64 | // return bytes_to_copy; 65 | //} 66 | 67 | std::string get_version_string() 68 | { 69 | #if defined(PVE_DDNS_CLIENT_VER) 70 | return PVE_DDNS_CLIENT_VER; 71 | #else 72 | return "dev"; 73 | #endif 74 | } 75 | 76 | bool str_iequals(const std::string & l, const std::string & r) 77 | { 78 | if (l.length() != r.length()) 79 | return false; 80 | #if WIN32 81 | return 0 == _stricmp(l.c_str(), r.c_str()); 82 | #else 83 | return 0 == strcasecmp(l.c_str(), r.c_str()); 84 | #endif 85 | } 86 | 87 | std::pair get_sub_domain(const std::string & domain) 88 | { 89 | size_t pos = std::string::npos; 90 | size_t dot_pos = std::string::npos; 91 | std::string token; 92 | while ((pos = domain.rfind('.', dot_pos)) != std::string::npos) 93 | { 94 | if (dot_pos != std::string::npos) 95 | break; 96 | dot_pos = pos - 1; 97 | pos = std::string::npos; 98 | } 99 | if (std::string::npos == pos) 100 | return { domain, "" }; 101 | return { domain.substr(pos + 1), domain.substr(0, pos) }; 102 | } 103 | 104 | size_t get_dns_service_key(const std::string & dns_type, const std::string & credentials) 105 | { 106 | std::hash str_hash; 107 | return str_hash(fmt::format("{}:{}", dns_type, credentials)); 108 | } 109 | 110 | // Function to check if the given string s is IPv4 or not 111 | bool is_ipv4(const std::string & s) 112 | { 113 | // Store the count of occurrence 114 | // of '.' in the given string 115 | int cnt = 0; 116 | 117 | // Traverse the string s 118 | for (int i = 0; i < s.size(); i++) 119 | { 120 | if (s[i] == '.') 121 | cnt++; 122 | } 123 | 124 | // Not a valid IP address 125 | if (cnt != 3) 126 | return false; 127 | 128 | // Stores the tokens 129 | std::vector tokens; 130 | 131 | // stringstream class check1 132 | std::stringstream check1(s); 133 | std::string intermediate; 134 | 135 | // Tokenizing w.r.t. '.' 136 | while (getline(check1, intermediate, '.')) 137 | { 138 | tokens.push_back(intermediate); 139 | } 140 | 141 | if (tokens.size() != 4) 142 | return false; 143 | 144 | // Check if all the tokenized strings 145 | // lies in the range [0, 255] 146 | for (int i = 0; i < tokens.size(); i++) 147 | { 148 | int num = 0; 149 | 150 | // Base Case 151 | if (tokens[i] == "0") 152 | continue; 153 | 154 | if (tokens[i].empty()) 155 | return false; 156 | 157 | for (int j = 0; j < tokens[i].size(); j++) 158 | { 159 | if (tokens[i][j] > '9' || tokens[i][j] < '0') 160 | return false; 161 | 162 | num *= 10; 163 | num += tokens[i][j] - '0'; 164 | 165 | if (num == 0) 166 | return false; 167 | } 168 | 169 | // Range check for num 170 | if (num > 255 || num < 0) 171 | return false; 172 | } 173 | 174 | return true; 175 | } 176 | 177 | // Function to check if the string represents a hexadecimal number 178 | bool check_hex(const std::string & s) 179 | { 180 | // Size of string s 181 | int n = static_cast(s.length()); 182 | 183 | // Iterate over string 184 | for (int i = 0; i < n; i++) 185 | { 186 | char ch = s[i]; 187 | 188 | // Check if the character is invalid 189 | if ((ch < '0' || ch > '9') 190 | && (ch < 'A' || ch > 'F') 191 | && (ch < 'a' || ch > 'f')) 192 | { 193 | return false; 194 | } 195 | } 196 | 197 | return true; 198 | } 199 | 200 | // Function to check if the given string S is IPv6 or not 201 | bool is_ipv6(const std::string & s) 202 | { 203 | // Store the count of occurrence 204 | // of ':' in the given string 205 | int cnt = 0; 206 | 207 | for (int i = 0; i < s.size(); i++) 208 | { 209 | if (s[i] == ':') 210 | cnt++; 211 | } 212 | 213 | // Not a valid IP Address 214 | if (cnt != 7) 215 | return false; 216 | 217 | // Stores the tokens 218 | std::vector tokens; 219 | 220 | // stringstream class check1 221 | std::stringstream check1(s); 222 | std::string intermediate; 223 | 224 | // Tokenizing w.r.t. ':' 225 | while (getline(check1, intermediate, ':')) 226 | { 227 | tokens.push_back(intermediate); 228 | } 229 | 230 | if (tokens.size() != 8) 231 | return false; 232 | 233 | // Check if all the tokenized strings 234 | // are in hexadecimal format 235 | for (int i = 0; i < tokens.size(); i++) 236 | { 237 | int len = static_cast(tokens[i].size()); 238 | 239 | if (!check_hex(tokens[i]) || len > 4 || len < 1) 240 | { 241 | return false; 242 | } 243 | } 244 | return true; 245 | } 246 | 247 | bool get_ip_from_ip_addr_result(const std::string& result, const std::string& iface, 248 | std::string& ipv4, std::string& ipv6) 249 | { 250 | std::istringstream f(result); 251 | std::string line; 252 | std::string iface_cur; 253 | 254 | ipv4.clear(); 255 | ipv6.clear(); 256 | 257 | while (std::getline(f, line)) 258 | { 259 | if (!ipv4.empty() && !ipv6.empty()) 260 | return true; 261 | 262 | const char * nptr = line.c_str(); 263 | char * endptr = nullptr; 264 | strtol(nptr, &endptr, 10); 265 | if (nptr != endptr) 266 | { 267 | std::string::size_type iface_begin = endptr - nptr + 2; 268 | std::string::size_type iface_end = line.find_first_of(':', iface_begin); 269 | if (std::string::npos != iface_end) 270 | iface_cur = line.substr(iface_begin, iface_end - iface_begin); 271 | } 272 | else if (iface_cur.find(iface) != std::string::npos) 273 | { 274 | std::string::size_type deprecated_pos = line.find("deprecated"); 275 | if (std::string::npos != deprecated_pos) 276 | continue; 277 | std::string::size_type inet6_pos = line.find("inet6 "); 278 | if (std::string::npos != inet6_pos) 279 | { 280 | if (ipv6.empty()) 281 | { 282 | std::string::size_type v6_ip_end = line.find_first_of('/', inet6_pos + 6); 283 | if (std::string::npos != v6_ip_end) 284 | { 285 | ipv6 = line.substr(inet6_pos + 6, v6_ip_end - inet6_pos - 6); 286 | if (ipv6.compare(0, 4, "fe80") == 0) 287 | ipv6.clear(); 288 | } 289 | } 290 | continue; 291 | } 292 | std::string::size_type inet_pos = line.find("inet "); 293 | if (std::string::npos != inet_pos) 294 | { 295 | if (ipv4.empty()) 296 | { 297 | std::string::size_type v4_ip_end = line.find_first_of('/', inet_pos + 5); 298 | if (std::string::npos != v4_ip_end) 299 | ipv4 = line.substr(inet_pos + 5, v4_ip_end - inet_pos - 5); 300 | } 301 | } 302 | } 303 | } 304 | 305 | return false; 306 | } 307 | 308 | bool get_ip_from_ipconfig_result(const std::string& result, const std::string& iface, 309 | std::string& ipv4, std::string& ipv6) 310 | { 311 | std::istringstream f(result); 312 | std::string line; 313 | bool found_iface = false; 314 | 315 | ipv4.clear(); 316 | ipv6.clear(); 317 | 318 | while (std::getline(f, line)) 319 | { 320 | if (!ipv4.empty() && !ipv6.empty()) 321 | return true; 322 | 323 | if (line.empty()) 324 | continue; 325 | 326 | if (!found_iface && line.find(iface) != std::string::npos) 327 | { 328 | found_iface = true; 329 | continue; 330 | } 331 | 332 | if (found_iface) 333 | { 334 | if (line.find("IPv6") != std::string::npos && ipv6.empty()) 335 | { 336 | std::string::size_type inet6_pos = line.find(": "); 337 | if (std::string::npos != inet6_pos) 338 | { 339 | ipv6 = line.substr(inet6_pos + 2); 340 | if (ipv6.compare(0, 4, "fe80") == 0) 341 | ipv6.clear(); 342 | } 343 | } 344 | else if (line.find("IPv4") != std::string::npos && ipv4.empty()) 345 | { 346 | std::string::size_type inet4_pos = line.find(": "); 347 | if (std::string::npos != inet4_pos) 348 | { 349 | ipv4 = line.substr(inet4_pos + 2); 350 | } 351 | } 352 | } 353 | } 354 | 355 | return false; 356 | } 357 | 358 | bool http_req(const std::string & url, const std::string & req_data, long timeout_ms, 359 | const std::vector & custom_headers, 360 | int & resp_code, std::string & resp_data) 361 | { 362 | return http_req(url, req_data, timeout_ms, custom_headers, "", resp_code, resp_data); 363 | } 364 | 365 | bool http_req(const std::string & url, const std::string & req_data, long timeout_ms, 366 | const std::vector & custom_headers, const std::string & method, 367 | int & resp_code, std::string & resp_data) 368 | { 369 | CURL * curl = curl_easy_init(); 370 | if (nullptr == curl) 371 | { 372 | LOG(ERROR) << "Failed to curl_easy_init!"; 373 | return false; 374 | } 375 | // curl_read_userdata read_userdata = { req_data, 0 }; 376 | curl_easy_setopt(curl, CURLoption::CURLOPT_URL, url.c_str()); 377 | curl_easy_setopt(curl, CURLoption::CURLOPT_TIMEOUT_MS, timeout_ms); 378 | if (!req_data.empty()) 379 | { 380 | if ("put" == method) 381 | { 382 | curl_easy_setopt(curl, CURLoption::CURLOPT_CUSTOMREQUEST, "PUT"); 383 | // curl_easy_setopt(curl, CURLoption::CURLOPT_UPLOAD, 1L); 384 | // curl_easy_setopt(curl, CURLoption::CURLOPT_READFUNCTION, read_string_callback); 385 | // curl_easy_setopt(curl, CURLoption::CURLOPT_READDATA, static_cast(&read_userdata)); 386 | // curl_easy_setopt(curl, CURLoption::CURLOPT_INFILESIZE_LARGE, req_data.length()); 387 | } 388 | else if ("delete" == method) 389 | curl_easy_setopt(curl, CURLoption::CURLOPT_CUSTOMREQUEST, "DELETE"); 390 | else 391 | curl_easy_setopt(curl, CURLoption::CURLOPT_POST, 1L); 392 | 393 | curl_easy_setopt(curl, CURLoption::CURLOPT_POSTFIELDS, req_data.c_str()); 394 | curl_easy_setopt(curl, CURLoption::CURLOPT_POSTFIELDSIZE_LARGE, static_cast(req_data.length())); 395 | } 396 | else 397 | { 398 | if ("put" == method) 399 | curl_easy_setopt(curl, CURLoption::CURLOPT_CUSTOMREQUEST, "PUT"); 400 | else if ("delete" == method) 401 | curl_easy_setopt(curl, CURLoption::CURLOPT_CUSTOMREQUEST, "DELETE"); 402 | } 403 | curl_easy_setopt(curl, CURLoption::CURLOPT_WRITEFUNCTION, write_string_callback); 404 | curl_easy_setopt(curl, CURLoption::CURLOPT_WRITEDATA, static_cast(&resp_data)); 405 | 406 | bool ret = false; 407 | do 408 | { 409 | char errbuf[CURL_ERROR_SIZE] = {}; 410 | curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); 411 | //#ifndef NDEBUG 412 | // curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); 413 | //#endif 414 | curl_slist * http_headers = nullptr; 415 | for (const auto & custom_header : custom_headers) 416 | { 417 | http_headers = curl_slist_append(http_headers, custom_header.c_str()); 418 | // http_headers = curl_slist_append(http_headers, "Accept: application/json"); 419 | // http_headers = curl_slist_append(http_headers, "Content-Type: application/json"); 420 | // http_headers = curl_slist_append(http_headers, "charset: utf-8"); 421 | } 422 | if (nullptr != http_headers) 423 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, http_headers); 424 | 425 | CURLcode curl_ret = curl_easy_perform(curl); 426 | if (curl_ret != CURLcode::CURLE_OK) 427 | { 428 | LOG(WARNING) << "curl_easy_perform fail, curl code is '" << curl_ret << "', error is '" << errbuf 429 | << "', url is '" << url << "'!"; 430 | break; 431 | } 432 | ret = true; 433 | 434 | long code = 0; 435 | curl_ret = curl_easy_getinfo(curl, CURLINFO::CURLINFO_RESPONSE_CODE, &code); 436 | if (CURLcode::CURLE_OK != curl_ret) 437 | { 438 | LOG(WARNING) << "curl_easy_getinfo fail, curl_code is '" << curl_ret << "', error is '" << errbuf << "'!"; 439 | break; 440 | } 441 | resp_code = static_cast(code); 442 | if (resp_code != 200) 443 | LOG(WARNING) << "'" << url << "' request failed, response code is '" << resp_code << "'!"; 444 | } while (false); 445 | 446 | curl_easy_cleanup(curl); 447 | 448 | return ret; 449 | } 450 | 451 | bool shell_execute(const std::string& cmd, std::string& result) 452 | { 453 | if (cmd.empty()) 454 | { 455 | LOG(WARNING) << "Invalid cmd!"; 456 | return false; 457 | } 458 | std::array buffer = {}; 459 | FILE * pipe = pve_popen(cmd.c_str(), "r"); 460 | if (nullptr == pipe) 461 | { 462 | LOG(WARNING) << "Failed to popen '" << cmd << "'!"; 463 | return false; 464 | } 465 | while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != nullptr) 466 | result += buffer.data(); 467 | const int res = pve_pclose(pipe); 468 | if (res != 0) 469 | { 470 | LOG(WARNING) << "Error pclose result '" << res << "' from execution of '" << cmd << "'!"; 471 | return false; 472 | } 473 | return true; 474 | } 475 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | //#ifdef WIN32 6 | //#include 7 | //#else 8 | //#include 9 | //#endif 10 | 11 | #include "fmt/format.h" 12 | #include "glog/logging.h" 13 | #include "curl/curl.h" 14 | #include "cmdline.h" 15 | 16 | #include "config.h" 17 | #include "utils.h" 18 | #include "public_ip/public_ip_getter.h" 19 | #include "dns_service/dns_service.h" 20 | #include "pve/pve_api_client.h" 21 | #include "pve/pve_pct_wrapper.h" 22 | 23 | // Main loop running flag 24 | static volatile bool g_running = true; 25 | // Public IP getter service instance 26 | static std::shared_ptr g_ip_getter; 27 | // DNS service instances 28 | static std::shared_ptr> g_dns_services; 29 | 30 | //#ifdef WIN32 31 | //static BOOL WINAPI ctrl_handler(DWORD fdw_ctrl_type) 32 | //{ 33 | // if (CTRL_C_EVENT == fdw_ctrl_type) 34 | // { 35 | // LOG(INFO) << "Received ctrl+c event, stopping..."; 36 | // g_running = false; 37 | // return TRUE; 38 | // } 39 | // 40 | // LOG(WARNING) << "Received ctrl event: " << fdw_ctrl_type << ", ignored!"; 41 | // return FALSE; 42 | //} 43 | //#else 44 | //#endif 45 | 46 | // Command line params handling 47 | static bool parse_cmd(int argc, char * argv[]) 48 | { 49 | if (argc < 1 || nullptr == argv || nullptr == argv[0]) 50 | { 51 | std::cerr << "Invalid command line params!" << std::endl; 52 | return false; 53 | } 54 | 55 | cmdline::parser p; 56 | p.add("version", 'v', "Print version"); 57 | p.add("help", 'h', "Show usage"); 58 | p.add("config", 'c', "Config yaml file to load", false, "./pve-ddns-client.yml"); 59 | p.add("log", 'l', "Log file path", false, "./"); 60 | 61 | const auto show_usage = [&p]() 62 | { 63 | const std::string usage = p.usage(); 64 | std::cout << usage << std::endl; 65 | }; 66 | 67 | bool ret = p.parse(argc, argv); 68 | if (ret) 69 | { 70 | if (p.exist("version")) 71 | { 72 | std::cout << "Ver " << get_version_string() << std::endl; 73 | ret = false; 74 | } 75 | else if (p.exist("help")) 76 | { 77 | ret = false; 78 | show_usage(); 79 | } 80 | else 81 | { 82 | bool args_valid = false; 83 | do 84 | { 85 | auto & config = Config::getInstance(); 86 | 87 | config._yml_path = p.get("config"); 88 | if (config._yml_path.empty()) break; 89 | 90 | config._log_path = p.get("log"); 91 | if (config._log_path.empty()) break; 92 | 93 | args_valid = true; 94 | } while (false); 95 | 96 | if (!args_valid) 97 | { 98 | ret = false; 99 | std::cerr << "Invalid params!" << std::endl; 100 | show_usage(); 101 | } 102 | } 103 | } 104 | else 105 | { 106 | std::cerr << "Failed to parse command line params!" << std::endl; 107 | show_usage(); 108 | } 109 | 110 | return ret; 111 | } 112 | 113 | static bool init_public_ip_getter() 114 | { 115 | if (nullptr != g_ip_getter) 116 | { 117 | LOG(WARNING) << "g_ip_getter is not nullptr!"; 118 | return false; 119 | } 120 | 121 | auto & cfg = Config::getInstance(); 122 | auto * ip_getter = PublicIpGetterFactory::create(cfg._public_ip_service); 123 | if (nullptr == ip_getter) 124 | { 125 | LOG(WARNING) << "Failed to create public ip getter " << cfg._public_ip_service << "!"; 126 | return false; 127 | } 128 | g_ip_getter = std::shared_ptr(ip_getter, [](IPublicIpGetter * ip_getter) 129 | { 130 | if (nullptr == ip_getter) 131 | return; 132 | PublicIpGetterFactory::destroy(ip_getter); 133 | }); 134 | if (!g_ip_getter->setCredentials(cfg._public_ip_credentials)) 135 | { 136 | LOG(WARNING) << "Failed to setCredentials to ip getter " << cfg._public_ip_service << "!"; 137 | g_ip_getter.reset(); 138 | return false; 139 | } 140 | // Initial retrieval of public IPv4 and IPv6 addresses; 141 | cfg._my_public_ipv4 = g_ip_getter->getIpv4(); 142 | cfg._my_public_ipv6 = g_ip_getter->getIpv6(); 143 | 144 | return true; 145 | } 146 | 147 | static void cleanup_public_ip_getter() 148 | { 149 | g_ip_getter.reset(); 150 | } 151 | 152 | static IDnsService * get_dns_service(const size_t service_key) 153 | { 154 | if (nullptr == g_dns_services) 155 | { 156 | LOG(WARNING) << "Invalid g_dns_services!"; 157 | return nullptr; 158 | } 159 | 160 | auto it = g_dns_services->find(service_key); 161 | if (g_dns_services->end() == it) 162 | return nullptr; 163 | return it->second; 164 | } 165 | 166 | static bool create_dns_service(const std::string & dns_type, const std::string & credentials) 167 | { 168 | if (nullptr == g_dns_services) 169 | { 170 | LOG(WARNING) << "Invalid g_dns_services!"; 171 | return false; 172 | } 173 | 174 | const size_t key = get_dns_service_key(dns_type, credentials); 175 | if (g_dns_services->find(key) == g_dns_services->end()) 176 | { 177 | IDnsService * dns_service = DnsServiceFactory::create(dns_type); 178 | if (nullptr == dns_service) 179 | { 180 | LOG(WARNING) << "Failed to create dns service " << dns_type << "!"; 181 | return false; 182 | } 183 | if (!dns_service->setCredentials(credentials)) 184 | { 185 | LOG(WARNING) << "Failed to setCredentials!"; 186 | DnsServiceFactory::destroy(dns_service); 187 | return false; 188 | } 189 | g_dns_services->emplace(key, dns_service); 190 | } 191 | 192 | return true; 193 | } 194 | 195 | static void cleanup_dns_services() 196 | { 197 | if (nullptr == g_dns_services) 198 | { 199 | LOG(WARNING) << "Invalid g_dns_services!"; 200 | return; 201 | } 202 | 203 | for (auto & kv : *g_dns_services) 204 | DnsServiceFactory::destroy(kv.second); 205 | g_dns_services->clear(); 206 | } 207 | 208 | static bool init_dns_services() 209 | { 210 | const auto & cfg = Config::getInstance(); 211 | if (!cfg._client_config.dns_type.empty() && !cfg._client_config.credentials.empty()) 212 | if (!create_dns_service(cfg._client_config.dns_type, cfg._client_config.credentials)) 213 | return false; 214 | 215 | if (!cfg._host_config.dns_type.empty() && !cfg._host_config.credentials.empty()) 216 | if (!create_dns_service(cfg._host_config.dns_type, cfg._host_config.credentials)) 217 | return false; 218 | 219 | for (auto & guest_config : cfg._guest_configs) 220 | { 221 | if (!guest_config.second.dns_type.empty() && !guest_config.second.credentials.empty()) 222 | { 223 | if (!create_dns_service(guest_config.second.dns_type, guest_config.second.credentials)) 224 | { 225 | cleanup_dns_services(); 226 | return false; 227 | } 228 | } 229 | } 230 | 231 | return true; 232 | } 233 | 234 | static void init_node_dns_records(const config_node & node) 235 | { 236 | Config & cfg = Config::getInstance(); 237 | const size_t dns_service_key = get_dns_service_key(node.dns_type, node.credentials); 238 | auto * dns_service = get_dns_service(dns_service_key); 239 | if (nullptr != dns_service) 240 | { 241 | for (const auto & domain : node.ipv4_domains) 242 | { 243 | auto found = cfg._ipv4_records.find(domain); 244 | if (cfg._ipv4_records.end() == found) 245 | { 246 | std::string ip = dns_service->getIpv4(domain); 247 | LOG(INFO) << "Domain '" << domain << "', A record is: '" << ip << "'."; 248 | cfg._ipv4_records.emplace( 249 | domain, 250 | dns_record_node 251 | { 252 | std::chrono::duration_cast( 253 | std::chrono::system_clock::now().time_since_epoch() 254 | ), 255 | ip 256 | } 257 | ); 258 | } 259 | } 260 | 261 | for (const auto & domain : node.ipv6_domains) 262 | { 263 | auto found = cfg._ipv6_records.find(domain); 264 | if (cfg._ipv6_records.end() == found) 265 | { 266 | std::string ip = dns_service->getIpv6(domain); 267 | LOG(INFO) << "Domain '" << domain << "', AAAA record is: '" << ip << "'."; 268 | cfg._ipv6_records.emplace( 269 | domain, 270 | dns_record_node 271 | { 272 | std::chrono::duration_cast( 273 | std::chrono::system_clock::now().time_since_epoch() 274 | ), 275 | ip 276 | } 277 | ); 278 | } 279 | } 280 | } 281 | } 282 | 283 | static void init_dns_records() 284 | { 285 | auto & cfg = Config::getInstance(); 286 | init_node_dns_records(cfg._client_config); 287 | init_node_dns_records(cfg._host_config); 288 | for (auto & guest : cfg._guest_configs) 289 | { 290 | const auto & dns_config = guest.second; 291 | init_node_dns_records(guest.second); 292 | } 293 | } 294 | 295 | static bool update_dns_records_v4(const config_node & config_node, const std::string & ip, IDnsService * dns_service) 296 | { 297 | if (ip.empty() || nullptr == dns_service) 298 | { 299 | LOG(WARNING) << "Invalid params!"; 300 | return false; 301 | } 302 | auto & cfg = Config::getInstance(); 303 | for (const auto & domain : config_node.ipv4_domains) 304 | { 305 | auto found = cfg._ipv4_records.find(domain); 306 | if (cfg._ipv4_records.end() == found) 307 | { 308 | LOG(WARNING) << "IPv4 domain '" << domain << "' dns record not found!"; 309 | return false; 310 | } 311 | if (found->second.last_ip != ip) 312 | { 313 | LOG(INFO) << "IPv4 domain '" << domain << "' dns record address changed from '" << found->second.last_ip 314 | << "' to '" << ip << "', updating..."; 315 | if (dns_service->setIpv4(domain, ip)) 316 | { 317 | LOG(INFO) << "IPv4 record of domain '" << domain << "' successfully updated from '" 318 | << found->second.last_ip << "' to '" << ip << "'."; 319 | found->second.last_ip = ip; 320 | found->second.last_get_time = std::chrono::duration_cast( 321 | std::chrono::system_clock::now().time_since_epoch() 322 | ); 323 | } 324 | else 325 | LOG(WARNING) << "Failed to update IPv4 record from '" << found->second.last_ip << "' to '" 326 | << ip << "' of domain '" << domain << "'!"; 327 | } 328 | // else 329 | // LOG(INFO) << "IPv4 domain '" << domain << "' dns record address '" << ip << "' not changed."; 330 | } 331 | return true; 332 | } 333 | 334 | static bool update_dns_records_v6(const config_node & config_node, const std::string & ip, IDnsService * dns_service) 335 | { 336 | if (ip.empty() || nullptr == dns_service) 337 | { 338 | LOG(WARNING) << "Invalid params!"; 339 | return false; 340 | } 341 | auto & cfg = Config::getInstance(); 342 | for (const auto & domain : config_node.ipv6_domains) 343 | { 344 | auto found = cfg._ipv6_records.find(domain); 345 | if (cfg._ipv6_records.end() == found) 346 | { 347 | LOG(WARNING) << "IPv6 domain '" << domain << "' dns record not found!"; 348 | return false; 349 | } 350 | if (found->second.last_ip != ip) 351 | { 352 | LOG(INFO) << "IPv6 domain '" << domain << "' dns record address changed from '" << found->second.last_ip 353 | << "' to '" << ip << "', updating..."; 354 | if (dns_service->setIpv6(domain, ip)) 355 | { 356 | LOG(INFO) << "IPv6 record of domain '" << domain << "' successfully updated from '" 357 | << found->second.last_ip << "' to '" << ip << "'."; 358 | found->second.last_ip = ip; 359 | found->second.last_get_time = std::chrono::duration_cast( 360 | std::chrono::system_clock::now().time_since_epoch() 361 | ); 362 | } 363 | else 364 | LOG(WARNING) << "Failed to update IPv6 record from '" << found->second.last_ip << "' to '" 365 | << ip << "' of domain '" << domain << "'!"; 366 | } 367 | // else 368 | // LOG(INFO) << "IPv6 domain '" << domain << "' dns record address '" << ip << "' not changed."; 369 | } 370 | return true; 371 | } 372 | 373 | static bool update_dns_records(const config_node & config_node, const std::string & ip, const bool is_v4) 374 | { 375 | auto & cfg = Config::getInstance(); 376 | size_t dns_service_key = get_dns_service_key(config_node.dns_type, config_node.credentials); 377 | auto * dns_service = get_dns_service(dns_service_key); 378 | if (nullptr == dns_service) 379 | { 380 | LOG(WARNING) << "Failed to find dns service of '" << config_node.dns_type << "'!"; 381 | return false; 382 | } 383 | 384 | if (is_v4) 385 | { 386 | if (!update_dns_records_v4(config_node, ip, dns_service)) 387 | return false; 388 | } 389 | else 390 | { 391 | if (!update_dns_records_v6(config_node, ip, dns_service)) 392 | return false; 393 | } 394 | 395 | return true; 396 | } 397 | 398 | static bool sync_host_static_v6_address(const std::shared_ptr & pve_api_client, 399 | const std::string & host_v4_addr, const std::string & host_v6_addr, 400 | const std::string & guest_v6_addr) 401 | { 402 | if (host_v6_addr.empty() || guest_v6_addr.empty()) 403 | return true; 404 | 405 | int counter = 1; 406 | auto host_4th_colon_pos = host_v6_addr.find(':'); 407 | while (host_4th_colon_pos != std::string::npos && counter < 4) 408 | { 409 | host_4th_colon_pos = host_v6_addr.find(':', host_4th_colon_pos + 1); 410 | ++counter; 411 | } 412 | if (counter != 4) 413 | { 414 | LOG(WARNING) << "Invalid host v6 address '" << host_v6_addr << "'!"; 415 | return false; 416 | } 417 | 418 | const std::string host_1st_part = host_v6_addr.substr(0, host_4th_colon_pos); 419 | const std::string host_2nd_part = host_v6_addr.substr(host_4th_colon_pos + 1); 420 | 421 | counter = 1; 422 | auto guest_4th_colon_pos = guest_v6_addr.find(':'); 423 | while (guest_4th_colon_pos != std::string::npos && counter < 4) 424 | { 425 | guest_4th_colon_pos = guest_v6_addr.find(':', guest_4th_colon_pos + 1); 426 | ++counter; 427 | } 428 | if (counter != 4) 429 | { 430 | LOG(WARNING) << "Invalid guest v6 address '" << guest_v6_addr << "'!"; 431 | return false; 432 | } 433 | 434 | const std::string guest_1st_part = guest_v6_addr.substr(0, guest_4th_colon_pos); 435 | if (host_1st_part == guest_1st_part) 436 | { 437 | // LOG(INFO) << "No need to sync host v6 static address, 1st part '" << host_1st_part << "' not changed."; 438 | return true; 439 | } 440 | 441 | const std::string new_host_v6_address = fmt::format("{}:{}", guest_1st_part, host_2nd_part); 442 | LOG(INFO) << "Host v6 static address 1st part changed from '" << host_1st_part << "' to '" 443 | << guest_1st_part << "', updating host static IPv6 address to '" << new_host_v6_address << "'..."; 444 | 445 | int retry_count = 0; 446 | while(retry_count < 5) 447 | { 448 | const auto & cfg = Config::getInstance(); 449 | if (!pve_api_client->setHostNetworkAddress(cfg._host_config.node, cfg._host_config.iface, 450 | host_v4_addr, new_host_v6_address)) 451 | { 452 | LOG(WARNING) << "Failed to update synced host static IPv6 address, retry in 1 minute(" 453 | << retry_count << ")..."; 454 | 455 | if (!pve_api_client->revertHostNetworkChange(cfg._host_config.node)) 456 | LOG(WARNING) << "Failed to revert host network change!"; 457 | else 458 | LOG(INFO) << "Host network change successfully reverted."; 459 | 460 | std::this_thread::sleep_for(std::chrono::minutes(1)); 461 | ++retry_count; 462 | continue; 463 | } 464 | 465 | LOG(INFO) << "Host static IPv6 address successfully updated, applying change..."; 466 | if (!pve_api_client->applyHostNetworkChange(cfg._host_config.node)) 467 | { 468 | LOG(WARNING) << "Failed to apply host network change, retry in 1 minute(" 469 | << retry_count << ")..."; 470 | 471 | if (!pve_api_client->revertHostNetworkChange(cfg._host_config.node)) 472 | LOG(WARNING) << "Failed to revert host network change!"; 473 | else 474 | LOG(INFO) << "Host network change successfully reverted."; 475 | 476 | std::this_thread::sleep_for(std::chrono::minutes(1)); 477 | ++retry_count; 478 | continue; 479 | } 480 | LOG(INFO) << "Host network change successfully applied!"; 481 | 482 | std::this_thread::sleep_for(std::chrono::seconds(10)); 483 | 484 | if (!update_dns_records(cfg._host_config, new_host_v6_address, false)) 485 | { 486 | LOG(WARNING) << "Failed to update synced host v6 dns records, retry in 1 minute(" 487 | << retry_count << ")"; 488 | std::this_thread::sleep_for(std::chrono::minutes(1)); 489 | ++retry_count; 490 | continue; 491 | } 492 | else 493 | LOG(INFO) << "Synced host v6 dns records successfully updated!"; 494 | 495 | break; 496 | } 497 | 498 | return true; 499 | } 500 | 501 | static bool initialize(int argc, char * argv[]) 502 | { 503 | //#ifdef WIN32 504 | // SetConsoleCtrlHandler(ctrl_handler, TRUE); 505 | //#endif 506 | if (!parse_cmd(argc, argv)) 507 | return false; 508 | 509 | Config & cfg = Config::getInstance(); 510 | const bool cfg_valid = cfg.loadConfig(cfg._yml_path); 511 | if (!cfg_valid) 512 | { 513 | std::cerr << "Failed to load config from '" << cfg._yml_path << "'!" << std::endl; 514 | return false; 515 | } 516 | 517 | FLAGS_log_dir = cfg._log_path; 518 | FLAGS_alsologtostderr = true; 519 | fLI::FLAGS_max_log_size = cfg._max_log_size_mb; 520 | fLI::FLAGS_logbufsecs = cfg._log_buf_secs; 521 | 522 | google::SetLogFilenameExtension(".log"); 523 | #ifndef NDEBUG 524 | google::SetLogDestination(google::GLOG_INFO, ""); 525 | #endif 526 | // Only one log file 527 | google::SetLogDestination(google::GLOG_ERROR, ""); 528 | google::SetLogDestination(google::GLOG_FATAL, ""); 529 | google::SetLogDestination(google::GLOG_WARNING, ""); 530 | google::EnableLogCleaner(cfg._log_overdue_days); 531 | google::InitGoogleLogging(argv[0]); 532 | google::InstallFailureSignalHandler(); 533 | 534 | curl_global_init(CURL_GLOBAL_ALL); 535 | 536 | return true; 537 | } 538 | 539 | static bool initialize_services(std::shared_ptr & pve_api_client, 540 | std::shared_ptr & pve_pct_wrapper) 541 | { 542 | g_dns_services = std::make_shared>(); 543 | if (!init_public_ip_getter()) 544 | { 545 | LOG(WARNING) << "Failed to init public ip!"; 546 | return false; 547 | } 548 | LOG(INFO) << "Public IP getter inited!"; 549 | if (!init_dns_services()) 550 | { 551 | LOG(WARNING) << "Failed to init dns services!"; 552 | return false; 553 | } 554 | LOG(INFO) << "All DNS services inited!"; 555 | init_dns_records(); 556 | LOG(INFO) << "Initial dns records updated!"; 557 | 558 | const Config & cfg = Config::getInstance(); 559 | // Only initialize PVE related stuff if needed 560 | if (!cfg._host_config.ipv4_domains.empty() || !cfg._host_config.ipv6_domains.empty() || 561 | !cfg._guest_configs.empty()) 562 | { 563 | pve_api_client = std::make_shared(); 564 | if (nullptr == pve_api_client) 565 | { 566 | LOG(ERROR) << "Failed to allocate PveApiClient!"; 567 | return false; 568 | } 569 | if (pve_api_client->init()) 570 | LOG(INFO) << "PVE API client inited!"; 571 | else 572 | { 573 | LOG(WARNING) << "PVE API client failed to init, but host and/or guest(s) node config present!"; 574 | return false; 575 | } 576 | 577 | pve_pct_wrapper = std::make_shared(); 578 | if (nullptr == pve_pct_wrapper) 579 | { 580 | LOG(ERROR) << "Failed to allocate PvePctWrapper!"; 581 | return false; 582 | } 583 | if (pve_pct_wrapper->init()) 584 | LOG(INFO) << "PVE pct wrapper inited!"; 585 | else 586 | LOG(WARNING) << "PVE pct wrapper failed to init, DDNS updating of LXC guests will not work!"; 587 | } 588 | 589 | return true; 590 | } 591 | 592 | static void update_client() 593 | { 594 | Config & cfg = Config::getInstance(); 595 | if (!cfg._client_config.ipv4_domains.empty()) 596 | { 597 | cfg._my_public_ipv4 = g_ip_getter->getIpv4(); 598 | if (cfg._my_public_ipv4.empty()) 599 | LOG(WARNING) << "Failed to get client public IPv4 address!"; 600 | else if (!update_dns_records(cfg._client_config, cfg._my_public_ipv4, true)) 601 | LOG(WARNING) << "Failed to update client v4 dns records!"; 602 | } 603 | 604 | if (!cfg._client_config.ipv6_domains.empty()) 605 | { 606 | cfg._my_public_ipv6 = g_ip_getter->getIpv6(); 607 | if (cfg._my_public_ipv6.empty()) 608 | LOG(WARNING) << "Failed to get client public IPv6 address!"; 609 | else if (!update_dns_records(cfg._client_config, cfg._my_public_ipv6, false)) 610 | LOG(WARNING) << "Failed to update client v6 dns record!"; 611 | } 612 | } 613 | 614 | static void update_host(const std::shared_ptr & pve_api_client, 615 | std::string & host_v4_addr, std::string & host_v6_addr) 616 | { 617 | const Config & cfg = Config::getInstance(); 618 | const bool enabled = !cfg._host_config.ipv4_domains.empty() || !cfg._host_config.ipv6_domains.empty(); 619 | 620 | if (nullptr == pve_api_client && enabled) 621 | { 622 | LOG(WARNING) << "Invalid pve_api_client while host update is needed!"; 623 | return; 624 | } 625 | 626 | if (enabled) 627 | { 628 | auto ret = pve_api_client->getHostIp(cfg._host_config.node, cfg._host_config.iface); 629 | host_v4_addr = ret.first; 630 | host_v6_addr = ret.second; 631 | if (!cfg._host_config.ipv4_domains.empty() && ret.first.empty()) 632 | { 633 | LOG(WARNING) << "Failed to get host IPv4 address!"; 634 | g_running = false; 635 | } 636 | else if (!cfg._host_config.ipv4_domains.empty() && !ret.first.empty()) 637 | { 638 | if (!update_dns_records(cfg._host_config, ret.first, true)) 639 | LOG(WARNING) << "Failed to update host v4 dns records!"; 640 | } 641 | 642 | if (!cfg._host_config.ipv6_domains.empty() && ret.second.empty()) 643 | { 644 | LOG(WARNING) << "Failed to get host IPv6 address!"; 645 | g_running = false; 646 | } 647 | else if (!cfg._host_config.ipv6_domains.empty() && !ret.second.empty()) 648 | { 649 | if (!update_dns_records(cfg._host_config, ret.second, false)) 650 | LOG(WARNING) << "Failed to update host v6 dns records!"; 651 | } 652 | } 653 | } 654 | 655 | static void update_guests(const std::shared_ptr & pve_api_client, 656 | const std::shared_ptr & pve_pct_wrapper, 657 | const std::string & host_v4_addr, const std::string & host_v6_addr) 658 | { 659 | const Config & cfg = Config::getInstance(); 660 | 661 | if ((nullptr == pve_api_client || nullptr == pve_pct_wrapper) && !cfg._guest_configs.empty()) 662 | { 663 | LOG(WARNING) << "Invalid pve_api_client and/or pve_pct_wrapper while guest update is needed!"; 664 | return; 665 | } 666 | 667 | std::string kvm_guest_v6_addr, lxc_guest_v6_addr; 668 | for (auto & guest : cfg._guest_configs) 669 | { 670 | std::pair ret = {}; 671 | if (pve_pct_wrapper->isLxcGuest(guest.first)) 672 | { 673 | ret = pve_pct_wrapper->getGuestIp(guest.first, guest.second.iface); 674 | if (!ret.second.empty() && lxc_guest_v6_addr.empty()) 675 | lxc_guest_v6_addr = ret.second; 676 | } 677 | else 678 | { 679 | ret = pve_api_client->getGuestIp(guest.second.node, guest.first, guest.second.iface); 680 | if (!ret.second.empty() && kvm_guest_v6_addr.empty()) 681 | kvm_guest_v6_addr = ret.second; 682 | } 683 | if (!guest.second.ipv4_domains.empty() && ret.first.empty()) 684 | { 685 | LOG(WARNING) << "Failed to get guest(vmid: " << guest.first << ") IPv4 address!"; 686 | g_running = false; 687 | } 688 | else if (!guest.second.ipv4_domains.empty() && !ret.first.empty()) 689 | { 690 | if (!update_dns_records(guest.second, ret.first, true)) 691 | LOG(WARNING) << "Failed to update guest(vmid: " << guest.first << ") v4 dns records!"; 692 | } 693 | 694 | if (!guest.second.ipv6_domains.empty() && ret.second.empty()) 695 | { 696 | LOG(WARNING) << "Failed to get guest(vmid: " << guest.first << ") IPv6 address!"; 697 | g_running = false; 698 | } 699 | else if (!guest.second.ipv6_domains.empty() && !ret.second.empty()) 700 | { 701 | if (!update_dns_records(guest.second, ret.second, false)) 702 | LOG(WARNING) << "Failed to update guest(vmid: " << guest.first << ") v6 dns records!"; 703 | } 704 | } 705 | 706 | std::string guest_v6_addr = kvm_guest_v6_addr.empty() ? lxc_guest_v6_addr : kvm_guest_v6_addr; 707 | if ((!cfg._host_config.ipv4_domains.empty() || !cfg._host_config.ipv6_domains.empty()) && 708 | cfg._sync_host_static_v6_address) 709 | { 710 | if (guest_v6_addr.empty()) 711 | LOG(WARNING) << "Sync host static IPv6 address enabled but no valid guest IPv6 address!"; 712 | else if (!sync_host_static_v6_address(pve_api_client, host_v4_addr, host_v6_addr, guest_v6_addr)) 713 | LOG(WARNING) << "Failed to sync host static IPv6 address!"; 714 | } 715 | } 716 | 717 | // main 718 | int main(int argc, char * argv[]) 719 | { 720 | if (!initialize(argc, argv)) 721 | return EXIT_SUCCESS; 722 | 723 | Config & cfg = Config::getInstance(); 724 | LOG(INFO) << "Starting up, ver " << get_version_string() << ", config loaded from '" << cfg._yml_path << "'."; 725 | LOG(INFO) << (cfg._service_mode ? "Running" : "Not running") << " in service mode..."; 726 | 727 | do 728 | { 729 | std::shared_ptr pve_api_client; 730 | std::shared_ptr pve_pct_wrapper; 731 | if (!initialize_services(pve_api_client, pve_pct_wrapper)) 732 | { 733 | LOG(WARNING) << "Failed to initialize_services!"; 734 | break; 735 | } 736 | 737 | // Service loop 738 | while (g_running) 739 | { 740 | auto elasped_time = std::chrono::system_clock::now().time_since_epoch() - cfg._last_update_time; 741 | if (elasped_time > cfg._update_interval) 742 | { 743 | cfg._last_update_time = std::chrono::duration_cast( 744 | std::chrono::system_clock::now().time_since_epoch() 745 | ); 746 | update_client(); 747 | std::string host_v4_addr, host_v6_addr; 748 | update_host(pve_api_client, host_v4_addr, host_v6_addr); 749 | update_guests(pve_api_client, pve_pct_wrapper, host_v4_addr, host_v6_addr); 750 | } 751 | else if (!cfg._service_mode) 752 | break; 753 | else 754 | std::this_thread::sleep_for(cfg._update_interval - elasped_time); 755 | } 756 | } while (false); 757 | 758 | LOG(INFO) << "Shutting down..."; 759 | cleanup_dns_services(); 760 | cleanup_public_ip_getter(); 761 | curl_global_cleanup(); 762 | google::ShutdownGoogleLogging(); 763 | 764 | return EXIT_SUCCESS; 765 | } 766 | --------------------------------------------------------------------------------