Skip to main content

trojan_config/
ddns.rs

1//! Dynamic DNS configuration.
2
3use serde::{Deserialize, Serialize};
4
5/// Dynamic DNS configuration.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct DdnsConfig {
8    /// Whether DDNS updates are enabled.
9    #[serde(default)]
10    pub enabled: bool,
11
12    /// Update interval in seconds.
13    #[serde(default = "default_ddns_interval")]
14    pub interval: u64,
15
16    /// URLs to detect public IPv4 address (tried in order, first success wins).
17    #[serde(default = "default_ipv4_urls")]
18    pub ipv4_urls: Vec<String>,
19
20    /// URLs to detect public IPv6 address (empty = disabled).
21    #[serde(default)]
22    pub ipv6_urls: Vec<String>,
23
24    /// Cloudflare DNS provider configuration.
25    #[serde(default)]
26    pub cloudflare: Option<CloudflareDdnsConfig>,
27}
28
29impl Default for DdnsConfig {
30    fn default() -> Self {
31        Self {
32            enabled: false,
33            interval: default_ddns_interval(),
34            ipv4_urls: default_ipv4_urls(),
35            ipv6_urls: Vec::new(),
36            cloudflare: None,
37        }
38    }
39}
40
41/// Cloudflare DNS provider configuration.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct CloudflareDdnsConfig {
44    /// Cloudflare API token with DNS edit permissions.
45    pub api_token: String,
46
47    /// Zone (domain) name, e.g. "example.com".
48    pub zone: String,
49
50    /// DNS record names to update, e.g. ["example.com", "*.example.com"].
51    pub records: Vec<String>,
52
53    /// Whether to enable Cloudflare CDN proxy for the records.
54    #[serde(default)]
55    pub proxied: bool,
56
57    /// DNS record TTL in seconds. 1 = automatic.
58    #[serde(default = "default_ddns_ttl")]
59    pub ttl: u32,
60}
61
62fn default_ddns_interval() -> u64 {
63    300
64}
65
66fn default_ddns_ttl() -> u32 {
67    1
68}
69
70fn default_ipv4_urls() -> Vec<String> {
71    vec![
72        "https://api.ipify.org".into(),
73        "https://ifconfig.me/ip".into(),
74        "https://ip.sb".into(),
75    ]
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn ddns_config_default() {
84        let cfg = DdnsConfig::default();
85        assert!(!cfg.enabled);
86        assert_eq!(cfg.interval, 300);
87        assert!(!cfg.ipv4_urls.is_empty());
88        assert!(cfg.ipv6_urls.is_empty());
89        assert!(cfg.cloudflare.is_none());
90    }
91
92    #[test]
93    fn ddns_config_deserialize_minimal() {
94        let toml_str = r#"enabled = true"#;
95        let cfg: DdnsConfig = toml::from_str(toml_str).unwrap();
96        assert!(cfg.enabled);
97        assert_eq!(cfg.interval, 300);
98        assert_eq!(cfg.ipv4_urls.len(), 3);
99    }
100
101    #[test]
102    fn ddns_config_deserialize_full() {
103        let toml_str = r#"
104enabled = true
105interval = 600
106ipv4_urls = ["https://api.ipify.org"]
107
108[cloudflare]
109api_token = "test-token"
110zone = "example.com"
111records = ["example.com", "*.example.com"]
112proxied = true
113ttl = 300
114"#;
115        let cfg: DdnsConfig = toml::from_str(toml_str).unwrap();
116        assert!(cfg.enabled);
117        assert_eq!(cfg.interval, 600);
118        assert_eq!(cfg.ipv4_urls, vec!["https://api.ipify.org"]);
119        let cf = cfg.cloudflare.unwrap();
120        assert_eq!(cf.api_token, "test-token");
121        assert_eq!(cf.zone, "example.com");
122        assert_eq!(cf.records, vec!["example.com", "*.example.com"]);
123        assert!(cf.proxied);
124        assert_eq!(cf.ttl, 300);
125    }
126}