Compare commits
13 Commits
c419bb38e9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c1a3b96a1d | |||
| 9d0e005ee5 | |||
| 066c991ab1 | |||
| 0afbc8ccf9 | |||
| 6613831f7a | |||
| f19a29dd93 | |||
| 42ef38f147 | |||
| 9fb9430319 | |||
| 8b60d3e280 | |||
| 57a9b9a0ca | |||
| fcb9d0e3b0 | |||
| f61baf53c8 | |||
| 3570eb02ea |
@@ -52,3 +52,6 @@ Module.symvers
|
|||||||
Mkfile.old
|
Mkfile.old
|
||||||
dkms.conf
|
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)
|
all: $(BUILD_DIR)/$(TARGET)
|
||||||
|
|
||||||
$(BUILD_DIR)/$(TARGET): $(OBJS)
|
$(BUILD_DIR)/$(TARGET): $(OBJS)
|
||||||
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
|
||||||
|
|
||||||
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
|
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR)
|
||||||
$(CC) $(CFLAGS) -c $< -o $@
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
$(BUILD_DIR):
|
$(BUILD_DIR):
|
||||||
mkdir -p $(BUILD_DIR)
|
mkdir -p $(BUILD_DIR)
|
||||||
|
|
||||||
clean:
|
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.
|
||||||
@@ -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 */
|
||||||
+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