aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTrumeet <17158086+Trumeet@users.noreply.github.com>2021-01-07 19:45:29 -0800
committerTrumeet <17158086+Trumeet@users.noreply.github.com>2021-01-07 19:45:29 -0800
commita5ed3da568f7542daa5e49ca0c1505f8c27d658c (patch)
tree80282452da704245e8a9a509f21b9f3d7a515ecb
downloaddn42peering-a5ed3da568f7542daa5e49ca0c1505f8c27d658c.tar
dn42peering-a5ed3da568f7542daa5e49ca0c1505f8c27d658c.tar.gz
dn42peering-a5ed3da568f7542daa5e49ca0c1505f8c27d658c.tar.bz2
dn42peering-a5ed3da568f7542daa5e49ca0c1505f8c27d658c.zip
First Commit
-rw-r--r--.gitignore88
-rw-r--r--.idea/.gitignore3
-rw-r--r--.idea/compiler.xml54
-rw-r--r--.idea/jarRepositories.xml30
-rw-r--r--.idea/misc.xml7
-rw-r--r--.idea/modules/dn42peering.iml2
-rw-r--r--.idea/modules/dn42peering.main.iml53
-rw-r--r--.idea/runConfigurations/Agent.xml23
-rw-r--r--.idea/runConfigurations/Central.xml28
-rw-r--r--.idea/vcs.xml6
-rw-r--r--Dockerfile13
-rw-r--r--README.md26
-rw-r--r--agent/build.gradle42
-rw-r--r--agent/src/main/java/moe/yuuta/dn42peering/agent/IOUtils.java27
-rw-r--r--agent/src/main/java/moe/yuuta/dn42peering/agent/Main.java45
-rw-r--r--agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/AgentServiceImpl.java112
-rw-r--r--agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/RPCVerticle.java26
-rw-r--r--agent/src/main/java/moe/yuuta/dn42peering/agent/provision/AsyncShell.java48
-rw-r--r--agent/src/main/java/moe/yuuta/dn42peering/agent/provision/IProvisionService.java80
-rw-r--r--agent/src/main/java/moe/yuuta/dn42peering/agent/provision/ProvisionServiceImpl.java286
-rw-r--r--agent/src/main/java/moe/yuuta/dn42peering/agent/provision/ProvisionVerticle.java36
-rw-r--r--agent/src/main/java/moe/yuuta/dn42peering/agent/provision/package-info.java4
-rw-r--r--agent/src/main/resources/bird2_v4.conf.ftlh5
-rw-r--r--agent/src/main/resources/bird2_v6.conf.ftlh4
-rw-r--r--agent/src/main/resources/wg.conf.ftlh20
-rw-r--r--build.gradle20
-rw-r--r--central/build.gradle46
-rw-r--r--central/src/main/java/com/wireguard/crypto/Curve25519.java501
-rw-r--r--central/src/main/java/com/wireguard/crypto/Key.java292
-rw-r--r--central/src/main/java/com/wireguard/crypto/KeyFormatException.java39
-rw-r--r--central/src/main/java/com/wireguard/util/NonNullForAll.java26
-rw-r--r--central/src/main/java/edazdarevic/commons/net/CIDRUtils.java142
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/IOUtils.java27
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/Main.java52
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/asn/ASN.java119
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/asn/ASNHandler.java202
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java152
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/asn/ASNVerticle.java56
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java52
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/asn/package-info.java4
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/jaba/OutParm.java22
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/jaba/Pair.java16
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/manage/ASNAuthProvider.java38
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/manage/ManageHandler.java1022
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/node/INodeService.java28
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/node/Node.java204
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/node/NodeServiceImpl.java64
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/node/NodeVerticle.java56
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/node/package-info.java4
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/peer/IPeerService.java53
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/peer/Peer.java333
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/peer/PeerServiceImpl.java194
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/peer/PeerVerticle.java56
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/peer/ProvisionStatus.java7
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/peer/package-info.java4
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/portal/FormException.java17
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/portal/HTTPException.java9
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/portal/HTTPPortalVerticle.java51
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/portal/ISubRouter.java10
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/utils/PasswordAuthentication.java139
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/whois/IWhoisService.java27
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/whois/WhoisObject.java56
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/whois/WhoisServiceImpl.java71
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/whois/WhoisVerticle.java35
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/whois/package-info.java4
-rw-r--r--central/src/main/resources/asn/index.ftlh33
-rw-r--r--central/src/main/resources/asn/success.ftlh20
-rw-r--r--central/src/main/resources/index.ftlh31
-rw-r--r--central/src/main/resources/manage/changepw.ftlh32
-rw-r--r--central/src/main/resources/manage/delete.ftlh25
-rw-r--r--central/src/main/resources/manage/edit.ftlh14
-rw-r--r--central/src/main/resources/manage/form.ftlh60
-rw-r--r--central/src/main/resources/manage/index.ftlh35
-rw-r--r--central/src/main/resources/manage/new.ftlh12
-rw-r--r--central/src/main/resources/manage/showconf.ftlh21
-rw-r--r--docs/QuickStart.md53
-rw-r--r--docs/agent/Configuration.md21
-rw-r--r--docs/central/Configuration.md46
-rw-r--r--docs/central/Database.md9
-rw-r--r--docs/central/sql/0-init.sql57
-rw-r--r--docs/central/sql/agent.sql24
-rw-r--r--gradle/wrapper/gradle-wrapper.jarbin0 -> 59203 bytes
-rw-r--r--gradle/wrapper/gradle-wrapper.properties5
-rwxr-xr-xgradlew185
-rw-r--r--gradlew.bat89
-rw-r--r--rpc-common/build.gradle45
-rw-r--r--rpc-common/src/main/java/moe/yuuta/dn42peering/RPC.java5
-rw-r--r--rpc-common/src/main/proto/agent.proto51
-rw-r--r--settings.gradle2
89 files changed, 6193 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c2c5c1f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,88 @@
+.gradle
+**/build/
+!src/**/build/
+
+# Ignore Gradle GUI config
+gradle-app.setting
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Cache of project
+.gradletasknamecache
+
+# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
+# gradle/wrapper/gradle-wrapper.properties
+
+
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/jarRepositories.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..26d3352
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
new file mode 100644
index 0000000..2a44701
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="CompilerConfiguration">
+ <annotationProcessing>
+ <profile name="Gradle Imported" enabled="true">
+ <outputRelativeToContentRoot value="true" />
+ <processorPath useClasspath="false">
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.vertx/vertx-codegen/4.0.1-SNAPSHOT/4edc23f1ebaeabf5fa00ec6ca159c52c3e4f969d/vertx-codegen-4.0.1-SNAPSHOT-processor.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.vertx/vertx-service-proxy/4.0.1-SNAPSHOT/4a538246ab4622cf9df586e7fb2b9146676fe93f/vertx-service-proxy-4.0.1-SNAPSHOT.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.vertx/vertx-core/4.0.1-SNAPSHOT/970f49c0b49b63f98126c85edfb4c1d09d87c1af/vertx-core-4.0.1-SNAPSHOT.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-core/2.11.3/c2351800432bdbdd8284c3f5a7f0782a352aa84a/jackson-core-2.11.3.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler-proxy/4.1.49.Final/6a2064cc62c7d18719742e1e101199c04c66356c/netty-handler-proxy-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http2/4.1.49.Final/ca35293757f80cd2460c80791757db261615dbe7/netty-codec-http2-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http/4.1.49.Final/4f30dbc462b26c588dffc0eb7552caef1a0f549e/netty-codec-http-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler/4.1.49.Final/c73443adb9d085d5dc2d5b7f3bdd91d5963976f7/netty-handler-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns/4.1.49.Final/281770b3bac1c54f7ac995e79d953b5a66094acb/netty-resolver-dns-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-socks/4.1.49.Final/df75527823f9fd13f6bd9d9098bd9eb786dcafb5/netty-codec-socks-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-dns/4.1.49.Final/7ed5faaea7ad4428902844151d7006121bf367a3/netty-codec-dns-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec/4.1.49.Final/20218de83c906348283f548c255650fd06030424/netty-codec-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport/4.1.49.Final/415ea7f326635743aec952fe2349ca45959e94a7/netty-transport-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-buffer/4.1.49.Final/8e819a81bca88d1e88137336f64531a53db0a4ad/netty-buffer-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver/4.1.49.Final/eb81e1f0eaa99e75983bf3d28cae2b103e0f3a34/netty-resolver-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-common/4.1.49.Final/927c8563a1662d869b145e70ce82ad89100f2c90/netty-common-4.1.49.Final.jar" />
+ </processorPath>
+ <module name="dn42peering.agent.main" />
+ </profile>
+ <profile name="Gradle Imported" enabled="true">
+ <outputRelativeToContentRoot value="true" />
+ <processorPath useClasspath="false">
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.vertx/vertx-sql-client-templates/4.0.1-SNAPSHOT/4959aaa959f2d450e362b69eaea9a384d002d1ff/vertx-sql-client-templates-4.0.1-SNAPSHOT.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.vertx/vertx-codegen/4.0.1-SNAPSHOT/4edc23f1ebaeabf5fa00ec6ca159c52c3e4f969d/vertx-codegen-4.0.1-SNAPSHOT-processor.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.vertx/vertx-service-proxy/4.0.1-SNAPSHOT/4a538246ab4622cf9df586e7fb2b9146676fe93f/vertx-service-proxy-4.0.1-SNAPSHOT.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.vertx/vertx-sql-client/4.0.1-SNAPSHOT/f845d420c5a6e3b1ad6964fb0b19619d0ab39299/vertx-sql-client-4.0.1-SNAPSHOT.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.vertx/vertx-core/4.0.1-SNAPSHOT/970f49c0b49b63f98126c85edfb4c1d09d87c1af/vertx-core-4.0.1-SNAPSHOT.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/com.fasterxml.jackson.core/jackson-core/2.11.3/c2351800432bdbdd8284c3f5a7f0782a352aa84a/jackson-core-2.11.3.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler-proxy/4.1.49.Final/6a2064cc62c7d18719742e1e101199c04c66356c/netty-handler-proxy-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http2/4.1.49.Final/ca35293757f80cd2460c80791757db261615dbe7/netty-codec-http2-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-http/4.1.49.Final/4f30dbc462b26c588dffc0eb7552caef1a0f549e/netty-codec-http-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-handler/4.1.49.Final/c73443adb9d085d5dc2d5b7f3bdd91d5963976f7/netty-handler-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver-dns/4.1.49.Final/281770b3bac1c54f7ac995e79d953b5a66094acb/netty-resolver-dns-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-socks/4.1.49.Final/df75527823f9fd13f6bd9d9098bd9eb786dcafb5/netty-codec-socks-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec-dns/4.1.49.Final/7ed5faaea7ad4428902844151d7006121bf367a3/netty-codec-dns-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec/4.1.49.Final/20218de83c906348283f548c255650fd06030424/netty-codec-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-transport/4.1.49.Final/415ea7f326635743aec952fe2349ca45959e94a7/netty-transport-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-buffer/4.1.49.Final/8e819a81bca88d1e88137336f64531a53db0a4ad/netty-buffer-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-resolver/4.1.49.Final/eb81e1f0eaa99e75983bf3d28cae2b103e0f3a34/netty-resolver-4.1.49.Final.jar" />
+ <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/io.netty/netty-common/4.1.49.Final/927c8563a1662d869b145e70ce82ad89100f2c90/netty-common-4.1.49.Final.jar" />
+ </processorPath>
+ <module name="dn42peering.central.main" />
+ </profile>
+ </annotationProcessing>
+ <bytecodeTargetLevel target="1.8" />
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
new file mode 100644
index 0000000..97f4d0c
--- /dev/null
+++ b/.idea/jarRepositories.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="RemoteRepositoriesConfiguration">
+ <remote-repository>
+ <option name="id" value="central" />
+ <option name="name" value="Maven Central repository" />
+ <option name="url" value="https://repo1.maven.org/maven2" />
+ </remote-repository>
+ <remote-repository>
+ <option name="id" value="jboss.community" />
+ <option name="name" value="JBoss Community repository" />
+ <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
+ </remote-repository>
+ <remote-repository>
+ <option name="id" value="MavenRepo" />
+ <option name="name" value="MavenRepo" />
+ <option name="url" value="https://repo.maven.apache.org/maven2/" />
+ </remote-repository>
+ <remote-repository>
+ <option name="id" value="BintrayJCenter" />
+ <option name="name" value="BintrayJCenter" />
+ <option name="url" value="https://jcenter.bintray.com/" />
+ </remote-repository>
+ <remote-repository>
+ <option name="id" value="maven" />
+ <option name="name" value="maven" />
+ <option name="url" value="https://oss.sonatype.org/content/repositories/snapshots" />
+ </remote-repository>
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..ddaffea
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ExternalStorageConfigurationManager" enabled="true" />
+ <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="false" project-jdk-name="11" project-jdk-type="JavaSDK">
+ <output url="file://$PROJECT_DIR$/out" />
+ </component>
+</project> \ No newline at end of file
diff --git a/.idea/modules/dn42peering.iml b/.idea/modules/dn42peering.iml
new file mode 100644
index 0000000..bfdf8a5
--- /dev/null
+++ b/.idea/modules/dn42peering.iml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.linked.project.id="dn42peering" external.linked.project.path="$MODULE_DIR$/../.." external.root.project.path="$MODULE_DIR$/../.." external.system.id="GRADLE" external.system.module.group="moe.yuuta" external.system.module.version="1.0" type="JAVA_MODULE" version="4" /> \ No newline at end of file
diff --git a/.idea/modules/dn42peering.main.iml b/.idea/modules/dn42peering.main.iml
new file mode 100644
index 0000000..3f1a522
--- /dev/null
+++ b/.idea/modules/dn42peering.main.iml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module external.system.module.type="sourceSet" type="JAVA_MODULE" version="4">
+ <component name="NewModuleRootManager">
+ <output url="file://$MODULE_DIR$/../../build/classes/java/main" />
+ <exclude-output />
+ <content url="file://$MODULE_DIR$/../../build/generated/sources/annotationProcessor/java/main">
+ <sourceFolder url="file://$MODULE_DIR$/../../build/generated/sources/annotationProcessor/java/main" isTestSource="false" generated="true" />
+ </content>
+ <content url="file://$MODULE_DIR$/../../central/src/main">
+ <sourceFolder url="file://$MODULE_DIR$/../../central/src/main/java" isTestSource="false" />
+ <sourceFolder url="file://$MODULE_DIR$/../../central/src/main/resources" type="java-resource" />
+ </content>
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="library" scope="PROVIDED" name="Gradle: com.google.code.findbugs:jsr305:3.0.2" level="project" />
+ <orderEntry type="library" name="Gradle: commons-validator:commons-validator:1.7" level="project" />
+ <orderEntry type="library" name="Gradle: commons-logging:commons-logging:1.2" level="project" />
+ <orderEntry type="library" name="Gradle: io.vertx:vertx-service-proxy:4.0.1-SNAPSHOT" level="project" />
+ <orderEntry type="library" name="Gradle: io.vertx:vertx-auth-common:4.0.1-SNAPSHOT" level="project" />
+ <orderEntry type="library" name="Gradle: io.netty:netty-buffer:4.1.49.Final" level="project" />
+ <orderEntry type="library" name="Gradle: com.fasterxml.jackson.core:jackson-core:2.11.3" level="project" />
+ <orderEntry type="library" name="Gradle: io.vertx:vertx-bridge-common:4.0.1-SNAPSHOT" level="project" />
+ <orderEntry type="library" name="Gradle: commons-beanutils:commons-beanutils:1.9.4" level="project" />
+ <orderEntry type="library" name="Gradle: org.apache.commons:commons-lang3:3.11" level="project" />
+ <orderEntry type="library" scope="PROVIDED" name="Gradle: io.vertx:vertx-codegen:4.0.1-SNAPSHOT" level="project" />
+ <orderEntry type="library" name="Gradle: io.vertx:vertx-mail-client:4.0.1-SNAPSHOT" level="project" />
+ <orderEntry type="library" name="Gradle: io.netty:netty-codec-http:4.1.49.Final" level="project" />
+ <orderEntry type="library" name="Gradle: io.vertx:vertx-sql-client:4.0.1-SNAPSHOT" level="project" />
+ <orderEntry type="library" name="Gradle: commons-digester:commons-digester:2.1" level="project" />
+ <orderEntry type="library" name="Gradle: io.vertx:vertx-web-validation:4.0.1-SNAPSHOT" level="project" />
+ <orderEntry type="library" name="Gradle: io.vertx:vertx-web:4.0.1-SNAPSHOT" level="project" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ <orderEntry type="library" name="Gradle: org.apache.commons:commons-text:1.9" level="project" />
+ <orderEntry type="library" name="Gradle: commons-net:commons-net:3.7.2" level="project" />
+ <orderEntry type="library" name="Gradle: io.vertx:vertx-web-templ-freemarker:4.0.1-SNAPSHOT" level="project" />
+ <orderEntry type="library" name="Gradle: io.vertx:vertx-mysql-client:4.0.1-SNAPSHOT" level="project" />
+ <orderEntry type="library" name="Gradle: io.vertx:vertx-sql-client-templates:4.0.1-SNAPSHOT" level="project" />
+ <orderEntry type="library" name="Gradle: io.vertx:vertx-core:4.0.1-SNAPSHOT" level="project" />
+ <orderEntry type="library" name="Gradle: commons-collections:commons-collections:3.2.2" level="project" />
+ <orderEntry type="library" name="Gradle: io.vertx:vertx-json-schema:4.0.1-SNAPSHOT" level="project" />
+ <orderEntry type="library" name="Gradle: io.vertx:vertx-web-common:4.0.1-SNAPSHOT" level="project" />
+ <orderEntry type="library" name="Gradle: org.freemarker:freemarker:2.3.29" level="project" />
+ <orderEntry type="library" name="Gradle: io.netty:netty-handler-proxy:4.1.49.Final" level="project" />
+ <orderEntry type="library" name="Gradle: io.netty:netty-codec-http2:4.1.49.Final" level="project" />
+ <orderEntry type="library" name="Gradle: io.netty:netty-handler:4.1.49.Final" level="project" />
+ <orderEntry type="library" name="Gradle: io.netty:netty-resolver-dns:4.1.49.Final" level="project" />
+ <orderEntry type="library" name="Gradle: io.netty:netty-transport:4.1.49.Final" level="project" />
+ <orderEntry type="library" name="Gradle: io.netty:netty-resolver:4.1.49.Final" level="project" />
+ <orderEntry type="library" name="Gradle: io.netty:netty-common:4.1.49.Final" level="project" />
+ <orderEntry type="library" name="Gradle: io.netty:netty-codec-socks:4.1.49.Final" level="project" />
+ <orderEntry type="library" name="Gradle: io.netty:netty-codec:4.1.49.Final" level="project" />
+ <orderEntry type="library" name="Gradle: io.netty:netty-codec-dns:4.1.49.Final" level="project" />
+ </component>
+</module> \ No newline at end of file
diff --git a/.idea/runConfigurations/Agent.xml b/.idea/runConfigurations/Agent.xml
new file mode 100644
index 0000000..c9e8255
--- /dev/null
+++ b/.idea/runConfigurations/Agent.xml
@@ -0,0 +1,23 @@
+<component name="ProjectRunConfigurationManager">
+ <configuration default="false" name="Agent" type="GradleRunConfiguration" factoryName="Gradle">
+ <ExternalSystemSettings>
+ <option name="executionName" />
+ <option name="externalProjectPath" value="$PROJECT_DIR$/agent" />
+ <option name="externalSystemIdString" value="GRADLE" />
+ <option name="scriptParameters" value="" />
+ <option name="taskDescriptions">
+ <list />
+ </option>
+ <option name="taskNames">
+ <list>
+ <option value=":run" />
+ </list>
+ </option>
+ <option name="vmOptions" value="" />
+ </ExternalSystemSettings>
+ <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+ <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+ <DebugAllEnabled>false</DebugAllEnabled>
+ <method v="2" />
+ </configuration>
+</component> \ No newline at end of file
diff --git a/.idea/runConfigurations/Central.xml b/.idea/runConfigurations/Central.xml
new file mode 100644
index 0000000..410a176
--- /dev/null
+++ b/.idea/runConfigurations/Central.xml
@@ -0,0 +1,28 @@
+<component name="ProjectRunConfigurationManager">
+ <configuration default="false" name="Central" type="GradleRunConfiguration" factoryName="Gradle">
+ <ExternalSystemSettings>
+ <option name="env">
+ <map>
+ <entry key="VERTXWEB_ENVIRONMENT" value="dev" />
+ </map>
+ </option>
+ <option name="executionName" />
+ <option name="externalProjectPath" value="$PROJECT_DIR$/central" />
+ <option name="externalSystemIdString" value="GRADLE" />
+ <option name="scriptParameters" value="" />
+ <option name="taskDescriptions">
+ <list />
+ </option>
+ <option name="taskNames">
+ <list>
+ <option value=":run" />
+ </list>
+ </option>
+ <option name="vmOptions" value="" />
+ </ExternalSystemSettings>
+ <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+ <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+ <DebugAllEnabled>false</DebugAllEnabled>
+ <method v="2" />
+ </configuration>
+</component> \ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="VcsDirectoryMappings">
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
+ </component>
+</project> \ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..19620bc
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,13 @@
+# Docker image for central
+FROM docker.io/openjdk:8-jdk AS builder
+
+ADD . /root/
+WORKDIR /root/
+RUN ./gradlew :central:installDist
+
+FROM docker.io/openjdk:8-jre-alpine AS runtime
+
+WORKDIR /
+COPY --from=0 /root/central/build/install/ ./usr/
+
+ENTRYPOINT [ '/usr/bin/central' ]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..df20f10
--- /dev/null
+++ b/README.md
@@ -0,0 +1,26 @@
+# dn42peering
+
+A dn42 auto-peering platform.
+
+## Components
+
+* **Central**: Installs on some machines. They will provide HTTP portal and store user data.
+* **Agent**: Installs on target nodes. They will receive commands from the **central** and apply changes to the system.
+
+Refer to project Wiki for more details.
+
+## Overview
+
+The central provides a HTTP portal, and connects to a MySQL database. It will perform most tasks, including user management and peering management.
+
+The central establishes connections to agents when necessary. It will ask the agents to perform provisioning tasks. For example, setup BGP and VPN tunnels.
+
+The whole project is written in Java, with the support of Vert.x framework. The communication between centrals and agents is done by gRPC.
+
+## Get Started
+
+See [Quick Start](docs/QuickStart.md) for more details.
+
+## License
+
+Proprietary Software with open source components. \ No newline at end of file
diff --git a/agent/build.gradle b/agent/build.gradle
new file mode 100644
index 0000000..570c479
--- /dev/null
+++ b/agent/build.gradle
@@ -0,0 +1,42 @@
+plugins {
+ id 'java'
+ id 'application'
+ id "com.google.protobuf" version "0.8.14"
+ id 'idea'
+}
+
+group 'moe.yuuta'
+version '1.0'
+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+application {
+ getMainClass().set('moe.yuuta.dn42peering.agent.Main')
+}
+
+dependencies {
+ compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
+ compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
+
+ implementation 'org.apache.commons:commons-text:1.9'
+ implementation 'commons-net:commons-net:3.7.2'
+
+ implementation 'commons-validator:commons-validator:1.7'
+ testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1'
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1'
+
+ implementation "io.vertx:vertx-core:${project.vertxVersion}"
+ implementation "io.vertx:vertx-web-templ-freemarker:${project.vertxVersion}"
+ implementation "io.vertx:vertx-service-proxy:${project.vertxVersion}"
+ implementation "io.vertx:vertx-grpc:${project.vertxVersion}"
+ annotationProcessor "io.vertx:vertx-codegen:${project.vertxVersion}:processor"
+ annotationProcessor "io.vertx:vertx-service-proxy:${project.vertxVersion}"
+ compileOnly "io.vertx:vertx-codegen:${project.vertxVersion}"
+
+ implementation project(':rpc-common')
+}
+
+test {
+ useJUnitPlatform()
+} \ No newline at end of file
diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/IOUtils.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/IOUtils.java
new file mode 100644
index 0000000..33a65fb
--- /dev/null
+++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/IOUtils.java
@@ -0,0 +1,27 @@
+package moe.yuuta.dn42peering.agent;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class IOUtils {
+ @Nonnull
+ public static String read(@Nonnull InputStream in) throws IOException {
+ StringBuilder data = new StringBuilder();
+ while(true) {
+ int i = in.read();
+ if(i == -1) break;
+ data.append((char)i);
+ }
+ in.close();
+ return data.toString();
+ }
+
+ @Nonnull
+ public static String readFromResource(@Nonnull ClassLoader cl, @Nonnull String name) throws IOException {
+ final InputStream in = cl.getResourceAsStream(name);
+ final String data = read(in);
+ in.close();
+ return data;
+ }
+}
diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/Main.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/Main.java
new file mode 100644
index 0000000..f46d8d0
--- /dev/null
+++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/Main.java
@@ -0,0 +1,45 @@
+package moe.yuuta.dn42peering.agent;
+
+import io.vertx.core.*;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import io.vertx.core.json.JsonObject;
+import moe.yuuta.dn42peering.agent.grpc.RPCVerticle;
+import moe.yuuta.dn42peering.agent.provision.ProvisionVerticle;
+
+import javax.annotation.Nonnull;
+import java.io.FileInputStream;
+import java.io.InputStream;
+
+public class Main {
+ public static void main(@Nonnull String... args) throws Throwable {
+ if(args.length != 1) {
+ System.err.println("Usage: central <path/to/config.json>");
+ System.exit(64);
+ return;
+ }
+
+ System.setProperty("vertx.logger-delegate-factory-class-name",
+ "io.vertx.core.logging.JULLogDelegateFactory");
+
+ final InputStream in = new FileInputStream(args[0]);
+ final JsonObject config = new JsonObject(IOUtils.read(in));
+ in.close();
+
+ final Vertx vertx = Vertx.vertx(new VertxOptions());
+ final DeploymentOptions options = new DeploymentOptions()
+ .setConfig(config)
+ .setInstances(Runtime.getRuntime().availableProcessors() * 2);
+ Logger logger = LoggerFactory.getLogger("Main");
+ CompositeFuture.all(
+ Future.<String>future(f -> vertx.deployVerticle(ProvisionVerticle.class.getName(), options, f)),
+ Future.<String>future(f -> vertx.deployVerticle(RPCVerticle.class.getName(), options, f))
+ ).onComplete(res -> {
+ if (res.succeeded()) {
+ logger.info("The server started.");
+ } else {
+ logger.error("Cannot deploy the server.", res.cause());
+ }
+ });
+ }
+}
diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/AgentServiceImpl.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/AgentServiceImpl.java
new file mode 100644
index 0000000..565def5
--- /dev/null
+++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/AgentServiceImpl.java
@@ -0,0 +1,112 @@
+package moe.yuuta.dn42peering.agent.grpc;
+
+import io.vertx.core.Future;
+import io.vertx.core.Vertx;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import moe.yuuta.dn42peering.agent.proto.*;
+import moe.yuuta.dn42peering.agent.provision.IProvisionService;
+
+import javax.annotation.Nonnull;
+
+class AgentServiceImpl extends VertxAgentGrpc.AgentVertxImplBase {
+ private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+
+ private final Vertx vertx;
+ private final IProvisionService provisionService;
+
+ AgentServiceImpl(@Nonnull Vertx vertx) {
+ this.vertx = vertx;
+ this.provisionService = IProvisionService.create(vertx);
+ }
+
+ @Override
+ public Future<BGPReply> provisionBGP(BGPRequest request) {
+ return Future.<Void>future(f -> provisionService.provisionBGP(
+ request.getNode().getIpv4(),
+ request.getNode().getIpv6(),
+ (int)request.getId(),
+ request.getIpv4(),
+ request.getIpv6().isEmpty() ? null : request.getIpv6(),
+ request.getDevice(),
+ request.getMpbgp(),
+ request.getAsn(),
+ f))
+ .compose(_v -> Future.succeededFuture(BGPReply.newBuilder().build()))
+ .onFailure(err -> logger.error(String.format("Cannot provision BGP for %d", request.getId()),
+ err));
+ }
+
+ @Override
+ public Future<BGPReply> reloadBGP(BGPRequest request) {
+ return Future.<Void>future(f -> provisionService.reloadBGP(
+ request.getNode().getIpv4(),
+ request.getNode().getIpv6(),
+ (int)request.getId(),
+ request.getIpv4(),
+ request.getIpv6().isEmpty() ? null : request.getIpv6(),
+ request.getDevice(),
+ request.getMpbgp(),
+ request.getAsn(),
+ f))
+ .compose(_v -> Future.succeededFuture(BGPReply.newBuilder().build()))
+ .onFailure(err -> logger.error(String.format("Cannot reload BGP for %d", request.getId()),
+ err));
+ }
+
+ @Override
+ public Future<BGPReply> deleteBGP(BGPRequest request) {
+ return Future.<Void>future(f -> provisionService.unprovisionBGP((int)request.getId(), f))
+ .compose(_v -> Future.succeededFuture(BGPReply.newBuilder().build()))
+ .onFailure(err -> logger.error(String.format("Cannot delete BGP for %d", request.getId()),
+ err));
+ }
+
+ @Override
+ public Future<WGReply> provisionWG(WGRequest request) {
+ return Future.<String>future(f -> provisionService.provisionVPNWireGuard(
+ request.getNode().getIpv4(),
+ request.getNode().getIpv6(),
+ (int)request.getId(),
+ request.getListenPort(),
+ request.getEndpoint(),
+ request.getPeerPubKey(),
+ request.getSelfPrivKey(),
+ request.getSelfPresharedSecret(),
+ request.getPeerIPv4(),
+ request.getPeerIPv6().isEmpty() ? null : request.getPeerIPv6(),
+ f))
+ .compose(dev -> Future.succeededFuture(WGReply.newBuilder()
+ .setDevice(dev).build()))
+ .onFailure(err -> logger.error(String.format("Cannot provision WireGuard for %d", request.getId()),
+ err));
+ }
+
+ @Override
+ public Future<WGReply> reloadWG(WGRequest request) {
+ return Future.<String>future(f -> provisionService.reloadVPNWireGuard(
+ request.getNode().getIpv4(),
+ request.getNode().getIpv6(),
+ (int)request.getId(),
+ request.getListenPort(),
+ request.getEndpoint(),
+ request.getPeerPubKey(),
+ request.getSelfPrivKey(),
+ request.getSelfPresharedSecret(),
+ request.getPeerIPv4(),
+ request.getPeerIPv6().isEmpty() ? null : request.getPeerIPv6(),
+ f))
+ .compose(dev -> Future.succeededFuture(WGReply.newBuilder()
+ .setDevice(dev).build()))
+ .onFailure(err -> logger.error(String.format("Cannot reload WireGuard for %d", request.getId()),
+ err));
+ }
+
+ @Override
+ public Future<WGReply> deleteWG(WGRequest request) {
+ return Future.<Void>future(f -> provisionService.unprovisionVPNWireGuard((int)request.getId(), f))
+ .compose(_v -> Future.succeededFuture(WGReply.newBuilder().build()))
+ .onFailure(err -> logger.error(String.format("Cannot delete WireGuard for %d", request.getId()),
+ err));
+ }
+}
diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/RPCVerticle.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/RPCVerticle.java
new file mode 100644
index 0000000..3390b47
--- /dev/null
+++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/grpc/RPCVerticle.java
@@ -0,0 +1,26 @@
+package moe.yuuta.dn42peering.agent.grpc;
+
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.Promise;
+import io.vertx.grpc.VertxServer;
+import io.vertx.grpc.VertxServerBuilder;
+import moe.yuuta.dn42peering.RPC;
+
+public class RPCVerticle extends AbstractVerticle {
+ private VertxServer server;
+
+ @Override
+ public void start(Promise<Void> startPromise) throws Exception {
+ server = VertxServerBuilder
+ .forAddress(vertx, vertx.getOrCreateContext().config().getString("internal_ip"),
+ RPC.AGENT_PORT)
+ .addService(new AgentServiceImpl(vertx))
+ .build()
+ .start(startPromise);
+ }
+
+ @Override
+ public void stop(Promise<Void> stopPromise) throws Exception {
+ server.shutdown(stopPromise);
+ }
+}
diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/AsyncShell.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/AsyncShell.java
new file mode 100644
index 0000000..f6bfd64
--- /dev/null
+++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/AsyncShell.java
@@ -0,0 +1,48 @@
+package moe.yuuta.dn42peering.agent.provision;
+
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+
+public class AsyncShell {
+ @Nonnull
+ public static Future<Integer> exec(@Nonnull Vertx vertx,
+ @Nonnull String... cmd) {
+ return vertx.executeBlocking(f -> {
+ try {
+ int res = new ProcessBuilder()
+ .command(cmd)
+ .redirectError(ProcessBuilder.Redirect.INHERIT)
+ .start()
+ .waitFor();
+ f.complete(res);
+ } catch (IOException | InterruptedException e) {
+ f.fail(e);
+ }
+ });
+ }
+
+ public static void exec(@Nonnull Vertx vertx,
+ @Nonnull Handler<AsyncResult<Integer>> handler,
+ @Nonnull String... cmd) {
+ exec(vertx, cmd).onComplete(handler);
+ }
+
+ @Nonnull
+ public static Future<Void> execSucc(@Nonnull Vertx vertx,
+ @Nonnull String... cmd) {
+ return Future.future(f -> exec(vertx, cmd)
+ .onSuccess(res -> {
+ if(res != 0) {
+ f.fail(String.format("Unexpected return code %d", res));
+ } else {
+ f.complete(null);
+ }
+ })
+ .onFailure(f::fail));
+ }
+}
diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/IProvisionService.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/IProvisionService.java
new file mode 100644
index 0000000..07083e7
--- /dev/null
+++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/IProvisionService.java
@@ -0,0 +1,80 @@
+package moe.yuuta.dn42peering.agent.provision;
+
+import io.vertx.codegen.annotations.Fluent;
+import io.vertx.codegen.annotations.ProxyGen;
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+@ProxyGen
+public interface IProvisionService {
+ String ADDRESS = IProvisionService.class.getName();
+
+ @Nonnull
+ static IProvisionService create(@Nonnull Vertx vertx) {
+ return new IProvisionServiceVertxEBProxy(vertx, ADDRESS);
+ }
+
+ @Fluent
+ @Nonnull
+ IProvisionService provisionBGP(@Nonnull String localIP4,
+ @Nonnull String localIP6,
+ int id,
+ @Nonnull String ipv4,
+ @Nullable String ipv6,
+ @Nullable String device,
+ boolean mpbgp,
+ @Nonnull String asn,
+ @Nonnull Handler<AsyncResult<Void>> handler);
+
+ @Fluent
+ @Nonnull
+ IProvisionService reloadBGP(@Nonnull String localIP4,
+ @Nonnull String localIP6,
+ int id,
+ @Nonnull String ipv4,
+ @Nullable String ipv6,
+ @Nullable String device,
+ boolean mpbgp,
+ @Nonnull String asn,
+ @Nonnull Handler<AsyncResult<Void>> handler);
+
+ @Fluent
+ @Nonnull
+ IProvisionService unprovisionBGP(int id, @Nonnull Handler<AsyncResult<Void>> handler);
+
+ @Fluent
+ @Nonnull
+ IProvisionService provisionVPNWireGuard(@Nonnull String localIP4,
+ @Nonnull String localIP6,
+ int id,
+ int listenPort,
+ @Nonnull String endpointWithPort,
+ @Nonnull String peerPubKey,
+ @Nonnull String selfPrivKey,
+ @Nonnull String selfPresharedSecret,
+ @Nonnull String peerIPv4,
+ @Nullable String peerIPv6,
+ @Nonnull Handler<AsyncResult<String>> handler);
+
+ @Fluent
+ @Nonnull
+ IProvisionService reloadVPNWireGuard(@Nonnull String localIP4,
+ @Nonnull String localIP6,
+ int id,
+ int listenPort,
+ @Nonnull String endpointWithPort,
+ @Nonnull String peerPubKey,
+ @Nonnull String selfPrivKey,
+ @Nonnull String selfPresharedSecret,
+ @Nonnull String peerIPv4,
+ @Nullable String peerIPv6,
+ @Nonnull Handler<AsyncResult<String>> handler);
+
+ @Fluent
+ @Nonnull
+ IProvisionService unprovisionVPNWireGuard(int id, @Nonnull Handler<AsyncResult<Void>> handler);
+}
diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/ProvisionServiceImpl.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/ProvisionServiceImpl.java
new file mode 100644
index 0000000..fa672c8
--- /dev/null
+++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/ProvisionServiceImpl.java
@@ -0,0 +1,286 @@
+package moe.yuuta.dn42peering.agent.provision;
+
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import io.vertx.core.file.AsyncFile;
+import io.vertx.core.file.OpenOptions;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import io.vertx.ext.web.common.template.TemplateEngine;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.util.HashMap;
+import java.util.Map;
+
+class ProvisionServiceImpl implements IProvisionService {
+ private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+
+ private final Vertx vertx;
+ private final TemplateEngine engine;
+
+ ProvisionServiceImpl(@Nonnull Vertx vertx, @Nonnull TemplateEngine engine) {
+ this.vertx = vertx;
+ this.engine = engine;
+ }
+
+ @Nonnull
+ private static String generateBGPPath(int id) {
+ return String.format("/etc/bird/peers/dn42_%d.conf", id);
+ }
+
+ @Nonnull
+ private static String generateWGPath(@Nonnull String dev) {
+ return String.format("/etc/wireguard/%s.conf", dev);
+ }
+
+ @Nonnull
+ private static String generateWireGuardDevName(long id) {
+ return String.format("wg_%d", id);
+ }
+
+ @Nonnull
+ private static String getLockNameForBGP(long id) {
+ return String.format("BGP:%d", id);
+ }
+
+ @Nonnull
+ private static String getLockNameForWG(long id) {
+ return String.format("WG:%d", id);
+ }
+
+ @Nonnull
+ private Future<Void> writeBGPConfig(@Nonnull String localIP4,
+ @Nonnull String localIP6,
+ int id,
+ @Nonnull String ipv4,
+ @Nullable String ipv6,
+ @Nullable String device,
+ boolean mpbgp,
+ @Nonnull String asn,
+ boolean create) {
+ final String asnNum = asn.replace("AS", "");
+ return vertx.fileSystem().open(generateBGPPath(id), new OpenOptions()
+ .setCreateNew(create)
+ .setTruncateExisting(true)
+ .setWrite(true))
+ .compose(asyncFile -> {
+ if (mpbgp) return Future.succeededFuture(asyncFile);
+ final Map<String, Object> params = new HashMap<>(3);
+ params.put("name", id);
+ params.put("ipv4", ipv4);
+ params.put("asn", asnNum);
+ return engine.render(params, "bird2_v4.conf.ftlh")
+ .compose(buffer -> asyncFile.write(buffer)
+ .compose(_v1 -> Future.succeededFuture(asyncFile)));
+ })
+ .compose(asyncFile -> {
+ if (ipv6 == null) return Future.succeededFuture(asyncFile);
+ final Map<String, Object> params = new HashMap<>(4);
+ params.put("name", id);
+ params.put("ipv6", ipv6);
+ params.put("asn", asnNum);
+ params.put("dev", device);
+ return engine.render(params, "bird2_v6.conf.ftlh")
+ .compose(buffer -> asyncFile.write(buffer)
+ .compose(_v1 -> Future.succeededFuture(asyncFile)));
+ })
+ .compose(AsyncFile::close);
+ }
+
+ @Nonnull
+ private Future<Void> writeWGConfig(boolean create,
+ @Nonnull String localIP4,
+ @Nonnull String localIP6,
+ @Nonnull String dev,
+ int listenPort,
+ @Nonnull String endpointWithPort,
+ @Nonnull String peerPubKey,
+ @Nonnull String selfPrivKey,
+ @Nonnull String selfPresharedSecret,
+ @Nonnull String peerIPv4,
+ @Nullable String peerIPv6) {
+ return vertx.fileSystem().open(generateWGPath(dev), new OpenOptions()
+ .setCreateNew(create)
+ .setTruncateExisting(true)
+ .setWrite(true))
+ .compose(asyncFile -> {
+ final Map<String, Object> params = new HashMap<>(9);
+ params.put("listen_port", listenPort);
+ params.put("self_priv_key", selfPrivKey);
+ params.put("dev", dev);
+ params.put("self_ipv4", localIP4);
+ params.put("peer_ipv4", peerIPv4);
+ params.put("peer_ipv6", peerIPv6);
+ if (peerIPv6 != null) {
+ try {
+ params.put("peer_ipv6_ll", Inet6Address.getByName(peerIPv6).isLinkLocalAddress());
+ } catch (IOException e) {
+ return Future.failedFuture(e);
+ }
+ }
+ params.put("self_ipv6", localIP6);
+ params.put("preshared_key", selfPresharedSecret);
+ params.put("endpoint", endpointWithPort);
+ params.put("peer_pub_key", peerPubKey);
+
+ return engine.render(params, "wg.conf.ftlh")
+ .compose(buffer -> asyncFile.write(buffer)
+ .compose(_v1 -> Future.succeededFuture(asyncFile)));
+ })
+ .compose(AsyncFile::close);
+ }
+
+ @Nonnull
+ private Future<Void> deleteBGPConfig(int id) {
+ return vertx.fileSystem().delete(generateBGPPath(id));
+ }
+
+ @Nonnull
+ private Future<Void> deleteWGConfig(@Nonnull String dev) {
+ return vertx.fileSystem().delete(generateWGPath(dev));
+ }
+
+ @Nonnull
+ @Override
+ public IProvisionService provisionBGP(@Nonnull String localIP4,
+ @Nonnull String localIP6,
+ int id,
+ @Nonnull String ipv4,
+ @Nullable String ipv6,
+ @Nullable String device,
+ boolean mpbgp,
+ @Nonnull String asn,
+ @Nonnull Handler<AsyncResult<Void>> handler) {
+ vertx.sharedData().getLocalLockWithTimeout(getLockNameForBGP(id), 1000)
+ .compose(lock ->
+ writeBGPConfig(localIP4, localIP6, id, ipv4, ipv6, device, mpbgp, asn, true)
+ .compose(_v -> AsyncShell.execSucc(vertx, "birdc", "configure"))
+ .onComplete(ar -> lock.release())
+ )
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IProvisionService reloadBGP(@Nonnull String localIP4,
+ @Nonnull String localIP6,
+ int id,
+ @Nonnull String ipv4,
+ @Nullable String ipv6,
+ @Nullable String device,
+ boolean mpbgp,
+ @Nonnull String asn,
+ @Nonnull Handler<AsyncResult<Void>> handler) {
+ vertx.sharedData().getLocalLockWithTimeout(getLockNameForBGP(id), 1000)
+ .compose(lock ->
+ writeBGPConfig(localIP4, localIP6, id, ipv4, ipv6, device, mpbgp, asn, false)
+ .compose(_v -> AsyncShell.execSucc(vertx, "birdc", "configure"))
+ .onComplete(ar -> lock.release())
+ )
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IProvisionService unprovisionBGP(int id, @Nonnull Handler<AsyncResult<Void>> handler) {
+ vertx.sharedData().getLocalLockWithTimeout(getLockNameForBGP(id), 1000)
+ .compose(lock ->
+ deleteBGPConfig(id)
+ .compose(_v -> AsyncShell.execSucc(vertx, "birdc", "configure"))
+ .onComplete(ar -> lock.release())
+ )
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IProvisionService provisionVPNWireGuard(@Nonnull String localIP4,
+ @Nonnull String localIP6,
+ int id,
+ int listenPort,
+ @Nonnull String endpointWithPort,
+ @Nonnull String peerPubKey,
+ @Nonnull String selfPrivKey,
+ @Nonnull String selfPresharedSecret,
+ @Nonnull String peerIPv4,
+ @Nullable String peerIPv6,
+ @Nonnull Handler<AsyncResult<String>> handler) {
+ vertx.sharedData()
+ .getLocalLockWithTimeout(getLockNameForWG(id), 1000)
+ .compose(lock -> writeWGConfig(true,
+ localIP4,
+ localIP6,
+ generateWireGuardDevName(id),
+ listenPort,
+ endpointWithPort,
+ peerPubKey,
+ selfPrivKey,
+ selfPresharedSecret,
+ peerIPv4,
+ peerIPv6)
+ .compose(_v -> AsyncShell.execSucc(vertx, "systemctl", "enable", "--now", "-q",
+ String.format("wg-quick@%s", generateWireGuardDevName(id))))
+ .onComplete(_v -> lock.release()))
+ .compose(_v -> Future.succeededFuture(generateWireGuardDevName(id)))
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IProvisionService reloadVPNWireGuard(@Nonnull String localIP4,
+ @Nonnull String localIP6,
+ int id,
+ int listenPort,
+ @Nonnull String endpointWithPort,
+ @Nonnull String peerPubKey,
+ @Nonnull String selfPrivKey,
+ @Nonnull String selfPresharedSecret,
+ @Nonnull String peerIPv4,
+ @Nullable String peerIPv6,
+ @Nonnull Handler<AsyncResult<String>> handler) {
+ vertx.sharedData()
+ .getLocalLockWithTimeout(getLockNameForWG(id), 1000)
+ .compose(lock -> writeWGConfig(false,
+ localIP4,
+ localIP6,
+ generateWireGuardDevName(id),
+ listenPort,
+ endpointWithPort,
+ peerPubKey,
+ selfPrivKey,
+ selfPresharedSecret,
+ peerIPv4,
+ peerIPv6)
+ .compose(_v -> AsyncShell.execSucc(vertx, "systemctl", "enable", "-q",
+ String.format("wg-quick@%s", generateWireGuardDevName(id))))
+ .compose(_v -> AsyncShell.execSucc(vertx, "systemctl", "reload-or-restart",
+ String.format("wg-quick@%s", generateWireGuardDevName(id))))
+ .onComplete(_v -> lock.release()))
+ .compose(_v -> Future.succeededFuture(generateWireGuardDevName(id)))
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IProvisionService unprovisionVPNWireGuard(int id, @Nonnull Handler<AsyncResult<Void>> handler) {
+ vertx.sharedData()
+ .getLocalLockWithTimeout(getLockNameForWG(id), 1000)
+ .compose(lock -> AsyncShell.execSucc(vertx, "systemctl", "disable", "--now", "-q",
+ String.format("wg-quick@%s", generateWireGuardDevName(id)))
+ // We need to stop the service first, then delete the configuration.
+ .compose(_v -> deleteWGConfig(generateWireGuardDevName(id)))
+ .onComplete(_v -> lock.release()))
+ .onComplete(handler);
+ return this;
+ }
+}
diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/ProvisionVerticle.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/ProvisionVerticle.java
new file mode 100644
index 0000000..eb2c678
--- /dev/null
+++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/ProvisionVerticle.java
@@ -0,0 +1,36 @@
+package moe.yuuta.dn42peering.agent.provision;
+
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.Promise;
+import io.vertx.core.eventbus.MessageConsumer;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.templ.freemarker.FreeMarkerTemplateEngine;
+import io.vertx.serviceproxy.ServiceBinder;
+
+public class ProvisionVerticle extends AbstractVerticle {
+ private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+
+ private MessageConsumer<JsonObject> consumer;
+
+ @Override
+ public void start(Promise<Void> startPromise) throws Exception {
+ consumer = new ServiceBinder(vertx)
+ .setAddress(IProvisionService.ADDRESS)
+ .register(IProvisionService.class, new ProvisionServiceImpl(vertx,
+ FreeMarkerTemplateEngine.create(vertx, "ftlh")));
+ consumer.completionHandler(ar -> {
+ if (ar.succeeded()) {
+ startPromise.complete();
+ } else {
+ startPromise.fail(ar.cause());
+ }
+ });
+ }
+
+ @Override
+ public void stop(Promise<Void> stopPromise) throws Exception {
+ consumer.unregister(stopPromise);
+ }
+}
diff --git a/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/package-info.java b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/package-info.java
new file mode 100644
index 0000000..672bd29
--- /dev/null
+++ b/agent/src/main/java/moe/yuuta/dn42peering/agent/provision/package-info.java
@@ -0,0 +1,4 @@
+@ModuleGen(groupPackage = "moe.yuuta.dn42peering.agent.provision", name = "provision")
+package moe.yuuta.dn42peering.agent.provision;
+
+import io.vertx.codegen.annotations.ModuleGen; \ No newline at end of file
diff --git a/agent/src/main/resources/bird2_v4.conf.ftlh b/agent/src/main/resources/bird2_v4.conf.ftlh
new file mode 100644
index 0000000..e698069
--- /dev/null
+++ b/agent/src/main/resources/bird2_v4.conf.ftlh
@@ -0,0 +1,5 @@
+protocol bgp ${name} from dnpeers {
+ neighbor ${ipv4} as ${asn};
+ direct;
+}
+
diff --git a/agent/src/main/resources/bird2_v6.conf.ftlh b/agent/src/main/resources/bird2_v6.conf.ftlh
new file mode 100644
index 0000000..d8f4306
--- /dev/null
+++ b/agent/src/main/resources/bird2_v6.conf.ftlh
@@ -0,0 +1,4 @@
+protocol bgp ${name}_v6 from dnpeers {
+ neighbor ${ipv6}%${dev} as ${asn};
+ direct;
+} \ No newline at end of file
diff --git a/agent/src/main/resources/wg.conf.ftlh b/agent/src/main/resources/wg.conf.ftlh
new file mode 100644
index 0000000..57ff790
--- /dev/null
+++ b/agent/src/main/resources/wg.conf.ftlh
@@ -0,0 +1,20 @@
+# Automatically Generated by dn42peering Agent.
+
+[Interface]
+ListenPort = ${listen_port?long?c}
+PrivateKey = ${self_priv_key}
+PostUp = ip addr add dev ${dev} ${self_ipv4}/32 peer ${peer_ipv4}/32
+<#if peer_ipv6??>
+<#if peer_ipv6_ll>
+PostUp = ip addr add dev ${dev} ${self_ipv6}/64
+<#else>
+PostUp = ip addr add dev ${dev} ${self_ipv6}/64 peer ${peer_ipv6}/64
+</#if>
+</#if>
+Table = off
+
+[Peer]
+PublicKey = ${peer_pub_key}
+PresharedKey = ${preshared_key}
+Endpoint = ${endpoint}
+AllowedIPs = 0.0.0.0/0, ::/0
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..cc98403
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,20 @@
+allprojects {
+ repositories {
+ maven {
+ url "https://oss.sonatype.org/content/repositories/snapshots"
+ mavenContent {
+ snapshotsOnly()
+ }
+ }
+
+ mavenCentral()
+ jcenter()
+ }
+}
+
+project.ext {
+ vertxVersion = "4.0.1-SNAPSHOT"
+ grpcVersion = '1.34.1'
+ protobufVersion = '3.14.0'
+}
+
diff --git a/central/build.gradle b/central/build.gradle
new file mode 100644
index 0000000..c4eb3b0
--- /dev/null
+++ b/central/build.gradle
@@ -0,0 +1,46 @@
+plugins {
+ id 'java'
+ id 'application'
+}
+
+group 'moe.yuuta'
+version '1.0'
+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+application {
+ getMainClass().set('moe.yuuta.dn42peering.Main')
+}
+
+dependencies {
+ compileOnly 'com.google.code.findbugs:jsr305:3.0.2'
+ implementation 'org.apache.commons:commons-text:1.9'
+ implementation 'commons-net:commons-net:3.7.2'
+
+ implementation 'commons-validator:commons-validator:1.7'
+ testImplementation 'org.junit.jupiter:junit-jupiter-api:5.3.1'
+ testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.3.1'
+
+ implementation "io.vertx:vertx-core:${project.vertxVersion}"
+ implementation "io.vertx:vertx-web:${project.vertxVersion}"
+ implementation "io.vertx:vertx-web-templ-freemarker:${project.vertxVersion}"
+ implementation "io.vertx:vertx-web-validation:${project.vertxVersion}"
+ implementation "io.vertx:vertx-auth-common:${project.vertxVersion}"
+ implementation "io.vertx:vertx-mysql-client:${project.vertxVersion}"
+ implementation "io.vertx:vertx-mail-client:${project.vertxVersion}"
+ implementation "io.vertx:vertx-service-proxy:${project.vertxVersion}"
+ implementation "io.vertx:vertx-sql-client-templates:${project.vertxVersion}"
+ implementation "io.vertx:vertx-config:${project.vertxVersion}"
+ annotationProcessor "io.vertx:vertx-sql-client-templates:${project.vertxVersion}"
+ annotationProcessor "io.vertx:vertx-codegen:${project.vertxVersion}:processor"
+ annotationProcessor "io.vertx:vertx-service-proxy:${project.vertxVersion}"
+ compileOnly "io.vertx:vertx-codegen:${project.vertxVersion}"
+ implementation "io.vertx:vertx-grpc:${project.vertxVersion}"
+
+ implementation project(':rpc-common')
+}
+
+test {
+ useJUnitPlatform()
+} \ No newline at end of file
diff --git a/central/src/main/java/com/wireguard/crypto/Curve25519.java b/central/src/main/java/com/wireguard/crypto/Curve25519.java
new file mode 100644
index 0000000..ad392f1
--- /dev/null
+++ b/central/src/main/java/com/wireguard/crypto/Curve25519.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright © 2016 Southern Storm Software, Pty Ltd.
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * https://raw.githubusercontent.com/WireGuard/wireguard-android/5b5ba88a97b5310c532b42b526f78d1c747965f8/tunnel/src/main/java/com/wireguard/crypto/Curve25519.java
+ */
+
+package com.wireguard.crypto;
+
+import com.wireguard.util.NonNullForAll;
+
+import javax.annotation.Nullable;
+import java.util.Arrays;
+
+/**
+ * Implementation of the Curve25519 elliptic curve algorithm.
+ * <p>
+ * This implementation was imported to WireGuard from noise-java:
+ * https://github.com/rweather/noise-java
+ * <p>
+ * This implementation is based on that from arduinolibs:
+ * https://github.com/rweather/arduinolibs
+ * <p>
+ * Differences in this version are due to using 26-bit limbs for the
+ * representation instead of the 8/16/32-bit limbs in the original.
+ * <p>
+ * References: http://cr.yp.to/ecdh.html, RFC 7748
+ */
+@SuppressWarnings({"MagicNumber", "NonConstantFieldWithUpperCaseName", "SuspiciousNameCombination"})
+@NonNullForAll
+public final class Curve25519 {
+ // Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.
+ private static final int NUM_LIMBS_255BIT = 10;
+ private static final int NUM_LIMBS_510BIT = 20;
+
+ private final int[] A;
+ private final int[] AA;
+ private final int[] B;
+ private final int[] BB;
+ private final int[] C;
+ private final int[] CB;
+ private final int[] D;
+ private final int[] DA;
+ private final int[] E;
+ private final long[] t1;
+ private final int[] t2;
+ private final int[] x_1;
+ private final int[] x_2;
+ private final int[] x_3;
+ private final int[] z_2;
+ private final int[] z_3;
+
+ /**
+ * Constructs the temporary state holder for Curve25519 evaluation.
+ */
+ private Curve25519() {
+ // Allocate memory for all of the temporary variables we will need.
+ x_1 = new int[NUM_LIMBS_255BIT];
+ x_2 = new int[NUM_LIMBS_255BIT];
+ x_3 = new int[NUM_LIMBS_255BIT];
+ z_2 = new int[NUM_LIMBS_255BIT];
+ z_3 = new int[NUM_LIMBS_255BIT];
+ A = new int[NUM_LIMBS_255BIT];
+ B = new int[NUM_LIMBS_255BIT];
+ C = new int[NUM_LIMBS_255BIT];
+ D = new int[NUM_LIMBS_255BIT];
+ E = new int[NUM_LIMBS_255BIT];
+ AA = new int[NUM_LIMBS_255BIT];
+ BB = new int[NUM_LIMBS_255BIT];
+ DA = new int[NUM_LIMBS_255BIT];
+ CB = new int[NUM_LIMBS_255BIT];
+ t1 = new long[NUM_LIMBS_510BIT];
+ t2 = new int[NUM_LIMBS_510BIT];
+ }
+
+ /**
+ * Conditional swap of two values.
+ *
+ * @param select Set to 1 to swap, 0 to leave as-is.
+ * @param x The first value.
+ * @param y The second value.
+ */
+ private static void cswap(int select, final int[] x, final int[] y) {
+ select = -select;
+ for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
+ final int dummy = select & (x[index] ^ y[index]);
+ x[index] ^= dummy;
+ y[index] ^= dummy;
+ }
+ }
+
+ /**
+ * Evaluates the Curve25519 curve.
+ *
+ * @param result Buffer to place the result of the evaluation into.
+ * @param offset Offset into the result buffer.
+ * @param privateKey The private key to use in the evaluation.
+ * @param publicKey The public key to use in the evaluation, or null
+ * if the base point of the curve should be used.
+ */
+ public static void eval(final byte[] result, final int offset,
+ final byte[] privateKey, @Nullable final byte[] publicKey) {
+ final Curve25519 state = new Curve25519();
+ try {
+ // Unpack the public key value. If null, use 9 as the base point.
+ Arrays.fill(state.x_1, 0);
+ if (publicKey != null) {
+ // Convert the input value from little-endian into 26-bit limbs.
+ for (int index = 0; index < 32; ++index) {
+ final int bit = (index * 8) % 26;
+ final int word = (index * 8) / 26;
+ final int value = publicKey[index] & 0xFF;
+ if (bit <= (26 - 8)) {
+ state.x_1[word] |= value << bit;
+ } else {
+ state.x_1[word] |= value << bit;
+ state.x_1[word] &= 0x03FFFFFF;
+ state.x_1[word + 1] |= value >> (26 - bit);
+ }
+ }
+
+ // Just in case, we reduce the number modulo 2^255 - 19 to
+ // make sure that it is in range of the field before we start.
+ // This eliminates values between 2^255 - 19 and 2^256 - 1.
+ state.reduceQuick(state.x_1);
+ state.reduceQuick(state.x_1);
+ } else {
+ state.x_1[0] = 9;
+ }
+
+ // Initialize the other temporary variables.
+ Arrays.fill(state.x_2, 0); // x_2 = 1
+ state.x_2[0] = 1;
+ Arrays.fill(state.z_2, 0); // z_2 = 0
+ System.arraycopy(state.x_1, 0, state.x_3, 0, state.x_1.length); // x_3 = x_1
+ Arrays.fill(state.z_3, 0); // z_3 = 1
+ state.z_3[0] = 1;
+
+ // Evaluate the curve for every bit of the private key.
+ state.evalCurve(privateKey);
+
+ // Compute x_2 * (z_2 ^ (p - 2)) where p = 2^255 - 19.
+ state.recip(state.z_3, state.z_2);
+ state.mul(state.x_2, state.x_2, state.z_3);
+
+ // Convert x_2 into little-endian in the result buffer.
+ for (int index = 0; index < 32; ++index) {
+ final int bit = (index * 8) % 26;
+ final int word = (index * 8) / 26;
+ if (bit <= (26 - 8))
+ result[offset + index] = (byte) (state.x_2[word] >> bit);
+ else
+ result[offset + index] = (byte) ((state.x_2[word] >> bit) | (state.x_2[word + 1] << (26 - bit)));
+ }
+ } finally {
+ // Clean up all temporary state before we exit.
+ state.destroy();
+ }
+ }
+
+ /**
+ * Subtracts two numbers modulo 2^255 - 19.
+ *
+ * @param result The result.
+ * @param x The first number to subtract.
+ * @param y The second number to subtract.
+ */
+ private static void sub(final int[] result, final int[] x, final int[] y) {
+ int index;
+ int borrow;
+
+ // Subtract y from x to generate the intermediate result.
+ borrow = 0;
+ for (index = 0; index < NUM_LIMBS_255BIT; ++index) {
+ borrow = x[index] - y[index] - ((borrow >> 26) & 0x01);
+ result[index] = borrow & 0x03FFFFFF;
+ }
+
+ // If we had a borrow, then the result has gone negative and we
+ // have to add 2^255 - 19 to the result to make it positive again.
+ // The top bits of "borrow" will be all 1's if there is a borrow
+ // or it will be all 0's if there was no borrow. Easiest is to
+ // conditionally subtract 19 and then mask off the high bits.
+ borrow = result[0] - ((-((borrow >> 26) & 0x01)) & 19);
+ result[0] = borrow & 0x03FFFFFF;
+ for (index = 1; index < NUM_LIMBS_255BIT; ++index) {
+ borrow = result[index] - ((borrow >> 26) & 0x01);
+ result[index] = borrow & 0x03FFFFFF;
+ }
+ result[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
+ }
+
+ /**
+ * Adds two numbers modulo 2^255 - 19.
+ *
+ * @param result The result.
+ * @param x The first number to add.
+ * @param y The second number to add.
+ */
+ private void add(final int[] result, final int[] x, final int[] y) {
+ int carry = x[0] + y[0];
+ result[0] = carry & 0x03FFFFFF;
+ for (int index = 1; index < NUM_LIMBS_255BIT; ++index) {
+ carry = (carry >> 26) + x[index] + y[index];
+ result[index] = carry & 0x03FFFFFF;
+ }
+ reduceQuick(result);
+ }
+
+ /**
+ * Destroy all sensitive data in this object.
+ */
+ private void destroy() {
+ // Destroy all temporary variables.
+ Arrays.fill(x_1, 0);
+ Arrays.fill(x_2, 0);
+ Arrays.fill(x_3, 0);
+ Arrays.fill(z_2, 0);
+ Arrays.fill(z_3, 0);
+ Arrays.fill(A, 0);
+ Arrays.fill(B, 0);
+ Arrays.fill(C, 0);
+ Arrays.fill(D, 0);
+ Arrays.fill(E, 0);
+ Arrays.fill(AA, 0);
+ Arrays.fill(BB, 0);
+ Arrays.fill(DA, 0);
+ Arrays.fill(CB, 0);
+ Arrays.fill(t1, 0L);
+ Arrays.fill(t2, 0);
+ }
+
+ /**
+ * Evaluates the curve for every bit in a secret key.
+ *
+ * @param s The 32-byte secret key.
+ */
+ private void evalCurve(final byte[] s) {
+ int sposn = 31;
+ int sbit = 6;
+ int svalue = s[sposn] | 0x40;
+ int swap = 0;
+
+ // Iterate over all 255 bits of "s" from the highest to the lowest.
+ // We ignore the high bit of the 256-bit representation of "s".
+ while (true) {
+ // Conditional swaps on entry to this bit but only if we
+ // didn't swap on the previous bit.
+ final int select = (svalue >> sbit) & 0x01;
+ swap ^= select;
+ cswap(swap, x_2, x_3);
+ cswap(swap, z_2, z_3);
+ swap = select;
+
+ // Evaluate the curve.
+ add(A, x_2, z_2); // A = x_2 + z_2
+ square(AA, A); // AA = A^2
+ sub(B, x_2, z_2); // B = x_2 - z_2
+ square(BB, B); // BB = B^2
+ sub(E, AA, BB); // E = AA - BB
+ add(C, x_3, z_3); // C = x_3 + z_3
+ sub(D, x_3, z_3); // D = x_3 - z_3
+ mul(DA, D, A); // DA = D * A
+ mul(CB, C, B); // CB = C * B
+ add(x_3, DA, CB); // x_3 = (DA + CB)^2
+ square(x_3, x_3);
+ sub(z_3, DA, CB); // z_3 = x_1 * (DA - CB)^2
+ square(z_3, z_3);
+ mul(z_3, z_3, x_1);
+ mul(x_2, AA, BB); // x_2 = AA * BB
+ mulA24(z_2, E); // z_2 = E * (AA + a24 * E)
+ add(z_2, z_2, AA);
+ mul(z_2, z_2, E);
+
+ // Move onto the next lower bit of "s".
+ if (sbit > 0) {
+ --sbit;
+ } else if (sposn == 0) {
+ break;
+ } else if (sposn == 1) {
+ --sposn;
+ svalue = s[sposn] & 0xF8;
+ sbit = 7;
+ } else {
+ --sposn;
+ svalue = s[sposn];
+ sbit = 7;
+ }
+ }
+
+ // Final conditional swaps.
+ cswap(swap, x_2, x_3);
+ cswap(swap, z_2, z_3);
+ }
+
+ /**
+ * Multiplies two numbers modulo 2^255 - 19.
+ *
+ * @param result The result.
+ * @param x The first number to multiply.
+ * @param y The second number to multiply.
+ */
+ private void mul(final int[] result, final int[] x, final int[] y) {
+ // Multiply the two numbers to create the intermediate result.
+ long v = x[0];
+ for (int i = 0; i < NUM_LIMBS_255BIT; ++i) {
+ t1[i] = v * y[i];
+ }
+ for (int i = 1; i < NUM_LIMBS_255BIT; ++i) {
+ v = x[i];
+ for (int j = 0; j < (NUM_LIMBS_255BIT - 1); ++j) {
+ t1[i + j] += v * y[j];
+ }
+ t1[i + NUM_LIMBS_255BIT - 1] = v * y[NUM_LIMBS_255BIT - 1];
+ }
+
+ // Propagate carries and convert back into 26-bit words.
+ v = t1[0];
+ t2[0] = ((int) v) & 0x03FFFFFF;
+ for (int i = 1; i < NUM_LIMBS_510BIT; ++i) {
+ v = (v >> 26) + t1[i];
+ t2[i] = ((int) v) & 0x03FFFFFF;
+ }
+
+ // Reduce the result modulo 2^255 - 19.
+ reduce(result, t2, NUM_LIMBS_255BIT);
+ }
+
+ /**
+ * Multiplies a number by the a24 constant, modulo 2^255 - 19.
+ *
+ * @param result The result.
+ * @param x The number to multiply by a24.
+ */
+ private void mulA24(final int[] result, final int[] x) {
+ final long a24 = 121665;
+ long carry = 0;
+ for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
+ carry += a24 * x[index];
+ t2[index] = ((int) carry) & 0x03FFFFFF;
+ carry >>= 26;
+ }
+ t2[NUM_LIMBS_255BIT] = ((int) carry) & 0x03FFFFFF;
+ reduce(result, t2, 1);
+ }
+
+ /**
+ * Raise x to the power of (2^250 - 1).
+ *
+ * @param result The result. Must not overlap with x.
+ * @param x The argument.
+ */
+ private void pow250(final int[] result, final int[] x) {
+ // The big-endian hexadecimal expansion of (2^250 - 1) is:
+ // 03FFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF
+ //
+ // The naive implementation needs to do 2 multiplications per 1 bit and
+ // 1 multiplication per 0 bit. We can improve upon this by creating a
+ // pattern 0000000001 ... 0000000001. If we square and multiply the
+ // pattern by itself we can turn the pattern into the partial results
+ // 0000000011 ... 0000000011, 0000000111 ... 0000000111, etc.
+ // This averages out to about 1.1 multiplications per 1 bit instead of 2.
+
+ // Build a pattern of 250 bits in length of repeated copies of 0000000001.
+ square(A, x);
+ for (int j = 0; j < 9; ++j)
+ square(A, A);
+ mul(result, A, x);
+ for (int i = 0; i < 23; ++i) {
+ for (int j = 0; j < 10; ++j)
+ square(A, A);
+ mul(result, result, A);
+ }
+
+ // Multiply bit-shifted versions of the 0000000001 pattern into
+ // the result to "fill in" the gaps in the pattern.
+ square(A, result);
+ mul(result, result, A);
+ for (int j = 0; j < 8; ++j) {
+ square(A, A);
+ mul(result, result, A);
+ }
+ }
+
+ /**
+ * Computes the reciprocal of a number modulo 2^255 - 19.
+ *
+ * @param result The result. Must not overlap with x.
+ * @param x The argument.
+ */
+ private void recip(final int[] result, final int[] x) {
+ // The reciprocal is the same as x ^ (p - 2) where p = 2^255 - 19.
+ // The big-endian hexadecimal expansion of (p - 2) is:
+ // 7FFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFEB
+ // Start with the 250 upper bits of the expansion of (p - 2).
+ pow250(result, x);
+
+ // Deal with the 5 lowest bits of (p - 2), 01011, from highest to lowest.
+ square(result, result);
+ square(result, result);
+ mul(result, result, x);
+ square(result, result);
+ square(result, result);
+ mul(result, result, x);
+ square(result, result);
+ mul(result, result, x);
+ }
+
+ /**
+ * Reduce a number modulo 2^255 - 19.
+ *
+ * @param result The result.
+ * @param x The value to be reduced. This array will be
+ * modified during the reduction.
+ * @param size The number of limbs in the high order half of x.
+ */
+ private void reduce(final int[] result, final int[] x, final int size) {
+ // Calculate (x mod 2^255) + ((x / 2^255) * 19) which will
+ // either produce the answer we want or it will produce a
+ // value of the form "answer + j * (2^255 - 19)". There are
+ // 5 left-over bits in the top-most limb of the bottom half.
+ int carry = 0;
+ int limb = x[NUM_LIMBS_255BIT - 1] >> 21;
+ x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
+ for (int index = 0; index < size; ++index) {
+ limb += x[NUM_LIMBS_255BIT + index] << 5;
+ carry += (limb & 0x03FFFFFF) * 19 + x[index];
+ x[index] = carry & 0x03FFFFFF;
+ limb >>= 26;
+ carry >>= 26;
+ }
+ if (size < NUM_LIMBS_255BIT) {
+ // The high order half of the number is short; e.g. for mulA24().
+ // Propagate the carry through the rest of the low order part.
+ for (int index = size; index < NUM_LIMBS_255BIT; ++index) {
+ carry += x[index];
+ x[index] = carry & 0x03FFFFFF;
+ carry >>= 26;
+ }
+ }
+
+ // The "j" value may still be too large due to the final carry-out.
+ // We must repeat the reduction. If we already have the answer,
+ // then this won't do any harm but we must still do the calculation
+ // to preserve the overall timing. The "j" value will be between
+ // 0 and 19, which means that the carry we care about is in the
+ // top 5 bits of the highest limb of the bottom half.
+ carry = (x[NUM_LIMBS_255BIT - 1] >> 21) * 19;
+ x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
+ for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
+ carry += x[index];
+ result[index] = carry & 0x03FFFFFF;
+ carry >>= 26;
+ }
+
+ // At this point "x" will either be the answer or it will be the
+ // answer plus (2^255 - 19). Perform a trial subtraction to
+ // complete the reduction process.
+ reduceQuick(result);
+ }
+
+ /**
+ * Reduces a number modulo 2^255 - 19 where it is known that the
+ * number can be reduced with only 1 trial subtraction.
+ *
+ * @param x The number to reduce, and the result.
+ */
+ private void reduceQuick(final int[] x) {
+ // Perform a trial subtraction of (2^255 - 19) from "x" which is
+ // equivalent to adding 19 and subtracting 2^255. We add 19 here;
+ // the subtraction of 2^255 occurs in the next step.
+ int carry = 19;
+ for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
+ carry += x[index];
+ t2[index] = carry & 0x03FFFFFF;
+ carry >>= 26;
+ }
+
+ // If there was a borrow, then the original "x" is the correct answer.
+ // If there was no borrow, then "t2" is the correct answer. Select the
+ // correct answer but do it in a way that instruction timing will not
+ // reveal which value was selected. Borrow will occur if bit 21 of
+ // "t2" is zero. Turn the bit into a selection mask.
+ final int mask = -((t2[NUM_LIMBS_255BIT - 1] >> 21) & 0x01);
+ final int nmask = ~mask;
+ t2[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
+ for (int index = 0; index < NUM_LIMBS_255BIT; ++index)
+ x[index] = (x[index] & nmask) | (t2[index] & mask);
+ }
+
+ /**
+ * Squares a number modulo 2^255 - 19.
+ *
+ * @param result The result.
+ * @param x The number to square.
+ */
+ private void square(final int[] result, final int[] x) {
+ mul(result, x, x);
+ }
+} \ No newline at end of file
diff --git a/central/src/main/java/com/wireguard/crypto/Key.java b/central/src/main/java/com/wireguard/crypto/Key.java
new file mode 100644
index 0000000..27edcd9
--- /dev/null
+++ b/central/src/main/java/com/wireguard/crypto/Key.java
@@ -0,0 +1,292 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * https://raw.githubusercontent.com/WireGuard/wireguard-android/5b5ba88a97b5310c532b42b526f78d1c747965f8/tunnel/src/main/java/com/wireguard/crypto/Key.java
+ */
+
+package com.wireguard.crypto;
+
+import com.wireguard.crypto.KeyFormatException.Type;
+import com.wireguard.util.NonNullForAll;
+
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.Arrays;
+
+/**
+ * Represents a WireGuard public or private key. This class uses specialized constant-time base64
+ * and hexadecimal codec implementations that resist side-channel attacks.
+ * <p>
+ * Instances of this class are immutable.
+ */
+@SuppressWarnings("MagicNumber")
+@NonNullForAll
+public final class Key {
+ private final byte[] key;
+
+ /**
+ * Constructs an object encapsulating the supplied key.
+ *
+ * @param key an array of bytes containing a binary key. Callers of this constructor are
+ * responsible for ensuring that the array is of the correct length.
+ */
+ private Key(final byte[] key) {
+ // Defensively copy to ensure immutability.
+ this.key = Arrays.copyOf(key, key.length);
+ }
+
+ /**
+ * Decodes a single 4-character base64 chunk to an integer in constant time.
+ *
+ * @param src an array of at least 4 characters in base64 format
+ * @param srcOffset the offset of the beginning of the chunk in {@code src}
+ * @return the decoded 3-byte integer, or some arbitrary integer value if the input was not
+ * valid base64
+ */
+ private static int decodeBase64(final char[] src, final int srcOffset) {
+ int val = 0;
+ for (int i = 0; i < 4; ++i) {
+ final char c = src[i + srcOffset];
+ val |= (-1
+ + ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64))
+ + ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70))
+ + ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5))
+ + ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63)
+ + ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64)
+ ) << (18 - 6 * i);
+ }
+ return val;
+ }
+
+ /**
+ * Encodes a single 4-character base64 chunk from 3 consecutive bytes in constant time.
+ *
+ * @param src an array of at least 3 bytes
+ * @param srcOffset the offset of the beginning of the chunk in {@code src}
+ * @param dest an array of at least 4 characters
+ * @param destOffset the offset of the beginning of the chunk in {@code dest}
+ */
+ private static void encodeBase64(final byte[] src, final int srcOffset,
+ final char[] dest, final int destOffset) {
+ final byte[] input = {
+ (byte) ((src[srcOffset] >>> 2) & 63),
+ (byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63),
+ (byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63),
+ (byte) ((src[2 + srcOffset]) & 63),
+ };
+ for (int i = 0; i < 4; ++i) {
+ dest[i + destOffset] = (char) (input[i] + 'A'
+ + (((25 - input[i]) >>> 8) & 6)
+ - (((51 - input[i]) >>> 8) & 75)
+ - (((61 - input[i]) >>> 8) & 15)
+ + (((62 - input[i]) >>> 8) & 3));
+ }
+ }
+
+ /**
+ * Decodes a WireGuard public or private key from its base64 string representation. This
+ * function throws a {@link KeyFormatException} if the source string is not well-formed.
+ *
+ * @param str the base64 string representation of a WireGuard key
+ * @return the decoded key encapsulated in an immutable container
+ */
+ public static Key fromBase64(final String str) throws KeyFormatException {
+ final char[] input = str.toCharArray();
+ if (input.length != Format.BASE64.length || input[Format.BASE64.length - 1] != '=')
+ throw new KeyFormatException(Format.BASE64, Type.LENGTH);
+ final byte[] key = new byte[Format.BINARY.length];
+ int i;
+ int ret = 0;
+ for (i = 0; i < key.length / 3; ++i) {
+ final int val = decodeBase64(input, i * 4);
+ ret |= val >>> 31;
+ key[i * 3] = (byte) ((val >>> 16) & 0xff);
+ key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
+ key[i * 3 + 2] = (byte) (val & 0xff);
+ }
+ final char[] endSegment = {
+ input[i * 4],
+ input[i * 4 + 1],
+ input[i * 4 + 2],
+ 'A',
+ };
+ final int val = decodeBase64(endSegment, 0);
+ ret |= (val >>> 31) | (val & 0xff);
+ key[i * 3] = (byte) ((val >>> 16) & 0xff);
+ key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
+
+ if (ret != 0)
+ throw new KeyFormatException(Format.BASE64, Type.CONTENTS);
+ return new Key(key);
+ }
+
+ /**
+ * Wraps a WireGuard public or private key in an immutable container. This function throws a
+ * {@link KeyFormatException} if the source data is not the correct length.
+ *
+ * @param bytes an array of bytes containing a WireGuard key in binary format
+ * @return the key encapsulated in an immutable container
+ */
+ public static Key fromBytes(final byte[] bytes) throws KeyFormatException {
+ if (bytes.length != Format.BINARY.length)
+ throw new KeyFormatException(Format.BINARY, Type.LENGTH);
+ return new Key(bytes);
+ }
+
+ /**
+ * Decodes a WireGuard public or private key from its hexadecimal string representation. This
+ * function throws a {@link KeyFormatException} if the source string is not well-formed.
+ *
+ * @param str the hexadecimal string representation of a WireGuard key
+ * @return the decoded key encapsulated in an immutable container
+ */
+ public static Key fromHex(final String str) throws KeyFormatException {
+ final char[] input = str.toCharArray();
+ if (input.length != Format.HEX.length)
+ throw new KeyFormatException(Format.HEX, Type.LENGTH);
+ final byte[] key = new byte[Format.BINARY.length];
+ int ret = 0;
+ for (int i = 0; i < key.length; ++i) {
+ int c;
+ int cNum;
+ int cNum0;
+ int cAlpha;
+ int cAlpha0;
+ int cVal;
+ final int cAcc;
+
+ c = input[i * 2];
+ cNum = c ^ 48;
+ cNum0 = ((cNum - 10) >>> 8) & 0xff;
+ cAlpha = (c & ~32) - 55;
+ cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
+ ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
+ cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
+ cAcc = cVal * 16;
+
+ c = input[i * 2 + 1];
+ cNum = c ^ 48;
+ cNum0 = ((cNum - 10) >>> 8) & 0xff;
+ cAlpha = (c & ~32) - 55;
+ cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
+ ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
+ cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
+ key[i] = (byte) (cAcc | cVal);
+ }
+ if (ret != 0)
+ throw new KeyFormatException(Format.HEX, Type.CONTENTS);
+ return new Key(key);
+ }
+
+ /**
+ * Generates a private key using the system's {@link SecureRandom} number generator.
+ *
+ * @return a well-formed random private key
+ */
+ public static Key generatePrivateKey() {
+ final SecureRandom secureRandom = new SecureRandom();
+ final byte[] privateKey = new byte[Format.BINARY.getLength()];
+ secureRandom.nextBytes(privateKey);
+ privateKey[0] &= 248;
+ privateKey[31] &= 127;
+ privateKey[31] |= 64;
+ return new Key(privateKey);
+ }
+
+ /**
+ * Generates a public key from an existing private key.
+ *
+ * @param privateKey a private key
+ * @return a well-formed public key that corresponds to the supplied private key
+ */
+ public static Key generatePublicKey(final Key privateKey) {
+ final byte[] publicKey = new byte[Format.BINARY.getLength()];
+ Curve25519.eval(publicKey, 0, privateKey.getBytes(), null);
+ return new Key(publicKey);
+ }
+
+ @Override
+ public boolean equals(final Object obj) {
+ if (obj == this)
+ return true;
+ if (obj == null || obj.getClass() != getClass())
+ return false;
+ final Key other = (Key) obj;
+ return MessageDigest.isEqual(key, other.key);
+ }
+
+ /**
+ * Returns the key as an array of bytes.
+ *
+ * @return an array of bytes containing the raw binary key
+ */
+ public byte[] getBytes() {
+ // Defensively copy to ensure immutability.
+ return Arrays.copyOf(key, key.length);
+ }
+
+ @Override
+ public int hashCode() {
+ int ret = 0;
+ for (int i = 0; i < key.length / 4; ++i)
+ ret ^= (key[i * 4 + 0] >> 0) + (key[i * 4 + 1] >> 8) + (key[i * 4 + 2] >> 16) + (key[i * 4 + 3] >> 24);
+ return ret;
+ }
+
+ /**
+ * Encodes the key to base64.
+ *
+ * @return a string containing the encoded key
+ */
+ public String toBase64() {
+ final char[] output = new char[Format.BASE64.length];
+ int i;
+ for (i = 0; i < key.length / 3; ++i)
+ encodeBase64(key, i * 3, output, i * 4);
+ final byte[] endSegment = {
+ key[i * 3],
+ key[i * 3 + 1],
+ 0,
+ };
+ encodeBase64(endSegment, 0, output, i * 4);
+ output[Format.BASE64.length - 1] = '=';
+ return new String(output);
+ }
+
+ /**
+ * Encodes the key to hexadecimal ASCII characters.
+ *
+ * @return a string containing the encoded key
+ */
+ public String toHex() {
+ final char[] output = new char[Format.HEX.length];
+ for (int i = 0; i < key.length; ++i) {
+ output[i * 2] = (char) (87 + (key[i] >> 4 & 0xf)
+ + ((((key[i] >> 4 & 0xf) - 10) >> 8) & ~38));
+ output[i * 2 + 1] = (char) (87 + (key[i] & 0xf)
+ + ((((key[i] & 0xf) - 10) >> 8) & ~38));
+ }
+ return new String(output);
+ }
+
+ /**
+ * The supported formats for encoding a WireGuard key.
+ */
+ public enum Format {
+ BASE64(44),
+ BINARY(32),
+ HEX(64);
+
+ private final int length;
+
+ Format(final int length) {
+ this.length = length;
+ }
+
+ public int getLength() {
+ return length;
+ }
+ }
+
+} \ No newline at end of file
diff --git a/central/src/main/java/com/wireguard/crypto/KeyFormatException.java b/central/src/main/java/com/wireguard/crypto/KeyFormatException.java
new file mode 100644
index 0000000..21eb592
--- /dev/null
+++ b/central/src/main/java/com/wireguard/crypto/KeyFormatException.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * https://raw.githubusercontent.com/WireGuard/wireguard-android/5b5ba88a97b5310c532b42b526f78d1c747965f8/tunnel/src/main/java/com/wireguard/crypto/KeyFormatException.java
+ */
+
+package com.wireguard.crypto;
+
+import com.wireguard.util.NonNullForAll;
+
+/**
+ * An exception thrown when attempting to parse an invalid key (too short, too long, or byte
+ * data inappropriate for the format). The format being parsed can be accessed with the
+ * {@link #getFormat} method.
+ */
+@NonNullForAll
+public final class KeyFormatException extends Exception {
+ private final Key.Format format;
+ private final Type type;
+
+ KeyFormatException(final Key.Format format, final Type type) {
+ this.format = format;
+ this.type = type;
+ }
+
+ public Key.Format getFormat() {
+ return format;
+ }
+
+ public Type getType() {
+ return type;
+ }
+
+ public enum Type {
+ CONTENTS,
+ LENGTH
+ }
+} \ No newline at end of file
diff --git a/central/src/main/java/com/wireguard/util/NonNullForAll.java b/central/src/main/java/com/wireguard/util/NonNullForAll.java
new file mode 100644
index 0000000..737fe39
--- /dev/null
+++ b/central/src/main/java/com/wireguard/util/NonNullForAll.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ *
+ * https://raw.githubusercontent.com/WireGuard/wireguard-android/5b5ba88a97b5310c532b42b526f78d1c747965f8/tunnel/src/main/java/com/wireguard/util/NonNullForAll.java
+ */
+
+package com.wireguard.util;
+
+import javax.annotation.Nonnull;
+import javax.annotation.meta.TypeQualifierDefault;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This annotation can be applied to a package, class or method to indicate that all
+ * class fields and method parameters and return values in that element are nonnull
+ * by default unless overridden.
+ */
+@Nonnull
+@TypeQualifierDefault({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+
+public @interface NonNullForAll {
+} \ No newline at end of file
diff --git a/central/src/main/java/edazdarevic/commons/net/CIDRUtils.java b/central/src/main/java/edazdarevic/commons/net/CIDRUtils.java
new file mode 100644
index 0000000..159f99d
--- /dev/null
+++ b/central/src/main/java/edazdarevic/commons/net/CIDRUtils.java
@@ -0,0 +1,142 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2013 Edin Dazdarevic (edin.dazdarevic@gmail.com)
+
+ * 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.
+ *
+ * */
+
+package edazdarevic.commons.net;
+
+import java.math.BigInteger;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class that enables to get an IP range from CIDR specification. It supports
+ * both IPv4 and IPv6.
+ */
+public class CIDRUtils {
+ private final String cidr;
+
+ private InetAddress inetAddress;
+ private InetAddress startAddress;
+ private InetAddress endAddress;
+ private final int prefixLength;
+
+
+ public CIDRUtils(String cidr) throws UnknownHostException {
+
+ this.cidr = cidr;
+
+ /* split CIDR to address and prefix part */
+ if (this.cidr.contains("/")) {
+ int index = this.cidr.indexOf("/");
+ String addressPart = this.cidr.substring(0, index);
+ String networkPart = this.cidr.substring(index + 1);
+
+ inetAddress = InetAddress.getByName(addressPart);
+ prefixLength = Integer.parseInt(networkPart);
+
+ calculate();
+ } else {
+ throw new IllegalArgumentException("not an valid CIDR format!");
+ }
+ }
+
+
+ private void calculate() throws UnknownHostException {
+
+ ByteBuffer maskBuffer;
+ int targetSize;
+ if (inetAddress.getAddress().length == 4) {
+ maskBuffer =
+ ByteBuffer
+ .allocate(4)
+ .putInt(-1);
+ targetSize = 4;
+ } else {
+ maskBuffer = ByteBuffer.allocate(16)
+ .putLong(-1L)
+ .putLong(-1L);
+ targetSize = 16;
+ }
+
+ BigInteger mask = (new BigInteger(1, maskBuffer.array())).not().shiftRight(prefixLength);
+
+ ByteBuffer buffer = ByteBuffer.wrap(inetAddress.getAddress());
+ BigInteger ipVal = new BigInteger(1, buffer.array());
+
+ BigInteger startIp = ipVal.and(mask);
+ BigInteger endIp = startIp.add(mask.not());
+
+ byte[] startIpArr = toBytes(startIp.toByteArray(), targetSize);
+ byte[] endIpArr = toBytes(endIp.toByteArray(), targetSize);
+
+ this.startAddress = InetAddress.getByAddress(startIpArr);
+ this.endAddress = InetAddress.getByAddress(endIpArr);
+
+ }
+
+ private byte[] toBytes(byte[] array, int targetSize) {
+ int counter = 0;
+ List<Byte> newArr = new ArrayList<Byte>();
+ while (counter < targetSize && (array.length - 1 - counter >= 0)) {
+ newArr.add(0, array[array.length - 1 - counter]);
+ counter++;
+ }
+
+ int size = newArr.size();
+ for (int i = 0; i < (targetSize - size); i++) {
+
+ newArr.add(0, (byte) 0);
+ }
+
+ byte[] ret = new byte[newArr.size()];
+ for (int i = 0; i < newArr.size(); i++) {
+ ret[i] = newArr.get(i);
+ }
+ return ret;
+ }
+
+ public String getNetworkAddress() {
+
+ return this.startAddress.getHostAddress();
+ }
+
+ public String getBroadcastAddress() {
+ return this.endAddress.getHostAddress();
+ }
+
+ public boolean isInRange(String ipAddress) throws UnknownHostException {
+ InetAddress address = InetAddress.getByName(ipAddress);
+ BigInteger start = new BigInteger(1, this.startAddress.getAddress());
+ BigInteger end = new BigInteger(1, this.endAddress.getAddress());
+ BigInteger target = new BigInteger(1, address.getAddress());
+
+ int st = start.compareTo(target);
+ int te = target.compareTo(end);
+
+ return (st == -1 || st == 0) && (te == -1 || te == 0);
+ }
+} \ No newline at end of file
diff --git a/central/src/main/java/moe/yuuta/dn42peering/IOUtils.java b/central/src/main/java/moe/yuuta/dn42peering/IOUtils.java
new file mode 100644
index 0000000..711ab89
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/IOUtils.java
@@ -0,0 +1,27 @@
+package moe.yuuta.dn42peering;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class IOUtils {
+ @Nonnull
+ public static String read(@Nonnull InputStream in) throws IOException {
+ StringBuilder data = new StringBuilder();
+ while(true) {
+ int i = in.read();
+ if(i == -1) break;
+ data.append((char)i);
+ }
+ in.close();
+ return data.toString();
+ }
+
+ @Nonnull
+ public static String readFromResource(@Nonnull ClassLoader cl, @Nonnull String name) throws IOException {
+ final InputStream in = cl.getResourceAsStream(name);
+ final String data = read(in);
+ in.close();
+ return data;
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/Main.java b/central/src/main/java/moe/yuuta/dn42peering/Main.java
new file mode 100644
index 0000000..595cb30
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/Main.java
@@ -0,0 +1,52 @@
+package moe.yuuta.dn42peering;
+
+import io.vertx.core.*;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import io.vertx.core.json.JsonObject;
+import moe.yuuta.dn42peering.asn.ASNVerticle;
+import moe.yuuta.dn42peering.node.NodeVerticle;
+import moe.yuuta.dn42peering.peer.PeerVerticle;
+import moe.yuuta.dn42peering.portal.HTTPPortalVerticle;
+import moe.yuuta.dn42peering.whois.WhoisVerticle;
+
+import javax.annotation.Nonnull;
+import java.io.FileInputStream;
+import java.io.InputStream;
+
+public class Main {
+ public static void main(@Nonnull String... args) throws Throwable {
+ if(args.length != 1) {
+ System.err.println("Usage: central <path/to/config.json>");
+ System.exit(64);
+ return;
+ }
+
+ System.setProperty("vertx.logger-delegate-factory-class-name",
+ "io.vertx.core.logging.JULLogDelegateFactory");
+
+ final InputStream in = new FileInputStream(args[0]);
+ final JsonObject config = new JsonObject(IOUtils.read(in));
+ in.close();
+
+ final Vertx vertx = Vertx.vertx(new VertxOptions());
+
+ final DeploymentOptions options = new DeploymentOptions()
+ .setConfig(config)
+ .setInstances(Runtime.getRuntime().availableProcessors() * 2);
+ Logger logger = LoggerFactory.getLogger("Main");
+ CompositeFuture.all(
+ Future.<String>future(f -> vertx.deployVerticle(PeerVerticle.class.getName(), options, f)),
+ Future.<String>future(f -> vertx.deployVerticle(WhoisVerticle.class.getName(), options, f)),
+ Future.<String>future(f -> vertx.deployVerticle(ASNVerticle.class.getName(), options, f)),
+ Future.<String>future(f -> vertx.deployVerticle(NodeVerticle.class.getName(), options, f)),
+ Future.<String>future(f -> vertx.deployVerticle(HTTPPortalVerticle.class.getName(), options, f))
+ ).onComplete(res -> {
+ if (res.succeeded()) {
+ logger.info("The server started.");
+ } else {
+ logger.error("Cannot deploy the server.", res.cause());
+ }
+ });
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/asn/ASN.java b/central/src/main/java/moe/yuuta/dn42peering/asn/ASN.java
new file mode 100644
index 0000000..2ea2f68
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/asn/ASN.java
@@ -0,0 +1,119 @@
+package moe.yuuta.dn42peering.asn;
+
+import io.vertx.codegen.annotations.DataObject;
+import io.vertx.codegen.format.SnakeCase;
+import io.vertx.core.json.JsonObject;
+import io.vertx.sqlclient.templates.annotations.Column;
+import io.vertx.sqlclient.templates.annotations.ParametersMapped;
+import io.vertx.sqlclient.templates.annotations.RowMapped;
+import io.vertx.sqlclient.templates.annotations.TemplateParameter;
+import moe.yuuta.dn42peering.utils.PasswordAuthentication;
+
+import javax.annotation.Nonnull;
+import java.util.Date;
+
+// DB Table: asn
+@DataObject
+@RowMapped(formatter = SnakeCase.class)
+@ParametersMapped(formatter = SnakeCase.class)
+public class ASN {
+ @Column(name = "asn")
+ @TemplateParameter(name = "asn")
+ @Nonnull
+ private String asn;
+
+ @Column(name = "password_hash")
+ @TemplateParameter(name = "password_hash")
+ @Nonnull
+ private String passwordHash;
+
+ @Column(name = "activated")
+ @TemplateParameter(name = "activated")
+ private boolean activated;
+
+ @Column(name = "register_date")
+ @TemplateParameter(name = "register_date")
+ private Date registerDate;
+
+ public ASN() {
+ this("", "", false, new Date());
+ }
+
+ public ASN(@Nonnull String asn, @Nonnull String passwordHash,
+ boolean activated,
+ @Nonnull Date registerDate) {
+ this.asn = asn;
+ this.passwordHash = passwordHash;
+ this.activated = activated;
+ this.registerDate = registerDate;
+ }
+
+ public ASN(@Nonnull JsonObject jsonObject) {
+ this(jsonObject.getString("asn"),
+ jsonObject.getString("password_hash"),
+ jsonObject.getBoolean("activated"),
+ new Date(jsonObject.getLong("register_date")));
+ }
+
+ /**
+ * Create an ASN object when registering using random password.
+ */
+ public ASN(@Nonnull String asn, @Nonnull String randomPassword) {
+ this(asn,
+ new PasswordAuthentication()
+ .hash(randomPassword.toCharArray()),
+ false,
+ new Date());
+ }
+
+ @Nonnull
+ public JsonObject toJson() {
+ return new JsonObject()
+ .put("asn", asn)
+ .put("password_hash", passwordHash)
+ .put("activated", activated)
+ .put("register_date", registerDate.getTime());
+ }
+
+ public final boolean auth(@Nonnull String password) {
+ return new PasswordAuthentication().authenticate(password.toCharArray(), passwordHash);
+ }
+
+ public String getAsn() {
+ return asn;
+ }
+
+ public boolean isActivated() {
+ return activated;
+ }
+
+ public Date getRegisterDate() {
+ return registerDate;
+ }
+
+ public void setActivated(boolean activated) {
+ this.activated = activated;
+ }
+
+ // BEGIN GETTERS / SETTERS
+
+ public void setAsn(@Nonnull String asn) {
+ this.asn = asn;
+ }
+
+ @Nonnull
+ public String getPasswordHash() {
+ return passwordHash;
+ }
+
+ public void setPasswordHash(@Nonnull String passwordHash) {
+ this.passwordHash = passwordHash;
+ }
+
+ public void setRegisterDate(Date registerDate) {
+ this.registerDate = registerDate;
+ }
+
+
+ // END GETTERS / SETTERS
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/asn/ASNHandler.java b/central/src/main/java/moe/yuuta/dn42peering/asn/ASNHandler.java
new file mode 100644
index 0000000..b498fe1
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/asn/ASNHandler.java
@@ -0,0 +1,202 @@
+package moe.yuuta.dn42peering.asn;
+
+import io.vertx.core.*;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.mail.MailClient;
+import io.vertx.ext.mail.MailConfig;
+import io.vertx.ext.mail.MailMessage;
+import io.vertx.ext.mail.MailResult;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.common.template.TemplateEngine;
+import io.vertx.ext.web.handler.BodyHandler;
+import io.vertx.ext.web.templ.freemarker.FreeMarkerTemplateEngine;
+import io.vertx.ext.web.validation.RequestParameters;
+import io.vertx.ext.web.validation.RequestPredicate;
+import io.vertx.ext.web.validation.ValidationHandler;
+import io.vertx.ext.web.validation.builder.Bodies;
+import io.vertx.json.schema.SchemaParser;
+import io.vertx.json.schema.SchemaRouter;
+import io.vertx.json.schema.SchemaRouterOptions;
+import io.vertx.json.schema.common.dsl.ObjectSchemaBuilder;
+import moe.yuuta.dn42peering.jaba.Pair;
+import moe.yuuta.dn42peering.portal.FormException;
+import moe.yuuta.dn42peering.portal.HTTPException;
+import moe.yuuta.dn42peering.portal.ISubRouter;
+import moe.yuuta.dn42peering.whois.IWhoisService;
+import moe.yuuta.dn42peering.whois.WhoisObject;
+import org.apache.commons.text.RandomStringGenerator;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static io.vertx.json.schema.common.dsl.Schemas.objectSchema;
+import static io.vertx.json.schema.common.dsl.Schemas.stringSchema;
+
+public class ASNHandler implements ISubRouter {
+ private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+
+ @Nonnull
+ @Override
+ public Router mount(@Nonnull Vertx vertx) {
+ final IASNService asnService = IASNService.createProxy(vertx, IASNService.ADDRESS);
+ final IWhoisService whoisService = IWhoisService.createProxy(vertx, IWhoisService.ADDRESS);
+ // Preserve. We need to get email address out of it later.
+ final JsonObject mailConfig = vertx.getOrCreateContext().config().getJsonObject("mail");
+ final MailClient mailClient = MailClient.create(vertx, new MailConfig(mailConfig));
+
+ final TemplateEngine engine = FreeMarkerTemplateEngine.create(vertx, "ftlh");
+ final Router router = Router.router(vertx);
+ router.get("/")
+ .produces("text/html")
+ .handler(ctx -> {
+ renderIndex(engine, null, null, res -> {
+ if(res.succeeded()) {
+ ctx.response().end(res.result());
+ } else {
+ ctx.fail(res.cause());
+ logger.error("Cannot render /asn.", res.cause());
+ }
+ });
+ });
+
+ final ObjectSchemaBuilder registerSchema = objectSchema()
+ .allowAdditionalProperties(false)
+ .requiredProperty("asn", stringSchema());
+ final SchemaParser parser = SchemaParser.createDraft7SchemaParser(
+ SchemaRouter.create(vertx, new SchemaRouterOptions()));
+ router.post().handler(BodyHandler.create().setBodyLimit(100 * 1024));
+ router.post("/")
+ .produces("text/html")
+ .handler(ValidationHandler
+ .builder(parser)
+ .body(Bodies.formUrlEncoded(registerSchema))
+ .predicate(RequestPredicate.BODY_REQUIRED)
+ .build())
+ .handler(ctx -> {
+ final JsonObject parameters = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY)
+ .body().getJsonObject();
+ final String upperASN = parameters.getString("asn").toUpperCase();
+ // Start: Check if the ASN exists.
+ Future.<Void>future(f -> asnService.exists(upperASN, true, true, ar -> {
+ if(ar.succeeded()) {
+ if(ar.result()) {
+ f.fail(new FormException("This ASN exists in our records. Please login instead of registering."));
+ } else {
+ f.complete();
+ }
+ } else {
+ f.fail(ar.cause());
+ }
+ }))
+ // Lookup ASN
+ .<WhoisObject>compose(exists ->
+ Future.future(f -> whoisService.query(upperASN, f)))
+ // Lookup emails
+ .<List<String>>compose(asnLookup -> {
+ if(asnLookup == null) {
+ return Future.failedFuture(new FormException("The ASN is not found in the DN42 registry."));
+ } else {
+ return Future.future(f -> asnService.lookupEmails(asnLookup, ar -> {
+ if(ar.succeeded()) {
+ if(ar.result().isEmpty()) {
+ f.fail(new FormException("The tech-c contact for this ASN does not have emails."));
+ } else {
+ f.complete(ar.result());
+ }
+ } else {
+ f.fail(ar.cause());
+ }
+ }));
+ }
+ })
+ // Generate random password and register.
+ .<Pair<String /* Random password */, List<String> /* Emails */>>compose(emails -> Future.future(f -> {
+ final String randomPassword = new RandomStringGenerator.Builder()
+ .withinRange('a', 'z')
+ .build()
+ .generate(15);
+ asnService.registerOrChangePassword(upperASN, randomPassword, ar -> {
+ if(ar.succeeded()) {
+ f.complete(new Pair<>(randomPassword, emails));
+ } else {
+ f.fail(ar.cause());
+ }
+ });
+ }))
+ // Send mails.
+ .compose(pair -> CompositeFuture.any(Stream.of(pair.b)
+ .map(mail -> new MailMessage()
+ .setFrom(mailConfig.getString("from"))
+ .setTo(mail)
+ .setText(String.format("Hi %s! Welcome to dn42 peering! Your peering initial password is %s. Make sure to change it.",
+ upperASN,
+ pair.a))
+ .setSubject("Peering initial password"))
+ .map(message -> Future.<MailResult>future(f -> mailClient.sendMail(message, f)))
+ .collect(Collectors.toList())))
+ // Render HTML or report errors
+ .onComplete(ar -> {
+ if(ar.succeeded()) {
+ // Get MailResult's out of the future.
+ final List<MailResult> sendRes = ar.result().list();
+ renderSuccess(engine, sendRes, res -> {
+ if(res.succeeded()) {
+ ctx.response().end(res.result());
+ } else {
+ ctx.fail(res.cause());
+ logger.error("Cannot render /asn (success).", res.cause());
+ }
+ });
+ } else {
+ if(ar.cause() instanceof HTTPException) {
+ ctx.response().setStatusCode(((HTTPException) ar.cause()).code).end();
+ } else if(ar.cause() instanceof FormException) {
+ renderIndex(engine,
+ Arrays.asList(((FormException) ar.cause()).errors.clone()),
+ upperASN,
+ res -> {
+ if(res.succeeded()) {
+ ctx.response().end(res.result());
+ } else {
+ ctx.fail(res.cause());
+ logger.error("Cannot render /asn (with errors).", res.cause());
+ }
+ });
+ } else {
+ logger.error(String.format("Cannot register ASN %s.", upperASN), ar.cause());
+ ctx.fail(ar.cause());
+ }
+ }
+ });
+ });
+
+ return router;
+ }
+
+ private void renderIndex(@Nonnull TemplateEngine engine,
+ @Nullable List<String> errors,
+ @Nullable String asn,
+ @Nonnull Handler<AsyncResult<Buffer>> handler) {
+ final Map<String, Object> root = new HashMap<>();
+ root.put("input_asn", asn == null ? "" : asn);
+ root.put("errors", errors);
+ engine.render(root, "asn/index.ftlh", handler);
+ }
+
+ private void renderSuccess(@Nonnull TemplateEngine engine,
+ @Nonnull List<MailResult> sendRes,
+ @Nonnull Handler<AsyncResult<Buffer>> handler) {
+ final Map<String, Object> root = new HashMap<>();
+ root.put("emails", sendRes.stream()
+ .filter(Objects::nonNull) // Nulls mean failures.
+ .flatMap(res -> res.getRecipients().stream())
+ .collect(Collectors.toList()));
+ engine.render(root, "asn/success.ftlh", handler);
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java b/central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java
new file mode 100644
index 0000000..4a05588
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/asn/ASNServiceImpl.java
@@ -0,0 +1,152 @@
+package moe.yuuta.dn42peering.asn;
+
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import io.vertx.mysqlclient.MySQLPool;
+import io.vertx.sqlclient.*;
+import io.vertx.sqlclient.templates.SqlTemplate;
+import moe.yuuta.dn42peering.utils.PasswordAuthentication;
+import moe.yuuta.dn42peering.whois.IWhoisService;
+import moe.yuuta.dn42peering.whois.WhoisObject;
+
+import javax.annotation.Nonnull;
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+class ASNServiceImpl implements IASNService {
+ private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+
+ private final Vertx vertx;
+ private final MySQLPool pool;
+
+ ASNServiceImpl(@Nonnull Vertx vertx, @Nonnull MySQLPool pool) {
+ this.vertx = vertx;
+ this.pool = pool;
+ }
+
+ @Nonnull
+ @Override
+ public IASNService exists(@Nonnull String asn, boolean requireActivationStatus, boolean activated,
+ @Nonnull Handler<AsyncResult<Boolean>> handler) {
+ final PreparedQuery<RowSet<Row>> preparedQuery =
+ !requireActivationStatus ? pool.preparedQuery("SELECT COUNT(asn) FROM asn WHERE asn = ?")
+ : pool.preparedQuery("SELECT COUNT(asn) FROM asn WHERE asn = ? AND activated = ?");
+ preparedQuery.execute(!requireActivationStatus ? Tuple.of(asn) : Tuple.of(asn, activated))
+ .compose(rows -> {
+ int count = rows.iterator().next().getInteger(0);
+ return Future.succeededFuture(count > 0);
+ })
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IASNService markAsActivated(@Nonnull String asn, @Nonnull Handler<AsyncResult<Void>> handler) {
+ pool.preparedQuery("UPDATE asn SET activated = 1 WHERE asn = ? AND activated = 0")
+ .execute(Tuple.of(asn))
+ .<Void>compose(rows -> {
+ return Future.succeededFuture(null);
+ })
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IASNService changePassword(@Nonnull String asn, @Nonnull String newPassword, @Nonnull Handler<AsyncResult<Void>> handler) {
+ final Map<String, Object> params = new HashMap<>(2);
+ params.put("asn", asn);
+ params.put("password_hash", new PasswordAuthentication()
+ .hash(newPassword.toCharArray()));
+ Future.<SqlResult<Void>>future(f -> SqlTemplate
+ .forUpdate(pool, "UPDATE asn SET password_hash = #{password_hash} WHERE asn = #{asn}")
+ .execute(params, f))
+ .<Void>compose(voidSqlResult -> Future.succeededFuture(null))
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IASNService auth(@Nonnull String asn, @Nonnull String password, @Nonnull Handler<AsyncResult<Boolean>> handler) {
+ Future.<RowSet<ASN>>future(f -> SqlTemplate
+ .forQuery(pool, "SELECT password_hash FROM asn WHERE asn = #{asn}")
+ .mapTo(ASNRowMapper.INSTANCE)
+ .mapFrom(ASNParametersMapper.INSTANCE)
+ .execute(new ASN(asn, "", false, new Date()), f))
+ .compose(rows -> {
+ // No such user
+ if(!rows.iterator().hasNext()) {
+ return Future.succeededFuture(false);
+ }
+ final ASN record = rows.iterator().next();
+ return Future.succeededFuture(record.auth(password));
+ })
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IASNService delete(@Nonnull String asn, @Nonnull Handler<AsyncResult<Void>> handler) {
+ Future.<SqlResult<Void>>future(f -> SqlTemplate.forUpdate(pool, "DELETE FROM asn WHERE asn = #{asn}")
+ .execute(Collections.singletonMap("asn", asn), f))
+ .<Void>compose(voidSqlResult -> Future.succeededFuture())
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IASNService lookupEmails(@Nonnull WhoisObject asn, @Nonnull Handler<AsyncResult<List<String>>> handler) {
+ if (!asn.containsKey("tech-c") || asn.get("tech-c").isEmpty()) {
+ handler.handle(Future.succeededFuture(null));
+ return this;
+ }
+ Future.<WhoisObject>future(f -> IWhoisService.createProxy(vertx, IWhoisService.ADDRESS)
+ .query(asn.get("tech-c").get(0) /* tech-c is only permitted to appear once */, f))
+ .compose(techLookup -> {
+ if(techLookup == null) {
+ return Future.succeededFuture(Collections.<String>emptyList());
+ } else {
+ final List<String> verifyMethodList = new ArrayList<>(3);
+ verifyMethodList.addAll(Stream.of(techLookup.getOrDefault("contact", Collections.emptyList()))
+ .flatMap(Collection::stream)
+ .map(contact -> {
+ final Matcher m =
+ Pattern.compile("[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+")
+ .matcher(contact);
+ final List<String> verifiers = new ArrayList<>(1);
+ while (m.find()) {
+ verifiers.add(m.group());
+ }
+ return verifiers;
+ })
+ .flatMap(List::stream)
+ .collect(Collectors.toList()));
+ return Future.succeededFuture(verifyMethodList);
+ }
+ })
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IASNService registerOrChangePassword(@Nonnull String asn, @Nonnull String newPassword, @Nonnull Handler<AsyncResult<Void>> handler) {
+ // TODO: No activated check here.
+ Future.<RowSet<Row>>future(f -> pool.preparedQuery("REPLACE INTO asn (asn, password_hash) VALUES(?, ?)")
+ .execute(Tuple.of(asn, new PasswordAuthentication().hash(newPassword.toCharArray())), f))
+ .<Void>compose(res -> Future.succeededFuture(null))
+ .onComplete(handler);
+ return this;
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/asn/ASNVerticle.java b/central/src/main/java/moe/yuuta/dn42peering/asn/ASNVerticle.java
new file mode 100644
index 0000000..032dc2f
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/asn/ASNVerticle.java
@@ -0,0 +1,56 @@
+package moe.yuuta.dn42peering.asn;
+
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.CompositeFuture;
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.core.eventbus.MessageConsumer;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import io.vertx.core.json.JsonObject;
+import io.vertx.mysqlclient.MySQLConnectOptions;
+import io.vertx.mysqlclient.MySQLPool;
+import io.vertx.serviceproxy.ServiceBinder;
+import io.vertx.sqlclient.PoolOptions;
+
+public class ASNVerticle extends AbstractVerticle {
+ private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+
+ private MessageConsumer<JsonObject> consumer;
+ private MySQLPool pool;
+
+ @Override
+ public void start(Promise<Void> startPromise) throws Exception {
+ final JsonObject json = vertx.getOrCreateContext().config().getJsonObject("database");
+ final MySQLConnectOptions opt = new MySQLConnectOptions(json);
+ pool = MySQLPool.pool(vertx, opt, new PoolOptions().setMaxSize(5));
+
+ consumer = new ServiceBinder(vertx)
+ .setAddress(IASNService.ADDRESS)
+ .register(IASNService.class, new ASNServiceImpl(vertx, pool));
+ consumer.completionHandler(ar -> {
+ if(ar.succeeded()) {
+ startPromise.complete();
+ } else {
+ startPromise.fail(ar.cause());
+ }
+ });
+ }
+
+ @Override
+ public void stop(Promise<Void> stopPromise) throws Exception {
+ CompositeFuture.all(
+ Future.future(f -> consumer.unregister(ar -> {
+ if(ar.succeeded()) f.complete();
+ else f.fail(ar.cause());
+ })),
+ Future.future(f -> pool.close(ar -> {
+ if(ar.succeeded()) f.complete();
+ else f.fail(ar.cause());
+ }))
+ ).onComplete(ar -> {
+ if(ar.succeeded()) stopPromise.complete();
+ else stopPromise.fail(ar.cause());
+ });
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java b/central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java
new file mode 100644
index 0000000..5da2a57
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/asn/IASNService.java
@@ -0,0 +1,52 @@
+package moe.yuuta.dn42peering.asn;
+
+import io.vertx.codegen.annotations.Fluent;
+import io.vertx.codegen.annotations.ProxyGen;
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import moe.yuuta.dn42peering.whois.WhoisObject;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+
+@ProxyGen
+public interface IASNService {
+ String ADDRESS = IASNService.class.getName();
+
+ @Nonnull
+ static IASNService createProxy(@Nonnull Vertx vertx, @Nonnull String address) {
+ return new IASNServiceVertxEBProxy(vertx, address);
+ }
+
+ @Fluent
+ @Nonnull
+ IASNService exists(@Nonnull String asn, boolean requireActivationStatus, boolean activated,
+ @Nonnull Handler<AsyncResult<Boolean>> handler);
+
+ @Fluent
+ @Nonnull
+ IASNService markAsActivated(@Nonnull String asn, @Nonnull Handler<AsyncResult<Void>> handler);
+
+ @Fluent
+ @Nonnull
+ IASNService changePassword(@Nonnull String asn, @Nonnull String newPassword,
+ @Nonnull Handler<AsyncResult<Void>> handler);
+
+ @Fluent
+ @Nonnull
+ IASNService registerOrChangePassword(@Nonnull String asn, @Nonnull String newPassword,
+ @Nonnull Handler<AsyncResult<Void>> handler);
+
+ @Fluent
+ @Nonnull
+ IASNService auth(@Nonnull String asn, @Nonnull String password, @Nonnull Handler<AsyncResult<Boolean>> handler);
+
+ @Fluent
+ @Nonnull
+ IASNService delete(@Nonnull String asn, @Nonnull Handler<AsyncResult<Void>> handler);
+
+ @Fluent
+ @Nonnull
+ IASNService lookupEmails(@Nonnull WhoisObject asn, @Nonnull Handler<AsyncResult<List<String>>> handler);
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/asn/package-info.java b/central/src/main/java/moe/yuuta/dn42peering/asn/package-info.java
new file mode 100644
index 0000000..aa3d70a
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/asn/package-info.java
@@ -0,0 +1,4 @@
+@ModuleGen(groupPackage = "moe.yuuta.dn42peering.asn", name = "asn")
+package moe.yuuta.dn42peering.asn;
+
+import io.vertx.codegen.annotations.ModuleGen; \ No newline at end of file
diff --git a/central/src/main/java/moe/yuuta/dn42peering/jaba/OutParm.java b/central/src/main/java/moe/yuuta/dn42peering/jaba/OutParm.java
new file mode 100644
index 0000000..37ce0cf
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/jaba/OutParm.java
@@ -0,0 +1,22 @@
+package moe.yuuta.dn42peering.jaba;
+
+/**
+ * Oh Jesus how can't Jaba have out parameters ?!
+ *
+ * The out parameters in C# is such a beautiful innovation,
+ *
+ * as I can get rid of exceptions and write C style functions,
+ *
+ * just like <pre>int func(class *out);</pre>.
+ */
+public final class OutParm<E> {
+ public E out;
+
+ public OutParm() {
+ this(null);
+ }
+
+ public OutParm(E out) {
+ this.out = out;
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/jaba/Pair.java b/central/src/main/java/moe/yuuta/dn42peering/jaba/Pair.java
new file mode 100644
index 0000000..f0dba78
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/jaba/Pair.java
@@ -0,0 +1,16 @@
+package moe.yuuta.dn42peering.jaba;
+
+public class Pair<A, B> {
+ public final A a;
+ public final B b;
+
+ public Pair(A a, B b) {
+ this.a = a;
+ this.b = b;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Pair { a = %s, b = %s }", a, b);
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/manage/ASNAuthProvider.java b/central/src/main/java/moe/yuuta/dn42peering/manage/ASNAuthProvider.java
new file mode 100644
index 0000000..7b9730d
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/manage/ASNAuthProvider.java
@@ -0,0 +1,38 @@
+package moe.yuuta.dn42peering.manage;
+
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.auth.User;
+import io.vertx.ext.auth.authentication.AuthenticationProvider;
+import moe.yuuta.dn42peering.asn.IASNService;
+
+import javax.annotation.Nonnull;
+
+class ASNAuthProvider implements AuthenticationProvider {
+ private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+
+ private final IASNService asnService;
+
+ ASNAuthProvider(@Nonnull IASNService asnService) {
+ this.asnService = asnService;
+ }
+
+ @Override
+ public void authenticate(JsonObject credentials, Handler<AsyncResult<User>> resultHandler) {
+ final String username = credentials.getString("username");
+ final String password = credentials.getString("password");
+ Future.<Boolean>future(f -> asnService.auth(username.toUpperCase(), password, f))
+ .compose(succ -> {
+ if(succ) {
+ return Future.succeededFuture(User.fromName(username.toUpperCase()));
+ } else {
+ return Future.failedFuture("Incorrect logon");
+ }
+ })
+ .onComplete(resultHandler);
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/manage/ManageHandler.java b/central/src/main/java/moe/yuuta/dn42peering/manage/ManageHandler.java
new file mode 100644
index 0000000..aa04ea5
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/manage/ManageHandler.java
@@ -0,0 +1,1022 @@
+package moe.yuuta.dn42peering.manage;
+
+import com.wireguard.crypto.Key;
+import com.wireguard.crypto.KeyFormatException;
+import edazdarevic.commons.net.CIDRUtils;
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.common.template.TemplateEngine;
+import io.vertx.ext.web.handler.BasicAuthHandler;
+import io.vertx.ext.web.handler.BodyHandler;
+import io.vertx.ext.web.handler.SessionHandler;
+import io.vertx.ext.web.sstore.LocalSessionStore;
+import io.vertx.ext.web.templ.freemarker.FreeMarkerTemplateEngine;
+import io.vertx.ext.web.validation.RequestParameters;
+import io.vertx.ext.web.validation.RequestPredicate;
+import io.vertx.ext.web.validation.ValidationHandler;
+import io.vertx.ext.web.validation.builder.Bodies;
+import io.vertx.json.schema.SchemaParser;
+import io.vertx.json.schema.SchemaRouter;
+import io.vertx.json.schema.SchemaRouterOptions;
+import io.vertx.json.schema.common.dsl.ObjectSchemaBuilder;
+import moe.yuuta.dn42peering.agent.proto.BGPRequest;
+import moe.yuuta.dn42peering.agent.proto.VertxAgentGrpc;
+import moe.yuuta.dn42peering.agent.proto.WGRequest;
+import moe.yuuta.dn42peering.asn.IASNService;
+import moe.yuuta.dn42peering.jaba.Pair;
+import moe.yuuta.dn42peering.node.INodeService;
+import moe.yuuta.dn42peering.node.Node;
+import moe.yuuta.dn42peering.peer.IPeerService;
+import moe.yuuta.dn42peering.peer.Peer;
+import moe.yuuta.dn42peering.peer.ProvisionStatus;
+import moe.yuuta.dn42peering.portal.FormException;
+import moe.yuuta.dn42peering.portal.HTTPException;
+import moe.yuuta.dn42peering.portal.ISubRouter;
+import moe.yuuta.dn42peering.whois.IWhoisService;
+import moe.yuuta.dn42peering.whois.WhoisObject;
+import org.apache.commons.validator.routines.InetAddressValidator;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static io.vertx.ext.web.validation.builder.Parameters.param;
+import static io.vertx.json.schema.common.dsl.Schemas.*;
+
+public class ManageHandler implements ISubRouter {
+ private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+
+ @Nonnull
+ @Override
+ @SuppressWarnings("unchecked")
+ public Router mount(@Nonnull Vertx vertx) {
+ final IASNService asnService = IASNService.createProxy(vertx, IASNService.ADDRESS);
+ final IWhoisService whoisService = IWhoisService.createProxy(vertx, IWhoisService.ADDRESS);
+ final IPeerService peerService = IPeerService.createProxy(vertx);
+ final INodeService nodeService = INodeService.createProxy(vertx);
+ final TemplateEngine engine = FreeMarkerTemplateEngine.create(vertx, "ftlh");
+
+ final Router router = Router.router(vertx);
+ router.post().handler(BodyHandler.create().setBodyLimit(100 * 1024));
+ router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx)));
+ router.route().handler(BasicAuthHandler.create(new ASNAuthProvider(asnService)));
+ router.route().handler(ctx -> {
+ // Mark as activated.
+ asnService.markAsActivated(ctx.user().principal().getString("username"), ar -> {
+ if (ar.succeeded()) {
+ ctx.next();
+ } else {
+ ctx.fail(ar.cause());
+ }
+ });
+ });
+
+ router.get("/")
+ .produces("text/html")
+ .handler(ctx -> {
+ final String asn = ctx.user().principal().getString("username");
+ Future.<List<moe.yuuta.dn42peering.peer.Peer>>future(f ->
+ peerService.listUnderASN(asn, f))
+ .<Buffer>compose(peers -> Future.future(f -> renderIndex(engine, asn, peers, f)))
+ .onComplete(res -> {
+ if (res.succeeded()) {
+ ctx.response().end(res.result());
+ } else {
+ ctx.fail(res.cause());
+ }
+ });
+ });
+
+ router.get("/new")
+ .produces("text/html")
+ .handler(ctx -> {
+ final String asn = ctx.user().principal().getString("username");
+ renderForm(engine, nodeService, true, asn, null, null, res -> {
+ if (res.succeeded()) {
+ ctx.response().end(res.result());
+ } else {
+ res.cause().printStackTrace();
+ ctx.fail(res.cause());
+ }
+ });
+ });
+
+ final ObjectSchemaBuilder registerSchema = objectSchema()
+ .allowAdditionalProperties(false)
+ .property("ipv4", stringSchema())
+ .property("ipv6", stringSchema())
+ .property("mpbgp", stringSchema())
+ .property("vpn", enumSchema("wg"))
+ .property("wg_endpoint", stringSchema())
+ .property("wg_endpoint_port", stringSchema())
+ .property("wg_pubkey", stringSchema())
+ .property("node", stringSchema());
+ final SchemaParser parser = SchemaParser.createDraft7SchemaParser(
+ SchemaRouter.create(vertx, new SchemaRouterOptions()));
+
+ router.post("/new")
+ .handler(BodyHandler.create().setBodyLimit(100 * 1024))
+ .handler(ValidationHandler
+ .builder(parser)
+ .body(Bodies.formUrlEncoded(registerSchema))
+ .predicate(RequestPredicate.BODY_REQUIRED)
+ .build())
+ .handler(ctx -> {
+ final String asn = ctx.user().principal().getString("username");
+ final JsonObject parameters = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY)
+ .body().getJsonObject();
+ // Parse peer
+ parseForm(nodeService, parameters)
+ .compose(peer -> {
+ // Keys are generated during parsing.
+ peer.setAsn(asn);
+ return Future.succeededFuture(peer);
+ }).compose(peer ->
+ Future.<WhoisObject>future(f -> whoisService.query(peer.getIpv4(), f))
+ .compose(whoisObject -> {
+ if (whoisObject == null ||
+ !whoisObject.containsKey("origin") ||
+ !whoisObject.get("origin").contains(asn)) {
+ return Future.failedFuture(new FormException(peer,
+ "The IPv4 address you specified does not have a route with your ASN."));
+ }
+ // Verify IPv6
+ try {
+ if (peer.getIpv6() != null && !peer.isIPv6LinkLocal()) {
+ return Future.<WhoisObject>future(f -> whoisService.query(peer.getIpv6(), f))
+ .compose(ipv6Whois -> {
+ if (ipv6Whois == null ||
+ !ipv6Whois.containsKey("origin") ||
+ !ipv6Whois.get("origin").contains(asn)) {
+ return Future.failedFuture(new FormException(peer,
+ "The IPv6 address you specified does not have a route with your ASN."));
+ } else {
+ return Future.succeededFuture(peer);
+ }
+ });
+ } else {
+ // Do not check for IPv6 address.
+ return Future.succeededFuture(peer);
+ }
+ } catch (IOException e) {
+ return Future.failedFuture(e);
+ }
+ })
+ ).<Peer>compose(peer -> Future.future(f -> peerService.isIPConflict(peer.getType(),
+ peer.getIpv4(),
+ peer.getIpv6(),
+ ar -> {
+ if (ar.succeeded()) {
+ if (ar.result()) {
+ f.fail(new FormException(peer,
+ "The IPv4 or IPv6 you specified conflicts with an existing peering with the same type."));
+ } else {
+ f.complete(peer);
+ }
+ } else {
+ f.fail(ar.cause());
+ }
+ }))
+ ).<Peer>compose(peer -> Future.future(f -> peerService.addNew(peer, ar -> {
+ if (ar.succeeded()) {
+ peer.setId((int) (long) ar.result());
+ f.complete(peer);
+ } else f.fail(ar.cause());
+ })))
+ .onSuccess(peer -> {
+ ctx.response()
+ .setStatusCode(303)
+ .putHeader("Location", "/manage")
+ .end();
+ provisionPeer(vertx, nodeService, peer).onComplete(ar ->
+ this.handleProvisionResult(peerService, peer, ar));
+ })
+ .onFailure(err -> {
+ if (err instanceof HTTPException) {
+ ctx.response().setStatusCode(((HTTPException) err).code).end();
+ } else if (err instanceof FormException) {
+ renderForm(engine, nodeService,
+ true, asn,
+ ((Peer) ((FormException) err).data),
+ Arrays.asList(((FormException) err).errors),
+ res -> {
+ if (res.succeeded()) {
+ ctx.response().end(res.result());
+ } else {
+ ctx.fail(res.cause());
+ }
+ });
+ } else {
+ logger.error("Cannot add peer.", err);
+ ctx.fail(err);
+ }
+ });
+ });
+
+ router.get("/edit")
+ .produces("text/html")
+ .handler(ValidationHandler
+ .builder(parser)
+ .queryParameter(param("id", stringSchema()))
+ .build())
+ .handler(ctx -> {
+ final String asn = ctx.user().principal().getString("username");
+ final String id = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY)
+ .queryParameter("id").getString();
+ Future.<Peer>future(f -> peerService.getSingle(asn, id, f))
+ .compose(peer -> {
+ if (peer == null) {
+ return Future.failedFuture(new HTTPException(404));
+ }
+ return Future.succeededFuture(peer);
+ })
+ .<Buffer>compose(peer -> Future.future(f -> renderForm(engine, nodeService, false,
+ asn, peer, null, f)))
+ .onComplete(res -> {
+ if (res.succeeded()) {
+ ctx.response().end(res.result());
+ } else {
+ res.cause().printStackTrace();
+ ctx.fail(res.cause());
+ }
+ });
+ });
+
+ router.post("/edit")
+ .handler(BodyHandler.create().setBodyLimit(100 * 1024))
+ .handler(ValidationHandler
+ .builder(parser)
+ .body(Bodies.formUrlEncoded(registerSchema))
+ .queryParameter(param("id", stringSchema()))
+ .predicate(RequestPredicate.BODY_REQUIRED)
+ .build())
+ .handler(ctx -> {
+ final String asn = ctx.user().principal().getString("username");
+ final JsonObject parameters = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY)
+ .body().getJsonObject();
+ final String id = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY)
+ .queryParameter("id").getString();
+ Future.<Peer>future(f -> peerService.getSingle(asn, id, f))
+ .compose(peer -> {
+ if (peer == null) {
+ return Future.failedFuture(new HTTPException(404));
+ }
+ return Future.succeededFuture(peer);
+ }).compose(existingPeer ->
+ parseForm(nodeService, parameters)
+ .compose(inPeer -> {
+ // Preserve keys
+ inPeer.setWgSelfPrivKey(existingPeer.getWgSelfPrivKey());
+ inPeer.setWgSelfPubkey(existingPeer.getWgSelfPubkey());
+ inPeer.setWgPresharedSecret(existingPeer.getWgPresharedSecret());
+ inPeer.setAsn(asn);
+ inPeer.setId(existingPeer.getId());
+ return Future.succeededFuture(new Pair<>(existingPeer, inPeer));
+ })
+ ).compose(peer -> {
+ final Peer inPeer = peer.b;
+ return Future.<WhoisObject>future(f -> whoisService.query(inPeer.getIpv4(), f))
+ .compose(whoisObject -> {
+ if (whoisObject == null ||
+ !whoisObject.containsKey("origin") ||
+ !whoisObject.get("origin").contains(asn)) {
+ return Future.failedFuture(new FormException(inPeer,
+ "The IPv4 address you specified does not have a route with your ASN."));
+ }
+ // Verify IPv6
+ try {
+ if (inPeer.getIpv6() != null && !inPeer.isIPv6LinkLocal()) {
+ return Future.<WhoisObject>future(f -> whoisService.query(inPeer.getIpv6(), f))
+ .compose(ipv6Whois -> {
+ if (ipv6Whois == null ||
+ !ipv6Whois.containsKey("origin") ||
+ !ipv6Whois.get("origin").contains(asn)) {
+ return Future.failedFuture(new FormException(inPeer,
+ "The IPv6 address you specified does not have a route with your ASN."));
+ } else {
+ return Future.succeededFuture(peer);
+ }
+ });
+ } else {
+ // Do not check for IPv6 address.
+ return Future.succeededFuture(peer);
+ }
+ } catch (IOException e) {
+ return Future.failedFuture(e);
+ }
+ });
+ }
+ ).<Pair<Peer /* Existing */, Peer /* Input */>>compose(peer -> {
+ final Peer existingPeer = peer.a;
+ final Peer inPeer = peer.b;
+ boolean needCheckIPv4Conflict;
+ boolean needCheckIPv6Conflict;
+
+ if (existingPeer.getType() != inPeer.getType()) {
+ needCheckIPv4Conflict = true;
+ needCheckIPv6Conflict = true;
+ } else {
+ needCheckIPv4Conflict =
+ !Objects.equals(existingPeer.getIpv4(), inPeer.getIpv4());
+ needCheckIPv6Conflict =
+ !Objects.equals(existingPeer.getIpv6(), inPeer.getIpv6());
+ if (inPeer.getIpv6() == null) needCheckIPv6Conflict = false;
+ }
+ final boolean nc6 = needCheckIPv6Conflict;
+ return Future.future(f -> peerService.isIPConflict(inPeer.getType(),
+ needCheckIPv4Conflict ? inPeer.getIpv4() : null,
+ nc6 ? inPeer.getIpv6() : null,
+ ar -> {
+ if (ar.succeeded()) {
+ if (ar.result()) {
+ f.fail(new FormException(peer,
+ "The IPv4 or IPv6 you specified conflicts with an existing peering with the same type."));
+ } else {
+ f.complete(peer);
+ }
+ } else {
+ f.fail(ar.cause());
+ }
+ }));
+ }
+ ).<Pair<Peer /* Existing */, Peer /* Input */>>compose(peer ->
+ Future.future(f -> peerService.updateTo(peer.b /* New Peer */, ar -> {
+ if (ar.succeeded()) f.complete(peer);
+ else f.fail(ar.cause());
+ })))
+ .onSuccess(pair -> {
+ ctx.response()
+ .setStatusCode(303)
+ .putHeader("Location", "/manage")
+ .end();
+ final Peer existingPeer = pair.a;
+ final Peer inPeer = pair.b;
+ reloadPeer(vertx, nodeService, existingPeer, inPeer).onComplete(ar ->
+ this.handleProvisionResult(peerService, inPeer, ar));
+ })
+ .onFailure(err -> {
+ if (err instanceof HTTPException) {
+ ctx.response().setStatusCode(((HTTPException) err).code).end();
+ } else if (err instanceof FormException) {
+ renderForm(engine,
+ nodeService,
+ false,
+ asn,
+ (((Pair<Peer, Peer>) ((FormException) err).data).b),
+ Arrays.asList(((FormException) err).errors),
+ res -> {
+ if (res.succeeded()) {
+ ctx.response().end(res.result());
+ } else {
+ ctx.fail(res.cause());
+ }
+ });
+ } else {
+ logger.error("Cannot edit peer.", err);
+ ctx.fail(err);
+ }
+ });
+ });
+
+ router.get("/delete")
+ .handler(ValidationHandler
+ .builder(parser)
+ .queryParameter(param("id", stringSchema()))
+ .build())
+ .handler(ctx -> {
+ final String asn = ctx.user().principal().getString("username");
+ final String id = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY)
+ .queryParameter("id").getString();
+ Future.<Peer>future(f -> peerService.getSingle(asn, id, f))
+ .compose(peer -> {
+ if (peer == null) {
+ return Future.failedFuture(new HTTPException(404));
+ }
+ return Future.succeededFuture(peer);
+ })
+ .compose(peer -> unprovisionPeer(vertx, nodeService, peer))
+ .compose(_v -> Future.<Void>future(f -> peerService.deletePeer(asn, id, f)))
+ .onSuccess(_id -> ctx.response()
+ .setStatusCode(303)
+ .putHeader("Location", "/manage")
+ .end())
+ .onFailure(err -> {
+ if (err instanceof HTTPException) {
+ ctx.response().setStatusCode(((HTTPException) err).code).end();
+ } else {
+ logger.error("Cannot delete peer.", err);
+ ctx.fail(err);
+ }
+ });
+ });
+
+ router.get("/change-password")
+ .produces("text/html")
+ .handler(ctx -> {
+ final String asn = ctx.user().principal().getString("username");
+ renderChangepw(engine, asn, null, res -> {
+ if (res.succeeded()) {
+ ctx.response().end(res.result());
+ } else {
+ res.cause().printStackTrace();
+ ctx.fail(res.cause());
+ }
+ });
+ });
+
+ router.post("/change-password")
+ .handler(BodyHandler.create().setBodyLimit(100 * 1024))
+ .handler(ValidationHandler
+ .builder(parser)
+ .body(Bodies.formUrlEncoded(objectSchema()
+ .property("passwd", stringSchema())
+ .property("confirm", stringSchema())
+ .allowAdditionalProperties(false)))
+ .predicate(RequestPredicate.BODY_REQUIRED)
+ .build())
+ .handler(ctx -> {
+ final String asn = ctx.user().principal().getString("username");
+ final JsonObject parameters = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY)
+ .body().getJsonObject();
+ final String passwd = parameters.getString("passwd");
+ final String confirm = parameters.getString("confirm");
+ Future.<Void>future(f -> {
+ if (passwd == null || confirm == null) {
+ f.fail(new FormException("Some fields are not supplied."));
+ } else {
+ if (!passwd.equals(confirm)) {
+ f.fail(new FormException("Two passwords do not match."));
+ } else {
+ f.complete();
+ }
+ }
+ }).<Void>compose(v -> Future.future(f -> asnService.changePassword(asn, passwd, f)))
+ .onSuccess(_void -> {
+ ctx.session().destroy();
+ ctx.response()
+ .setStatusCode(303)
+ .putHeader("Location", "/manage")
+ .end();
+ })
+ .onFailure(err -> {
+ if (err instanceof HTTPException) {
+ ctx.response().setStatusCode(((HTTPException) err).code).end();
+ } else if (err instanceof FormException) {
+ renderChangepw(engine, asn,
+ Arrays.asList(((FormException) err).errors),
+ res -> {
+ if (res.succeeded()) {
+ ctx.response().end(res.result());
+ } else {
+ res.cause().printStackTrace();
+ ctx.fail(res.cause());
+ }
+ });
+ } else {
+ logger.error("Cannot change password.", err);
+ ctx.fail(err);
+ }
+ });
+ });
+
+ router.get("/delete-account")
+ .produces("text/html")
+ .handler(ctx -> {
+ final String asn = ctx.user().principal().getString("username");
+ renderDA(engine, asn, null, res -> {
+ if (res.succeeded()) {
+ ctx.response().end(res.result());
+ } else {
+ res.cause().printStackTrace();
+ ctx.fail(res.cause());
+ }
+ });
+ });
+
+ router.post("/delete-account")
+ .produces("text/html")
+ .handler(ctx -> {
+ final String asn = ctx.user().principal().getString("username");
+ Future.<Void>future(f -> peerService.existsUnderASN(asn, ar -> {
+ if (ar.succeeded()) {
+ if (ar.result()) {
+ f.fail(new FormException("There are still active peers. Delete them before deleting the account."));
+ } else {
+ f.complete(null);
+ }
+ } else {
+ f.fail(ar.cause());
+ }
+ })).<Void>compose(v -> Future.future(f -> asnService.delete(asn, f)))
+ .onSuccess(_void -> {
+ ctx.session().destroy();
+ ctx.response()
+ .setStatusCode(303)
+ .putHeader("Location", "/")
+ .end();
+ })
+ .onFailure(err -> {
+ if (err instanceof HTTPException) {
+ ctx.response().setStatusCode(((HTTPException) err).code).end();
+ } else if (err instanceof FormException) {
+ renderDA(engine, asn,
+ Arrays.asList(((FormException) err).errors),
+ res -> {
+ if (res.succeeded()) {
+ ctx.response().end(res.result());
+ } else {
+ res.cause().printStackTrace();
+ ctx.fail(res.cause());
+ }
+ });
+ } else {
+ logger.error("Cannot delete account.", err);
+ ctx.fail(err);
+ }
+ });
+ });
+
+ router.get("/show-configuration")
+ .produces("text/html")
+ .handler(ValidationHandler
+ .builder(parser)
+ .queryParameter(param("id", stringSchema()))
+ .build())
+ .handler(ctx -> {
+ final String asn = ctx.user().principal().getString("username");
+ final String id = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY)
+ .queryParameter("id").getString();
+ Future.<Peer>future(f -> peerService.getSingle(asn, id, f))
+ .compose(peer -> {
+ if (peer == null) {
+ return Future.failedFuture(new HTTPException(404));
+ }
+ return Future.succeededFuture(peer);
+ })
+ .compose(peer -> renderShowConfig(nodeService, engine, peer))
+ .onComplete(res -> {
+ if (res.succeeded()) {
+ ctx.response().end(res.result());
+ } else {
+ res.cause().printStackTrace();
+ ctx.fail(res.cause());
+ }
+ });
+ });
+
+ return router;
+ }
+
+ @Nonnull
+ private Future<Peer> parseForm(@Nonnull INodeService nodeService,
+ @Nonnull JsonObject form) {
+ // Parse form
+ int nodeId = -1;
+ if (form.containsKey("node")) {
+ try {
+ nodeId = Integer.parseInt(form.getString("node"));
+ } catch (NumberFormatException ignored) {
+ }
+ if(nodeId == -1) {
+ return Future.failedFuture(new FormException("The node selection is invalid."));
+ }
+ }
+
+ int n = nodeId;
+ return Future.<Node>future(f -> nodeService.getNode(n, f))
+ .compose(node -> {
+ try {
+ final List<String> errors = new ArrayList<>(10);
+ if(node == null) {
+ errors.add("The node selection is invalid.");
+ }
+ Peer.VPNType type = null;
+ if (form.containsKey("vpn")) {
+ final String rawVPN = form.getString("vpn");
+ if (rawVPN == null) {
+ errors.add("Tunneling type is not specified.");
+ } else
+ switch (rawVPN) {
+ case "wg":
+ type = Peer.VPNType.WIREGUARD;
+ break;
+ default:
+ errors.add("Tunneling type is unexpected.");
+ break;
+ }
+ } else {
+ errors.add("Tunneling type is not specified.");
+ }
+
+ String ipv4 = null;
+ if (form.containsKey("ipv4")) {
+ final String preIpv4 = form.getString("ipv4");
+ if (preIpv4 == null || preIpv4.isEmpty()) {
+ errors.add("IPv4 address is not specified.");
+ } else {
+ if (InetAddressValidator.getInstance().isValidInet4Address(preIpv4)) {
+ if (!new CIDRUtils("172.20.0.0/14").isInRange(preIpv4)) {
+ errors.add("IPv4 address is illegal. It must be a dn42 IPv4 address (172.20.x.x to 172.23.x.x).");
+ } else {
+ ipv4 = preIpv4;
+ }
+ } else
+ errors.add("IPv4 address is illegal. Cannot parse your address.");
+ }
+ } else {
+ errors.add("IPv4 address is not specified.");
+ }
+
+ String ipv6 = null;
+ if (form.containsKey("ipv6")) {
+ final String preIpv6 = form.getString("ipv6");
+ if (preIpv6 != null && !preIpv6.isEmpty()) {
+ if (InetAddressValidator.getInstance().isValidInet6Address(preIpv6)) {
+ if (!new CIDRUtils("fd00::/8").isInRange(preIpv6) &&
+ !Inet6Address.getByName(preIpv6).isLinkLocalAddress()) {
+ errors.add("IPv6 address is illegal. It must be a dn42 or link-local IPv6 address.");
+ } else {
+ ipv6 = preIpv6;
+ }
+ } else
+ errors.add("IPv6 address is illegal. Cannot parse your address.");
+ }
+ }
+
+ boolean mpbgp = false;
+ if (form.containsKey("mpbgp")) {
+ if (ipv6 == null) {
+ errors.add("MP-BGP cannot be enabled if you do not have a valid IPv6 address.");
+ } else {
+ mpbgp = true;
+ }
+ }
+
+ String wgEndpoint = null;
+ boolean wgEndpointCorrect = false;
+ if (form.containsKey("wg_endpoint")) {
+ if (type == Peer.VPNType.WIREGUARD) {
+ final String preIpv4 = form.getString("wg_endpoint");
+ if (preIpv4 == null || preIpv4.isEmpty()) {
+ errors.add("WireGuard tunneling is not selected but WireGuard Endpoint configuration appears.");
+ } else {
+ if (InetAddressValidator.getInstance().isValidInet4Address(preIpv4)) {
+ if (new CIDRUtils("10.0.0.0/8").isInRange(preIpv4) ||
+ new CIDRUtils("192.168.0.0/16").isInRange(preIpv4) ||
+ new CIDRUtils("172.16.0.0/23").isInRange(preIpv4)) {
+ errors.add("WireGuard EndPoint is illegal. It must not be an internal address.");
+ } else {
+ wgEndpoint = preIpv4;
+ wgEndpointCorrect = true;
+ }
+ } else
+ errors.add("WireGuard EndPoint is illegal. Cannot parse your address.");
+ }
+ } else {
+ errors.add("WireGuard tunneling is not selected but WireGuard Endpoint configuration appears.");
+ }
+ }
+
+ int wgEndpointPort = -1;
+ if (form.containsKey("wg_endpoint_port")) {
+ if (type == Peer.VPNType.WIREGUARD) {
+ if (wgEndpointCorrect) {
+ try {
+ wgEndpointPort = Integer.parseInt(form.getString("wg_endpoint_port"));
+ if (wgEndpointPort < 0 || wgEndpointPort > 65535) {
+ errors.add("WireGuard EndPoint port must be in UDP port range.");
+ wgEndpointPort = -1;
+ }
+ } catch (NumberFormatException | NullPointerException ignored) {
+ errors.add("WireGuard EndPoint port is not valid. It must be a number.");
+ }
+ } else {
+ errors.add("WireGuard EndPoint IP is not specified, but port is specified.");
+ }
+ } else {
+ errors.add("WireGuard tunneling is not selected but WireGuard Endpoint configuration appears.");
+ }
+ }
+
+ String wgPubKey = null;
+ if (form.containsKey("wg_pubkey")) {
+ if (type == Peer.VPNType.WIREGUARD) {
+ wgPubKey = form.getString("wg_pubkey");
+ if (wgPubKey == null || wgPubKey.isEmpty()) {
+ errors.add("WireGuard public key is not specified.");
+ } else {
+ try {
+ Key.fromBase64(wgPubKey);
+ } catch (KeyFormatException e) {
+ errors.add("WireGuard public key is not valid.");
+ wgPubKey = null;
+ }
+ }
+ } else {
+ errors.add("WireGuard tunneling is not selected but WireGuard public key appears.");
+ }
+ } else {
+ if (type == Peer.VPNType.WIREGUARD) {
+ errors.add("WireGuard public key is not specified.");
+ }
+ }
+
+ if(node != null && !node.getSupportedVPNTypes().contains(type)) {
+ errors.add(String.format("Node %s does not support VPN type %s.", node.getName(),
+ type));
+ }
+
+ Peer peer;
+ if (type == Peer.VPNType.WIREGUARD) {
+ peer = new Peer(ipv4, ipv6, wgEndpoint, wgEndpointPort, wgPubKey, mpbgp, n);
+ } else {
+ peer = new Peer(
+ -1,
+ type,
+ null, /* ASN: To be filled later */
+ ipv4,
+ ipv6,
+ wgEndpoint,
+ wgEndpointPort,
+ null, /* Self public key: Generate later if needed */
+ null, /* Self private key: Generate later if needed */
+ wgPubKey,
+ null /* Preshared Secret: Generate later if needed */,
+ ProvisionStatus.NOT_PROVISIONED,
+ mpbgp,
+ n
+ );
+ }
+ if(errors.isEmpty()) {
+ return Future.succeededFuture(peer);
+ } else {
+ return Future.failedFuture(new FormException(peer, errors.toArray(new String[]{})));
+ }
+ } catch (IOException e) {
+ return Future.failedFuture(e);
+ }
+ });
+ }
+
+ private void renderIndex(@Nonnull TemplateEngine engine,
+ @Nonnull String asn, @Nonnull List<Peer> peers,
+ @Nonnull Handler<AsyncResult<Buffer>> handler) {
+ final Map<String, Object> root = new HashMap<>();
+ root.put("asn", asn);
+ root.put("peers", peers.stream()
+ .map(peer -> {
+ final Map<String, Object> map = new HashMap<>();
+ map.put("id", peer.getId());
+ map.put("ipv4", peer.getIpv4());
+ map.put("ipv6", peer.getIpv6());
+ map.put("type", peer.getType());
+ map.put("provisionStatus", peer.getProvisionStatus());
+ return map;
+ })
+ .collect(Collectors.toList()));
+ engine.render(root, "manage/index.ftlh", handler);
+ }
+
+ @SuppressWarnings("unchecked")
+ private void renderForm(@Nonnull TemplateEngine engine,
+ @Nonnull INodeService nodeService,
+ boolean newForm,
+ @Nonnull String asn, @Nullable Peer peer, @Nullable List<String> errors,
+ @Nonnull Handler<AsyncResult<Buffer>> handler) {
+ Future.future(nodeService::listNodes)
+ .compose(list -> {
+ final Map<String, Object> root = new HashMap<>();
+ root.put("asn", asn);
+ root.put("nodes", list.stream()
+ .map(node -> {
+ final Map<String, Object> map = new HashMap<>(10);
+ map.put("id", node.getId());
+ map.put("name", node.getName());
+ map.put("public_ip", node.getPublicIp());
+ map.put("notice", node.getNotice());
+ map.put("asn", node.getAsn());
+ map.put("vpn_types", node.getSupportedVPNTypes());
+ return map;
+ })
+ .collect(Collectors.toList()));
+ if (peer != null) {
+ root.put("ipv4", peer.getIpv4());
+ root.put("ipv6", peer.getIpv6());
+ switch (peer.getType()) {
+ case WIREGUARD:
+ root.put("typeWireguard", true);
+ break;
+ }
+ root.put("wgEndpoint", peer.getWgEndpoint());
+ root.put("wgEndpointPort", peer.getWgEndpointPort());
+ root.put("wgPubkey", peer.getWgPeerPubkey());
+ root.put("mpbgp", peer.isMpbgp());
+ root.put("node_checked", peer.getNode());
+ root.put("id", peer.getId());
+ } else {
+ root.put("typeWireguard", true);
+ root.put("mpbgp", false);
+ root.put("node_checked", ((List<Map<String, Object>>)root.get("nodes")).get(0).get("id"));
+ }
+ if(!newForm && peer != null)
+ root.put("action", "/manage/edit?id=" + peer.getId());
+ else
+ root.put("action", "/manage/new");
+ root.put("errors", errors);
+ return engine.render(root, newForm ? "manage/new.ftlh" : "manage/edit.ftlh");
+ })
+ .onComplete(handler);
+ }
+
+ private void renderChangepw(@Nonnull TemplateEngine engine,
+ @Nonnull String asn, @Nullable List<String> errors,
+ @Nonnull Handler<AsyncResult<Buffer>> handler) {
+ final Map<String, Object> root = new HashMap<>();
+ root.put("asn", asn);
+ root.put("errors", errors);
+ engine.render(root, "manage/changepw.ftlh", handler);
+ }
+
+ private void renderDA(@Nonnull TemplateEngine engine,
+ @Nonnull String asn, @Nullable List<String> errors,
+ @Nonnull Handler<AsyncResult<Buffer>> handler) {
+ final Map<String, Object> root = new HashMap<>();
+ root.put("asn", asn);
+ root.put("errors", errors);
+ engine.render(root, "manage/delete.ftlh", handler);
+ }
+
+ @Nonnull
+ private Future<Buffer> renderShowConfig(@Nonnull INodeService nodeService,
+ @Nonnull TemplateEngine engine,
+ @Nonnull Peer peer) {
+ return Future.<Node>future(f -> nodeService.getNode(peer.getNode(), f))
+ .compose(node -> {
+ final Map<String, Object> root = new HashMap<>();
+ root.put("ipv4", peer.getIpv4());
+ root.put("ipv6", peer.getIpv6());
+ switch (peer.getType()) {
+ case WIREGUARD:
+ root.put("typeWireguard", true);
+ break;
+ }
+ root.put("wgPort", peer.calcWireGuardPort());
+ root.put("wgEndpoint", peer.getWgEndpoint());
+ root.put("wgEndpointPort", peer.getWgEndpointPort());
+ root.put("wgPresharedSecret", peer.getWgPresharedSecret());
+ root.put("wgSelfPubkey", peer.getWgSelfPubkey());
+ root.put("mpbgp", peer.isMpbgp());
+
+ if(node == null) {
+ root.put("ipv4", "This node is currently down! Edit the peer to choose another one.");
+ root.put("ipv6", "This node is currently down! Edit the peer to choose another one.");
+ root.put("asn", "This node is currently down! Edit the peer to choose another one.");
+ root.put("endpoint", "This node is currently down! Edit the peer to choose another one.");
+ } else {
+ root.put("ipv4", node.getDn42Ip4());
+ root.put("ipv6", node.getDn42Ip6());
+ root.put("asn", node.getAsn());
+ root.put("endpoint", node.getPublicIp());
+ }
+
+ return engine.render(root, "manage/showconf.ftlh");
+ });
+
+ }
+
+ @Nonnull
+ private Future<Void> reloadPeer(@Nonnull Vertx vertx,
+ @Nonnull INodeService nodeService,
+ @Nonnull Peer existingPeer, @Nonnull Peer inPeer) {
+ // Check if we can reload on the fly.
+ // Otherwise, we can only deprovision and provision.
+ // This will cause unnecessary wastes.
+ final boolean canReload = inPeer.getType() == existingPeer.getType() &&
+ inPeer.getNode() == existingPeer.getNode();
+ Future<Void> future;
+ if (canReload) {
+ future = Future.<Node>future(f -> nodeService.getNode(inPeer.getNode(), f))
+ .compose(node -> {
+ if(node == null || !node.getSupportedVPNTypes().contains(inPeer.getType())) {
+ return Future.failedFuture("The node does not exist");
+ }
+ return Future.succeededFuture(node);
+ })
+ .compose(node -> {
+ final VertxAgentGrpc.AgentVertxStub stub = VertxAgentGrpc.newVertxStub(node.toChannel(vertx));
+ switch (existingPeer.getType()) {
+ case WIREGUARD:
+ return stub.reloadWG(
+ inPeer.toWGRequest().setNode(node.toRPCNode()).build()
+ ).compose(wgReply -> Future.succeededFuture(new Pair<>(node, wgReply.getDevice())));
+ default:
+ throw new UnsupportedOperationException("Bug: Unknown type.");
+ }
+ })
+ .compose(pair -> {
+ final VertxAgentGrpc.AgentVertxStub stub = VertxAgentGrpc.newVertxStub(pair.a.toChannel(vertx));
+ return stub.reloadBGP(inPeer.toBGPRequest()
+ .setNode(pair.a.toRPCNode())
+ .setDevice(pair.b)
+ .build())
+ .compose(reply -> Future.succeededFuture(null));
+ });
+ } else {
+ future = unprovisionPeer(vertx, nodeService, existingPeer)
+ .compose(f -> provisionPeer(vertx, nodeService, inPeer));
+ }
+ return future;
+ }
+
+ private Future<Void> unprovisionPeer(@Nonnull Vertx vertx,
+ @Nonnull INodeService nodeService,
+ @Nonnull Peer existingPeer) {
+ return Future.<Node>future(f -> nodeService.getNode(existingPeer.getNode(), f))
+ .compose(node -> {
+ if(node == null) {
+ return Future.failedFuture("The node does not exist");
+ }
+ return Future.succeededFuture(node);
+ })
+ .compose(node -> {
+ final VertxAgentGrpc.AgentVertxStub stub = VertxAgentGrpc.newVertxStub(node.toChannel(vertx));
+ switch (existingPeer.getType()) {
+ case WIREGUARD:
+ return stub.deleteWG(WGRequest.newBuilder().setId(existingPeer.getId()).build())
+ .compose(wgReply -> Future.succeededFuture(node));
+ default:
+ throw new UnsupportedOperationException("Bug: Unknown type.");
+ }
+ })
+ .compose(node -> {
+ final VertxAgentGrpc.AgentVertxStub stub = VertxAgentGrpc.newVertxStub(node.toChannel(vertx));
+ return stub.deleteBGP(BGPRequest.newBuilder().setId(existingPeer.getId())
+ .build())
+ .compose(reply -> Future.succeededFuture(null));
+ })
+ ;
+ }
+
+ @Nonnull
+ private Future<Void> provisionPeer(@Nonnull Vertx vertx,
+ @Nonnull INodeService nodeService,
+ @Nonnull Peer inPeer) {
+ return Future.<Node>future(f -> nodeService.getNode(inPeer.getNode(), f))
+ .compose(node -> {
+ if(node == null || !node.getSupportedVPNTypes().contains(inPeer.getType())) {
+ return Future.failedFuture("The node does not exist");
+ }
+ return Future.succeededFuture(node);
+ })
+ .compose(node -> {
+ final VertxAgentGrpc.AgentVertxStub stub = VertxAgentGrpc.newVertxStub(node.toChannel(vertx));
+ switch (inPeer.getType()) {
+ case WIREGUARD:
+ return stub.provisionWG(
+ inPeer.toWGRequest().setNode(node.toRPCNode()).build()
+ ).compose(wgReply -> Future.succeededFuture(new Pair<>(node, wgReply.getDevice())));
+ default:
+ throw new UnsupportedOperationException("Bug: Unknown type.");
+ }
+ })
+ .compose(pair -> {
+ final VertxAgentGrpc.AgentVertxStub stub = VertxAgentGrpc.newVertxStub(pair.a.toChannel(vertx));
+ return stub.provisionBGP(inPeer.toBGPRequest()
+ .setNode(pair.a.toRPCNode())
+ .setDevice(pair.b)
+ .build())
+ .compose(reply -> Future.succeededFuture(null));
+ });
+ }
+
+ private void handleProvisionResult(@Nonnull IPeerService peerService,
+ @Nonnull Peer inPeer,
+ @Nonnull AsyncResult<Void> res) {
+ if(res.succeeded()) {
+ peerService.changeProvisionStatus(inPeer.getId(),
+ ProvisionStatus.PROVISIONED, ar -> {
+ if (ar.failed()) {
+ logger.error(String.format("Cannot update %d to provisioned.", inPeer.getId()), ar.cause());
+ }
+ });
+ } else {
+ logger.error(String.format("Cannot provision %d.", inPeer.getId()), res.cause());
+ peerService.changeProvisionStatus(inPeer.getId(),
+ ProvisionStatus.FAIL, ar -> {
+ if (ar.failed()) {
+ logger.error(String.format("Cannot update %d to failed.", inPeer.getId()), ar.cause());
+ }
+ });
+ }
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/node/INodeService.java b/central/src/main/java/moe/yuuta/dn42peering/node/INodeService.java
new file mode 100644
index 0000000..9323faa
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/node/INodeService.java
@@ -0,0 +1,28 @@
+package moe.yuuta.dn42peering.node;
+
+import io.vertx.codegen.annotations.Fluent;
+import io.vertx.codegen.annotations.ProxyGen;
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+
+@ProxyGen
+public interface INodeService {
+ String ADDRESS = INodeService.class.getName();
+
+ @Nonnull
+ static INodeService createProxy(@Nonnull Vertx vertx) {
+ return new INodeServiceVertxEBProxy(vertx, ADDRESS);
+ }
+
+ @Fluent
+ @Nonnull
+ INodeService listNodes(@Nonnull Handler<AsyncResult<List<Node>>> handler);
+
+ @Fluent
+ @Nonnull
+ INodeService getNode(int id, @Nonnull Handler<AsyncResult<Node>> handler);
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/node/Node.java b/central/src/main/java/moe/yuuta/dn42peering/node/Node.java
new file mode 100644
index 0000000..31c0be2
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/node/Node.java
@@ -0,0 +1,204 @@
+package moe.yuuta.dn42peering.node;
+
+import io.grpc.ManagedChannel;
+import io.vertx.codegen.annotations.DataObject;
+import io.vertx.codegen.annotations.GenIgnore;
+import io.vertx.codegen.format.SnakeCase;
+import io.vertx.core.Vertx;
+import io.vertx.core.json.JsonObject;
+import io.vertx.grpc.VertxChannelBuilder;
+import io.vertx.sqlclient.templates.annotations.Column;
+import io.vertx.sqlclient.templates.annotations.ParametersMapped;
+import io.vertx.sqlclient.templates.annotations.RowMapped;
+import io.vertx.sqlclient.templates.annotations.TemplateParameter;
+import moe.yuuta.dn42peering.peer.Peer;
+
+import javax.annotation.Nonnull;
+import java.util.ArrayList;
+import java.util.List;
+
+@DataObject
+@RowMapped(formatter = SnakeCase.class)
+@ParametersMapped(formatter = SnakeCase.class)
+public class Node {
+ @Column(name = "id")
+ @TemplateParameter(name = "id")
+ private int id;
+
+ @Column(name = "public_ip")
+ @TemplateParameter(name = "public_ip")
+ private String publicIp;
+
+ @Column(name = "dn42_ip4")
+ @TemplateParameter(name = "dn42_ip4")
+ private String dn42Ip4;
+
+ @Column(name = "dn42_ip6")
+ @TemplateParameter(name = "dn42_ip6")
+ private String dn42Ip6;
+
+ @Column(name = "asn")
+ @TemplateParameter(name = "asn")
+ private String asn;
+
+ @Column(name = "internal_ip")
+ @TemplateParameter(name = "internal_ip")
+ private String internalIp;
+
+ @Column(name = "internal_port")
+ @TemplateParameter(name = "internal_port")
+ private int internalPort;
+
+ @Column(name = "name")
+ @TemplateParameter(name = "name")
+ private String name;
+
+ @Column(name = "notice")
+ @TemplateParameter(name = "notice")
+ private String notice;
+
+ @Column(name = "vpn_type_wg")
+ @TemplateParameter(name = "vpn_type_wg")
+ private boolean wireguard;
+
+ public Node() {}
+
+ public Node(@Nonnull JsonObject object) {
+ this.id = object.getInteger("id");
+ this.publicIp = object.getString("public_ip");
+ this.dn42Ip4 = object.getString("dn42_ip4");
+ this.dn42Ip6 = object.getString("dn42_ip6");
+ this.asn = object.getString("asn");
+ this.internalIp = object.getString("internal_ip");
+ this.internalPort = object.getInteger("internal_port");
+ this.name = object.getString("name");
+ this.notice = object.getString("notice");
+ this.wireguard = object.getBoolean("wireguard");
+ }
+
+ @Nonnull
+ public JsonObject toJson() {
+ return new JsonObject()
+ .put("id", id)
+ .put("public_ip", publicIp)
+ .put("dn42_ip4", dn42Ip4)
+ .put("dn42_ip6", dn42Ip6)
+ .put("asn", asn)
+ .put("internal_ip", internalIp)
+ .put("internal_port", internalPort)
+ .put("name", name)
+ .put("notice", notice)
+ .put("wireguard", wireguard);
+ }
+
+ @GenIgnore
+ @Nonnull
+ public List<Peer.VPNType> getSupportedVPNTypes() {
+ final List<Peer.VPNType> vpnTypes = new ArrayList<>(1);
+ if(wireguard) vpnTypes.add(Peer.VPNType.WIREGUARD);
+ return vpnTypes;
+ }
+
+ @GenIgnore
+ @Nonnull
+ public moe.yuuta.dn42peering.agent.proto.Node toRPCNode() {
+ return moe.yuuta.dn42peering.agent.proto.Node.newBuilder()
+ .setId(id)
+ .setIpv4(dn42Ip4)
+ .setIpv6(dn42Ip6)
+ .build();
+ }
+
+ @GenIgnore
+ @Nonnull
+ public ManagedChannel toChannel(@Nonnull Vertx vertx) {
+ return VertxChannelBuilder.forAddress(vertx, internalIp, internalPort)
+ .usePlaintext()
+ .build();
+ }
+
+ // BEGIN GETTER / SETTER
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public String getPublicIp() {
+ return publicIp;
+ }
+
+ public void setPublicIp(String publicIp) {
+ this.publicIp = publicIp;
+ }
+
+ public String getDn42Ip4() {
+ return dn42Ip4;
+ }
+
+ public void setDn42Ip4(String dn42Ip4) {
+ this.dn42Ip4 = dn42Ip4;
+ }
+
+ public String getDn42Ip6() {
+ return dn42Ip6;
+ }
+
+ public void setDn42Ip6(String dn42Ip6) {
+ this.dn42Ip6 = dn42Ip6;
+ }
+
+ public String getAsn() {
+ return asn;
+ }
+
+ public void setAsn(String asn) {
+ this.asn = asn;
+ }
+
+ public String getInternalIp() {
+ return internalIp;
+ }
+
+ public void setInternalIp(String internalIp) {
+ this.internalIp = internalIp;
+ }
+
+ public int getInternalPort() {
+ return internalPort;
+ }
+
+ public void setInternalPort(int internalPort) {
+ this.internalPort = internalPort;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getNotice() {
+ return notice;
+ }
+
+ public void setNotice(String notice) {
+ this.notice = notice;
+ }
+
+ public boolean isWireguard() {
+ return wireguard;
+ }
+
+ public void setWireguard(boolean wireguard) {
+ this.wireguard = wireguard;
+ }
+
+
+ // END GETTER / SETTER
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/node/NodeServiceImpl.java b/central/src/main/java/moe/yuuta/dn42peering/node/NodeServiceImpl.java
new file mode 100644
index 0000000..37d3036
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/node/NodeServiceImpl.java
@@ -0,0 +1,64 @@
+package moe.yuuta.dn42peering.node;
+
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import io.vertx.mysqlclient.MySQLPool;
+import io.vertx.sqlclient.templates.SqlTemplate;
+
+import javax.annotation.Nonnull;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+class NodeServiceImpl implements INodeService {
+ private final MySQLPool pool;
+ private final Vertx vertx;
+
+ NodeServiceImpl(@Nonnull Vertx vertx, @Nonnull MySQLPool mySQLPool) {
+ this.vertx = vertx;
+ this.pool = mySQLPool;
+ }
+
+ @Nonnull
+ @Override
+ public INodeService listNodes(@Nonnull Handler<AsyncResult<List<Node>>> handler) {
+ SqlTemplate
+ .forQuery(pool, "SELECT id, public_ip, dn42_ip4, dn42_ip6, asn, " +
+ "internal_ip, internal_port, name, notice, vpn_type_wg " +
+ "FROM node")
+ .mapTo(NodeRowMapper.INSTANCE)
+ .execute(null)
+ .compose(nodeRowMappers -> {
+ final List<Node> nodes = new ArrayList<>(10);
+ for (Node node : nodeRowMappers)
+ nodes.add(node);
+ return Future.succeededFuture(nodes);
+ })
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public INodeService getNode(int id, @Nonnull Handler<AsyncResult<Node>> handler) {
+ SqlTemplate
+ .forQuery(pool, "SELECT id, public_ip, asn, " +
+ "dn42_ip4, dn42_ip6, " +
+ "internal_ip, internal_port, name, notice, vpn_type_wg " +
+ "FROM node " +
+ "WHERE id = #{id}")
+ .mapTo(NodeRowMapper.INSTANCE)
+ .execute(Collections.singletonMap("id", id))
+ .compose(nodeRowMappers -> {
+ if(nodeRowMappers.iterator().hasNext()) {
+ return Future.succeededFuture(nodeRowMappers.iterator().next());
+ } else {
+ return Future.succeededFuture(null);
+ }
+ })
+ .onComplete(handler);
+ return this;
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/node/NodeVerticle.java b/central/src/main/java/moe/yuuta/dn42peering/node/NodeVerticle.java
new file mode 100644
index 0000000..3607e08
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/node/NodeVerticle.java
@@ -0,0 +1,56 @@
+package moe.yuuta.dn42peering.node;
+
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.CompositeFuture;
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.core.eventbus.MessageConsumer;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import io.vertx.core.json.JsonObject;
+import io.vertx.mysqlclient.MySQLConnectOptions;
+import io.vertx.mysqlclient.MySQLPool;
+import io.vertx.serviceproxy.ServiceBinder;
+import io.vertx.sqlclient.PoolOptions;
+
+public class NodeVerticle extends AbstractVerticle {
+ private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+
+ private MessageConsumer<JsonObject> consumer;
+ private MySQLPool pool;
+
+ @Override
+ public void start(Promise<Void> startPromise) throws Exception {
+ final JsonObject json = vertx.getOrCreateContext().config().getJsonObject("database");
+ final MySQLConnectOptions opt = new MySQLConnectOptions(json);
+ pool = MySQLPool.pool(vertx, opt, new PoolOptions().setMaxSize(5));
+
+ consumer = new ServiceBinder(vertx)
+ .setAddress(INodeService.ADDRESS)
+ .register(INodeService.class, new NodeServiceImpl(vertx, pool));
+ consumer.completionHandler(ar -> {
+ if(ar.succeeded()) {
+ startPromise.complete();
+ } else {
+ startPromise.fail(ar.cause());
+ }
+ });
+ }
+
+ @Override
+ public void stop(Promise<Void> stopPromise) throws Exception {
+ CompositeFuture.all(
+ Future.future(f -> consumer.unregister(ar -> {
+ if(ar.succeeded()) f.complete();
+ else f.fail(ar.cause());
+ })),
+ Future.future(f -> pool.close(ar -> {
+ if(ar.succeeded()) f.complete();
+ else f.fail(ar.cause());
+ }))
+ ).onComplete(ar -> {
+ if(ar.succeeded()) stopPromise.complete();
+ else stopPromise.fail(ar.cause());
+ });
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/node/package-info.java b/central/src/main/java/moe/yuuta/dn42peering/node/package-info.java
new file mode 100644
index 0000000..c4e6eaf
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/node/package-info.java
@@ -0,0 +1,4 @@
+@ModuleGen(groupPackage = "moe.yuuta.dn42peering.node", name = "node")
+package moe.yuuta.dn42peering.node;
+
+import io.vertx.codegen.annotations.ModuleGen; \ No newline at end of file
diff --git a/central/src/main/java/moe/yuuta/dn42peering/peer/IPeerService.java b/central/src/main/java/moe/yuuta/dn42peering/peer/IPeerService.java
new file mode 100644
index 0000000..5f4a7af
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/peer/IPeerService.java
@@ -0,0 +1,53 @@
+package moe.yuuta.dn42peering.peer;
+
+import io.vertx.codegen.annotations.Fluent;
+import io.vertx.codegen.annotations.ProxyGen;
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.List;
+
+@ProxyGen
+public interface IPeerService {
+ String ADDRESS = IPeerService.class.getName();
+
+ @Nonnull
+ static IPeerService createProxy(@Nonnull Vertx vertx) {
+ return new IPeerServiceVertxEBProxy(vertx, ADDRESS);
+ }
+
+ @Fluent
+ @Nonnull
+ IPeerService listUnderASN(@Nonnull String asn, @Nonnull Handler<AsyncResult<List<Peer>>> handler);
+
+ @Fluent
+ @Nonnull
+ IPeerService getSingle(@Nonnull String asn, String id, @Nonnull Handler<AsyncResult<Peer>> handler);
+
+ @Fluent
+ @Nonnull
+ IPeerService deletePeer(@Nonnull String asn, String id, @Nonnull Handler<AsyncResult<Void>> handler);
+
+ @Fluent
+ @Nonnull
+ IPeerService updateTo(@Nonnull Peer peer, @Nonnull Handler<AsyncResult<Void>> handler);
+
+ @Fluent
+ @Nonnull
+ IPeerService addNew(@Nonnull Peer peer, @Nonnull Handler<AsyncResult<Long>> handler);
+
+ @Fluent
+ @Nonnull
+ IPeerService existsUnderASN(@Nonnull String asn, @Nonnull Handler<AsyncResult<Boolean>> handler);
+
+ @Fluent
+ @Nonnull
+ IPeerService isIPConflict(@Nonnull Peer.VPNType type, @Nullable String ipv4, @Nullable String ipv6, @Nonnull Handler<AsyncResult<Boolean>> handler);
+
+ @Fluent
+ @Nonnull
+ IPeerService changeProvisionStatus(int id, @Nonnull ProvisionStatus provisionStatus, @Nonnull Handler<AsyncResult<Void>> handler);
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/peer/Peer.java b/central/src/main/java/moe/yuuta/dn42peering/peer/Peer.java
new file mode 100644
index 0000000..31560ad
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/peer/Peer.java
@@ -0,0 +1,333 @@
+package moe.yuuta.dn42peering.peer;
+
+import com.wireguard.crypto.Key;
+import com.wireguard.crypto.KeyFormatException;
+import io.vertx.codegen.annotations.DataObject;
+import io.vertx.codegen.annotations.GenIgnore;
+import io.vertx.codegen.format.SnakeCase;
+import io.vertx.core.json.JsonObject;
+import io.vertx.sqlclient.templates.annotations.Column;
+import io.vertx.sqlclient.templates.annotations.ParametersMapped;
+import io.vertx.sqlclient.templates.annotations.RowMapped;
+import io.vertx.sqlclient.templates.annotations.TemplateParameter;
+import moe.yuuta.dn42peering.agent.proto.BGPRequest;
+import moe.yuuta.dn42peering.agent.proto.WGRequest;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.net.Inet6Address;
+
+// DB Table: peer
+@DataObject(jsonPropertyNameFormatter = SnakeCase.class)
+@RowMapped(formatter = SnakeCase.class)
+@ParametersMapped(formatter = SnakeCase.class)
+public class Peer {
+ public enum VPNType {
+ WIREGUARD
+ }
+
+ @Column(name = "id")
+ @TemplateParameter(name = "id")
+ private int id;
+
+ @Column(name = "type")
+ @TemplateParameter(name = "type")
+ private VPNType type;
+
+ @Column(name = "asn")
+ @TemplateParameter(name = "asn")
+ private String asn;
+
+ @Column(name = "ipv4")
+ @TemplateParameter(name = "ipv4")
+ private String ipv4;
+
+ @Column(name = "ipv6")
+ @TemplateParameter(name = "ipv6")
+ private String ipv6;
+
+ @Column(name = "wg_endpoint")
+ @TemplateParameter(name = "wg_endpoint")
+ private String wgEndpoint;
+
+ @Column(name = "wg_endpoint_port")
+ @TemplateParameter(name = "wg_endpoint_port")
+ private int wgEndpointPort;
+
+ @Column(name = "wg_self_pubkey")
+ @TemplateParameter(name = "wg_self_pubkey")
+ private String wgSelfPubkey;
+
+ @Column(name = "wg_self_privkey")
+ @TemplateParameter(name = "wg_self_privkey")
+ private String wgSelfPrivKey;
+
+ @Column(name = "wg_peer_pubkey")
+ @TemplateParameter(name = "wg_peer_pubkey")
+ private String wgPeerPubkey;
+
+ @Column(name = "wg_preshared_secret")
+ @TemplateParameter(name = "wg_preshared_secret")
+ private String wgPresharedSecret;
+
+ @Column(name = "provision_status")
+ @TemplateParameter(name = "provision_status")
+ private ProvisionStatus provisionStatus;
+
+ @Column(name = "mpbgp")
+ @TemplateParameter(name = "mpbgp")
+ private boolean mpbgp;
+
+ @Column(name = "node")
+ @TemplateParameter(name = "node")
+ private int node;
+
+ public Peer() {}
+
+ public Peer(int id,
+ VPNType type,
+ String asn,
+ String ipv4,
+ String ipv6,
+ String wgEndpoint,
+ int wgEndpointPort,
+ String wgSelfPubkey,
+ String wgSelfPrivKey,
+ String wgPeerPubkey,
+ String wgPresharedSecret,
+ ProvisionStatus provisionStatus,
+ boolean mpbgp,
+ int node) {
+ this.id = id;
+ this.type = type;
+ this.asn = asn;
+ this.ipv4 = ipv4;
+ this.ipv6 = ipv6;
+ this.wgEndpoint = wgEndpoint;
+ this.wgEndpointPort = wgEndpointPort;
+ this.wgSelfPubkey = wgSelfPubkey;
+ this.wgSelfPrivKey = wgSelfPrivKey;
+ this.wgPeerPubkey = wgPeerPubkey;
+ this.wgPresharedSecret = wgPresharedSecret;
+ this.provisionStatus = provisionStatus;
+ this.mpbgp = mpbgp;
+ this.node = node;
+ }
+
+ public Peer(@Nonnull JsonObject jsonObject) {
+ this(jsonObject.getInteger("id"),
+ jsonObject.getString("type") == null ? null : VPNType.valueOf(jsonObject.getString("type")),
+ jsonObject.getString("asn"),
+ jsonObject.getString("ipv4"),
+ jsonObject.getString("ipv6"),
+ jsonObject.getString("wg_endpoint"),
+ jsonObject.getInteger("wg_endpoint_port"),
+ jsonObject.getString("wg_self_pubkey"),
+ jsonObject.getString("wg_self_privkey"),
+ jsonObject.getString("wg_peer_pubkey"),
+ jsonObject.getString("wg_preshared_secret"),
+ jsonObject.getString("provision_status") == null ? null :
+ ProvisionStatus.valueOf(jsonObject.getString("provision_status")),
+ jsonObject.getBoolean("mpbgp"),
+ jsonObject.getInteger("node"));
+ }
+
+ /**
+ * Create an instance with unfinished new WireGuard peering.
+ * Keys are generated. ASN is not filled in. ID is set to -1. ProvisionStatus is set to not provisioned.
+ */
+ public Peer(String ipv4,
+ String ipv6,
+ String wgEndpoint,
+ int wgEndpointPort,
+ String wgPeerPubkey,
+ boolean mpbgp,
+ int node) {
+ this(-1,
+ VPNType.WIREGUARD,
+ null /* ASN: To be filled later */,
+ ipv4,
+ ipv6,
+ wgEndpoint,
+ wgEndpointPort,
+ null /* Public key: generate later */,
+ Key.generatePrivateKey().toBase64(),
+ wgPeerPubkey,
+ Key.generatePrivateKey().toBase64(),
+ ProvisionStatus.NOT_PROVISIONED,
+ mpbgp,
+ node);
+ try {
+ wgSelfPubkey = Key.generatePublicKey(Key.fromBase64(wgSelfPrivKey)).toBase64();
+ } catch (KeyFormatException e) { /* Should not happen */
+ throw new RuntimeException(e); }
+ }
+
+ @Nonnull
+ public JsonObject toJson() {
+ return new JsonObject()
+ .put("id", id)
+ .put("type", type)
+ .put("asn", asn)
+ .put("ipv4", ipv4)
+ .put("ipv6", ipv6)
+ .put("wg_endpoint", wgEndpoint)
+ .put("wg_endpoint_port", wgEndpointPort)
+ .put("wg_self_pubkey", wgSelfPubkey)
+ .put("wg_self_privkey", wgSelfPrivKey)
+ .put("wg_peer_pubkey", wgPeerPubkey)
+ .put("wg_preshared_secret", wgPresharedSecret)
+ .put("provision_status", provisionStatus)
+ .put("mpbgp", mpbgp)
+ .put("node", node);
+ }
+
+ @Nonnull
+ public String calcWireGuardPort() {
+ return asn.substring(asn.length() - 5);
+ }
+
+ @GenIgnore
+ public boolean isIPv6LinkLocal() throws IOException {
+ return Inet6Address.getByName(ipv6).isLinkLocalAddress();
+ }
+
+ @GenIgnore
+ public WGRequest.Builder toWGRequest() {
+ return WGRequest.newBuilder()
+ .setId(getId())
+ .setListenPort(Integer.parseInt(calcWireGuardPort()))
+ .setEndpoint(String.format("%s:%d", getWgEndpoint(), getWgEndpointPort()))
+ .setPeerPubKey(getWgPeerPubkey())
+ .setSelfPrivKey(getWgSelfPrivKey())
+ .setSelfPresharedSecret(getWgPresharedSecret())
+ .setPeerIPv4(getIpv4())
+ .setPeerIPv6(getIpv6() == null ? "" : getIpv6());
+ }
+
+ @GenIgnore
+ public BGPRequest.Builder toBGPRequest() {
+ return BGPRequest.newBuilder()
+ .setId(getId())
+ .setAsn(getAsn())
+ .setIpv4(getIpv4())
+ .setIpv6(getIpv6() == null ? "" : getIpv6())
+ .setMpbgp(isMpbgp());
+ }
+
+ // START GETTERS / SETTERS
+
+ public int getId() {
+ return id;
+ }
+
+ public void setId(int id) {
+ this.id = id;
+ }
+
+ public VPNType getType() {
+ return type;
+ }
+
+ public void setType(VPNType type) {
+ this.type = type;
+ }
+
+ public String getAsn() {
+ return asn;
+ }
+
+ public void setAsn(String asn) {
+ this.asn = asn;
+ }
+
+ public String getIpv4() {
+ return ipv4;
+ }
+
+ public void setIpv4(String ipv4) {
+ this.ipv4 = ipv4;
+ }
+
+ public String getIpv6() {
+ return ipv6;
+ }
+
+ public void setIpv6(String ipv6) {
+ this.ipv6 = ipv6;
+ }
+
+ public String getWgEndpoint() {
+ return wgEndpoint;
+ }
+
+ public void setWgEndpoint(String wgEndpoint) {
+ this.wgEndpoint = wgEndpoint;
+ }
+
+ public int getWgEndpointPort() {
+ return wgEndpointPort;
+ }
+
+ public void setWgEndpointPort(int wgEndpointPort) {
+ this.wgEndpointPort = wgEndpointPort;
+ }
+
+ public String getWgSelfPubkey() {
+ return wgSelfPubkey;
+ }
+
+ public void setWgSelfPubkey(String wgSelfPubkey) {
+ this.wgSelfPubkey = wgSelfPubkey;
+ }
+
+ public String getWgSelfPrivKey() {
+ return wgSelfPrivKey;
+ }
+
+ public void setWgSelfPrivKey(String wgSelfPrivKey) {
+ this.wgSelfPrivKey = wgSelfPrivKey;
+ }
+
+ public String getWgPeerPubkey() {
+ return wgPeerPubkey;
+ }
+
+ public void setWgPeerPubkey(String wgPeerPubkey) {
+ this.wgPeerPubkey = wgPeerPubkey;
+ }
+
+ public String getWgPresharedSecret() {
+ return wgPresharedSecret;
+ }
+
+ public void setWgPresharedSecret(String wgPresharedSecret) {
+ this.wgPresharedSecret = wgPresharedSecret;
+ }
+
+ public ProvisionStatus getProvisionStatus() {
+ return provisionStatus;
+ }
+
+ public void setProvisionStatus(ProvisionStatus provisionStatus) {
+ this.provisionStatus = provisionStatus;
+ }
+
+ public boolean isMpbgp() {
+ return mpbgp;
+ }
+
+ public void setMpbgp(boolean mpbgp) {
+ this.mpbgp = mpbgp;
+ }
+
+ public int getNode() {
+ return node;
+ }
+
+ public void setNode(int node) {
+ this.node = node;
+ }
+
+ // END GETTERS / SETTERS
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/peer/PeerServiceImpl.java b/central/src/main/java/moe/yuuta/dn42peering/peer/PeerServiceImpl.java
new file mode 100644
index 0000000..99c31af
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/peer/PeerServiceImpl.java
@@ -0,0 +1,194 @@
+package moe.yuuta.dn42peering.peer;
+
+import io.vertx.core.*;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import io.vertx.mysqlclient.MySQLClient;
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.RowSet;
+import io.vertx.sqlclient.SqlClient;
+import io.vertx.sqlclient.SqlResult;
+import io.vertx.sqlclient.templates.SqlTemplate;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.*;
+
+class PeerServiceImpl implements IPeerService {
+ private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+
+ private final Vertx vertx;
+ private final SqlClient pool;
+
+ PeerServiceImpl(@Nonnull Vertx vertx, @Nonnull SqlClient sql) {
+ this.vertx = vertx;
+ this.pool = sql;
+ }
+
+ @Nonnull
+ @Override
+ public IPeerService listUnderASN(@Nonnull String asn, @Nonnull Handler<AsyncResult<List<Peer>>> handler) {
+ SqlTemplate
+ .forQuery(pool, "SELECT id, type, asn, ipv4, ipv6, " +
+ "wg_endpoint, wg_endpoint_port, " +
+ "wg_self_pubkey, wg_self_privkey, wg_peer_pubkey, wg_preshared_secret, " +
+ "provision_status, mpbgp, node FROM peer " +
+ "WHERE asn = #{asn}")
+ .mapTo(PeerRowMapper.INSTANCE)
+ .execute(Collections.singletonMap("asn", asn))
+ .compose(peers -> {
+ final List<Peer> peerList = new ArrayList<>();
+ for (Peer peer : peers) peerList.add(peer);
+ return Future.succeededFuture(peerList);
+ })
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IPeerService existsUnderASN(@Nonnull String asn, @Nonnull Handler<AsyncResult<Boolean>> handler) {
+ SqlTemplate
+ .forQuery(pool, "SELECT COUNT(id) FROM peer " +
+ "WHERE asn = #{asn}")
+ .execute(Collections.singletonMap("asn", asn))
+ .compose(rows -> Future.succeededFuture(rows.iterator().next().getInteger(0) > 0))
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IPeerService updateTo(@Nonnull Peer peer, @Nonnull Handler<AsyncResult<Void>> handler) {
+ SqlTemplate
+ .forUpdate(pool, "UPDATE peer SET " +
+ "type = #{type}, " +
+ "ipv4 = #{ipv4}, " +
+ "ipv6 = #{ipv6}, " +
+ "wg_endpoint = #{wg_endpoint}, " +
+ "wg_endpoint_port = #{wg_endpoint_port}, " +
+ "wg_self_pubkey = #{wg_self_pubkey}, " +
+ "wg_self_privkey = #{wg_self_privkey}, " +
+ "wg_peer_pubkey = #{wg_peer_pubkey}, " +
+ "wg_preshared_secret = #{wg_preshared_secret}, " +
+ "provision_status = #{provision_status}, " +
+ "mpbgp = #{mpbgp}, " +
+ "node = #{node} " +
+ "WHERE id = #{id} AND asn = #{asn}")
+ .mapFrom(PeerParametersMapper.INSTANCE)
+ .execute(peer)
+ .<Void>compose(res -> Future.succeededFuture(null))
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IPeerService addNew(@Nonnull Peer peer, @Nonnull Handler<AsyncResult<Long>> handler) {
+ peer.setId(0);
+ Future.<RowSet<Peer>>future(f -> SqlTemplate
+ .forQuery(pool, "INSERT INTO peer VALUES (#{id}, #{type}, #{asn}, " +
+ "#{ipv4}, #{ipv6}, " +
+ "#{wg_endpoint}, #{wg_endpoint_port}, " +
+ "#{wg_self_pubkey}, #{wg_self_privkey}, " +
+ "#{wg_peer_pubkey}, #{wg_preshared_secret}, " +
+ "#{provision_status}, #{mpbgp}, #{node}" +
+ ")")
+ .mapFrom(PeerParametersMapper.INSTANCE)
+ .mapTo(PeerRowMapper.INSTANCE)
+ .execute(peer, f))
+ .compose(rows -> Future.succeededFuture(rows.property(MySQLClient.LAST_INSERTED_ID)))
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IPeerService isIPConflict(@Nonnull Peer.VPNType type, @Nullable String ipv4, @Nullable String ipv6, @Nonnull Handler<AsyncResult<Boolean>> handler) {
+ final List<Future> futures = new ArrayList<>(2);
+ if (ipv4 != null) {
+ final Map<String, Object> params = new HashMap<>(3);
+ params.put("type", type.toString());
+ params.put("ip", ipv4);
+
+ futures.add(Future.<RowSet<Row>>future(f -> SqlTemplate
+ .forQuery(pool, "SELECT COUNT(id) FROM peer WHERE type = #{type} AND ipv4 = #{ip}")
+ .execute(params, f))
+ .compose(rows -> Future.succeededFuture(rows.iterator().next().getInteger(0) > 0)));
+ }
+ if (ipv6 != null) {
+ final Map<String, Object> params = new HashMap<>(3);
+ params.put("type", type.toString());
+ params.put("ip", ipv6);
+
+ futures.add(Future.<RowSet<Row>>future(f -> SqlTemplate
+ .forQuery(pool, "SELECT COUNT(id) FROM peer WHERE type = #{type} AND ipv6 = #{ip}")
+ .execute(params, f))
+ .compose(rows -> Future.succeededFuture(rows.iterator().next().getInteger(0) > 0)));
+ }
+ if(futures.isEmpty()) {
+ Future.succeededFuture(false).onComplete(handler);
+ return this;
+ }
+ CompositeFuture.all(futures)
+ .compose(future -> {
+ final List<Boolean> res = future.list();
+ for (boolean b : res) {
+ if (b) return Future.succeededFuture(true);
+ }
+ return Future.succeededFuture(false);
+ })
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IPeerService getSingle(@Nonnull String asn, String id, @Nonnull Handler<AsyncResult<Peer>> handler) {
+ final Map<String, Object> params = new HashMap<>(2);
+ params.put("id", id);
+ params.put("asn", asn);
+ Future.<RowSet<Peer>>future(f -> SqlTemplate
+ .forQuery(pool, "SELECT id, type, asn, " +
+ "ipv4, ipv6, " +
+ "wg_endpoint, wg_endpoint_port, " +
+ "wg_self_pubkey, wg_self_privkey, " +
+ "wg_peer_pubkey, wg_preshared_secret, " +
+ "provision_status, mpbgp, node " +
+ "FROM peer " +
+ "WHERE id = #{id} AND asn = #{asn}")
+ .mapTo(PeerRowMapper.INSTANCE)
+ .execute(params, f))
+ .compose(peers -> Future.succeededFuture(peers.iterator().next()))
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IPeerService deletePeer(@Nonnull String asn, String id, @Nonnull Handler<AsyncResult<Void>> handler) {
+ final Map<String, Object> params = new HashMap<>(2);
+ params.put("id", id);
+ params.put("asn", asn);
+ Future.<SqlResult<Void>>future(f -> SqlTemplate
+ .forUpdate(pool, "DELETE FROM peer WHERE id = #{id} AND asn = #{asn}")
+ .execute(params, f))
+ .<Void>compose(voidSqlResult -> Future.succeededFuture(null))
+ .onComplete(handler);
+ return this;
+ }
+
+ @Nonnull
+ @Override
+ public IPeerService changeProvisionStatus(int id, @Nonnull ProvisionStatus provisionStatus, @Nonnull Handler<AsyncResult<Void>> handler) {
+ final Map<String, Object> params = new HashMap<>(2);
+ params.put("id", id);
+ params.put("provision_status", provisionStatus);
+ SqlTemplate
+ .forUpdate(pool, "UPDATE peer SET provision_status = #{provision_status} WHERE id = #{id}")
+ .execute(params)
+ .compose(res -> Future.<Void>succeededFuture(null))
+ .onComplete(handler);
+ return this;
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/peer/PeerVerticle.java b/central/src/main/java/moe/yuuta/dn42peering/peer/PeerVerticle.java
new file mode 100644
index 0000000..d13dfec
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/peer/PeerVerticle.java
@@ -0,0 +1,56 @@
+package moe.yuuta.dn42peering.peer;
+
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.CompositeFuture;
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.core.eventbus.MessageConsumer;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import io.vertx.core.json.JsonObject;
+import io.vertx.mysqlclient.MySQLConnectOptions;
+import io.vertx.mysqlclient.MySQLPool;
+import io.vertx.serviceproxy.ServiceBinder;
+import io.vertx.sqlclient.PoolOptions;
+
+public class PeerVerticle extends AbstractVerticle {
+ private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+
+ private MessageConsumer<JsonObject> consumer;
+ private MySQLPool pool;
+
+ @Override
+ public void start(Promise<Void> startPromise) throws Exception {
+ final JsonObject json = vertx.getOrCreateContext().config().getJsonObject("database");
+ final MySQLConnectOptions opt = new MySQLConnectOptions(json);
+ pool = MySQLPool.pool(vertx, opt, new PoolOptions().setMaxSize(5));
+
+ consumer = new ServiceBinder(vertx)
+ .setAddress(IPeerService.ADDRESS)
+ .register(IPeerService.class, new PeerServiceImpl(vertx, pool));
+ consumer.completionHandler(ar -> {
+ if(ar.succeeded()) {
+ startPromise.complete();
+ } else {
+ startPromise.fail(ar.cause());
+ }
+ });
+ }
+
+ @Override
+ public void stop(Promise<Void> stopPromise) throws Exception {
+ CompositeFuture.all(
+ Future.future(f -> consumer.unregister(ar -> {
+ if(ar.succeeded()) f.complete();
+ else f.fail(ar.cause());
+ })),
+ Future.future(f -> pool.close(ar -> {
+ if(ar.succeeded()) f.complete();
+ else f.fail(ar.cause());
+ }))
+ ).onComplete(ar -> {
+ if(ar.succeeded()) stopPromise.complete();
+ else stopPromise.fail(ar.cause());
+ });
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/peer/ProvisionStatus.java b/central/src/main/java/moe/yuuta/dn42peering/peer/ProvisionStatus.java
new file mode 100644
index 0000000..d7afbb4
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/peer/ProvisionStatus.java
@@ -0,0 +1,7 @@
+package moe.yuuta.dn42peering.peer;
+
+public enum ProvisionStatus {
+ PROVISIONED,
+ NOT_PROVISIONED,
+ FAIL
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/peer/package-info.java b/central/src/main/java/moe/yuuta/dn42peering/peer/package-info.java
new file mode 100644
index 0000000..4bf2c3e
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/peer/package-info.java
@@ -0,0 +1,4 @@
+@ModuleGen(groupPackage = "moe.yuuta.dn42peering.peer", name = "peer")
+package moe.yuuta.dn42peering.peer;
+
+import io.vertx.codegen.annotations.ModuleGen; \ No newline at end of file
diff --git a/central/src/main/java/moe/yuuta/dn42peering/portal/FormException.java b/central/src/main/java/moe/yuuta/dn42peering/portal/FormException.java
new file mode 100644
index 0000000..f1e7663
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/portal/FormException.java
@@ -0,0 +1,17 @@
+package moe.yuuta.dn42peering.portal;
+
+import javax.annotation.Nonnull;
+
+public class FormException extends Exception {
+ public String[] errors;
+ public final Object data;
+
+ public FormException(@Nonnull String... errors) {
+ this(null, errors);
+ }
+
+ public FormException(Object data, @Nonnull String... errors) {
+ this.errors = errors;
+ this.data = data;
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/portal/HTTPException.java b/central/src/main/java/moe/yuuta/dn42peering/portal/HTTPException.java
new file mode 100644
index 0000000..9faadb5
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/portal/HTTPException.java
@@ -0,0 +1,9 @@
+package moe.yuuta.dn42peering.portal;
+
+public class HTTPException extends Exception {
+ public final int code;
+
+ public HTTPException(int code) {
+ this.code = code;
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/portal/HTTPPortalVerticle.java b/central/src/main/java/moe/yuuta/dn42peering/portal/HTTPPortalVerticle.java
new file mode 100644
index 0000000..9ce34ba
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/portal/HTTPPortalVerticle.java
@@ -0,0 +1,51 @@
+package moe.yuuta.dn42peering.portal;
+
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.Promise;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.common.template.TemplateEngine;
+import io.vertx.ext.web.templ.freemarker.FreeMarkerTemplateEngine;
+import moe.yuuta.dn42peering.asn.ASNHandler;
+import moe.yuuta.dn42peering.manage.ManageHandler;
+
+public class HTTPPortalVerticle extends AbstractVerticle {
+ private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+
+ @Override
+ public void start(Promise<Void> startPromise) throws Exception {
+ final TemplateEngine engine = FreeMarkerTemplateEngine.create(vertx, "ftlh");
+
+ final Router router = Router.router(vertx);
+ router.get("/")
+ .produces("text/html")
+ .handler(ctx -> {
+ final JsonObject data = new JsonObject()
+ .put("name", config().getJsonObject("http").getValue("name"));
+ engine.render(data, "index.ftlh", res -> {
+ if(res.succeeded()) {
+ ctx.response().end(res.result());
+ } else {
+ ctx.fail(res.cause());
+ logger.error("Cannot render index.", res.cause());
+ }
+ });
+ });
+ router.mountSubRouter("/asn", new ASNHandler().mount(vertx));
+ router.mountSubRouter("/manage", new ManageHandler().mount(vertx));
+ router.errorHandler(500, ctx -> {
+ logger.error("Generic Error", ctx.failure());
+ });
+ vertx.createHttpServer()
+ .requestHandler(router)
+ .listen(8080, res -> {
+ if(res.succeeded()) {
+ startPromise.complete();
+ } else {
+ startPromise.fail(res.cause());
+ }
+ });
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/portal/ISubRouter.java b/central/src/main/java/moe/yuuta/dn42peering/portal/ISubRouter.java
new file mode 100644
index 0000000..d75d041
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/portal/ISubRouter.java
@@ -0,0 +1,10 @@
+package moe.yuuta.dn42peering.portal;
+
+import io.vertx.core.Vertx;
+import io.vertx.ext.web.Router;
+
+import javax.annotation.Nonnull;
+
+public interface ISubRouter {
+ @Nonnull Router mount(@Nonnull Vertx vertx);
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/utils/PasswordAuthentication.java b/central/src/main/java/moe/yuuta/dn42peering/utils/PasswordAuthentication.java
new file mode 100644
index 0000000..2934b31
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/utils/PasswordAuthentication.java
@@ -0,0 +1,139 @@
+package moe.yuuta.dn42peering.utils;
+
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Hash passwords for storage, and test passwords against password tokens.
+ *
+ * Instances of this class can be used concurrently by multiple threads.
+ *
+ * @author erickson
+ * @see <a href="http://stackoverflow.com/a/2861125/3474">StackOverflow</a>
+ */
+public final class PasswordAuthentication {
+
+ /**
+ * Each token produced by this class uses this identifier as a prefix.
+ */
+ public static final String ID = "$31$";
+
+ /**
+ * The minimum recommended cost, used by default
+ */
+ public static final int DEFAULT_COST = 16;
+
+ private static final String ALGORITHM = "PBKDF2WithHmacSHA1";
+
+ private static final int SIZE = 128;
+
+ private static final Pattern layout = Pattern.compile("\\$31\\$(\\d\\d?)\\$(.{43})");
+
+ private final SecureRandom random;
+
+ private final int cost;
+
+ public PasswordAuthentication() {
+ this(DEFAULT_COST);
+ }
+
+ /**
+ * Create a password manager with a specified cost
+ *
+ * @param cost the exponential computational cost of hashing a password, 0 to 30
+ */
+ public PasswordAuthentication(int cost) {
+ iterations(cost); /* Validate cost */
+ this.cost = cost;
+ this.random = new SecureRandom();
+ }
+
+ private static int iterations(int cost) {
+ if ((cost < 0) || (cost > 30))
+ throw new IllegalArgumentException("cost: " + cost);
+ return 1 << cost;
+ }
+
+ /**
+ * Hash a password for storage.
+ *
+ * @return a secure authentication token to be stored for later authentication
+ */
+ public String hash(char[] password) {
+ byte[] salt = new byte[SIZE / 8];
+ random.nextBytes(salt);
+ byte[] dk = pbkdf2(password, salt, 1 << cost);
+ byte[] hash = new byte[salt.length + dk.length];
+ System.arraycopy(salt, 0, hash, 0, salt.length);
+ System.arraycopy(dk, 0, hash, salt.length, dk.length);
+ Base64.Encoder enc = Base64.getUrlEncoder().withoutPadding();
+ return ID + cost + '$' + enc.encodeToString(hash);
+ }
+
+ /**
+ * Authenticate with a password and a stored password token.
+ *
+ * @return true if the password and token match
+ */
+ public boolean authenticate(char[] password, String token) {
+ Matcher m = layout.matcher(token);
+ if (!m.matches())
+ throw new IllegalArgumentException("Invalid token format");
+ int iterations = iterations(Integer.parseInt(m.group(1)));
+ byte[] hash = Base64.getUrlDecoder().decode(m.group(2));
+ byte[] salt = Arrays.copyOfRange(hash, 0, SIZE / 8);
+ byte[] check = pbkdf2(password, salt, iterations);
+ int zero = 0;
+ for (int idx = 0; idx < check.length; ++idx)
+ zero |= hash[salt.length + idx] ^ check[idx];
+ return zero == 0;
+ }
+
+ private static byte[] pbkdf2(char[] password, byte[] salt, int iterations) {
+ KeySpec spec = new PBEKeySpec(password, salt, iterations, SIZE);
+ try {
+ SecretKeyFactory f = SecretKeyFactory.getInstance(ALGORITHM);
+ return f.generateSecret(spec).getEncoded();
+ }
+ catch (NoSuchAlgorithmException ex) {
+ throw new IllegalStateException("Missing algorithm: " + ALGORITHM, ex);
+ }
+ catch (InvalidKeySpecException ex) {
+ throw new IllegalStateException("Invalid SecretKeyFactory", ex);
+ }
+ }
+
+ /**
+ * Hash a password in an immutable {@code String}.
+ *
+ * <p>Passwords should be stored in a {@code char[]} so that it can be filled
+ * with zeros after use instead of lingering on the heap and elsewhere.
+ *
+ * @deprecated Use {@link #hash(char[])} instead
+ */
+ @Deprecated
+ public String hash(String password) {
+ return hash(password.toCharArray());
+ }
+
+ /**
+ * Authenticate with a password in an immutable {@code String} and a stored
+ * password token.
+ *
+ * @deprecated Use {@link #authenticate(char[],String)} instead.
+ * @see #hash(String)
+ */
+ @Deprecated
+ public boolean authenticate(String password, String token) {
+ return authenticate(password.toCharArray(), token);
+ }
+
+} \ No newline at end of file
diff --git a/central/src/main/java/moe/yuuta/dn42peering/whois/IWhoisService.java b/central/src/main/java/moe/yuuta/dn42peering/whois/IWhoisService.java
new file mode 100644
index 0000000..0b773bf
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/whois/IWhoisService.java
@@ -0,0 +1,27 @@
+package moe.yuuta.dn42peering.whois;
+
+import io.vertx.codegen.annotations.Fluent;
+import io.vertx.codegen.annotations.ProxyGen;
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+
+import javax.annotation.Nonnull;
+
+@ProxyGen
+public interface IWhoisService {
+ String ADDRESS = IWhoisService.class.getName();
+
+ static IWhoisService create(@Nonnull Vertx vertx) {
+ return new WhoisServiceImpl(vertx);
+ }
+
+ @Nonnull
+ static IWhoisService createProxy(@Nonnull Vertx vertx, @Nonnull String address) {
+ return new IWhoisServiceVertxEBProxy(vertx, address);
+ }
+
+ @Fluent
+ @Nonnull
+ IWhoisService query(@Nonnull String handle, @Nonnull Handler<AsyncResult<WhoisObject>> handler);
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisObject.java b/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisObject.java
new file mode 100644
index 0000000..37ad75d
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisObject.java
@@ -0,0 +1,56 @@
+package moe.yuuta.dn42peering.whois;
+
+import io.vertx.codegen.annotations.DataObject;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+
+import javax.annotation.Nonnull;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@DataObject(generateConverter = true)
+public class WhoisObject {
+ private Map<String, JsonArray> map;
+
+ public WhoisObject(JsonObject jsonObject) {
+ map = new HashMap<>(jsonObject.size());
+ jsonObject.stream()
+ .forEach(entity -> {
+ map.put(entity.getKey(), (JsonArray) entity.getValue());
+ });
+ }
+
+ public WhoisObject(@Nonnull Map<String, List<String>> map) {
+ this.map = new HashMap<>(map.size());
+ map.keySet().forEach(key -> {
+ final JsonArray array = new JsonArray();
+ map.get(key).forEach(array::add);
+ this.map.put(key, array);
+ });
+ }
+
+ @Nonnull
+ public JsonObject toJson() {
+ final JsonObject jsonObject = new JsonObject();
+ map.keySet()
+ .forEach(key -> jsonObject.put(key, map.get(key)));
+ return jsonObject;
+ }
+
+ public boolean containsKey(@Nonnull String key) {
+ return map.containsKey(key);
+ }
+
+ public List<String> getOrDefault(@Nonnull String key, List<String> def) {
+ if(containsKey(key)) return map.get(key).stream().map(obj -> (String)obj).collect(Collectors.toList());
+ return def;
+ }
+
+ @Nonnull
+ public List<String> get(@Nonnull String key) {
+ return getOrDefault(key, Collections.emptyList());
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisServiceImpl.java b/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisServiceImpl.java
new file mode 100644
index 0000000..57b167e
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisServiceImpl.java
@@ -0,0 +1,71 @@
+package moe.yuuta.dn42peering.whois;
+
+import io.vertx.core.AsyncResult;
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.core.Vertx;
+import moe.yuuta.dn42peering.jaba.OutParm;
+import org.apache.commons.net.whois.WhoisClient;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class WhoisServiceImpl implements IWhoisService {
+ private final Vertx vertx;
+
+ WhoisServiceImpl(@Nonnull Vertx vertx) {
+ this.vertx = vertx;
+ }
+
+ @Nonnull
+ @Override
+ public IWhoisService query(@Nonnull String handle, @Nonnull Handler<AsyncResult<WhoisObject>> handler) {
+ final WhoisClient whoisClient = new WhoisClient();
+ vertx.<String>executeBlocking(f -> {
+ try {
+ whoisClient.connect(vertx.getOrCreateContext().config().getString("whois"));
+ final String result = whoisClient.query(handle);
+ whoisClient.disconnect();
+ f.complete(result);
+ } catch (IOException e) {
+ f.fail(e);
+ }
+ }).compose(rawOutput -> {
+ if(rawOutput == null) return Future.succeededFuture(null);
+ else return Future.succeededFuture(parseObject(rawOutput));
+ }).onComplete(handler);
+ return this;
+ }
+
+ @Nullable
+ private static WhoisObject parseObject(@Nonnull String s) {
+ final Map<String, List<String>> obj = new HashMap<>(5);
+ for(final String line : s.split("\n")) {
+ final OutParm<String> key = new OutParm<>();
+ final OutParm<String> value = new OutParm<>();
+ if(!parseLine(line, key, value)) {
+ continue;
+ }
+ final List<String> values = obj.containsKey(key.out) ? obj.get(key.out) : new ArrayList<>(1);
+ values.add(value.out);
+ obj.put(key.out, values);
+ }
+ if(obj.isEmpty()) return null;
+ return new WhoisObject(obj);
+ }
+
+ /* Testing only */ static boolean parseLine(@Nonnull String line, @Nonnull OutParm<String> key, @Nonnull OutParm<String> value) {
+ if(line.startsWith("#") || line.startsWith("%")) return false;
+ if(line.length() < 20) return false;
+ final String part1 = line.substring(0, 20);
+ final String part2 = line.substring(20);
+ key.out = part1.split(":")[0];
+ value.out = part2;
+ return true;
+ }
+}
diff --git a/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisVerticle.java b/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisVerticle.java
new file mode 100644
index 0000000..d4cc83a
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/whois/WhoisVerticle.java
@@ -0,0 +1,35 @@
+package moe.yuuta.dn42peering.whois;
+
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.Promise;
+import io.vertx.core.eventbus.MessageConsumer;
+import io.vertx.core.impl.logging.Logger;
+import io.vertx.core.impl.logging.LoggerFactory;
+import io.vertx.core.json.JsonObject;
+import io.vertx.serviceproxy.ServiceBinder;
+
+public class WhoisVerticle extends AbstractVerticle {
+ private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
+
+ private MessageConsumer<JsonObject> consumer;
+
+ @Override
+ public void start(Promise<Void> startPromise) throws Exception {
+ consumer = new ServiceBinder(vertx)
+ .setAddress(IWhoisService.ADDRESS)
+ .register(IWhoisService.class, IWhoisService.create(vertx));
+ consumer.completionHandler(ar -> {
+ if(ar.succeeded()) {
+ startPromise.complete();
+ } else {
+ startPromise.fail(ar.cause());
+ }
+ });
+ }
+
+ @Override
+ public void stop(Promise<Void> stopPromise) throws Exception {
+ consumer.unregister(stopPromise);
+ }
+}
+
diff --git a/central/src/main/java/moe/yuuta/dn42peering/whois/package-info.java b/central/src/main/java/moe/yuuta/dn42peering/whois/package-info.java
new file mode 100644
index 0000000..8342b97
--- /dev/null
+++ b/central/src/main/java/moe/yuuta/dn42peering/whois/package-info.java
@@ -0,0 +1,4 @@
+@ModuleGen(groupPackage = "moe.yuuta.dn42peering.whois", name = "whois")
+package moe.yuuta.dn42peering.whois;
+
+import io.vertx.codegen.annotations.ModuleGen; \ No newline at end of file
diff --git a/central/src/main/resources/asn/index.ftlh b/central/src/main/resources/asn/index.ftlh
new file mode 100644
index 0000000..073866b
--- /dev/null
+++ b/central/src/main/resources/asn/index.ftlh
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="utf-8" />
+ <title>Register your ASN</title>
+</head>
+<body>
+<h1>Register your ASN</h1>
+<p>You may register your ASN now. One ASN can have multiple peers with us.</p>
+<#if errors??>
+<div>
+ <p style="color:red">Errors in the previous form:</p>
+ <ul>
+ <#list errors as error>
+ <li>${error}</li>
+ </#list>
+ </ul>
+</div>
+</#if>
+<form action="/asn" method="post">
+ <label for="asn">ASN (You cannot change this after registration):</label><br />
+ <input type="text" id="asn" name="asn" required
+ placeholder="AS4242422980"
+ pattern="[aA][sS]424242[0-9][0-9][0-9][0-9]"
+ value="${input_asn}"><br />
+ <br />
+
+ <label for="submit">When you click the following button, we will send an email with initial password to your tech-c person (All possible emails). Make sure you have access to that!</label><br />
+ <label for="submit">If you did not receive the email, you may contact us for manual password reset.</label><br />
+ <input type="submit" id="submit">
+</form>
+</body>
+</html> \ No newline at end of file
diff --git a/central/src/main/resources/asn/success.ftlh b/central/src/main/resources/asn/success.ftlh
new file mode 100644
index 0000000..15e6600
--- /dev/null
+++ b/central/src/main/resources/asn/success.ftlh
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Register your ASN</title>
+</head>
+<body>
+<h1>Your ASN is registered</h1>
+<p>Please check your email for initial password. We had sent the password to the following emails addresses:</p>
+<div>
+ <ul>
+ <#list emails as email>
+ <li>${email}</li>
+ </#list>
+ </ul>
+</div>
+<p>Important: You may re-register at any time if you did not rececive the email. However, upon one successful login, you will not be able to re-register.</p>
+<a href="/manage">Login Now</a> | <a href="/">Back to homepage</a>
+</body>
+</html> \ No newline at end of file
diff --git a/central/src/main/resources/index.ftlh b/central/src/main/resources/index.ftlh
new file mode 100644
index 0000000..7b5051e
--- /dev/null
+++ b/central/src/main/resources/index.ftlh
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang=en>
+<head>
+ <meta charset=utf-8>
+ <title>${name} dn42 Automatic Peering</title>
+</head>
+<body>
+<h1>${name} dn42 Peering</h1>
+<p><b>Already a peer? <a href="/manage">Login</a>.</b></p>
+<h2>Peering Process</h2>
+<ol>
+ <li>You: Register and verify your ASN.</li>
+ <li>You: Add a peer and submit your info.</li>
+ <li>We: Validate your info automatically.</li>
+ <li>We: If nothing goes wrong, we will automatically connect your endpoint.</li>
+ <li>You: Update your configuration to peer with us.</li>
+ <li>We: Constantly check your connection.</li>
+ <li>If you decided to stop peering with us, just login and delete your connection. We will stop connecting with you. The whole process is automated.</li>
+</ol>
+<h2>Special Notices (Please read)</h2>
+<ul>
+ <li>Accounts are not shared across our endpoints.</li>
+</ul>
+<h2>Start peering today!</h2>
+<ol>
+ <li>Register your ASN <a href="/asn">Here</a>.</li>
+ <li>Login to your <a href="/manage">Management Portal</a>.</li>
+ <li>Start peering by <a href="/manage/new">Creating a Peer</a>.</li>
+</ol>
+</body>
+</html>
diff --git a/central/src/main/resources/manage/changepw.ftlh b/central/src/main/resources/manage/changepw.ftlh
new file mode 100644
index 0000000..481c0d0
--- /dev/null
+++ b/central/src/main/resources/manage/changepw.ftlh
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Change Password | Manage your peering</title>
+</head>
+<body>
+<h1>Change Password</h1>
+<p>You are logged in as: ${asn}.</p>
+<#if errors??>
+<div>
+ <p style="color:red">Errors in the previous form:</p>
+ <ul>
+ <#list errors as error>
+ <li>${error}</li>
+ </#list>
+ </ul>
+</div>
+</#if>
+<form action="/manage/change-password" method="post">
+ <label for="passwd">New Password:</label><br />
+ <input type="password" id="passwd" name="passwd" required
+ placeholder="p@ssw0rd!"><br />
+ <br />
+ <label for="confirm">Confirm Password:</label><br />
+ <input type="password" id="confirm" name="confirm" required
+ placeholder="p@ssw0rd!"><br />
+ <br />
+ <input type="submit" id="submit">
+</form>
+</body>
+</html> \ No newline at end of file
diff --git a/central/src/main/resources/manage/delete.ftlh b/central/src/main/resources/manage/delete.ftlh
new file mode 100644
index 0000000..13453ae
--- /dev/null
+++ b/central/src/main/resources/manage/delete.ftlh
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Delete Account | Manage your peering</title>
+</head>
+<body>
+<h1>Delete Account</h1>
+<p>You are logged in as: ${asn}.</p>
+<#if errors??>
+<div>
+ <p style="color:red">Errors in the previous form:</p>
+ <ul>
+ <#list errors as error>
+ <li>${error}</li>
+ </#list>
+ </ul>
+</div>
+</#if>
+<form action="/manage/delete-account" method="post">
+ <label for="submit">Are you sure to delete your account? This will delete the ASN record on this node.</label><br />
+ <input type="submit" id="submit">
+</form>
+</body>
+</html> \ No newline at end of file
diff --git a/central/src/main/resources/manage/edit.ftlh b/central/src/main/resources/manage/edit.ftlh
new file mode 100644
index 0000000..251566d
--- /dev/null
+++ b/central/src/main/resources/manage/edit.ftlh
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Edit Peer | Manage your peering</title>
+</head>
+<body>
+<h1>Edit Peer</h1>
+<p>You are logged in as: ${asn}.</p>
+<#include "form.ftlh">
+<h2>More Actions</h2>
+<a href="/manage/delete?id=${id}">Delete</a>
+</body>
+</html> \ No newline at end of file
diff --git a/central/src/main/resources/manage/form.ftlh b/central/src/main/resources/manage/form.ftlh
new file mode 100644
index 0000000..4beb3bb
--- /dev/null
+++ b/central/src/main/resources/manage/form.ftlh
@@ -0,0 +1,60 @@
+<#if errors??>
+<div>
+ <p style="color:red">Errors in the previous form:</p>
+ <ul>
+ <#list errors as error>
+ <li>${error}</li>
+ </#list>
+ </ul>
+</div>
+</#if>
+<form action="${action}" method="post">
+ <label for="ipv4">dn42 IPv4 Address:</label><br />
+ <input type="text" id="ipv4" name="ipv4" required
+ placeholder="172.22.114.10"
+ pattern="^172\.2[0-3](\.([1-9]?\d|[12]\d\d)){2}$"
+ value="${(ipv4)!}"><br />
+ <br />
+ <label for="ipv6">dn42 IPv6 Address (Optional):</label><br />
+ <input type="text" id="ipv6" name="ipv6"
+ placeholder="fe80::2980"
+ value="${(ipv6)!}"><br />
+ <br />
+ <input type="checkbox" id="mpbgp" name="mpbgp" value="mpbgp" ${mpbgp?string('checked', '')}>
+ <label for="mpbgp">Enable MP-BGP (Only works when you provide a IPv6 address)</label><br /><br />
+
+ <label>Node:</label><br />
+ <#list nodes as node>
+ <input type="radio" id="node_${node.id}" name="node" value="${node.id}" required ${(node_checked == node.id)?string('checked', '')}>
+ <label for="node_${node.id}">${node.name} (${node.public_ip}, ${node.asn}, ${node.vpn_types?join(",")})
+ <#if node.notice??><br />${node.notice?no_esc}</#if>
+ </label><br />
+ </#list>
+ <br />
+
+ <label>Tunneling Method:</label><br />
+ <input type="radio" id="wg" name="vpn" value="wg" required ${typeWireguard?string('checked', '')}>
+ <label for="wg">WireGuard</label><br />
+ <br />
+
+ <label for="wg_endpoint"><b>WireGuard Specific Settings:</b></label><br />
+ <label for="wg_endpoint">WireGuard Endpoint IP (Optional if you do not have a public IPv4 address):</label><br />
+ <input type="text" id="wg_endpoint" name="wg_endpoint"
+ placeholder="114.51.4.191"
+ pattern="[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+"
+ value="${(wgEndpoint)!}"><br />
+ <br />
+ <label for="wg_endpoint_port">WireGuard Endpoint Port (Optional if you do not have a public IPv4 address):</label><br />
+ <input type="text" id="wg_endpoint_port" name="wg_endpoint_port"
+ placeholder="19198"
+ pattern="[0-9]+"
+ value="${(wgEndpointPort?long?c)!}"><br />
+ <br />
+ <label for="wg_pubkey">WireGuard Public Key:</label><br />
+ <input type="text" id="wg_pubkey" name="wg_pubkey" required
+ placeholder="CdX7EQwezTPaVjhp1Kw29RsmYYN3nTjPxOln4WaPOAU="
+ value="${(wgPubkey)!}"><br />
+ <br />
+
+ <input type="submit">
+</form> \ No newline at end of file
diff --git a/central/src/main/resources/manage/index.ftlh b/central/src/main/resources/manage/index.ftlh
new file mode 100644
index 0000000..983dc63
--- /dev/null
+++ b/central/src/main/resources/manage/index.ftlh
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Manage your peering</title>
+</head>
+<body>
+<h1>Manage your peering</h1>
+<p>You are logged in as: ${asn}.</p>
+<h2>Peers</h2>
+<p><a href="/manage/new">New Peer</a></p>
+<table style="width: 100%">
+ <tr>
+ <th>IPv4</th>
+ <th>IPv6</th>
+ <th>Method</th>
+ <th>Provision</th>
+ <th>Actions</th>
+ </tr>
+ <#list peers as peer>
+ <tr>
+ <td>${peer.ipv4}</td>
+ <td><#if peer.ipv6??>${peer.ipv6}<#else>None</#if></td>
+ <td>${peer.type}</td>
+ <td>${peer.provisionStatus}</td>
+ <td><a href="/manage/edit?id=${peer.id}">Edit</a> |
+ <a href="/manage/show-configuration?id=${peer.id}">Example Conf</a></td>
+ </tr>
+ </#list>
+</table>
+<h2>More Actions</h2>
+<a href="/manage/change-password">Change Password</a> |
+<a href="/manage/delete-account">Delete My Account</a>
+</body>
+</html> \ No newline at end of file
diff --git a/central/src/main/resources/manage/new.ftlh b/central/src/main/resources/manage/new.ftlh
new file mode 100644
index 0000000..9b572ec
--- /dev/null
+++ b/central/src/main/resources/manage/new.ftlh
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Add New Peer | Manage your peering</title>
+</head>
+<body>
+<h1>Add New Peer</h1>
+<p>You are logged in as: ${asn}.</p>
+<#include "form.ftlh">
+</body>
+</html> \ No newline at end of file
diff --git a/central/src/main/resources/manage/showconf.ftlh b/central/src/main/resources/manage/showconf.ftlh
new file mode 100644
index 0000000..5c5f24d
--- /dev/null
+++ b/central/src/main/resources/manage/showconf.ftlh
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Peering Configuration</title>
+</head>
+<body>
+<h1>Peering Configuration</h1>
+<p>Here are the information needed for you to setup:</p>
+<ul>
+ <li>WireGuard Endpoint: ${endpoint}:${wgPort}</li>
+ <li>dn42 IPv4 Address: ${ipv4}</li>
+ <li>dn42 IPv6 Address: ${ipv6}</li>
+ <li>ASN: ${asn}</li>
+ <li>WireGuard Public Key: ${wgSelfPubkey}</li>
+ <li>WireGuard Preshared Secret: ${wgPresharedSecret}</li>
+ <li>MP-BGP: ${mpbgp?string('Yes', 'No')}</li>
+</ul>
+<a href="/manage">Back</a>
+</body>
+</html> \ No newline at end of file
diff --git a/docs/QuickStart.md b/docs/QuickStart.md
new file mode 100644
index 0000000..ed42ef5
--- /dev/null
+++ b/docs/QuickStart.md
@@ -0,0 +1,53 @@
+# Quick Start
+
+This is a quick guide of setting dn42peering up.
+
+## Prepare a local copy of dn42 registry and regularly update it
+
+You need to have a local copy of the dn42 registry, and regularly update it.
+
+Important: You need to delete all contents from `data/inetnum` and `data/inet6num`.
+
+Finally, enable and start `whois42d` service.
+
+## Prepare the SQL database
+
+Create a database in a MySQL instance, then create required tables.
+
+Refer to [Database](central/Database.md) for more details.
+
+## Install the central
+
+Go to the download section and download the prebuilt central package, or build it yourself.
+
+## Create a VPN tunnel between central and your first node
+
+You may want to use WireGuard or similar VPN technologies to establish a dedicated tunnel between them.
+
+Please notice that the communication between central and agent is not encrypted, so be sure to use a VPN.
+
+Also, do not conflict the VPN address with dn42 addresses.
+
+## Install the agent
+
+Go to the download section and download the prebuilt agent package, or build it yourself.
+
+## Register the agent in the database and configure the agent
+
+Create a record in the database / node table as [agent.sql](sql/agent.sql)
+
+Create the agent configuration as shown in [Agent Configuration](agent/Configuration.md)
+
+Then, start the agent.
+
+## Register a mail service account and get the SMTP credentials
+
+For example, MailGun.
+
+## Configure the central
+
+See [Central Configuration](central/Configuration.md)
+
+## Start the central
+
+Finally, start the central. \ No newline at end of file
diff --git a/docs/agent/Configuration.md b/docs/agent/Configuration.md
new file mode 100644
index 0000000..9d9af8f
--- /dev/null
+++ b/docs/agent/Configuration.md
@@ -0,0 +1,21 @@
+# Configuration references
+
+The configuration format of agent is JSON.
+
+## Reference
+
+```json
+{
+ "internal_ip": "<Your internal controlling IPv4 address without port, see below # Controlling>"
+}
+```
+
+For both IP addresses, prefixes must not be included.
+
+## Controlling
+
+The central communicates with agents using gRPC. An internal IP address is required for that.
+
+The only requirement for internal IP address is that the agent can communicate with central. Agents are not required to communicate with each other.
+
+Though is it OK to use the dn42 IP address, it is strongly recommended to create a separete VPN tunnel between each agent and central combination, as some provision failure could cause the agent to disconnected. \ No newline at end of file
diff --git a/docs/central/Configuration.md b/docs/central/Configuration.md
new file mode 100644
index 0000000..05cf3bb
--- /dev/null
+++ b/docs/central/Configuration.md
@@ -0,0 +1,46 @@
+# Configuration references
+
+The configuration format of central is JSON.
+
+## Reference
+
+```json
+{
+ "database": {
+ "port": 3306,
+ "host": "host.name.or.ip.for.MySQL.database",
+ "database": "mysql database",
+ "user": "test",
+ "password": "123456"
+ },
+ "http": {
+ "name": "<Site name> It will appear like <name> dn42 peering."
+ },
+ "mail": {
+ "hostname": "SMTP hostname",
+ "port": 587,
+ "starttls": "DISABLED / OPTIONAL / REQUIRED",
+ "username": "SMTP username",
+ "password": "SMTP password",
+ "from": "Postmaster <postmaster@example.tld>"
+ },
+ "whois": "localhost (Whois hostname. See below # Whois)"
+}
+```
+
+## Database
+
+A MySQL database with predefined schema is required for central to operate. See [Database](Database.md) section for more details.
+
+## SMTP
+
+A smtp server is required for email verification. A recommendation is mail services like MailGun.
+
+## Whois
+
+A whois server that is capable of looking up dn42 ASN's and routes is required for ASN and IP verification.
+
+An example is [whois42d](https://github.com/Mic92/whois42d), but make sure you delete all contents under `data/inetnum` and `data/inet6num` or whois42d will not lookup routes.
+
+## Configuration Location and Reloading
+
diff --git a/docs/central/Database.md b/docs/central/Database.md
new file mode 100644
index 0000000..bfe014b
--- /dev/null
+++ b/docs/central/Database.md
@@ -0,0 +1,9 @@
+# Database
+
+A MySQL database is required for dn42peering central to work. Before installing the central program, make sure you have a proper database with tables setup.
+
+Here is the SQL used to setup a production database:
+
+See [0-init.sql](sql/0-init.sql) for creating initial schema.
+
+Note that the SQL script will update with schema updates. Please follow the docs/sql/ with the latest version. \ No newline at end of file
diff --git a/docs/central/sql/0-init.sql b/docs/central/sql/0-init.sql
new file mode 100644
index 0000000..17ca074
--- /dev/null
+++ b/docs/central/sql/0-init.sql
@@ -0,0 +1,57 @@
+SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
+START TRANSACTION;
+
+CREATE TABLE `asn` (
+ `asn` char(20) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `password_hash` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `activated` bit(1) NOT NULL DEFAULT b'0',
+ `register_date` timestamp NOT NULL DEFAULT current_timestamp()
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+CREATE TABLE `node` (
+ `id` int(10) UNSIGNED NOT NULL,
+ `public_ip` varchar(21) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `dn42_ip4` varchar(21) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `dn42_ip6` varchar(39) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `asn` char(20) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `internal_ip` varchar(15) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `internal_port` smallint(5) UNSIGNED NOT NULL,
+ `name` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `notice` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `vpn_type_wg` tinyint(1) NOT NULL DEFAULT 1
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+CREATE TABLE `peer` (
+ `id` int(11) NOT NULL,
+ `type` enum('WIREGUARD') COLLATE utf8mb4_unicode_ci NOT NULL,
+ `asn` char(20) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `ipv4` varchar(15) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `ipv6` varchar(39) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `wg_endpoint` varchar(15) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `wg_endpoint_port` smallint(5) UNSIGNED DEFAULT NULL,
+ `wg_self_pubkey` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `wg_self_privkey` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `wg_peer_pubkey` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `wg_preshared_secret` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `provision_status` enum('NOT_PROVISIONED','PROVISIONED','FAIL') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'NOT_PROVISIONED',
+ `mpbgp` tinyint(1) NOT NULL,
+ `node` int(11) NOT NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+
+
+ALTER TABLE `asn`
+ ADD PRIMARY KEY (`asn`);
+
+ALTER TABLE `node`
+ ADD PRIMARY KEY (`id`);
+
+ALTER TABLE `peer`
+ ADD PRIMARY KEY (`id`);
+
+
+ALTER TABLE `node`
+ MODIFY `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT;
+
+ALTER TABLE `peer`
+ MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
+COMMIT;
diff --git a/docs/central/sql/agent.sql b/docs/central/sql/agent.sql
new file mode 100644
index 0000000..2ec84bf
--- /dev/null
+++ b/docs/central/sql/agent.sql
@@ -0,0 +1,24 @@
+-- Use this script to add a new node to the database.
+-- Note: Current node support is minimal: Do not edit or delete the node after creation.
+-- A GUI will be provided later with full support.
+
+INSERT INTO `node`
+(public_ip,
+dn42_ip4,
+dn42_ip6,
+asn,
+internal_ip,
+internal_port,
+name,
+notice,
+vpn_type_wg)
+VALUES
+('127.0.0.1', -- The public IP address to display
+'172.23.105.1', -- The dn42 IPv4 address (No prefixes)
+'fe80:2980::1', -- The dn42 or link local IPv6 address (No prefixes)
+'AS4242422980', -- The ASN of this node to display. It is possible to have multiple ASNs in different nodes.
+'127.0.0.1', -- The internal address for management. See agent/Configuration.md for more details. Must be the same with agent configuration.
+49200, -- Currently only support 49200
+'North America GCP', -- Display name
+'<b>North America</b> 的 GCP', -- Optional notice to display. Support HTML.
+1) \ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..e708b1c
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..be52383
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..ac1b06f
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/rpc-common/build.gradle b/rpc-common/build.gradle
new file mode 100644
index 0000000..81bf422
--- /dev/null
+++ b/rpc-common/build.gradle
@@ -0,0 +1,45 @@
+plugins {
+ id 'java'
+ id "com.google.protobuf" version "0.8.14"
+ id 'idea'
+}
+
+group 'moe.yuuta'
+version '1.0'
+
+def protocVersion = protobufVersion
+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+dependencies {
+ compileOnly 'javax.annotation:javax.annotation-api:1.3.2'
+ implementation "io.vertx:vertx-core:${project.vertxVersion}"
+ implementation "io.vertx:vertx-grpc:${project.vertxVersion}"
+ implementation "io.grpc:grpc-protobuf:${project.grpcVersion}"
+ implementation "io.grpc:grpc-stub:${project.grpcVersion}"
+}
+
+test {
+ useJUnitPlatform()
+}
+
+protobuf {
+ protoc {
+ artifact = "com.google.protobuf:protoc:$protocVersion"
+ }
+ plugins {
+ grpc {
+ artifact = "io.grpc:protoc-gen-grpc-java:${project.grpcVersion}"
+ }
+ vertx {
+ artifact = "io.vertx:vertx-grpc-protoc-plugin:${project.vertxVersion}"
+ }
+ }
+ generateProtoTasks {
+ all()*.plugins {
+ grpc
+ vertx
+ }
+ }
+} \ No newline at end of file
diff --git a/rpc-common/src/main/java/moe/yuuta/dn42peering/RPC.java b/rpc-common/src/main/java/moe/yuuta/dn42peering/RPC.java
new file mode 100644
index 0000000..3fe243b
--- /dev/null
+++ b/rpc-common/src/main/java/moe/yuuta/dn42peering/RPC.java
@@ -0,0 +1,5 @@
+package moe.yuuta.dn42peering;
+
+public final class RPC {
+ public static final int AGENT_PORT = 49200;
+}
diff --git a/rpc-common/src/main/proto/agent.proto b/rpc-common/src/main/proto/agent.proto
new file mode 100644
index 0000000..126fbd2
--- /dev/null
+++ b/rpc-common/src/main/proto/agent.proto
@@ -0,0 +1,51 @@
+syntax = "proto3";
+
+option java_multiple_files = true;
+option java_package = "moe.yuuta.dn42peering.agent.proto";
+option java_outer_classname = "AgentProto";
+package moe.yuuta.dn42peering.agent;
+
+service Agent {
+ rpc ProvisionBGP (BGPRequest) returns (BGPReply) {}
+ rpc ReloadBGP (BGPRequest) returns (BGPReply) {}
+ rpc DeleteBGP (BGPRequest) returns (BGPReply) {}
+
+ rpc ProvisionWG (WGRequest) returns (WGReply) {}
+ rpc ReloadWG (WGRequest) returns (WGReply) {}
+ rpc DeleteWG (WGRequest) returns (WGReply) {}
+}
+
+message BGPRequest {
+ Node node = 1;
+ uint64 id = 2;
+ string asn = 3;
+ bool mpbgp = 4;
+ string ipv4 = 5;
+ string ipv6 = 6;
+ string device = 7;
+}
+
+message BGPReply {
+}
+
+message WGRequest {
+ Node node = 1;
+ uint64 id = 2;
+ uint32 listenPort = 3;
+ string endpoint = 4;
+ string peerPubKey = 5;
+ string selfPrivKey = 6;
+ string selfPresharedSecret = 7;
+ string peerIPv4 = 8;
+ string peerIPv6 = 9;
+}
+
+message WGReply {
+ string device = 1;
+}
+
+message Node {
+ uint64 id = 1;
+ string ipv4 = 2;
+ string ipv6 = 3;
+} \ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..e75b493
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name = 'dn42peering'
+include ':central', ':agent', ':rpc-common' \ No newline at end of file