Compare commits
17 Commits
c62a32af9b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c1a3b96a1d | |||
| 9d0e005ee5 | |||
| 066c991ab1 | |||
| 0afbc8ccf9 | |||
| 6613831f7a | |||
| f19a29dd93 | |||
| 42ef38f147 | |||
| 9fb9430319 | |||
| 8b60d3e280 | |||
| 57a9b9a0ca | |||
| fcb9d0e3b0 | |||
| f61baf53c8 | |||
| 3570eb02ea | |||
| c419bb38e9 | |||
| 29ecb39719 | |||
| ebec053d2c | |||
| 61cd3a3b4f |
@@ -52,3 +52,6 @@ Module.symvers
|
||||
Mkfile.old
|
||||
dkms.conf
|
||||
|
||||
# Build output
|
||||
build/
|
||||
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
FROM alpine:3.22.4 AS builder
|
||||
|
||||
RUN apk add --no-cache \
|
||||
ca-certificates \
|
||||
gcc \
|
||||
musl-dev \
|
||||
make \
|
||||
pkgconf \
|
||||
curl-dev \
|
||||
curl-static \
|
||||
cjson-dev \
|
||||
cjson-static \
|
||||
zstd-dev \
|
||||
zstd-static \
|
||||
openssl-dev \
|
||||
openssl-libs-static \
|
||||
nghttp2-dev \
|
||||
nghttp2-static \
|
||||
zlib-dev \
|
||||
zlib-static \
|
||||
brotli-dev \
|
||||
brotli-static \
|
||||
c-ares-dev \
|
||||
libidn2-dev \
|
||||
libidn2-static \
|
||||
libpsl-dev \
|
||||
libpsl-static \
|
||||
libunistring-dev \
|
||||
libunistring-static
|
||||
|
||||
WORKDIR /build
|
||||
COPY src/ src/
|
||||
COPY Makefile .
|
||||
|
||||
RUN make STATIC=1
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
|
||||
COPY --from=builder /etc/ssl/cert.pem /etc/ssl/cert.pem
|
||||
COPY --from=builder /build/build/dns-keeper /dns-keeper
|
||||
|
||||
ENTRYPOINT ["/dns-keeper"]
|
||||
@@ -22,13 +22,13 @@ OBJS := $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRCS))
|
||||
all: $(BUILD_DIR)/$(TARGET)
|
||||
|
||||
$(BUILD_DIR)/$(TARGET): $(OBJS)
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||
|
||||
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
$(BUILD_DIR):
|
||||
mkdir -p $(BUILD_DIR)
|
||||
mkdir -p $(BUILD_DIR)
|
||||
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR)
|
||||
rm -rf $(BUILD_DIR)
|
||||
|
||||
@@ -1,3 +1,80 @@
|
||||
# dns-keeper
|
||||
# DNS Keeper
|
||||
|
||||
A security-hardened dynamic DNS updater with self-verification and multi-API IP consensus.
|
||||
A small but robust program to keep DNS records updated with your public _(external)_ IPv4 address.
|
||||
|
||||
Mostly like DDNS but using API driven updates.
|
||||
Currently only gandi's liveDNS API is supported.
|
||||
|
||||
## How it works
|
||||
|
||||
Every `POLL_INTERVAL` seconds, DNS Keeper will check if the IPv4 in the domain(s) is still
|
||||
valid by seeing if `DNS_KEEPER_RECORD`.`DNS_KEEPER_DOMAIN` still points to itself.
|
||||
|
||||
> [!NOTE]
|
||||
> On startup DNS Keeper starts a detached thread with a UDP socket on which it answers
|
||||
> a random set of challenge bytes.
|
||||
> DNS Keeper will send an empty data-frame to its own socket using
|
||||
> the DNS resolved IPv4 address to see if it gets a response with the correct challenge bytes.
|
||||
> The challenge bytes are changed before every self-check.
|
||||
|
||||
If DNS Keeper fails to get a response from itself for [3](./src/selfcheck.h#6)
|
||||
times it will check it's cached IP against the response of multiple external
|
||||
[services](./src/ipcheck.c#8) that return your public IPv4.
|
||||
|
||||
If the returned public IP is the same as the cached one,
|
||||
DNS Keepr will see that as drift.
|
||||
If this happens too much it will revalidate its own cache against the self-record.
|
||||
|
||||
If the returned public IP is different from the cached one,
|
||||
DNS Keeper will check the new IP using its own socket again,
|
||||
this time without DNS.
|
||||
If it proves to be correct it will update all A record of the specified `DOMAINS`
|
||||
who's value match the old IP to the new IP.
|
||||
The chached IP is only updated after the self-record is updated correctly.
|
||||
|
||||
> [!NOTE]
|
||||
> DNS Keeper enforces a strict majority amongst the services.
|
||||
> If a strict majority cannot be reached or not enough services
|
||||
> were able to respond, it will take no action and reset the failed counter.
|
||||
> This is to prevent any issues should external services be compromised
|
||||
> and to avoid spamming these services in case something else is wrong.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Field | Descript | Default |
|
||||
| ------------------- | ---------------------------------------------------------------------------------------- | ------- |
|
||||
| `GANDI_API_TOKEN` | Gandi API Token with permission to "See and renew domain names". | |
|
||||
| `DOMAINS` | The domains _(eg. "example.noreal,demo.notreal")_ to keep the A records up to date of. | |
|
||||
| `DNS_KEEPER_PORT` | The port on which DNS Keepr runs its self-check UDP Socket. | 65535 |
|
||||
| `DNS_KEEPER_DOMAIN` | The domain on which the record pointing to dns-keeper lives. (eg. example.notreal). | |
|
||||
| `DNS_KEEPER_RECORD` | DNS record that points to your public IP/DNS-Keeper (eg. _dnsk). | |
|
||||
| `POLL_INTERVAL` | The interval (in seconds) at which DNS Keeper should check if it can still reach itself. | 5 |
|
||||
|
||||
## Setup
|
||||
|
||||
You can the binary using the dynamically linked or static using make.
|
||||
|
||||
```sh
|
||||
# Dynamically linked
|
||||
make
|
||||
|
||||
# Static:
|
||||
make STATIC=1
|
||||
```
|
||||
|
||||
The dynamically linked binary is smaller but requires dependencies:
|
||||
libcurl, libcjson and pthread to be on the target machine.
|
||||
|
||||
The static binary is slightly bigger but is fully self contained.
|
||||
|
||||
### Docker Image
|
||||
|
||||
The [Dockerfile](./Dockerfile) in this repo builds a minimal image with just the static binary and CA certificates.
|
||||
|
||||
```sh
|
||||
docker build -t dns-keeper .
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Because the image is just the c binary with ca certs, there is no shell to exec into.
|
||||
> `docker exec` will not work for debugging.
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "config.h"
|
||||
#include "log.h"
|
||||
|
||||
static int load_env_str(const char *env_name, char **out, int unset) {
|
||||
const char *env_var = getenv(env_name);
|
||||
if (!env_var || env_var[0] == '\0') {
|
||||
log_error("Missing required env var %s", env_name);
|
||||
return -1;
|
||||
}
|
||||
*out = strdup(env_var);
|
||||
if (!*out) { log_error("Memory allocation failed"); return -1; }
|
||||
if (unset) unsetenv(env_name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int load_env_int(
|
||||
const char *env_name,
|
||||
int *out,
|
||||
int min,
|
||||
int max,
|
||||
int fallback
|
||||
) {
|
||||
const char *env_var = getenv(env_name);
|
||||
if (!env_var || env_var[0] == '\0') { *out = fallback; return 0; }
|
||||
|
||||
char *end;
|
||||
errno = 0;
|
||||
long parsed = strtol(env_var, &end, 10);
|
||||
|
||||
if (errno != 0 || *end != '\0') {
|
||||
log_error("%s needs to be an integer", env_name);
|
||||
return -1;
|
||||
}
|
||||
if (parsed < min || parsed > max) {
|
||||
log_error("%s needs to be within range (%d-%d)", env_name, min, max);
|
||||
return -1;
|
||||
}
|
||||
*out = (int)parsed;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int load_env_strs(
|
||||
const char *env_name,
|
||||
char ***out,
|
||||
int *count,
|
||||
int max_str_len
|
||||
) {
|
||||
char *str = NULL;
|
||||
if (load_env_str(env_name, &str, 0)) return -1;
|
||||
|
||||
*count = 1;
|
||||
for (char *p = str; *p; p++)
|
||||
if (*p == ',') (*count)++;
|
||||
|
||||
for (char *start = str, *p = str; ; p++) {
|
||||
if (*p == ',' || *p == '\0') {
|
||||
if ((int)(p - start) > max_str_len) {
|
||||
log_error("'%.*s' in %s exceeds max length (%d)",
|
||||
(int)(p - start), start, env_name, max_str_len);
|
||||
free(str);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (*p == '\0') break;
|
||||
start = p + 1;
|
||||
}
|
||||
}
|
||||
|
||||
char **arr = malloc(*count * sizeof(char *));
|
||||
if (!arr) { log_error("Memory allocation failed"); free(str); return -1; }
|
||||
|
||||
int i = 0;
|
||||
char *token = strtok(str, ",");
|
||||
while (token) {
|
||||
arr[i++] = strdup(token);
|
||||
token = strtok(NULL, ",");
|
||||
}
|
||||
|
||||
free(str);
|
||||
*out = arr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int config_load(Config *cfg) {
|
||||
if (load_env_str(ENV_GANDI_TOKEN, &cfg->api_token, 1)) return -1;
|
||||
|
||||
if (load_env_str(ENV_DNS_KEEPER_DOMAIN, &cfg->dns_keeper_domain, 0)) return -1;
|
||||
if (load_env_str(ENV_DNS_KEEPER_RECORD, &cfg->dns_keeper_record, 0)) return -1;
|
||||
if (strlen(cfg->dns_keeper_record) + 1 + strlen(cfg->dns_keeper_domain) > CONFIG_MAX_DOMAIN_LEN) {
|
||||
log_error("%s.%s exceeds max domain length (%d)",
|
||||
ENV_DNS_KEEPER_RECORD, ENV_DNS_KEEPER_DOMAIN, CONFIG_MAX_DOMAIN_LEN);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (load_env_int(ENV_DNS_KEEPER_PORT, &cfg->dns_keeper_port,
|
||||
1, 65535, DEFAULT_DNS_KEEPER_PORT)) return -1;
|
||||
if (load_env_int(ENV_POLL_INTERVAL, &cfg->poll_interval,
|
||||
1, INT_MAX, DEFAULT_POLL_INTERVAL)) return -1;
|
||||
|
||||
if (load_env_strs(ENV_DOMAINS, &cfg->domains, &cfg->domain_count, CONFIG_MAX_DOMAIN_LEN)) return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void config_free(Config *cfg) {
|
||||
if (cfg->domains) {
|
||||
for (int i = 0; i < cfg->domain_count; i++) {
|
||||
free(cfg->domains[i]);
|
||||
}
|
||||
free(cfg->domains);
|
||||
cfg->domains = NULL;
|
||||
cfg->domain_count = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef CONFIG_H
|
||||
#define CONFIG_H
|
||||
|
||||
#define ENV_GANDI_TOKEN "GANDI_API_TOKEN"
|
||||
#define ENV_DOMAINS "DOMAINS"
|
||||
#define ENV_DNS_KEEPER_PORT "DNS_KEEPER_PORT"
|
||||
#define ENV_DNS_KEEPER_DOMAIN "DNS_KEEPER_DOMAIN"
|
||||
#define ENV_DNS_KEEPER_RECORD "DNS_KEEPER_RECORD"
|
||||
#define ENV_POLL_INTERVAL "POLL_INTERVAL"
|
||||
|
||||
#define DEFAULT_DNS_KEEPER_PORT 65535u
|
||||
#define DEFAULT_POLL_INTERVAL 5u
|
||||
#define CONFIG_MAX_DOMAIN_LEN 253u /* RFC 1035 max FQDN length */
|
||||
|
||||
typedef struct {
|
||||
char *api_token;
|
||||
char *dns_keeper_domain;
|
||||
char *dns_keeper_record;
|
||||
int dns_keeper_port;
|
||||
int poll_interval;
|
||||
char **domains;
|
||||
int domain_count;
|
||||
} Config;
|
||||
|
||||
int config_load(Config *cfg);
|
||||
void config_free(Config *cfg);
|
||||
|
||||
#endif /* CONFIG_H */
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <curl/curl.h>
|
||||
#include <cjson/cJSON.h>
|
||||
#include <ctype.h>
|
||||
#include "gandi.h"
|
||||
#include "log.h"
|
||||
|
||||
#define GANDI_API_BASE "https://api.gandi.net/v5/livedns"
|
||||
|
||||
static char s_api_key[256] = {0};
|
||||
|
||||
typedef struct {
|
||||
char *buf;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
} DynBuf;
|
||||
|
||||
static size_t write_cb(void *data, size_t size, size_t nmemb, void *userp) {
|
||||
DynBuf *out = (DynBuf *)userp;
|
||||
size_t total = size * nmemb;
|
||||
|
||||
if (out->len + total + 1 > out->cap) {
|
||||
size_t new_cap = out->cap ? out->cap * 2 : 4096;
|
||||
while (new_cap < out->len + total + 1) new_cap *= 2;
|
||||
char *tmp = realloc(out->buf, new_cap);
|
||||
if (!tmp) return 0;
|
||||
out->buf = tmp;
|
||||
out->cap = new_cap;
|
||||
}
|
||||
|
||||
memcpy(out->buf + out->len, data, total);
|
||||
out->len += total;
|
||||
out->buf[out->len] = '\0';
|
||||
return total;
|
||||
}
|
||||
|
||||
int gandi_init(const char *api_key) {
|
||||
if (!api_key || api_key[0] == '\0') return -1;
|
||||
strncpy(s_api_key, api_key, sizeof(s_api_key) - 1);
|
||||
s_api_key[sizeof(s_api_key) - 1] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
static CURL *make_curl(DynBuf *buf, struct curl_slist **headers_out) {
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return NULL;
|
||||
|
||||
char auth_header[320];
|
||||
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer %s", s_api_key);
|
||||
|
||||
struct curl_slist *headers = NULL;
|
||||
headers = curl_slist_append(headers, auth_header);
|
||||
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||
curl_easy_setopt(curl, CURLOPT_CAINFO, "/etc/ssl/certs/ca-certificates.crt");
|
||||
curl_easy_setopt(curl, CURLOPT_CAPATH, "/etc/ssl/certs");
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, buf);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
|
||||
*headers_out = headers;
|
||||
return curl;
|
||||
}
|
||||
|
||||
static int check_http_code(CURL *curl) {
|
||||
long code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code);
|
||||
|
||||
if (code == 401 || code == 403) return -2;
|
||||
if (code < 200 || code >= 300) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int patch_record(
|
||||
const char *domain,
|
||||
const char *rec_name,
|
||||
const char *old_ip,
|
||||
const char *new_ip
|
||||
) {
|
||||
cJSON *patch = cJSON_CreateObject();
|
||||
cJSON *add_arr = cJSON_CreateArray();
|
||||
cJSON *rem_arr = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(add_arr, cJSON_CreateString(new_ip));
|
||||
cJSON_AddItemToArray(rem_arr, cJSON_CreateString(old_ip));
|
||||
cJSON_AddItemToObject(patch, "add_rrset_values", add_arr);
|
||||
cJSON_AddItemToObject(patch, "remove_rrset_values", rem_arr);
|
||||
char *patch_str = cJSON_PrintUnformatted(patch);
|
||||
cJSON_Delete(patch);
|
||||
|
||||
char url[512];
|
||||
snprintf(url, sizeof(url),
|
||||
"%s/domains/%s/records/%s/A", GANDI_API_BASE, domain, rec_name);
|
||||
|
||||
DynBuf buf = {0};
|
||||
struct curl_slist *headers = NULL;
|
||||
CURL *curl = make_curl(&buf, &headers);
|
||||
if (!curl) { free(patch_str); return -1; }
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");
|
||||
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, patch_str);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
int ret = (res == CURLE_OK) ? check_http_code(curl) : -1;
|
||||
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
free(buf.buf);
|
||||
free(patch_str);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int gandi_get_self_ip(const char *domain, const char *record_name, char *out_ip) {
|
||||
char url[512];
|
||||
snprintf(url, sizeof(url),
|
||||
"%s/domains/%s/records/%s/A", GANDI_API_BASE, domain, record_name);
|
||||
|
||||
DynBuf buf = {0};
|
||||
struct curl_slist *headers = NULL;
|
||||
CURL *curl = make_curl(&buf, &headers);
|
||||
if (!curl) return -1;
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
|
||||
int ret = -1;
|
||||
|
||||
CURLcode cres = curl_easy_perform(curl);
|
||||
if (cres != CURLE_OK) {
|
||||
log_error("gandi: curl error for %s: (%d) %s",
|
||||
url, (int)cres, curl_easy_strerror(cres));
|
||||
ret = -1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
ret = check_http_code(curl);
|
||||
if (ret == -2) log_error("gandi: auth error (HTTP 401/403) for %s", url);
|
||||
else if (ret != 0) log_error("gandi: non-2xx HTTP response for %s", url);
|
||||
if (ret != 0) goto cleanup;
|
||||
|
||||
if (!buf.buf || buf.buf[0] == '\0') {
|
||||
log_error("gandi: empty response body for %s", url);
|
||||
ret = -1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_Parse(buf.buf);
|
||||
if (!root) {
|
||||
log_error("gandi: JSON parse failed for %s", url);
|
||||
ret = -1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
cJSON *values = cJSON_GetObjectItem(root, "rrset_values");
|
||||
if (!cJSON_IsArray(values) || cJSON_GetArraySize(values) <= 0) {
|
||||
log_warn("gandi: rrset_values missing/empty for %s (record %s.%s type A)",
|
||||
url, record_name, domain);
|
||||
cJSON_Delete(root);
|
||||
ret = -3;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
const char *ip = cJSON_GetStringValue(cJSON_GetArrayItem(values, 0));
|
||||
if (!ip || ip[0] == '\0') {
|
||||
log_warn("gandi: rrset_values[0] not a string for %s", url);
|
||||
cJSON_Delete(root);
|
||||
ret = -3;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
strncpy(out_ip, ip, GANDI_MAX_IP_LEN - 1);
|
||||
out_ip[GANDI_MAX_IP_LEN - 1] = '\0';
|
||||
|
||||
cJSON_Delete(root);
|
||||
ret = 0;
|
||||
|
||||
cleanup:
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
free(buf.buf);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int gandi_update_domains(const char *domain, const char *old_ip, const char *new_ip) {
|
||||
char url[512];
|
||||
snprintf(url, sizeof(url),
|
||||
"%s/domains/%s/records", GANDI_API_BASE, domain);
|
||||
|
||||
DynBuf buf = {0};
|
||||
struct curl_slist *headers = NULL;
|
||||
CURL *curl = make_curl(&buf, &headers);
|
||||
if (!curl) return -1;
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url);
|
||||
|
||||
int ret = -1;
|
||||
int matched = 0;
|
||||
int failed = 0;
|
||||
int n = 0;
|
||||
cJSON *records = NULL;
|
||||
|
||||
if (curl_easy_perform(curl) != CURLE_OK) goto cleanup;
|
||||
|
||||
ret = check_http_code(curl);
|
||||
if (ret == -2) log_error("gandi: auth error (HTTP 401/403) for %s", url);
|
||||
else if (ret != 0) log_error("gandi: non-2xx HTTP response for %s", url);
|
||||
if (ret != 0) goto cleanup;
|
||||
|
||||
records = cJSON_Parse(buf.buf);
|
||||
if (!records || !cJSON_IsArray(records)) {
|
||||
ret = -1;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
n = cJSON_GetArraySize(records);
|
||||
for (int i = 0; i < n; i++) {
|
||||
cJSON *rec = cJSON_GetArrayItem(records, i);
|
||||
cJSON *type = cJSON_GetObjectItem(rec, "rrset_type");
|
||||
if (!cJSON_IsString(type) || strcmp(type->valuestring, "A") != 0) continue;
|
||||
|
||||
cJSON *values = cJSON_GetObjectItem(rec, "rrset_values");
|
||||
if (!cJSON_IsArray(values)) continue;
|
||||
|
||||
int has_old = 0;
|
||||
int vcount = cJSON_GetArraySize(values);
|
||||
for (int j = 0; j < vcount; j++) {
|
||||
const char *v = cJSON_GetStringValue(cJSON_GetArrayItem(values, j));
|
||||
if (v && strcmp(v, old_ip) == 0) { has_old = 1; break; }
|
||||
}
|
||||
if (!has_old) continue;
|
||||
|
||||
cJSON *name_item = cJSON_GetObjectItem(rec, "rrset_name");
|
||||
const char *rec_name = cJSON_IsString(name_item) ? name_item->valuestring : "@";
|
||||
|
||||
int pret = patch_record(domain, rec_name, old_ip, new_ip);
|
||||
if (pret == 0) {
|
||||
log_info("gandi: [%s] updated record \"%s\" A: %s -> %s",
|
||||
domain, rec_name, old_ip, new_ip);
|
||||
matched++;
|
||||
} else {
|
||||
log_error("gandi: [%s] failed to patch record \"%s\" (err %d)",
|
||||
domain, rec_name, pret);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (matched == 0 && failed == 0) ret = -3;
|
||||
else if (failed > 0) ret = -1;
|
||||
else ret = 0;
|
||||
|
||||
cleanup:
|
||||
curl_slist_free_all(headers);
|
||||
curl_easy_cleanup(curl);
|
||||
free(buf.buf);
|
||||
cJSON_Delete(records);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int gandi_update_record(
|
||||
const char *domain,
|
||||
const char *record_name,
|
||||
const char *old_ip,
|
||||
const char *new_ip
|
||||
) {
|
||||
int ret = patch_record(domain, record_name, old_ip, new_ip);
|
||||
if (ret == 0)
|
||||
log_info("gandi: [%s] updated record \"%s\" A: %s -> %s",
|
||||
domain, record_name, old_ip, new_ip);
|
||||
else
|
||||
log_error("gandi: [%s] failed to patch record \"%s\" (err %d)",
|
||||
domain, record_name, ret);
|
||||
return ret;
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
#ifndef GANDI_H
|
||||
#define GANDI_H
|
||||
|
||||
#define GANDI_MAX_IP_LEN 46
|
||||
|
||||
/*
|
||||
* Stores the API key internally.
|
||||
* Must be called once at startup before calling other gandi methods.
|
||||
*
|
||||
* Returns 0 on success.
|
||||
* Returns -1 if api_key is NULL or empty.
|
||||
*/
|
||||
int gandi_init(const char *api_key);
|
||||
|
||||
/*
|
||||
* Fetches the current IPv4 addr from the special gandi fqdn
|
||||
*
|
||||
* Returns 0 on success.
|
||||
* Returns -1 on network or curl error.
|
||||
* Returns -2 on API auth error (401/403).
|
||||
* Returns -3 if the record was not found or has no values.
|
||||
*/
|
||||
int gandi_get_self_ip(const char *domain, const char *record_name, char *out_ip);
|
||||
|
||||
/*
|
||||
* Fetches all records of all domains,
|
||||
* looks for all A records where rrset_value
|
||||
* is set to `old_ip` and patches each one to `new_ip`.
|
||||
*
|
||||
* Returns 0 on success (all records updated).
|
||||
* Returns -1 on network or curl error, or if any patch failed.
|
||||
* Returns -2 on API auth error (401/403).
|
||||
* Returns -3 if no A records had old_ip.
|
||||
*/
|
||||
int gandi_update_domains(const char *fqdn, const char *old_ip, const char *new_ip);
|
||||
|
||||
/*
|
||||
* Patches an A record's rrset_value from `old_ip` to `new_ip`.
|
||||
*
|
||||
* Returns 0 on success.
|
||||
* Returns -1 on network or curl error.
|
||||
* Returns -2 on API auth error (401/403).
|
||||
*/
|
||||
int gandi_update_record(const char *domain, const char *record_name,
|
||||
const char *old_ip, const char *new_ip);
|
||||
|
||||
#endif /* GANDI_H */
|
||||
@@ -0,0 +1,87 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <curl/curl.h>
|
||||
#include "ipcheck.h"
|
||||
#include "log.h"
|
||||
|
||||
static const char *SERVICES[] = {
|
||||
"https://ipv4.icanhazip.com",
|
||||
"https://api4.ipify.org",
|
||||
"https://ipv4.wtfismyip.com/text",
|
||||
"https://v4.ident.me",
|
||||
"https://ipv4.am.i.mullvad.net/ip",
|
||||
"https://4.ident.me",
|
||||
"https://4.tnedi.me",
|
||||
};
|
||||
|
||||
#define SERVICE_COUNT (int)(sizeof(SERVICES) / sizeof(SERVICES[0]))
|
||||
|
||||
typedef struct {
|
||||
char buf[IPCHECK_MAX_IP_LEN];
|
||||
size_t len;
|
||||
} IPBuf;
|
||||
|
||||
static size_t write_cb(void *data, size_t size, size_t nmemb, void *userp) {
|
||||
IPBuf *out = (IPBuf *)userp;
|
||||
size_t total = size * nmemb;
|
||||
if (out->len + total >= IPCHECK_MAX_IP_LEN) return 0;
|
||||
memcpy(out->buf + out->len, data, total);
|
||||
out->len += total;
|
||||
return total;
|
||||
}
|
||||
|
||||
int ipcheck_get_public_ip(char *out_ip) {
|
||||
char results[SERVICE_COUNT][IPCHECK_MAX_IP_LEN];
|
||||
int valid = 0;
|
||||
|
||||
CURL *curl = curl_easy_init();
|
||||
if (!curl) return -1;
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT, IPCHECK_TIMEOUT_SECS);
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
|
||||
for (int i = 0; i < SERVICE_COUNT; i++) {
|
||||
IPBuf buf = { .buf = {0}, .len = 0 };
|
||||
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf);
|
||||
curl_easy_setopt(curl, CURLOPT_URL, SERVICES[i]);
|
||||
|
||||
CURLcode res = curl_easy_perform(curl);
|
||||
|
||||
if (res != CURLE_OK) continue;
|
||||
|
||||
long http_code = 0;
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||
if (http_code != 200) continue;
|
||||
|
||||
buf.buf[buf.len] = '\0';
|
||||
if (sscanf(buf.buf, IPCHECK_IP_SCAN_FMT, results[valid]) != 1) continue;
|
||||
valid++;
|
||||
}
|
||||
|
||||
curl_easy_cleanup(curl);
|
||||
|
||||
if (valid < IPCHECK_MIN_RESPONDERS) {
|
||||
log_warn("ipcheck: only %d/%d services responded, need at least %d",
|
||||
valid, SERVICE_COUNT, IPCHECK_MIN_RESPONDERS);
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < valid; i++) {
|
||||
int count = 0;
|
||||
for (int j = 0; j < valid; j++) {
|
||||
if (strcmp(results[i], results[j]) == 0) count++;
|
||||
}
|
||||
if (count * 2 > valid) {
|
||||
strncpy(out_ip, results[i], IPCHECK_MAX_IP_LEN - 1);
|
||||
out_ip[IPCHECK_MAX_IP_LEN - 1] = '\0';
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
log_warn("ipcheck: no strict majority among %d/%d responders",
|
||||
valid, SERVICE_COUNT);
|
||||
return -2;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef IPCHECK_H
|
||||
#define IPCHECK_H
|
||||
|
||||
#define IPCHECK_MIN_RESPONDERS 3
|
||||
#define IPCHECK_TIMEOUT_SECS 10L
|
||||
#define IPCHECK_MAX_IP_LEN 46
|
||||
#define IPCHECK_IP_SCAN_FMT "%45s"
|
||||
|
||||
/*
|
||||
* Tries to get the public IP by seeing if a strict majority
|
||||
* of services agree. At least a minimum amount of services
|
||||
* need to respond.
|
||||
*
|
||||
* Returns 0 on success.
|
||||
* Returns -1 if fewer than IPCHECK_MIN_RESPONDERS answered.
|
||||
* Returns -2 if no strict majority was reached (tie or split).
|
||||
*/
|
||||
int ipcheck_get_public_ip(char *out_ip);
|
||||
|
||||
#endif /* IPCHECK_H */
|
||||
@@ -0,0 +1,43 @@
|
||||
#include "log.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <time.h>
|
||||
|
||||
void log_init(void) {
|
||||
setvbuf(stdout, NULL, _IOLBF, 0);
|
||||
}
|
||||
|
||||
static void ts(char *out, size_t out_sz) {
|
||||
time_t now = time(NULL);
|
||||
struct tm tm;
|
||||
localtime_r(&now, &tm);
|
||||
strftime(out, out_sz, "%Y-%m-%d %H:%M:%S", &tm);
|
||||
}
|
||||
|
||||
static void vlog(FILE *f, const char *level, const char *fmt, va_list ap) {
|
||||
char t[32];
|
||||
ts(t, sizeof(t));
|
||||
fprintf(f, "%s %s: ", t, level);
|
||||
vfprintf(f, fmt, ap);
|
||||
fputc('\n', f);
|
||||
fflush(f);
|
||||
}
|
||||
|
||||
void log_info(const char *fmt, ...) {
|
||||
va_list ap; va_start(ap, fmt);
|
||||
vlog(stdout, "INFO", fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
void log_warn(const char *fmt, ...) {
|
||||
va_list ap; va_start(ap, fmt);
|
||||
vlog(stderr, "WARN", fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
void log_error(const char *fmt, ...) {
|
||||
va_list ap; va_start(ap, fmt);
|
||||
vlog(stderr, "ERROR", fmt, ap);
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#ifndef LOG_H
|
||||
#define LOG_H
|
||||
|
||||
void log_init(void);
|
||||
|
||||
void log_info(const char *fmt, ...);
|
||||
void log_warn(const char *fmt, ...);
|
||||
void log_error(const char *fmt, ...);
|
||||
|
||||
#endif
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
#include <unistd.h>
|
||||
#include <curl/curl.h>
|
||||
#include "config.h"
|
||||
#include "gandi.h"
|
||||
#include "ipcheck.h"
|
||||
#include "log.h"
|
||||
#include "selfcheck.h"
|
||||
|
||||
static volatile sig_atomic_t g_running = 1;
|
||||
|
||||
static void handle_signal(int sig) {
|
||||
(void)sig;
|
||||
g_running = 0;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
struct sigaction sa = {0};
|
||||
sa.sa_handler = handle_signal;
|
||||
sigaction(SIGINT, &sa, NULL);
|
||||
sigaction(SIGTERM, &sa, NULL);
|
||||
|
||||
Config cfg = {0};
|
||||
if (config_load(&cfg) != 0) return EXIT_FAILURE;
|
||||
|
||||
log_init();
|
||||
curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
|
||||
int ret = EXIT_SUCCESS;
|
||||
|
||||
if (gandi_init(cfg.api_token) != 0) {
|
||||
log_error("gandi_init failed");
|
||||
ret = EXIT_FAILURE;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
char cached_ip[GANDI_MAX_IP_LEN] = {0};
|
||||
if (gandi_get_self_ip(cfg.dns_keeper_domain, cfg.dns_keeper_record, cached_ip) != 0) {
|
||||
log_error("Failed to fetch initial IP from self record %s.%s",
|
||||
cfg.dns_keeper_record, cfg.dns_keeper_domain);
|
||||
ret = EXIT_FAILURE;
|
||||
goto cleanup;
|
||||
}
|
||||
log_info("Self record points to %s", cached_ip);
|
||||
|
||||
char self_fqdn[256];
|
||||
snprintf(self_fqdn, sizeof(self_fqdn), "%s.%s", cfg.dns_keeper_record, cfg.dns_keeper_domain);
|
||||
|
||||
if (selfcheck_start_listener(cfg.dns_keeper_port) != 0) {
|
||||
log_error("Failed to start self-check listener on port %d", cfg.dns_keeper_port);
|
||||
ret = EXIT_FAILURE;
|
||||
goto cleanup;
|
||||
}
|
||||
log_info("Listening on UDP port %d", cfg.dns_keeper_port);
|
||||
|
||||
int fail_count = 0; /* consecutive self-check failures */
|
||||
int drift_count = 0; /* when external IP matches cache but self-check fails */
|
||||
|
||||
while (g_running) {
|
||||
if (selfcheck_domain(self_fqdn, cfg.dns_keeper_port) == 0) {
|
||||
fail_count = 0;
|
||||
drift_count = 0;
|
||||
sleep(cfg.poll_interval);
|
||||
continue;
|
||||
}
|
||||
|
||||
fail_count++;
|
||||
if (fail_count < SELFCHECK_FAIL_THRESHOLD) {
|
||||
sleep(cfg.poll_interval);
|
||||
continue;
|
||||
}
|
||||
|
||||
fail_count = 0;
|
||||
char public_ip[IPCHECK_MAX_IP_LEN] = {0};
|
||||
if (ipcheck_get_public_ip(public_ip) != 0) {
|
||||
log_warn("ipcheck: failed to determine public IP, skipping cycle");
|
||||
sleep(cfg.poll_interval);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strcmp(public_ip, cached_ip) == 0) {
|
||||
drift_count++;
|
||||
if (drift_count >= SELFCHECK_DRIFT_THRESHOLD) {
|
||||
char gandi_ip[GANDI_MAX_IP_LEN] = {0};
|
||||
if (gandi_get_self_ip(cfg.dns_keeper_domain, cfg.dns_keeper_record, gandi_ip) == 0
|
||||
&& strcmp(gandi_ip, cached_ip) != 0) {
|
||||
log_info("Cache drift: cached %s but Gandi has %s, resyncing",
|
||||
cached_ip, gandi_ip);
|
||||
strncpy(cached_ip, gandi_ip, GANDI_MAX_IP_LEN - 1);
|
||||
cached_ip[GANDI_MAX_IP_LEN - 1] = '\0';
|
||||
}
|
||||
drift_count = 0;
|
||||
}
|
||||
sleep(SELFCHECK_COOLDOWN_S);
|
||||
continue;
|
||||
}
|
||||
|
||||
drift_count = 0;
|
||||
log_info("IP change detected: %s -> %s", cached_ip, public_ip);
|
||||
|
||||
if (selfcheck_ip_str(public_ip, cfg.dns_keeper_port) != 0) {
|
||||
log_warn("Direct check of %s failed, deferring update", public_ip);
|
||||
sleep(SELFCHECK_COOLDOWN_S);
|
||||
continue;
|
||||
}
|
||||
|
||||
int failed_updates = 0;
|
||||
int self_domain_in_list = 0;
|
||||
for (int i = 0; i < cfg.domain_count; i++) {
|
||||
if (strcmp(cfg.domains[i], cfg.dns_keeper_domain) == 0)
|
||||
self_domain_in_list = 1;
|
||||
|
||||
int gret = gandi_update_domains(cfg.domains[i], cached_ip, public_ip);
|
||||
if (gret == -3) {
|
||||
log_info("gandi: [%s] no matching A records for %s, skipping",
|
||||
cfg.domains[i], cached_ip);
|
||||
} else if (gret != 0) {
|
||||
failed_updates = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!self_domain_in_list) {
|
||||
int sret = gandi_update_record(cfg.dns_keeper_domain, cfg.dns_keeper_record,
|
||||
cached_ip, public_ip);
|
||||
if (sret != 0) {
|
||||
log_error("Failed to update self record, cached IP not advanced");
|
||||
failed_updates = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!failed_updates) {
|
||||
strncpy(cached_ip, public_ip, GANDI_MAX_IP_LEN - 1);
|
||||
cached_ip[GANDI_MAX_IP_LEN - 1] = '\0';
|
||||
}
|
||||
|
||||
if (failed_updates) log_error("One or more updates failed this cycle");
|
||||
|
||||
sleep(SELFCHECK_COOLDOWN_S);
|
||||
}
|
||||
|
||||
log_info("Shutting down");
|
||||
cleanup:
|
||||
curl_global_cleanup();
|
||||
config_free(&cfg);
|
||||
return ret;
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <stdint.h>
|
||||
#include <pthread.h>
|
||||
#include <sys/socket.h>
|
||||
#include <poll.h>
|
||||
#include <sys/random.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <netdb.h>
|
||||
#include "log.h"
|
||||
#include "selfcheck.h"
|
||||
|
||||
static uint8_t s_challenge[SELFCHECK_CHALLENGE_LEN];
|
||||
static pthread_mutex_t s_challenge_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
static int rand_bytes(uint8_t *buf, size_t len) {
|
||||
return (getrandom(buf, len, 0) == (ssize_t)len) ? 0 : -1;
|
||||
}
|
||||
|
||||
static void *listener_thread(void *arg) {
|
||||
int port = *(int *)arg;
|
||||
free(arg);
|
||||
|
||||
int fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (fd < 0) { log_error("selfcheck listener: socket %s", strerror(errno)); return NULL; }
|
||||
|
||||
int opt = 1;
|
||||
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||
|
||||
struct sockaddr_in addr = {0};
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons((uint16_t)port);
|
||||
addr.sin_addr.s_addr = INADDR_ANY;
|
||||
|
||||
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||
log_error("selfcheck listener: bind %s", strerror(errno));
|
||||
close(fd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
uint8_t discard[256];
|
||||
struct sockaddr_in sender;
|
||||
socklen_t sender_len;
|
||||
|
||||
for (;;) {
|
||||
sender_len = sizeof(sender);
|
||||
ssize_t n = recvfrom(fd, discard, sizeof(discard), 0,
|
||||
(struct sockaddr *)&sender, &sender_len);
|
||||
if (n < 0) continue;
|
||||
|
||||
uint8_t response[SELFCHECK_CHALLENGE_LEN];
|
||||
pthread_mutex_lock(&s_challenge_mutex);
|
||||
memcpy(response, s_challenge, SELFCHECK_CHALLENGE_LEN);
|
||||
pthread_mutex_unlock(&s_challenge_mutex);
|
||||
|
||||
sendto(fd, response, SELFCHECK_CHALLENGE_LEN, 0,
|
||||
(struct sockaddr *)&sender, sender_len);
|
||||
}
|
||||
|
||||
close(fd);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int check_ip(struct in_addr addr, int port) {
|
||||
uint8_t challenge[SELFCHECK_CHALLENGE_LEN];
|
||||
if (rand_bytes(challenge, SELFCHECK_CHALLENGE_LEN) != 0) return -1;
|
||||
pthread_mutex_lock(&s_challenge_mutex);
|
||||
memcpy(s_challenge, challenge, SELFCHECK_CHALLENGE_LEN);
|
||||
pthread_mutex_unlock(&s_challenge_mutex);
|
||||
|
||||
int fd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
|
||||
struct sockaddr_in dst = {0};
|
||||
dst.sin_family = AF_INET;
|
||||
dst.sin_port = htons((uint16_t)port);
|
||||
dst.sin_addr = addr;
|
||||
|
||||
uint8_t payload = 0;
|
||||
if (sendto(fd, &payload, sizeof(payload), 0,
|
||||
(struct sockaddr *)&dst, sizeof(dst)) < 0) {
|
||||
close(fd); return -1;
|
||||
}
|
||||
|
||||
struct pollfd pfd = { .fd = fd, .events = POLLIN };
|
||||
if (poll(&pfd, 1, SELFCHECK_RECV_TIMEOUT_S * 1000) <= 0) { close(fd); return -1; }
|
||||
|
||||
uint8_t reply[SELFCHECK_CHALLENGE_LEN + 1];
|
||||
ssize_t n = recvfrom(fd, reply, sizeof(reply), 0, NULL, NULL);
|
||||
close(fd);
|
||||
|
||||
if (n != SELFCHECK_CHALLENGE_LEN) return -2;
|
||||
return (memcmp(challenge, reply, SELFCHECK_CHALLENGE_LEN) == 0) ? 0 : -2;
|
||||
}
|
||||
|
||||
int selfcheck_start_listener(int port) {
|
||||
if (rand_bytes(s_challenge, SELFCHECK_CHALLENGE_LEN) != 0) {
|
||||
log_error("selfcheck: failed get random bytes for challenge");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int *pport = malloc(sizeof(int));
|
||||
if (!pport) return -1;
|
||||
*pport = port;
|
||||
|
||||
pthread_t tid;
|
||||
pthread_attr_t attr;
|
||||
pthread_attr_init(&attr);
|
||||
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
|
||||
int r = pthread_create(&tid, &attr, listener_thread, pport);
|
||||
pthread_attr_destroy(&attr);
|
||||
if (r != 0) { free(pport); return -1; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
int selfcheck_domain(const char *domain, int port) {
|
||||
struct addrinfo hints = {0}, *res = NULL;
|
||||
hints.ai_family = AF_INET;
|
||||
hints.ai_socktype = SOCK_DGRAM;
|
||||
|
||||
if (getaddrinfo(domain, NULL, &hints, &res) != 0 || !res) {
|
||||
if (res) freeaddrinfo(res);
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct in_addr addr = ((struct sockaddr_in *)res->ai_addr)->sin_addr;
|
||||
freeaddrinfo(res);
|
||||
|
||||
return check_ip(addr, port);
|
||||
}
|
||||
|
||||
int selfcheck_ip_str(const char *ip, int port) {
|
||||
struct in_addr addr;
|
||||
if (inet_pton(AF_INET, ip, &addr) != 1) return -1;
|
||||
return check_ip(addr, port);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
#ifndef SELFCHECK_H
|
||||
#define SELFCHECK_H
|
||||
|
||||
#define SELFCHECK_CHALLENGE_LEN 16
|
||||
#define SELFCHECK_RECV_TIMEOUT_S 2
|
||||
#define SELFCHECK_FAIL_THRESHOLD 3
|
||||
#define SELFCHECK_DRIFT_THRESHOLD 10
|
||||
#define SELFCHECK_COOLDOWN_S 60
|
||||
|
||||
/*
|
||||
* Init challenge buffer and start UDP socket listener thread.
|
||||
* The socket discards any datagram and always
|
||||
* responsds with the challenge bytes.
|
||||
*
|
||||
* Returns 0 on success.
|
||||
* Returns -1 on failure.
|
||||
*/
|
||||
int selfcheck_start_listener(int port);
|
||||
|
||||
/*
|
||||
* Checks if the random challenge bytes are received
|
||||
* when sending an empty datagram to the domain and port.
|
||||
*
|
||||
* Returns 0 if the response matches.
|
||||
* Returns -1 on DNS failure, send failure, or timeout.
|
||||
* Returns -2 if the response did not match.
|
||||
*/
|
||||
int selfcheck_domain(const char *domain, int port);
|
||||
|
||||
/*
|
||||
* Same as selfcheck_domain but sends directly to `ip` without DNS.
|
||||
*
|
||||
* Returns 0 if the response matches.
|
||||
* Returns -1 on send failure or timeout.
|
||||
* Returns -2 if the response did not match.
|
||||
*/
|
||||
int selfcheck_ip_str(const char *ip, int port);
|
||||
|
||||
#endif /* SELFCHECK_H */
|
||||
Reference in New Issue
Block a user