├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── cloudflaredd.c └── ddns.service /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | # Custom Entries 55 | .vscode/ 56 | cloudflaredd -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Morgan Gallant 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC ?= gcc 2 | CFLAGS = -O3 3 | LDFLAGS = -lcurl 4 | BINARY = cloudflaredd 5 | 6 | SRCS = cloudflaredd.c 7 | 8 | all: 9 | $(CC) $(CFLAGS) $(SRCS) $(LDFLAGS) -o $(BINARY) 10 | 11 | clean: 12 | $(RM) $(BINARY) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | __cloudflaredd__ is a small script meant to run as a systemd process to automatically upload dynamic dns changes to a set of domain dns records managed by [Cloudflare](https://www.cloudflare.com/). Essentially, every three minutes, we use a protocol named STUN (defined in [RFC 5389](https://tools.ietf.org/html/rfc5389)) to retrieve our public ip address. If the address changed from the previous iteration, we update cloudflare records with new data. Using STUN is great because it's super fast (a single UDP-based round trip) and totally free because Google hosts free public STUN servers, which are typically used within the ICE protocol for nat traversal (which is heavily used in Google's WebRTC framework). 2 | 3 | ## Installation & Setup 4 | 5 | The project is meant to run as a `systemctl` service in the background, and thus is super lightweight. It's implemented as a single C file with a dependency on [libcurl](https://curl.haxx.se/libcurl/). To download and build the project, simply do the following: 6 | ``` 7 | git clone git@github.com:MorganGallant/cloudflaredd.git 8 | cd cloudflaredd 9 | make 10 | ``` 11 | 12 | __Before building__, you will want to edit the static array defined on L253 of `cloudflaredd.c`. Here, you can enter in all of your specific credentials and dns records that you want to update whenever a change of ip occurs. These are the defaults, which will not work: 13 | ``` 14 | static cf_dns_record_t cf_target_dns_records[] = { 15 | {.identifer = NULL, 16 | .name = "your-cool-domain.com", 17 | .type = "A", 18 | .zone = "zone-id-goes-here", 19 | .api_token = "your-api-token-goes-here", 20 | .proxied = false}}; 21 | ``` 22 | For example, if you wanted to edit the `test.morgangallant.com` A record, and the proxied `dev.morgangallant.com` A record, the structure should look like the following: (`identifier` can be NULL if you do not know the DNS Record ID) 23 | ``` 24 | static cf_dns_record_t cf_target_dns_records[] = { 25 | { 26 | .identifer = NULL, 27 | .name = "test.morgangallant.com", 28 | .type = "A", 29 | .zone = "zone-id-goes-here", 30 | .api_token = "your-api-token-goes-here", 31 | .proxied = false 32 | }, 33 | { 34 | .identifer = NULL, 35 | .name = "dev.morgangallant.com", 36 | .type = "A", 37 | .zone = "zone-id-goes-here", 38 | .api_token = "your-api-token-goes-here", 39 | .proxied = true 40 | } 41 | }; 42 | ``` 43 | 44 | If it would benefit people, I'd gladly improve on the configuration aspect of cloudflaredd. Perhaps a .toml file or .yml file to describe the configuration, rather than having to compile it in. Let me know! 45 | 46 | ## Systemd Setup 47 | 48 | To setup this client to run in the background, simply copy the `ddns.service` file to your systemd directory (probably `/etc/systemd/system`). Once you have done this, edit the file and change the `ExecStart` option to have the absolute path to the cloudflaredd binary. Then, once this file is saved, you can do the following to enable the systemd service to start at login: 49 | ``` 50 | sudo systemctl start ddns 51 | sudo systemctl enable ddns # if you want cloudflareddns to start at login 52 | ``` 53 | 54 | ## Support / Future Features 55 | 56 | As always, feel free to submit issues / pull requests to better the codebase. This is a small project that I needed to do for my tower, since my apartment doesn't have a static ip address. If you want to get in contact, send me an [email](mailto:morgan@morgangallant.com)! 57 | -------------------------------------------------------------------------------- /cloudflaredd.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Morgan Gallant 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | /* STUN Implementation */ 35 | 36 | typedef struct { 37 | char *addr; 38 | int port; 39 | } stun_addr_t; 40 | 41 | // Parses an address into a stun_addr_t structure. 42 | static stun_addr_t *stun_addr_create(const char *address) { 43 | char *offset = strstr(address, ":"); 44 | if (!offset) 45 | return NULL; 46 | 47 | stun_addr_t *resp_addr = malloc(sizeof(stun_addr_t)); 48 | assert(resp_addr != NULL); 49 | 50 | // Copy the address without the port number to the return structure. 51 | long addr_len = offset - address; 52 | resp_addr->addr = malloc(addr_len * sizeof(char) + 1); 53 | memcpy(resp_addr->addr, address, addr_len); 54 | resp_addr->addr[addr_len] = '\0'; 55 | 56 | // Parse the port number. 57 | char *ptr; 58 | resp_addr->port = (int)strtol(offset + 1, &ptr, 10); 59 | assert(*ptr == '\0'); 60 | 61 | return resp_addr; 62 | } 63 | 64 | // Cleanup a stun_addr_t structure. 65 | static void stun_addr_free(stun_addr_t **addr) { 66 | if (!(*addr)) 67 | return; 68 | free((*addr)->addr); 69 | free(*addr); 70 | *addr = NULL; 71 | } 72 | 73 | typedef struct { 74 | uint16_t type; 75 | uint16_t size; 76 | uint32_t cookie; 77 | uint32_t transaction_id[3]; 78 | } stun_message_header_t; 79 | 80 | typedef struct { 81 | int fd; 82 | struct sockaddr_in localaddr; 83 | struct sockaddr_in remoteaddr; 84 | stun_message_header_t req; // we need to store req to verify trans id 85 | } stun_client_t; 86 | 87 | // Initialize a socket address structure. 88 | static void init_sockaddr(struct sockaddr_in *a, struct in_addr dst, int p) { 89 | memset(a, 0, sizeof(*a)); 90 | a->sin_family = AF_INET; 91 | a->sin_addr = dst; 92 | a->sin_port = htons(p); 93 | } 94 | 95 | // Initialize a stun client for a given server address. 96 | static stun_client_t *stun_client_create(const stun_addr_t *address) { 97 | struct addrinfo *results = NULL; 98 | struct addrinfo hints = {}; 99 | hints.ai_family = AF_INET; 100 | hints.ai_socktype = SOCK_STREAM; 101 | int32_t res = getaddrinfo(address->addr, NULL, &hints, &results); 102 | if (res != 0) 103 | return NULL; 104 | 105 | struct in_addr stunaddr; 106 | if (results) 107 | stunaddr = ((struct sockaddr_in *)results->ai_addr)->sin_addr; 108 | else 109 | return NULL; 110 | 111 | stun_client_t *resp = malloc(sizeof(stun_client_t)); 112 | init_sockaddr(&resp->remoteaddr, stunaddr, address->port); 113 | const struct in_addr localdst = {.s_addr = INADDR_ANY}; 114 | init_sockaddr(&resp->localaddr, localdst, 0); 115 | 116 | // Bind the socket to the local address before returning. 117 | resp->fd = socket(AF_INET, SOCK_DGRAM, 0); 118 | res = bind(resp->fd, (struct sockaddr *)&resp->localaddr, 119 | sizeof(resp->localaddr)); 120 | if (res != 0) { 121 | free(resp); 122 | freeaddrinfo(results); 123 | return NULL; 124 | } 125 | 126 | freeaddrinfo(results); 127 | return resp; 128 | } 129 | 130 | // Defined in RFC 5389, must be passed with all requests. 131 | static uint32_t stun_magic_cookie = 0x2112A442; 132 | 133 | // Send the request to the stun server. 134 | static bool stun_client_send_request(stun_client_t *client) { 135 | client->req.type = htons(0x0001); // binding request 136 | client->req.size = htons(0x0000); // request size 137 | client->req.cookie = htonl(stun_magic_cookie); // magic cookie 138 | for (int i = 0; i < 3; ++i) 139 | client->req.transaction_id[i] = rand(); // random transaction id 140 | 141 | ssize_t res = sendto(client->fd, &client->req, sizeof(client->req), 0, 142 | (struct sockaddr *)&client->remoteaddr, 143 | sizeof(client->remoteaddr)); 144 | if (res < 0) 145 | return false; 146 | 147 | // Set timeout for the request and indicate success. 148 | struct timeval timeout = {/*secs=*/5, 0}; 149 | setsockopt(client->fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); 150 | return true; 151 | } 152 | 153 | typedef struct { 154 | uint16_t type; 155 | uint16_t size; 156 | } stun_attribute_header_t; 157 | 158 | typedef struct { 159 | uint8_t reserved; 160 | uint8_t family; 161 | uint16_t port; 162 | uint32_t address; 163 | } stun_xor_mapped_addr_t; 164 | 165 | // Defined in RFC 5389, used to check the response message type. 166 | static uint16_t stun_binding_response_type = 0x0101; 167 | 168 | // Defined in RFC 5389, used to scan for the address response attribute. 169 | static uint16_t stun_xor_mapped_addr_type = 0x0020; 170 | 171 | // Read the response from the stun server and decode our public address. 172 | static char *stun_client_read_response(stun_client_t *client) { 173 | char buffer[1 << 12]; 174 | ssize_t response_size = read(client->fd, buffer, sizeof(buffer)); 175 | if (response_size < 0) 176 | return NULL; 177 | 178 | // Ensure that the response was valid by checking type and transaction id. 179 | char *ptr = buffer; 180 | stun_message_header_t *header = (stun_message_header_t *)ptr; 181 | if (header->type != htons(stun_binding_response_type)) 182 | return NULL; 183 | for (int i = 0; i < 3; ++i) 184 | if (header->transaction_id[i] != client->req.transaction_id[i]) 185 | return NULL; 186 | 187 | // Parse the body of the response once we verified that it is right type. 188 | ptr += sizeof(stun_message_header_t); 189 | while (ptr < buffer + response_size) { 190 | stun_attribute_header_t *att_header = (stun_attribute_header_t *)ptr; 191 | if (att_header->type == htons(stun_xor_mapped_addr_type)) { 192 | ptr += sizeof(stun_attribute_header_t); 193 | stun_xor_mapped_addr_t *addr = (stun_xor_mapped_addr_t *)ptr; 194 | 195 | // Decode the address using the magic cookie and return. 196 | uint32_t public_addr = htonl(addr->address) ^ stun_magic_cookie; 197 | char *resp_buffer = malloc(INET_ADDRSTRLEN * sizeof(char)); 198 | int l = 199 | snprintf(resp_buffer, INET_ADDRSTRLEN - 1, "%d.%d.%d.%d", 200 | ((public_addr >> 24) & 0xFF), ((public_addr >> 16) & 0xFF), 201 | ((public_addr >> 8) & 0xFF), (public_addr & 0xFF)); 202 | resp_buffer[l] = '\0'; 203 | return resp_buffer; 204 | } 205 | ptr += sizeof(stun_attribute_header_t) + att_header->size; 206 | } 207 | return NULL; 208 | } 209 | 210 | // Cleanup the stun client by closing the bound socket. 211 | static void stun_client_cleanup(stun_client_t **client) { 212 | if (!(*client)) 213 | return; 214 | close((*client)->fd); 215 | free(*client); 216 | *client = NULL; 217 | } 218 | 219 | // The address of the public Google-hosted stun server to use. 220 | #define GOOGLE_STUN_ADDR "stun.l.google.com:19302" 221 | 222 | // Get the public ip address of this server. 223 | static char *stun_get_pub_ip(const char *stun_server_addr) { 224 | stun_addr_t *addr = stun_addr_create(stun_server_addr); 225 | stun_client_t *client = stun_client_create(addr); 226 | 227 | char *pub_ip = NULL; 228 | 229 | bool res = stun_client_send_request(client); 230 | if (!res) 231 | goto done; 232 | 233 | pub_ip = stun_client_read_response(client); 234 | 235 | done: 236 | stun_addr_free(&addr); 237 | stun_client_cleanup(&client); 238 | return pub_ip; 239 | } 240 | 241 | /* Cloudflare API Implementation */ 242 | 243 | typedef struct { 244 | char *identifer; 245 | char *name; 246 | char *type; 247 | char *zone; 248 | char *api_token; 249 | bool proxied; 250 | } cf_dns_record_t; 251 | 252 | // The set of dns records to update with our new public address. 253 | static cf_dns_record_t cf_target_dns_records[] = { 254 | {.identifer = NULL, 255 | .name = "your-cool-domain.com", 256 | .type = "A", 257 | .zone = "zone-id-goes-here", 258 | .api_token = "your-api-token-goes-here", 259 | .proxied = false}}; 260 | 261 | // Construct a query url for retrieving an identifier from a dns record. 262 | static char *cf_construct_query_url(cf_dns_record_t *rec) { 263 | size_t url_len = strlen(rec->name) + strlen(rec->type) + strlen(rec->zone); 264 | url_len += 68; 265 | char *url = malloc((url_len + 1) * sizeof(char)); // null terminator + 1 266 | snprintf(url, url_len, 267 | "https://api.cloudflare.com/client/v4/zones/%s/" 268 | "dns_records?type=%s&name=%s", 269 | rec->zone, rec->type, rec->name); 270 | url[url_len] = '\0'; 271 | return url; 272 | } 273 | 274 | // Construct an authorization token header string for the cloudflare request. 275 | static char *cf_construct_auth_token(const cf_dns_record_t *rec) { 276 | size_t token_len = 23 + strlen(rec->api_token); 277 | char *token = malloc((token_len + 1) * sizeof(char)); 278 | snprintf(token, token_len, "Authorization: Bearer %s", rec->api_token); 279 | token[token_len] = '\0'; 280 | return token; 281 | } 282 | 283 | typedef struct { 284 | char *data; 285 | size_t len; 286 | } cf_message_t; 287 | 288 | // Initialize a cf_message_t to default values. 289 | static cf_message_t *cf_message_init() { 290 | cf_message_t *resp = malloc(sizeof(cf_message_t)); 291 | resp->data = strdup(""); 292 | resp->len = 0; 293 | return resp; 294 | } 295 | 296 | // Cleans up any allocated memory from a cf_message_t. 297 | static void cf_message_cleanup(cf_message_t **msg) { 298 | if (!(*msg)) 299 | return; 300 | if ((*msg)->data) 301 | free((*msg)->data); 302 | free(*msg); 303 | *msg = NULL; 304 | } 305 | 306 | // CURL callback function for storing response in cf_message_t object. 307 | // Credit: https://curl.haxx.se/libcurl/c/postinmemory.html 308 | static size_t cf_mem_write_cb(void *resp, size_t size, size_t nmemb, void *up) { 309 | size_t real_size = size * nmemb; 310 | cf_message_t *resp_buf = (cf_message_t *)up; 311 | 312 | resp_buf->data = realloc(resp_buf->data, resp_buf->len + real_size + 1); 313 | assert(resp_buf->data != NULL); 314 | 315 | memcpy(&(resp_buf->data[resp_buf->len]), resp, real_size); 316 | resp_buf->len += real_size; 317 | resp_buf->data[resp_buf->len] = '\0'; 318 | return real_size; 319 | } 320 | 321 | // Configures a cloudflare api request with libcurl. 322 | static CURL *cf_setup_request(const char *url, const cf_dns_record_t *record, 323 | cf_message_t *dest) { 324 | CURL *curl = curl_easy_init(); 325 | if (!curl) 326 | return NULL; 327 | 328 | curl_easy_setopt(curl, CURLOPT_URL, url); 329 | curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); 330 | 331 | curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, cf_mem_write_cb); 332 | curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)dest); 333 | 334 | return curl; 335 | } 336 | 337 | // Update a dns record structure with its identifier. 338 | static bool cf_get_identifier(cf_dns_record_t *record) { 339 | bool ret_val = false; 340 | 341 | // Fast path, we already have an identifier for this record. 342 | if (record->identifer != NULL && strlen(record->identifer) != 0) 343 | return true; 344 | 345 | char *url = cf_construct_query_url(record); 346 | char *token = cf_construct_auth_token(record); 347 | cf_message_t *resp = cf_message_init(); 348 | 349 | CURL *curl; 350 | CURLcode res; 351 | struct curl_slist *headers = NULL; 352 | 353 | curl = cf_setup_request(url, record, resp); 354 | if (!curl) 355 | goto cleanup; 356 | 357 | headers = curl_slist_append(headers, token); 358 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); 359 | 360 | res = curl_easy_perform(curl); 361 | if (res != CURLE_OK) 362 | goto cleanup; 363 | 364 | record->identifer = malloc(33 * sizeof(char)); 365 | memcpy(record->identifer, resp->data + 18, 32); 366 | 367 | ret_val = true; 368 | 369 | cleanup: 370 | if (url) 371 | free(url); 372 | if (token) 373 | free(token); 374 | if (resp) 375 | cf_message_cleanup(&resp); 376 | curl_easy_cleanup(curl); 377 | curl_slist_free_all(headers); 378 | return ret_val; 379 | } 380 | 381 | // A compile-time constant macro for finding the size of an array. 382 | #define ARRAY_LEN(arr) (sizeof(arr) / sizeof(arr[0])) 383 | 384 | // Fill all the identifiers for an array of dns records. 385 | static bool cf_fill_identifiers(cf_dns_record_t *records, size_t count) { 386 | for (size_t i = 0; i < count; ++i) { 387 | bool res = cf_get_identifier(&records[i]); 388 | if (!res) 389 | return false; 390 | } 391 | return true; 392 | } 393 | 394 | static char *cf_format_up_url(const cf_dns_record_t *record) { 395 | // Sanity check. 396 | if (!record->identifer) 397 | return NULL; 398 | 399 | size_t len = strlen(record->zone) + strlen(record->identifer); 400 | len += 57; 401 | char *buffer = malloc((len + 1) * sizeof(char)); 402 | snprintf(buffer, len, 403 | "https://api.cloudflare.com/client/v4/zones/%s/dns_records/%s", 404 | record->zone, record->identifer); 405 | buffer[len] = '\0'; 406 | return buffer; 407 | } 408 | 409 | // A macro to easily format a boolean as a string. 410 | #define BOOL_STR(x) ((x) ? "true" : "false") 411 | 412 | // Creates a request json object to update an existing dns record.xx 413 | static char *cf_format_up_req(const cf_dns_record_t *rec, const char *content) { 414 | const char *p = BOOL_STR(rec->proxied); 415 | size_t len = 416 | strlen(rec->type) + strlen(rec->name) + strlen(content) + strlen(p); 417 | len += 54; 418 | char *msg = malloc((len + 1) * sizeof(char)); 419 | snprintf(msg, len, 420 | "{\"type\":\"%s\",\"name\":\"%s\",\"content\":\"%s\",\"ttl\":1," 421 | "\"proxied\":%s}", 422 | rec->type, rec->name, content, p); 423 | msg[len] = '\0'; 424 | return msg; 425 | } 426 | 427 | // Updates the contents of a cloudflare dns record with new content. 428 | static bool cf_update_content(const cf_dns_record_t *record, 429 | const char *content) { 430 | bool ret_val = false; 431 | 432 | char *url = cf_format_up_url(record); 433 | char *req_data = cf_format_up_req(record, content); 434 | char *token = cf_construct_auth_token(record); 435 | cf_message_t *resp = cf_message_init(); 436 | 437 | CURL *curl; 438 | CURLcode res; 439 | struct curl_slist *headers = NULL; 440 | 441 | curl = cf_setup_request(url, record, resp); 442 | if (!curl) 443 | goto cleanup; 444 | 445 | headers = curl_slist_append(headers, "Content-Type: application/json"); 446 | headers = curl_slist_append(headers, token); 447 | curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); 448 | 449 | curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req_data); 450 | curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT"); 451 | 452 | res = curl_easy_perform(curl); 453 | if (res != CURLE_OK) 454 | goto cleanup; 455 | 456 | ret_val = true; 457 | 458 | cleanup: 459 | if (url) 460 | free(url); 461 | if (req_data) 462 | free(req_data); 463 | if (token) 464 | free(token); 465 | if (resp) 466 | cf_message_cleanup(&resp); 467 | curl_easy_cleanup(curl); 468 | curl_slist_free_all(headers); 469 | return ret_val; 470 | } 471 | 472 | // Updates all the cloudflare records with new content. 473 | static bool cf_update_all(cf_dns_record_t *records, size_t count, 474 | const char *content) { 475 | for (int i = 0; i < count; ++i) { 476 | bool res = cf_update_content(&records[i], content); 477 | if (!res) { 478 | return false; 479 | } 480 | } 481 | return true; 482 | } 483 | 484 | // Check if this computers public address changed. 485 | static bool pub_ip_did_change(char *prev_ip) { 486 | char *curr_ip = stun_get_pub_ip(GOOGLE_STUN_ADDR); 487 | if (strcmp(prev_ip, curr_ip) == 0) 488 | return false; 489 | 490 | strcpy(prev_ip, curr_ip); 491 | free(curr_ip); 492 | return true; 493 | } 494 | 495 | int main() { 496 | curl_global_init(CURL_GLOBAL_ALL); 497 | srand(time(NULL)); 498 | 499 | char public_addr[INET_ADDRSTRLEN] = {'\0'}; 500 | while (true) { 501 | if (pub_ip_did_change(public_addr)) { 502 | size_t num_records = ARRAY_LEN(cf_target_dns_records); 503 | bool res = cf_fill_identifiers(cf_target_dns_records, num_records); 504 | if (!res) { 505 | printf("Failed to fill identifiers for target dns records.\n"); 506 | break; 507 | } 508 | res = cf_update_all(cf_target_dns_records, num_records, public_addr); 509 | if (!res) { 510 | printf("Failed to update dns records with new content."); 511 | break; 512 | } 513 | printf("Updated records for new IP: %s.\n", public_addr); 514 | } 515 | sleep(180); // 180 seconds = 3 mins 516 | } 517 | 518 | // Makes valgrind happy 519 | for (int i = 0; i < ARRAY_LEN(cf_target_dns_records); ++i) 520 | if (cf_target_dns_records[i].identifer) 521 | free(cf_target_dns_records[i].identifer); 522 | 523 | curl_global_cleanup(); 524 | } 525 | -------------------------------------------------------------------------------- /ddns.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Cloudflare DDNS 3 | After=network.target 4 | 5 | [Service] 6 | Type=simple 7 | ExecStart=/home/morgan/Desktop/cloudflaredd/cloudflaredd # CHANGE THIS LINE 8 | Restart=always 9 | 10 | [Install] 11 | WantedBy=multi-user.target --------------------------------------------------------------------------------