Implement updating of DuckDNS

This commit is contained in:
Ryan Harg 2024-11-29 13:54:55 +01:00
parent 552454cf6e
commit 115eeec47d
9 changed files with 128 additions and 39 deletions

View file

@ -1,7 +1,9 @@
# ddnsclient # ddnsclient
This application can be use to update dynamic hostnames. Currently it implements only the ddnss.de provider, but it This application can be use to update dynamic hostnames. Currently it implements the following DynDNS providers:
is extensible and more providers can be added easily.
- ddnss.de
- duckdns.org
This project uses Quarkus. This project uses Quarkus.

View file

@ -45,7 +45,7 @@ public class Updater {
} }
void run() { void run() {
log.trace("Updater running."); log.info("Updater running.");
if (config.isEmpty()) { if (config.isEmpty()) {
throw new IllegalStateException("Missing configuration"); throw new IllegalStateException("Missing configuration");
@ -58,7 +58,7 @@ public class Updater {
if (updateMap.containsKey(cfg.hostname()) if (updateMap.containsKey(cfg.hostname())
&& Duration.between(updateMap.get(cfg.hostname()), LocalDateTime.now()).toSeconds() < backoff.toSeconds()) { && Duration.between(updateMap.get(cfg.hostname()), LocalDateTime.now()).toSeconds() < backoff.toSeconds()) {
log.debug("Back-off period, skipping update."); log.info("Back-off period, skipping update.");
return; return;
} }
@ -73,7 +73,7 @@ public class Updater {
dynDns.update(cfg.hostname(), publicIps, new DyndnsAuth(null, null, cfg.token())); dynDns.update(cfg.hostname(), publicIps, new DyndnsAuth(null, null, cfg.token()));
updateMap.put(cfg.hostname(), LocalDateTime.now()); updateMap.put(cfg.hostname(), LocalDateTime.now());
} else { } else {
log.debugf("Hostname is up-to-date.", cfg.hostname()); log.infof("Hostname %s is up-to-date.", cfg.hostname());
} }
}); });
} }

View file

