From 3570eb02ea0859c0bf91019a908929b1d74b65fa Mon Sep 17 00:00:00 2001 From: FoxyHunter Date: Mon, 1 Jun 2026 01:31:35 +0200 Subject: [PATCH] Feat: fetch public ip against from multiple services --- src/ipcheck.c | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/ipcheck.h | 20 ++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 src/ipcheck.c create mode 100644 src/ipcheck.h diff --git a/src/ipcheck.c b/src/ipcheck.c new file mode 100644 index 0000000..b8ae298 --- /dev/null +++ b/src/ipcheck.c @@ -0,0 +1,89 @@ +#include +#include +#include +#include +#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_CAINFO, "/etc/ssl/certs/ca-certificates.crt"); + curl_easy_setopt(curl, CURLOPT_CAPATH, "/etc/ssl/certs"); + 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; +} diff --git a/src/ipcheck.h b/src/ipcheck.h new file mode 100644 index 0000000..573223e --- /dev/null +++ b/src/ipcheck.h @@ -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 */