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
This application can be use to update dynamic hostnames. Currently it implements only the ddnss.de provider, but it
is extensible and more providers can be added easily.
This application can be use to update dynamic hostnames. Currently it implements the following DynDNS providers:
- ddnss.de
- duckdns.org
This project uses Quarkus.

View file

@ -45,7 +45,7 @@ public class Updater {
}
void run() {
log.trace("Updater running.");
log.info("Updater running.");
if (config.isEmpty()) {
throw new IllegalStateException("Missing configuration");
@ -58,7 +58,7 @@ public class Updater {
if (updateMap.containsKey(cfg.hostname())
&& 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;
}
@ -73,7 +73,7 @@ public class Updater {
dynDns.update(cfg.hostname(), publicIps, new DyndnsAuth(null, null, cfg.token()));
updateMap.put(cfg.hostname(), LocalDateTime.now());
} 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 java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.text.MessageFormat;
@ -29,26 +27,27 @@ public class Ddnss implements DynDns {
}
public void update(String hostname, IPs currentIps, DyndnsAuth auth) {
String updateUrl = MessageFormat.format("https://ddnss.de/upd.php?key={0}&host={1}&ip={2}&ip6={3}",
auth.token(), hostname, currentIps.v4(), currentIps.v6());
log.tracef("Updating ddnss hostname %s", hostname);
try (HttpClient http = httpClientFactory.create()) {
HttpRequest request = HttpRequest.newBuilder()
.header("User-Agent", "ddns-client")
.GET()
.uri(URI.create(updateUrl))
.build();
HttpResponse<String> response = http.send(request, HttpResponse.BodyHandlers.ofString());
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);
}
log.info("Hostname updated.");
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://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.IPs;
import java.net.URI;
import java.net.http.HttpRequest;
public interface DynDns {
String name();
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 jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.jboss.logging.Logger;
import org.xbill.DNS.Record;
import org.xbill.DNS.*;
@ -11,6 +12,8 @@ import java.util.Optional;
@ApplicationScoped
public class DnsJava implements DnsResolver {
private static final Logger log = Logger.getLogger(DnsJava.class);
private final 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
public IPs resolve(String hostname) {
String v4 = getIp(hostname, Type.A).orElseThrow(() -> new RuntimeException("Missing A Record"));
String v6 = getIp(hostname, Type.AAAA).orElseThrow(() -> new RuntimeException("Missing AAAA Record"));
Optional<String> v4 = getIp(hostname, RecordType.A);
Optional<String> v6 = getIp(hostname, RecordType.AAAA);
return new IPs(v4, v6);
}
private Optional<String> getIp(String hostname, int type) {
private Optional<String> getIp(String hostname, RecordType type) {
try {
SimpleResolver resolver = new SimpleResolver(resolverAddress);
Lookup lookup = new Lookup(hostname, type);
Lookup lookup = new Lookup(hostname, type.value);
lookup.setResolver(resolver);
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) {
if (record instanceof ARecord) {
return Optional.of(((ARecord) record).getAddress().getHostAddress());
@ -45,4 +53,15 @@ public class DnsJava implements DnsResolver {
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 Optional<String> configuredProvider;
PublicIpLookup(
CurlProcessFactory curlProcessFactory,
@ConfigProperty(name = "ddnsclient.ip-provider") Optional<String> configuredProvider
) {
PublicIpLookup(CurlProcessFactory curlProcessFactory, @ConfigProperty(name = "ddnsclient.ip-provider") Optional<String> configuredProvider) {
this.curlProcessFactory = curlProcessFactory;
this.configuredProvider = configuredProvider;
}
@ -33,10 +30,9 @@ public class PublicIpLookup {
void onStart(@Observes StartupEvent event) {
try {
configuredProvider.ifPresent(it -> {
log.tracef("Setting ip lookup provider %s", it);
provider = Optional.of(IpLookupProviders.get(it));
}
);
log.tracef("Setting ip lookup provider %s", it);
provider = Optional.of(IpLookupProviders.get(it));
});
} catch (Exception e) {
throw new IllegalStateException("Unknow provider configured!", e);
}
@ -48,8 +44,8 @@ public class PublicIpLookup {
try {
String provider = this.provider.map(it -> it.url).orElseGet(() -> IpLookupProviders.random().url);
String v4 = getIp(provider, CurlProcessFactory.IpClass.V4);
String v6 = getIp(provider, CurlProcessFactory.IpClass.V6);
Optional<String> v4 = getIp(provider, CurlProcessFactory.IpClass.V4);
Optional<String> v6 = getIp(provider, CurlProcessFactory.IpClass.V6);
return new IPs(v4, v6);
} 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 {
Process curlProcess = curlProcessFactory.create(provider, ipClass).start();
curlProcess.waitFor();
@ -65,9 +61,10 @@ public class PublicIpLookup {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String ip = reader.readLine();
log.tracef("Retrieved ip: %s", ip);
return ip;
return Optional.of(ip);
} 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;
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
void test() {
IPs result = new DnsJava("9.9.9.9").resolve("example.com");
assertThat(result.v4()).isNotBlank();
assertThat(result.v6()).isNotBlank();
assertThat(result.v4()).isPresent();
assertThat(result.v6()).isPresent();
}
}