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
+
+
+
+ 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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+