@ -6,9 +6,7 @@ import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient; import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.text.MessageFormat; import java.text.MessageFormat;
@ -29,26 +27,27 @@ public class Ddnss implements DynDns {
} }
public void update(String hostname, IPs currentIps, DyndnsAuth auth) { public void update(String hostname, IPs currentIps, DyndnsAuth auth) {
log.tracef("Updating ddnss hostname %s", hostname);
String updateUrl = MessageFormat.format("https://ddnss.de/upd.php?key={0}&host={1}&ip={2}&ip6={3}",
auth.token(), hostname, currentIps.v4(), currentIps.v6());
try (HttpClient http = httpClientFactory.create()) { try (HttpClient http = httpClientFactory.create()) {
String updateUrl = getUpdateUrl(hostname, currentIps, auth.token());
HttpRequest request = HttpRequest.newBuilder() HttpResponse<String> response = http.send(getRequest(updateUrl), HttpResponse.BodyHandlers.ofString());
.header("User-Agent", "ddns-client")
.GET()
.uri(URI.create(updateUrl))
.build();
HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) { if (response.statusCode() != 200) {
log.errorf("Couldn't update hostname %s." + hostname); log.errorf("Couldn't update hostname %s." + hostname);
} }
log.info("Hostname updated."); log.infof("Hostname %s updated.", hostname);
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
private String getUpdateUrl(String hostname, IPs currentIps, String token) {
if (currentIps.v4().isPresent() && currentIps.v6().isPresent()) {
return MessageFormat.format("https://ddnss.de/upd.php?key={0}&host={1}&ip={2}&ip6={3}", token, hostname, currentIps.v4().get(), currentIps.v6().get());
} else if (currentIps.v4().isPresent()) {
return MessageFormat.format("https://ddnss.de/upd.php?key={0}&host={1}&ip={2}", token, hostname, currentIps.v4());
}
throw new RuntimeException("No ips to update!");
}
} }

View file

@ -0,0 +1,56 @@
package de.rpr.ddnsclient.dyndns;
import de.rpr.ddnsclient.model.DyndnsAuth;
import de.rpr.ddnsclient.model.IPs;
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger;
import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpResponse;
import java.text.MessageFormat;
@ApplicationScoped
public class DuckDNS implements DynDns {
private static final Logger log = Logger.getLogger(DuckDNS.class);
private final HttpClientFactory httpClientFactory;
public DuckDNS(HttpClientFactory httpClientFactory) {
this.httpClientFactory = httpClientFactory;
}
@Override
public String name() {
return "duckdns";
}
@Override
public void update(String hostname, IPs currentIps, DyndnsAuth auth) {
log.tracef("Updating duckdns hostname %s", hostname);
try (HttpClient http = httpClientFactory.create()) {
String updateUrl = getUpdateUrl(hostname, currentIps, auth.token());
HttpResponse<String> response = http.send(getRequest(updateUrl), HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
log.errorf("Couldn't update hostname %s." + hostname);
} else if (!response.body().equals("OK")) {
throw new RuntimeException("Couldn't update hostname!");
}
log.infof("Hostname %s updated.", hostname);
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}
private String getUpdateUrl(String hostname, IPs currentIps, String token) {
if (currentIps.v4().isPresent() && currentIps.v6().isPresent()) {
return MessageFormat.format("https://www.duckdns.org/update?domains={0}&token={1}&ip={2}&ipv6={3}", hostname, token, currentIps.v4().get(), currentIps.v6().get());
} else if (currentIps.v4().isPresent()) {
return MessageFormat.format("https://www.duckdns.org/update?domains={0}&token={1}&ip={2}", hostname, token, currentIps.v4().get());
}
throw new RuntimeException("No ips to update!");
}
}

View file

@ -3,9 +3,20 @@ package de.rpr.ddnsclient.dyndns;
import de.rpr.ddnsclient.model.DyndnsAuth; import de.rpr.ddnsclient.model.DyndnsAuth;
import de.rpr.ddnsclient.model.IPs; import de.rpr.ddnsclient.model.IPs;
import java.net.URI;
import java.net.http.HttpRequest;
public interface DynDns { public interface DynDns {
String name(); String name();
void update(String hostname, IPs currentIps, DyndnsAuth auth); void update(String hostname, IPs currentIps, DyndnsAuth auth);
default HttpRequest getRequest(String updateUrl) {
return HttpRequest.newBuilder()
.header("User-Agent", "ddns-client")
.GET()
.uri(URI.create(updateUrl))
.build();
}
} }

View file

@ -3,6 +3,7 @@ package de.rpr.ddnsclient.lookup;
import de.rpr.ddnsclient.model.IPs; import de.rpr.ddnsclient.model.IPs;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import org.xbill.DNS.Record; import org.xbill.DNS.Record;
import org.xbill.DNS.*; import org.xbill.DNS.*;
@ -11,6 +12,8 @@ import java.util.Optional;
@ApplicationScoped @ApplicationScoped
public class DnsJava implements DnsResolver { public class DnsJava implements DnsResolver {
private static final Logger log = Logger.getLogger(DnsJava.class);
private final String resolverAddress; private final String resolverAddress;
public DnsJava(@ConfigProperty(name = "ddnsclient.dns.resolver", defaultValue = "9.9.9.9") String resolverAddress) { public DnsJava(@ConfigProperty(name = "ddnsclient.dns.resolver", defaultValue = "9.9.9.9") String resolverAddress) {
@ -19,19 +22,24 @@ public class DnsJava implements DnsResolver {
@Override @Override
public IPs resolve(String hostname) { public IPs resolve(String hostname) {
String v4 = getIp(hostname, Type.A).orElseThrow(() -> new RuntimeException("Missing A Record")); Optional<String> v4 = getIp(hostname, RecordType.A);
String v6 = getIp(hostname, Type.AAAA).orElseThrow(() -> new RuntimeException("Missing AAAA Record")); Optional<String> v6 = getIp(hostname, RecordType.AAAA);
return new IPs(v4, v6); return new IPs(v4, v6);
} }
private Optional<String> getIp(String hostname, int type) { private Optional<String> getIp(String hostname, RecordType type) {
try { try {
SimpleResolver resolver = new SimpleResolver(resolverAddress); SimpleResolver resolver = new SimpleResolver(resolverAddress);
Lookup lookup = new Lookup(hostname, type); Lookup lookup = new Lookup(hostname, type.value);
lookup.setResolver(resolver); lookup.setResolver(resolver);
Record[] records = lookup.run(); Record[] records = lookup.run();
if (records == null || records.length == 0) {
log.infof("No record for type %s found", type.name());
return Optional.empty();
}
for (Record record : records) { for (Record record : records) {
if (record instanceof ARecord) { if (record instanceof ARecord) {
return Optional.of(((ARecord) record).getAddress().getHostAddress()); return Optional.of(((ARecord) record).getAddress().getHostAddress());
@ -45,4 +53,15 @@ public class DnsJava implements DnsResolver {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
private enum RecordType {
A(1),
AAAA(28);
private final int value; // org.bill.DNS.Type.AAAA
RecordType(int value) {
this.value = value;
}
}
} }

View file

@ -20,10 +20,7 @@ public class PublicIpLookup {
private final CurlProcessFactory curlProcessFactory; private final CurlProcessFactory curlProcessFactory;
private final Optional<String> configuredProvider; private final Optional<String> configuredProvider;
PublicIpLookup( PublicIpLookup(CurlProcessFactory curlProcessFactory, @ConfigProperty(name = "ddnsclient.ip-provider") Optional<String> configuredProvider) {
CurlProcessFactory curlProcessFactory,
@ConfigProperty(name = "ddnsclient.ip-provider") Optional<String> configuredProvider
) {
this.curlProcessFactory = curlProcessFactory; this.curlProcessFactory = curlProcessFactory;
this.configuredProvider = configuredProvider; this.configuredProvider = configuredProvider;
} }
@ -33,10 +30,9 @@ public class PublicIpLookup {
void onStart(@Observes StartupEvent event) { void onStart(@Observes StartupEvent event) {
try { try {
configuredProvider.ifPresent(it -> { configuredProvider.ifPresent(it -> {
log.tracef("Setting ip lookup provider %s", it); log.tracef("Setting ip lookup provider %s", it);
provider = Optional.of(IpLookupProviders.get(it)); provider = Optional.of(IpLookupProviders.get(it));
} });
);
} catch (Exception e) { } catch (Exception e) {
throw new IllegalStateException("Unknow provider configured!", e); throw new IllegalStateException("Unknow provider configured!", e);
} }
@ -48,8 +44,8 @@ public class PublicIpLookup {
try { try {
String provider = this.provider.map(it -> it.url).orElseGet(() -> IpLookupProviders.random().url); String provider = this.provider.map(it -> it.url).orElseGet(() -> IpLookupProviders.random().url);
String v4 = getIp(provider, CurlProcessFactory.IpClass.V4); Optional<String> v4 = getIp(provider, CurlProcessFactory.IpClass.V4);
String v6 = getIp(provider, CurlProcessFactory.IpClass.V6); Optional<String> v6 = getIp(provider, CurlProcessFactory.IpClass.V6);
return new IPs(v4, v6); return new IPs(v4, v6);
} catch (Exception e) { } catch (Exception e) {
@ -57,7 +53,7 @@ public class PublicIpLookup {
} }
} }
private String getIp(String provider, CurlProcessFactory.IpClass ipClass) { private Optional<String> getIp(String provider, CurlProcessFactory.IpClass ipClass) {
try { try {
Process curlProcess = curlProcessFactory.create(provider, ipClass).start(); Process curlProcess = curlProcessFactory.create(provider, ipClass).start();
curlProcess.waitFor(); curlProcess.waitFor();
@ -65,9 +61,10 @@ public class PublicIpLookup {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String ip = reader.readLine(); String ip = reader.readLine();
log.tracef("Retrieved ip: %s", ip); log.tracef("Retrieved ip: %s", ip);
return ip; return Optional.of(ip);
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); log.errorf("Couldn't lookup ip. Provider: %s, ipClass: %s", provider, ipClass.name());
return Optional.empty();
} }
} }

View file

@ -1,6 +1,11 @@
package de.rpr.ddnsclient.model; package de.rpr.ddnsclient.model;
public record IPs(String v4, String v6) { import java.util.Optional;
public record IPs(Optional<String> v4, Optional<String> v6) {
public IPs(String v4, String v6) {
this(Optional.ofNullable(v4), Optional.ofNullable(v6));
}
} }

View file

@ -10,7 +10,7 @@ class DnsJavaTest {
@Test @Test
void test() { void test() {
IPs result = new DnsJava("9.9.9.9").resolve("example.com"); IPs result = new DnsJava("9.9.9.9").resolve("example.com");
assertThat(result.v4()).isNotBlank(); assertThat(result.v4()).isPresent();
assertThat(result.v6()).isNotBlank(); assertThat(result.v6()).isPresent();
} }
} }