diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff1f934 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Cloudflare & DynDNS + +Good for everyone who wants to use Cloudflare with their local ip but has a changing ip address. + + +## Config +```yaml +# Your cloudflare api token +apiToken: "" +# Your cloudflare email (required for auth) +cfEmail: "" +# The zoneId of the zone you want to update (Can be seen on the right of the domain dashboard) +zoneId: "" +dnsRecords: + - name: "myendpoint.example.com" + type: "A" +``` + +This project uses Cloudflare APIs only. It uses cloudflares cgi tracing endpoint to resolve the current public address and then handles changes accordingly. + +## How to run +- Build with maven (java 11) +- Run on a machine like a server or a Raspberry Pi +- start once, edit configuration +- start again + +## Motivation +This is a simple project used by f.e. our smart home endpoints, nginx web server etc. + +Mental support provided by @iTzFreeHD \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d7ec8ab --- /dev/null +++ b/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + + de.tobiasgrether + CF-DynDNS + 1.0-SNAPSHOT + + + 2.14.0 + + + + jitpack.io + https://jitpack.io + + + + + + org.ini4j + ini4j + 0.5.4 + + + org.yaml + snakeyaml + 1.29 + + + org.projectlombok + lombok + 1.18.20 + provided + + + com.konghq + unirest-java + 3.11.09 + + + org.apache.logging.log4j + log4j-slf4j-impl + 2.13.3 + + + org.apache.logging.log4j + log4j-api + ${log4j2.version} + compile + + + org.apache.logging.log4j + log4j-core + ${log4j2.version} + compile + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 11 + 11 + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.2 + + + + true + lib/ + de.tobiasgrether.dyndns.Bootstrap + + + DynDNS-Exporter + + + + org.apache.maven.plugins + maven-shade-plugin + 1.6 + + + package + + shade + + + + + + + \ No newline at end of file diff --git a/src/main/java/de/tobiasgrether/dyndns/Bootstrap.java b/src/main/java/de/tobiasgrether/dyndns/Bootstrap.java new file mode 100644 index 0000000..a1107b9 --- /dev/null +++ b/src/main/java/de/tobiasgrether/dyndns/Bootstrap.java @@ -0,0 +1,13 @@ +package de.tobiasgrether.dyndns; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Bootstrap { + private static final Logger logger = LoggerFactory.getLogger("Main"); + + public static void main(String[] args){ + logger.info("Starting up DynDNS.."); + DynDNS dnsManager = new DynDNS(); + } +} diff --git a/src/main/java/de/tobiasgrether/dyndns/DynDNS.java b/src/main/java/de/tobiasgrether/dyndns/DynDNS.java new file mode 100644 index 0000000..6ba5eb4 --- /dev/null +++ b/src/main/java/de/tobiasgrether/dyndns/DynDNS.java @@ -0,0 +1,95 @@ +package de.tobiasgrether.dyndns; + +import de.tobiasgrether.dyndns.model.DNSListResult; +import de.tobiasgrether.dyndns.model.DNSRecord; +import de.tobiasgrether.dyndns.model.config.ConfigModel; +import de.tobiasgrether.dyndns.model.config.RecordEntry; +import de.tobiasgrether.dyndns.util.ConfigParser; +import kong.unirest.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.StringReader; +import java.util.Properties; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class DynDNS { + + private final Logger logger = LoggerFactory.getLogger("DynDNS"); + + private ConfigModel model; + private String lastKnownAddress; + private final ObjectMapper mapper = new JsonObjectMapper(); + + public DynDNS() { + try { + this.model = ConfigParser.parseConfig(this); + } catch (IOException e) { + this.logger.error("Error while parsing configuration file, exiting...", e); + return; + } + + ScheduledExecutorService requestRoundExecutor = Executors.newSingleThreadScheduledExecutor(); + requestRoundExecutor.scheduleAtFixedRate(this::checkCurrentPublicIP, 0, 30, TimeUnit.SECONDS); + } + + private void checkCurrentPublicIP() { + HttpResponse response = Unirest.get("https://cloudflare.com/cdn-cgi/trace") + .asString(); + + if (response.getStatus() == 200) { + try { + Properties p = new Properties(); + p.load(new StringReader(response.getBody())); + String currentIp = p.getProperty("ip"); + if (!currentIp.equals(this.lastKnownAddress)) { + this.triggerDNSRewrite(currentIp); + this.lastKnownAddress = currentIp; + } + } catch (Throwable t) { + this.logger.error("Error while handling ini file", t); + } + + } else { + this.logger.warn("Could not parse current public ip from Cloudflare. Error " + response.getStatus() + ": " + response.getStatusText()); + } + } + + private void triggerDNSRewrite(String currentIp) { + this.logger.warn("Detected new public ip, rewriting dns records.."); + + HttpResponse result = Unirest.get("https://api.cloudflare.com/client/v4/zones/" + this.model.getZoneId() + "/dns_records") + .header("X-Auth-Key", this.model.getApiToken()) + .header("X-Auth-Email", this.model.getCfEmail()) + .asJson(); + + DNSListResult parsedResult = this.mapper.readValue(result.getBody().toString(), DNSListResult.class); + if (parsedResult.isSuccess()) { + for (DNSRecord record : parsedResult.getResult()) { + if (this.model.getDnsRecords().contains(new RecordEntry(record.getName(), "")) && !record.getContent().equals(currentIp)) { + record.setContent(currentIp); + HttpResponse update = Unirest.put("https://api.cloudflare.com/client/v4/zones/" + this.model.getZoneId() + "/dns_records/" + record.getId()) + .body(this.mapper.writeValue(record)) + .header("X-Auth-Key", this.model.getApiToken()) + .header("X-Auth-Email", this.model.getCfEmail()) + .asJson(); + if (update.getStatus() == 200) { + this.logger.info("DNS Record updated: {} (Record type {}) now has IP {}", record.getName(), record.getType(), currentIp); + } + } + } + } + this.logger.info("Record rewrite complete, new public ip is " + currentIp); + } + + public Logger getLogger() { + return logger; + } + + public ConfigModel getModel() { + return model; + } +} diff --git a/src/main/java/de/tobiasgrether/dyndns/model/CloudflareTraceResult.java b/src/main/java/de/tobiasgrether/dyndns/model/CloudflareTraceResult.java new file mode 100644 index 0000000..0081764 --- /dev/null +++ b/src/main/java/de/tobiasgrether/dyndns/model/CloudflareTraceResult.java @@ -0,0 +1,23 @@ +package de.tobiasgrether.dyndns.model; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class CloudflareTraceResult { + + public Object fl; + private String h; + private String ip; + private String ts; + private String visit_scheme; + private String uag; + private String colo; + private String http; + private String loc; + private String tls; + private String sni; + private boolean warp; + private boolean gateway; +} diff --git a/src/main/java/de/tobiasgrether/dyndns/model/DNSListResult.java b/src/main/java/de/tobiasgrether/dyndns/model/DNSListResult.java new file mode 100644 index 0000000..c3d64a6 --- /dev/null +++ b/src/main/java/de/tobiasgrether/dyndns/model/DNSListResult.java @@ -0,0 +1,11 @@ +package de.tobiasgrether.dyndns.model; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public class DNSListResult { + private boolean success; + private DNSRecord[] result; +} diff --git a/src/main/java/de/tobiasgrether/dyndns/model/DNSRecord.java b/src/main/java/de/tobiasgrether/dyndns/model/DNSRecord.java new file mode 100644 index 0000000..4638a34 --- /dev/null +++ b/src/main/java/de/tobiasgrether/dyndns/model/DNSRecord.java @@ -0,0 +1,143 @@ +package de.tobiasgrether.dyndns.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; + +public class DNSRecord { + @SerializedName("id") + @Expose + private String id; + @SerializedName("type") + @Expose + private String type; + @SerializedName("name") + @Expose + private String name; + @SerializedName("content") + @Expose + private String content; + @SerializedName("proxiable") + @Expose + private Boolean proxiable; + @SerializedName("proxied") + @Expose + private Boolean proxied; + @SerializedName("ttl") + @Expose + private Integer ttl; + @SerializedName("locked") + @Expose + private Boolean locked; + @SerializedName("zone_id") + @Expose + private String zoneId; + @SerializedName("zone_name") + @Expose + private String zoneName; + @SerializedName("modified_on") + @Expose + private String modifiedOn; + @SerializedName("created_on") + @Expose + private String createdOn; + + public DNSRecord() { + } + + public String getId() { + return this.id; + } + + public String getType() { + return this.type; + } + + public String getName() { + return this.name; + } + + public String getContent() { + return this.content; + } + + public Boolean getProxiable() { + return this.proxiable; + } + + public Boolean getProxied() { + return this.proxied; + } + + public Integer getTtl() { + return this.ttl; + } + + public Boolean getLocked() { + return this.locked; + } + + public String getZoneId() { + return this.zoneId; + } + + public String getZoneName() { + return this.zoneName; + } + + public String getModifiedOn() { + return this.modifiedOn; + } + + public String getCreatedOn() { + return this.createdOn; + } + + public void setId(String id) { + this.id = id; + } + + public void setType(String type) { + this.type = type; + } + + public void setName(String name) { + this.name = name; + } + + public void setContent(String content) { + this.content = content; + } + + public void setProxiable(Boolean proxiable) { + this.proxiable = proxiable; + } + + public void setProxied(Boolean proxied) { + this.proxied = proxied; + } + + public void setTtl(Integer ttl) { + this.ttl = ttl; + } + + public void setLocked(Boolean locked) { + this.locked = locked; + } + + public void setZoneId(String zoneId) { + this.zoneId = zoneId; + } + + public void setZoneName(String zoneName) { + this.zoneName = zoneName; + } + + public void setModifiedOn(String modifiedOn) { + this.modifiedOn = modifiedOn; + } + + public void setCreatedOn(String createdOn) { + this.createdOn = createdOn; + } +} + diff --git a/src/main/java/de/tobiasgrether/dyndns/model/config/ConfigModel.java b/src/main/java/de/tobiasgrether/dyndns/model/config/ConfigModel.java new file mode 100644 index 0000000..8fccff5 --- /dev/null +++ b/src/main/java/de/tobiasgrether/dyndns/model/config/ConfigModel.java @@ -0,0 +1,19 @@ +package de.tobiasgrether.dyndns.model.config; + +import lombok.Getter; +import lombok.ToString; + +import java.util.List; + +@Getter +@ToString +public class ConfigModel { + + public String apiToken; + + public String cfEmail; + + public String zoneId; + + public List dnsRecords; +} diff --git a/src/main/java/de/tobiasgrether/dyndns/model/config/RecordEntry.java b/src/main/java/de/tobiasgrether/dyndns/model/config/RecordEntry.java new file mode 100644 index 0000000..a26fbbb --- /dev/null +++ b/src/main/java/de/tobiasgrether/dyndns/model/config/RecordEntry.java @@ -0,0 +1,27 @@ +package de.tobiasgrether.dyndns.model.config; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.Objects; + +@Getter +@ToString +@AllArgsConstructor +@NoArgsConstructor +public class RecordEntry { + + public String name; + public String type; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RecordEntry that = (RecordEntry) o; + return Objects.equals(name, that.name) && Objects.equals(type, that.type); + } + +} diff --git a/src/main/java/de/tobiasgrether/dyndns/util/ConfigParser.java b/src/main/java/de/tobiasgrether/dyndns/util/ConfigParser.java new file mode 100644 index 0000000..6c46eca --- /dev/null +++ b/src/main/java/de/tobiasgrether/dyndns/util/ConfigParser.java @@ -0,0 +1,29 @@ +package de.tobiasgrether.dyndns.util; + +import de.tobiasgrether.dyndns.DynDNS; +import de.tobiasgrether.dyndns.model.config.ConfigModel; +import org.yaml.snakeyaml.Yaml; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class ConfigParser { + public static ConfigModel parseConfig(DynDNS dns) throws IOException { + Yaml yaml = new Yaml(); + Path currentRelativePath = Paths.get(""); + File f = new File(currentRelativePath.toAbsolutePath() + "/config.yml"); + + if (!f.exists()) { + Files.copy(dns.getClass().getClassLoader().getResourceAsStream("config.yml"), f.toPath()); + dns.getLogger().warn("DEFAULT CONFIGURATION HAS BEEN GENERATED. PLEASE CONFIGURE THE FILE, THEN RUN AGAIN"); + Runtime.getRuntime().exit(0); + } + + ConfigModel model = yaml.loadAs(new FileInputStream(f), ConfigModel.class); + return model; + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..96fe0ea --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,6 @@ +apiToken: "" +cfEmail: "" +zoneId: "" +dnsRecords: + - name: "myendpoint.example.com" + type: "A" \ No newline at end of file diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml new file mode 100644 index 0000000..e5004dc --- /dev/null +++ b/src/main/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + +