udyfi

Small client for dy.fi (and possibly other services) DNS renewals
git clone https://git.inz.fi/udyfi/
Log | Files | Refs

commit 96d411cb6794581dc06b32f05bf8dbc873390e2a
Author: Santtu Lakkala <inz@inz.fi>
Date:   Wed,  8 Apr 2020 10:31:15 +0300

Initial import

Diffstat:
AMakefile | 18++++++++++++++++++
Audyfi.c | 695+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 713 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,18 @@ +CFLAGS ?= -Os +CFLAGS += -W -Wall -std=c99 -pedantic -D_POSIX_C_SOURCE=200809L +PREFIX ?= /usr/local +BINDIR := $(PREFIX)/bin +IFADDRS_CFLAGS ?= -DUSE_IFADDRS +OPENSSL_CFLAGS += $(shell pkg-config openssl --cflags && echo -DUSE_OPENSSL) +LIBS := $(shell pkg-config openssl --libs) +all: udyfi + +udyfi: udyfi.c + $(CROSS)$(CC) $(CFLAGS) $(OPENSSL_CFLAGS) $(IFADDRS_CFLAGS) $< -o $@ $(LIBS) + +clean: + rm -f udyfi + +install: udyfi + install -d $(DESTDIR)$(BINDIR) + install udyfi $(DESTDIR)$(BINDIR) diff --git a/udyfi.c b/udyfi.c @@ -0,0 +1,695 @@ +/* + * MIT License + * + * Copyright (c) 2016-2020 Santtu Lakkala + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR + * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ +#include <sys/socket.h> +#include <fcntl.h> +#include <unistd.h> +#include <netdb.h> +#include <string.h> +#include <stdio.h> +#include <errno.h> +#include <stdint.h> +#include <stdlib.h> +#include <stdbool.h> +#ifdef USE_IFADDRS +#include <ifaddrs.h> +#define __USE_MISC +#include <net/if.h> +#endif +#include <time.h> +#ifdef USE_OPENSSL +#include <openssl/ssl.h> +#endif + +static const time_t minimum_refresh = 5 * 24 * 60 * 60; +static const time_t minimum_refresh_rand = 10 * 60 * 60; + +static const time_t check_interval = 4 * 60; +static const time_t check_interval_rand = 2 * 60; + +static const char *checkip_service = "checkip.dy.fi"; +static const char *checkip_port = "80"; + +static const char *dyndns_service = "dy.fi"; +static const char *dyndns_port = "80"; + +static ssize_t http_get(const char *server, + const char *port, + const char *path, + const char *headers, + const char *iface, + int af, + char *buffer, + size_t bufsize +#ifdef USE_OPENSSL + , SSL_CTX *ctx +#endif + ) +{ + struct addrinfo *res; + struct addrinfo *i; + struct addrinfo *k; + struct addrinfo hints = { .ai_family = af }; + struct addrinfo *local = NULL; +#ifdef USE_IFADDRS + struct ifaddrs *addrs = NULL; + struct ifaddrs *j; +#endif + int r; + int s; + int sock; +#ifdef USE_OPENSSL + SSL *ssl = NULL; +#endif + + if (iface) { +#if USE_IFADDRS + bool if_found = false; + if ((r = getifaddrs(&addrs))) + return -1; + for (j = addrs; j; j = j->ifa_next) { + if (!strcmp(j->ifa_name, iface)) { + if_found = true; + if (((j->ifa_flags & (IFF_UP | + IFF_BROADCAST | + IFF_LOOPBACK)) == + (IFF_UP | IFF_BROADCAST))) + break; + } + } + if (if_found && !j) { + freeifaddrs(addrs); + return -1; + } + + if (!if_found) { + freeifaddrs(addrs); + addrs = NULL; + +#endif + r = getaddrinfo(iface, NULL, &hints, &local); + if (r < 0) + return -1; +#if USE_IFADDRS + } +#endif + } + + if ((r = getaddrinfo(server, port, &hints, &res))) { +#if USE_IFADDRS + if (addrs) + freeifaddrs(addrs); +#endif + if (local) + freeaddrinfo(local); + return -1; + } + + for (i = res; i; i = i->ai_next) { + sock = socket(i->ai_family, i->ai_socktype, i->ai_protocol); + +#if USE_IFADDRS + if (addrs) { + for (j = addrs; j; j = j->ifa_next) { + if (!strcmp(j->ifa_name, iface) && + (((j->ifa_flags & (IFF_UP | + IFF_BROADCAST | + IFF_LOOPBACK)) == + (IFF_UP | IFF_BROADCAST))) && + j->ifa_addr->sa_family == i->ai_family) + break; + } + if (j) { + if ((r = bind(sock, + j->ifa_addr, sizeof(struct sockaddr_storage)))) { + close(sock); + continue; + } + } + } else +#endif + if (local) { + for (k = local; k; k = k->ai_next) { + if (k->ai_family == i->ai_family && + !bind(sock, k->ai_addr, k->ai_addrlen)) + break; + } + if (!k) { + close(sock); + continue; + } + } + + if (!connect(sock, i->ai_addr, i->ai_addrlen)) + break; + close(sock); + } + + freeaddrinfo(res); +#if USE_IFADDRS + if (addrs) + freeifaddrs(addrs); +#endif + if (local) + freeaddrinfo(local); + + if (!i) + return -1; + + if ((r = snprintf(buffer, bufsize, + "GET %s HTTP/1.0\r\nHost: %s\r\n%s\n", + path, server, headers ? headers : "")) >= (int)bufsize) { + close(sock); + return -1; + } + +#ifdef USE_OPENSSL + if (ctx) { + ssl = SSL_new(ctx); + + if (!ssl) { + close(sock); + return -1; + } + SSL_set_fd(ssl, sock); + SSL_set_tlsext_host_name(ssl, server); + SSL_connect(ssl); + } + + if (ssl) { + if ((s = SSL_write(ssl, buffer, r)) < r) { + SSL_free(ssl); + close(sock); + return -1; + } + } else +#endif + if ((s = write(sock, buffer, r)) < r) { + close(sock); + return -1; + } + + s = 0; + do { +#ifdef USE_OPENSSL + if (ssl) + r = SSL_read(ssl, buffer + s, bufsize - s - 1); + else +#endif + r = read(sock, buffer + s, bufsize - s - 1); + if (r < 0) { +#ifdef USE_OPENSSL + if (ssl) + SSL_free(ssl); +#endif + close(sock); + return -1; + } + s += r; + } while (r && s < (int)bufsize); +#ifdef USE_OPENSSL + if (ssl) + SSL_free(ssl); +#endif + close(sock); + buffer[s] = '\0'; + + return s; +} + +static int check_ip(const char *server, + const char *port, + const char *iface, + int af, + char *ip_buffer, + size_t ip_size +#ifdef USE_OPENSSL + , SSL_CTX *ctx +#endif + ) +{ + char buffer[1024]; + const char *ip; + size_t iplen; + + if (http_get(server, port, "/", + NULL, + iface, + af, + buffer, + sizeof(buffer) +#ifdef USE_OPENSSL + , ctx +#endif + ) < 0) + return -1; + + printf("%s\n", buffer); + ip = strstr(buffer, "Current IP Address: "); + if (!ip) + return -1; + ip += sizeof("Current IP Address: ") - 1; + + iplen = strspn(ip, "0123456789."); + if (iplen > ip_size) + return -1; + sprintf(ip_buffer, "%.*s", (int)iplen, ip); + + return 0; +} + +static int auth_token(const char *user, + const char *password, + char *buffer, + size_t b_len) +{ + static const char *base64 = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + size_t ulen = strlen(user); + size_t plen = strlen(password); + size_t i; + size_t bits = 0; + uint32_t d = 0; + char *wp = buffer; + + if ((ulen + plen + 3) / 3 * 4 >= b_len) + return -1; + + for (i = 0; i < ulen; i++) { + d = (d << 8) | user[i]; + bits += 8; + while (bits >= 6) + *wp++ = base64[(d >> (bits -= 6)) & 0x3f]; + } + d = (d << 8) | ':'; + bits += 8; + for (i = 0; i < plen; i++) { + d = (d << 8) | password[i]; + bits += 8; + while (bits >= 6) + *wp++ = base64[(d >> (bits -= 6)) & 0x3f]; + } + + if (bits) + *wp++ = base64[(d << (6 - bits)) & 0x3f]; + while ((wp - buffer) & 3) + *wp++ = '='; + *wp = '\0'; + + return wp - buffer; +} + +static const char *find_body(const char *response, + size_t len, size_t *blen) +{ + size_t i; + + for (i = 0; i < len - 1; i++) { + if (response[i] == '\n') { + if (response[i + 1] == '\n') { + *blen = len - i - 2; + return response + i + 2; + } else if (i < len - 2 && response[i + 1] == '\r' && + response[i + 2] == '\n') { + *blen = len - i - 3; + return response + i + 3; + } + } + } + + return NULL; +} + +enum update_status { + UP_BADAUTH, + UP_NOHOST, + UP_NOTFQDN, + UP_BADIP, + UP_NOCHG, + UP_GOOD, + UP_DNSERR, + UP_ABUSE, + UP_UNKNOWN, + UP_INTERNAL, + UP_NETWORK +}; + +static const char *statusstr[] = { + "badauth", + "nohost", + "notfqdn", + "badip ", + "nochg", + "good ", + "dnserr", + "abuse" +}; + +static int update_ip(const char *server, + const char *port, + const char *hosts, + const char *username, + const char *password, + const char *iface, + int af +#ifdef USE_OPENSSL + , SSL_CTX *ctx +#endif + ) +{ + char buffer[1024]; + char auth_buffer[1024]; + char path_buffer[1024]; + int pos; + int r; + size_t bl; + const char *body; + + pos = snprintf(auth_buffer, sizeof(auth_buffer), + "Authorization: Basic "); + if (pos >= (int)sizeof(auth_buffer)) + return UP_INTERNAL; + if ((r = auth_token(username, password, + auth_buffer + pos, sizeof(auth_buffer) - pos)) < 0) + return UP_INTERNAL; + pos += r; + pos += snprintf(auth_buffer + pos, sizeof(auth_buffer) - pos, + "\r\n"); + if (pos >= (int)sizeof(auth_buffer)) + return UP_INTERNAL; + + pos = snprintf(path_buffer, sizeof(path_buffer), + "/nic/update?hostname=%s", hosts); + if (pos >= (int)sizeof(path_buffer)) + return UP_INTERNAL; + + if ((r = http_get(server, port, path_buffer, + auth_buffer, iface, af, + buffer, sizeof(buffer) +#ifdef USE_OPENSSL + , ctx +#endif + )) < 0) + return UP_NETWORK; + + if ((body = find_body(buffer, r, + &bl))) { + for (r = 0; r < UP_UNKNOWN; r++) { + if (!strncmp(body, statusstr[r], + strlen(statusstr[r]))) + break; + } + } else { + r = UP_UNKNOWN; + } + + return r; +} + +static void usage(const char *argv0) { + fprintf(stderr, + "Usage: %s " + "-u <username> " + "-p <password> " + "[-i <" +#ifdef USE_IFADDRS + "interface/" +#endif + "local address>] " + "[-c <checkip server>] " + "[-C <checkip port>] " + "[-d <dyndns server>] " + "[-D <dyndns port>] " + "[-b] " + "[-P <pidfile>] " +#ifdef USE_OPENSSL + "[-s] " + "[-S] " +#endif + "[-6] " + "[-4] " + "host[,host...]\n", + argv0); +} + +static void write_pidfile(const char *pidfile, pid_t pid) +{ + FILE *pdf = fopen(pidfile, "w"); + if (pdf) { + fprintf(pdf, "%ld\n", (long)pid); + fclose(pdf); + } +} + +static void daemonize(const char *pidfile) { + pid_t pid; + if ((pid = fork())) { + if (pid < 0) + exit(EXIT_FAILURE); + if (pidfile) { + write_pidfile(pidfile, pid); + } + exit(EXIT_SUCCESS); + } + setsid(); + fclose(stdin); + fclose(stdout); +} + +int main(int argc, char **argv) +{ + const char *username = NULL; + const char *pidfile = NULL; + const char *iface = NULL; + char password[256] = ""; + int i; + time_t next_refresh = 0; + char ipbuf1[64]; + char ipbuf2[64] = ""; + char *ip = ipbuf1; + char *prev_ip = ipbuf2; + int background = 0; + int af = AF_UNSPEC; + FILE *log = stderr; + const char *logfile = NULL; + +#ifdef USE_OPENSSL + int port_set = 0; + int check_port_set = 0; + int ssl = 0; + int check_ssl = 0; + const SSL_METHOD *method; + SSL_CTX *ctx = NULL; +#endif + + while ((i = getopt(argc, argv, "u:p:c:C:d:D:bP:l:i:46" +#ifdef USE_OPENSSL + "sS" +#endif + )) != -1) { + switch (i) { + case 'i': + iface = optarg; + break; + case 'u': + username = optarg; + break; + case 'p': + snprintf(password, sizeof(password), + "%s", optarg); + memset(optarg, 0, strlen(optarg)); + break; + case 'c': + checkip_service = optarg; + break; + case '4': + af = AF_INET; + break; + case '6': + af = AF_INET6; + break; + case 'C': + checkip_port = optarg; +#ifdef USE_OPENSSL + check_port_set = 1; +#endif + break; + case 'd': + dyndns_service = optarg; + break; + case 'D': + dyndns_port = optarg; +#ifdef USE_OPENSSL + port_set = 1; +#endif + break; + case 'b': + background = 1; + break; + case 'P': + pidfile = optarg; + break; + case 'l': + logfile = optarg; + break; +#ifdef USE_OPENSSL + case 's': + ssl = 1; + break; + case 'S': + check_ssl = 1; + break; +#endif + default: + usage(argv[0]); + return EXIT_FAILURE; + } + } + +#ifdef USE_OPENSSL + if (ssl || check_ssl) { + SSL_library_init(); + OpenSSL_add_all_algorithms(); + method = SSLv23_client_method(); + ctx = SSL_CTX_new(method); + SSL_CTX_set_default_verify_paths(ctx); + + if (!ctx) { + fprintf(stderr, "SSL/TLS initialization failed.\n"); + return EXIT_FAILURE; + } + + if (ssl && !port_set) + dyndns_port = "443"; + if (check_ssl && !check_port_set) + checkip_port = "443"; + } +#endif + + if (optind >= argc || !username) { + usage(argv[0]); + return EXIT_FAILURE; + } + + srand(time(NULL)); + + if (logfile) + log = fopen(logfile, "a"); + + if (background) + daemonize(pidfile); + else if (pidfile) + write_pidfile(pidfile, getpid()); + + fprintf(log, "udyfi started\n"); + + for (;;) { + time_t now = time(NULL); + + if (check_ip(checkip_service, checkip_port, iface, af, + ip, sizeof(ipbuf1) +#ifdef USE_OPENSSL + , check_ssl ? ctx : NULL +#endif + ) >= 0) { + char *tmp; + + if ((strcmp(ip, prev_ip) || now > next_refresh)) { + switch (update_ip(dyndns_service, dyndns_port, + argv[optind], + username, password, iface, af +#ifdef USE_OPENSSL + , ssl ? ctx : NULL +#endif + )) { + case UP_BADAUTH: + fprintf(log, "FATAL: " + "Authentication error\n"); + return EXIT_FAILURE; + case UP_NOHOST: + fprintf(log, "FATAL: " + "No hostnames specified\n"); + return EXIT_FAILURE; + case UP_NOTFQDN: + fprintf(log, "FATAL: " + "Malformed hostnames\n"); + return EXIT_FAILURE; + case UP_BADIP: + fprintf(log, "WARNING: " + "Server rejected our IP\n"); + break; + case UP_NOCHG: + fprintf(log, "IP %s refreshed\n", + ip); + break; + case UP_GOOD: + fprintf(log, "%s now point to %s\n", + argv[optind], ip); + break; + case UP_DNSERR: + fprintf(log, "WARNING: " + "Temporary service error\n"); + break; + case UP_ABUSE: + fprintf(log, "FATAL: " + "We are flagged as abuse\n"); + return EXIT_FAILURE; + case UP_UNKNOWN: + fprintf(log, "WARNING: " + "Unknown status reply\n"); + break; + case UP_INTERNAL: + fprintf(log, "FATAL: " + "Internal error, likely run " + "out of buffer\n"); + return EXIT_FAILURE; + case UP_NETWORK: + fprintf(log, "WARNING: " + "Networking error\n"); + break; + } + + next_refresh = now + minimum_refresh + + rand() * minimum_refresh_rand / + RAND_MAX; + fprintf(log, "Next forced update at %s", + ctime(&next_refresh)); + fflush(log); + } + + tmp = ip; + ip = prev_ip; + prev_ip = tmp; + } else { + fprintf(log, "IP check failed.\n"); + fflush(log); + } + + sleep(check_interval + + rand() * check_interval_rand / RAND_MAX); + } + + return 0; +}