diff --git a/ports/android/.gitignore b/ports/android/.gitignore
new file mode 100644
index 0000000..5edb4ee
--- /dev/null
+++ b/ports/android/.gitignore
@@ -0,0 +1,10 @@
+*.iml
+.gradle
+/local.properties
+/.idea/libraries
+/.idea/modules.xml
+/.idea/workspace.xml
+.DS_Store
+/build
+/captures
+.externalNativeBuild
diff --git a/ports/android/.project b/ports/android/.project
new file mode 100644
index 0000000..3964dd3
--- /dev/null
+++ b/ports/android/.project
@@ -0,0 +1,17 @@
+
+
+ android
+ Project android created by Buildship.
+
+
+
+
+ org.eclipse.buildship.core.gradleprojectbuilder
+
+
+
+
+
+ org.eclipse.buildship.core.gradleprojectnature
+
+
diff --git a/ports/android/.settings/org.eclipse.buildship.core.prefs b/ports/android/.settings/org.eclipse.buildship.core.prefs
new file mode 100644
index 0000000..e889521
--- /dev/null
+++ b/ports/android/.settings/org.eclipse.buildship.core.prefs
@@ -0,0 +1,2 @@
+connection.project.dir=
+eclipse.preferences.version=1
diff --git a/ports/android/app/.gitignore b/ports/android/app/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/ports/android/app/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/ports/android/app/build.gradle b/ports/android/app/build.gradle
new file mode 100644
index 0000000..d168a4b
--- /dev/null
+++ b/ports/android/app/build.gradle
@@ -0,0 +1,42 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 28
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 23
+ versionCode 1
+ versionName "1.0"
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ externalNativeBuild {
+ cmake {
+ cppFlags ""
+ }
+ }
+ ndk {
+ // Tells Gradle to build outputs for the following ABIs and package
+ // them into your APK.
+ abiFilters 'armeabi-v7a'
+ }
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ externalNativeBuild {
+ cmake {
+ path "../../../CMakeLists.txt"
+ }
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation 'com.android.support:appcompat-v7:28.0.0-alpha3'
+ implementation 'com.android.support.constraint:constraint-layout:1.1.2'
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'com.android.support.test:runner:1.0.2'
+ androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+}
\ No newline at end of file
diff --git a/ports/android/app/proguard-rules.pro b/ports/android/app/proguard-rules.pro
new file mode 100644
index 0000000..f1b4245
--- /dev/null
+++ b/ports/android/app/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/ports/android/app/src/androidTest/java/com/example/zerotier/ExampleInstrumentedTest.java b/ports/android/app/src/androidTest/java/com/example/zerotier/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..fe8e36c
--- /dev/null
+++ b/ports/android/app/src/androidTest/java/com/example/zerotier/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package com.example.zerotier;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("com.example.zerotier", appContext.getPackageName());
+ }
+}
diff --git a/ports/android/app/src/main/AndroidManifest.xml b/ports/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..42365c2
--- /dev/null
+++ b/ports/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ports/android/app/src/main/java/com/example/zerotier/MainActivity.java b/ports/android/app/src/main/java/com/example/zerotier/MainActivity.java
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/ports/android/app/src/main/java/com/example/zerotier/MainActivity.java
@@ -0,0 +1 @@
+
diff --git a/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTier.java b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTier.java
new file mode 100644
index 0000000..d1b87fb
--- /dev/null
+++ b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTier.java
@@ -0,0 +1,194 @@
+/*
+ * ZeroTier SDK - Network Virtualization Everywhere
+ * Copyright (C) 2011-2019 ZeroTier, Inc. https://www.zerotier.com/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ * --
+ *
+ * You can be released from the requirements of the license by purchasing
+ * a commercial license. Buying such a license is mandatory as soon as you
+ * develop commercial closed-source software that incorporates or links
+ * directly against ZeroTier software without disclosing the source code
+ * of your own application.
+ */
+
+package com.zerotier.libzt;
+
+import java.net.*;
+
+public class ZeroTier
+{
+ static
+ {
+ // loads libzt.so or libzt.dylib
+ System.loadLibrary("zt");
+ init();
+ }
+
+ public static int ZTS_ERR_OK = 0; // Everything is ok
+ public static int ZTS_ERR_INVALID_ARG = -1; // A parameter provided by the user application is invalid (e.g. our of range, NULL, etc)
+ public static int ZTS_ERR_SERVICE = -2; // The service isn't initialized or is for some other reason currently unavailable
+ public static int ZTS_ERR_INVALID_OP = -3; // For some reason this API operation is not permitted (perhaps the service is still starting?)
+
+ // Events generated by ZeroTier service or by libzt
+ // See ext/ZeroTierOne/include/ZeroTierOne.h
+ public static int EVENT_NONE = -1;
+ // Node-specific events
+ public static int EVENT_NODE_UP = 0;
+ public static int EVENT_NODE_OFFLINE = 1;
+ public static int EVENT_NODE_ONLINE = 2;
+ public static int EVENT_NODE_DOWN = 3;
+ public static int EVENT_NODE_IDENTITY_COLLISION = 4;
+ // libzt node events
+ public static int EVENT_NODE_UNRECOVERABLE_ERROR = 16;
+ public static int EVENT_NODE_NORMAL_TERMINATION = 17;
+ // Network-specific events
+ public static int EVENT_NETWORK_NOT_FOUND = 32;
+ public static int EVENT_NETWORK_CLIENT_TOO_OLD = 33;
+ public static int EVENT_NETWORK_REQUESTING_CONFIG = 34;
+ public static int EVENT_NETWORK_OK = 35;
+ public static int EVENT_NETWORK_ACCESS_DENIED = 36;
+ public static int EVENT_NETWORK_READY_IP4 = 37;
+ public static int EVENT_NETWORK_READY_IP6 = 38;
+ public static int EVENT_NETWORK_DOWN = 39;
+ // lwIP netif events
+ public static int EVENT_NETIF_UP_IP4 = 64;
+ public static int EVENT_NETIF_UP_IP6 = 65;
+ public static int EVENT_NETIF_DOWN_IP4 = 66;
+ public static int EVENT_NETIF_DOWN_IP6 = 67;
+ public static int EVENT_NETIF_REMOVED = 68;
+ public static int EVENT_NETIF_LINK_UP = 69;
+ public static int EVENT_NETIF_LINK_DOWN = 70;
+ public static int EVENT_NETIF_NEW_ADDRESS = 71;
+ // Peer events
+ public static int EVENT_PEER_P2P = 96;
+ public static int EVENT_PEER_RELAY = 97;
+ public static int EVENT_PEER_UNREACHABLE = 98;
+
+ // Socket protocol types
+ public static int SOCK_STREAM = 0x00000001;
+ public static int SOCK_DGRAM = 0x00000002;
+ public static int SOCK_RAW = 0x00000003;
+ // Socket family types
+ public static int AF_INET = 0x00000002;
+ public static int AF_INET6 = 0x0000000a;
+ public static int PF_INET = AF_INET;
+ public static int PF_INET6 = AF_INET6;
+ // Used as level numbers for setsockopt() and getsockopt()
+ public static int IPPROTO_IP = 0x00000000;
+ public static int IPPROTO_ICMP = 0x00000001;
+ public static int IPPROTO_TCP = 0x00000006;
+ public static int IPPROTO_UDP = 0x00000011;
+ public static int IPPROTO_IPV6 = 0x00000029;
+ public static int IPPROTO_ICMPV6 = 0x0000003a;
+ public static int IPPROTO_UDPLITE = 0x00000088;
+ public static int IPPROTO_RAW = 0x000000ff;
+ // send() and recv() flags
+ public static int MSG_PEEK = 0x00000001;
+ public static int MSG_WAITALL = 0x00000002;
+ public static int MSG_OOB = 0x00000004;
+ public static int MSG_DONTWAIT = 0x00000008;
+ public static int MSG_MORE = 0x00000010;
+ // fnctl() commands
+ public static int F_GETFL = 0x00000003;
+ public static int F_SETFL = 0x00000004;
+ // fnctl() flags
+ public static int O_NONBLOCK = 0x00000001;
+ public static int O_NDELAY = 0x00000001;
+ // Shutdown commands
+ public static int SHUT_RD = 0x00000000;
+ public static int SHUT_WR = 0x00000001;
+ public static int SHUT_RDWR = 0x00000002;
+ // ioctl() commands
+ public static int FIONREAD = 0x4008667F;
+ public static int FIONBIO = 0x8008667E;
+ // Socket level option number
+ public static int SOL_SOCKET = 0x00000FFF;
+ // Socket options
+ public static int SO_REUSEADDR = 0x00000004;
+ public static int SO_KEEPALIVE = 0x00000008;
+ public static int SO_BROADCAST = 0x00000020;
+ // Socket options
+ public static int SO_DEBUG = 0x00000001; // NOT YET SUPPORTED
+ public static int SO_ACCEPTCONN = 0x00000002;
+ public static int SO_DONTROUTE = 0x00000010; // NOT YET SUPPORTED
+ public static int SO_USELOOPBACK = 0x00000040; // NOT YET SUPPORTED
+ public static int SO_LINGER = 0x00000080;
+ public static int SO_DONTLINGER = ((int)(~SO_LINGER));
+ public static int SO_OOBINLINE = 0x00000100; // NOT YET SUPPORTED
+ public static int SO_REUSEPORT = 0x00000200; // NOT YET SUPPORTED
+ public static int SO_SNDBUF = 0x00001001; // NOT YET SUPPORTED
+ public static int SO_RCVBUF = 0x00001002;
+ public static int SO_SNDLOWAT = 0x00001003; // NOT YET SUPPORTED
+ public static int SO_RCVLOWAT = 0x00001004; // NOT YET SUPPORTED
+ public static int SO_SNDTIMEO = 0x00001005;
+ public static int SO_RCVTIMEO = 0x00001006;
+ public static int SO_ERROR = 0x00001007;
+ public static int SO_TYPE = 0x00001008;
+ public static int SO_CONTIMEO = 0x00001009; // NOT YET SUPPORTED
+ public static int SO_NO_CHECK = 0x0000100a;
+ // IPPROTO_IP options
+ public static int IP_TOS = 0x00000001;
+ public static int IP_TTL = 0x00000002;
+ // IPPROTO_TCP options
+ public static int TCP_NODELAY = 0x00000001;
+ public static int TCP_KEEPALIVE = 0x00000002;
+ public static int TCP_KEEPIDLE = 0x00000003;
+ public static int TCP_KEEPINTVL = 0x00000004;
+ public static int TCP_KEEPCNT = 0x00000005;
+
+ public static native void start(String path, ZeroTierEventListener callbackClass, int port);
+ public static native void stop();
+ public static native int join(long nwid);
+ public static native int leave(long nwid);
+ public static native long get_node_id();
+ public static native int get_num_assigned_addresses(long nwid);
+ public static native void get_6plane_addr(long nwid, long nodeId, ZeroTierSocketAddress addr);
+ public static native void get_rfc4193_addr(long nwid, long nodeId, ZeroTierSocketAddress addr);
+ public static native int get_peer_status(long nodeId);
+
+ public static native int socket(int family, int type, int protocol);
+ public static native int connect(int fd, ZeroTierSocketAddress addr);
+ public static native int bind(int fd, ZeroTierSocketAddress addr);
+ public static native int listen(int fd, int backlog);
+ public static native int accept(int fd, ZeroTierSocketAddress addr);
+ public static native int accept4(int fd, String addr, int port);
+
+ public static native int setsockopt(int fd, int level, int optname, ZeroTierSocketOptionValue optval);
+ public static native int getsockopt(int fd, int level, int optname, ZeroTierSocketOptionValue optval);
+
+ public static native int read(int fd, byte[] buf);
+ public static native int read_offset(int fd, byte[] buf, int offset, int len);
+ public static native int read_length(int fd, byte[] buf, int len);
+ public static native int recv(int fd, byte[] buf, int flags);
+ public static native int recvfrom(int fd, byte[] buf, int flags, ZeroTierSocketAddress addr);
+
+ public static native int write(int fd, byte[] buf);
+ public static native int write_byte(int fd, byte b);
+ public static native int write_offset(int fd, byte[] buf, int offset, int len);
+ public static native int sendto(int fd, byte[] buf, int flags, ZeroTierSocketAddress addr);
+ public static native int send(int fd, byte[] buf, int flags);
+
+ public static native int shutdown(int fd, int how);
+ public static native int close(int fd);
+
+ public static native boolean getsockname(int fd, ZeroTierSocketAddress addr);
+ public static native int getpeername(int fd, ZeroTierSocketAddress addr);
+ public static native int fcntl(int sock, int cmd, int flag);
+ public static native int ioctl(int fd, long request, ZeroTierIoctlArg arg);
+ public static native int select(int nfds, ZeroTierFileDescriptorSet readfds, ZeroTierFileDescriptorSet writefds, ZeroTierFileDescriptorSet exceptfds, int timeout_sec, int timeout_usec);
+
+ public static native void init(); // Only to be called by static initializer of this class
+}
diff --git a/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierEventListener.java b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierEventListener.java
new file mode 100644
index 0000000..fd5e54e
--- /dev/null
+++ b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierEventListener.java
@@ -0,0 +1,9 @@
+package com.zerotier.libzt;
+
+public interface ZeroTierEventListener {
+
+ /*
+ * Called when an even occurs in the native section of the ZeroTier library service
+ */
+ public void onZeroTierEvent(long nwid, int eventCode);
+}
\ No newline at end of file
diff --git a/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierFileDescriptorSet.java b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierFileDescriptorSet.java
new file mode 100644
index 0000000..000902b
--- /dev/null
+++ b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierFileDescriptorSet.java
@@ -0,0 +1,28 @@
+package com.zerotier.libzt;
+
+public class ZeroTierFileDescriptorSet
+{
+ byte[] fds_bits = new byte[1024];
+
+ public void CLR(int fd)
+ {
+ fds_bits[fd] = 0x00;
+ }
+
+ public boolean ISSET(int fd)
+ {
+ return fds_bits[fd] == 0x01;
+ }
+
+ public void SET(int fd)
+ {
+ fds_bits[fd] = 0x01;
+ }
+
+ public void ZERO()
+ {
+ for (int i=0; i<1024; i++) {
+ fds_bits[i] = 0x00;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierInputStream.java b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierInputStream.java
new file mode 100644
index 0000000..7a02d3a
--- /dev/null
+++ b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierInputStream.java
@@ -0,0 +1,209 @@
+package com.zerotier.libzt;
+
+import com.zerotier.libzt.ZeroTier;
+
+import java.io.*;
+import java.util.Objects;
+
+public class ZeroTierInputStream extends InputStream
+{
+ private static final int MAX_SKIP_BUFFER_SIZE = 2048;
+ private static final int DEFAULT_BUFFER_SIZE = 8192;
+
+ /*
+ * File descriptor used by lower native layer
+ */
+ int zfd;
+
+ /*
+ *
+ */
+ public int available()
+ throws IOException
+ {
+ return 0; // NOT YET SUPPORTED
+ }
+
+ /*
+ *
+ */
+ public void close()
+ throws IOException
+ {
+ /* Note: this operation currently only stops RX on a socket that is shared
+ between both I/OStreams. This means that closing this stream will only shutdown
+ that aspect of the socket but not actually close it and free resources. Resources
+ will be properly freed when the socket implementation's close() is called or if
+ both I/OStreams are closed separately */
+ ZeroTier.shutdown(zfd, ZeroTier.SHUT_RD);
+ zfd = -1;
+ }
+
+ /*
+ *
+ */
+ public void mark(int readlimit)
+ {
+ System.err.println("mark: ZeroTierInputStream does not currently support this feature");
+ }
+
+ /*
+ *
+ */
+ public void reset()
+ throws IOException
+ {
+ System.err.println("reset: ZeroTierInputStream does not currently support this feature");
+ }
+
+ /*
+ *
+ */
+ public boolean markSupported()
+ {
+ return false; // mark() is not supported
+ }
+
+ /*
+ *
+ */
+ public long transferTo(OutputStream out)
+ throws IOException
+ {
+ Objects.requireNonNull(out, "out must not be null");
+ int bytesTransferred = 0, bytesRead;
+ byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
+ while (((bytesRead = ZeroTier.read(zfd, buf)) >= 0)) {
+ out.write(buf, 0, bytesRead);
+ bytesTransferred += bytesRead;
+ }
+ return bytesTransferred;
+ }
+
+ /*
+ *
+ */
+ public int read()
+ throws IOException
+ {
+ byte[] buf = new byte[1];
+ // Unlike a native read(), if nothing is read we should return -1
+ int retval = ZeroTier.read(zfd, buf);
+ if ((retval == 0) | (retval == -104) /* EINTR, from SO_RCVTIMEO */) {
+ return -1;
+ }
+ if (retval < 0) {
+ throw new IOException("read(), errno="+retval);
+ }
+ return buf[0];
+ }
+
+ /*
+ *
+ */
+ public int read(byte[] b)
+ throws IOException
+ {
+ Objects.requireNonNull(b, "input byte array must not be null");
+ // Unlike a native read(), if nothing is read we should return -1
+ int retval = ZeroTier.read(zfd, b);
+ if ((retval == 0) | (retval == -104) /* EINTR, from SO_RCVTIMEO */) {
+ return -1;
+ }
+ if (retval < 0) {
+ throw new IOException("read(b), errno="+retval);
+ }
+ return retval;
+ }
+
+ /*
+ *
+ */
+ public int read(byte[] b, int off, int len)
+ throws IOException
+ {
+ Objects.requireNonNull(b, "input byte array must not be null");
+ if ((off < 0) | (len < 0) | (len > b.length - off)) {
+ throw new IndexOutOfBoundsException("invalid argument");
+ }
+ if (len == 0) {
+ return 0;
+ }
+ // Unlike a native read(), if nothing is read we should return -1
+ int retval = ZeroTier.read_offset(zfd, b, off, len);
+ if ((retval == 0) | (retval == -104) /* EINTR, from SO_RCVTIMEO */) {
+ return -1;
+ }
+ if (retval < 0) {
+ throw new IOException("read(b,off,len), errno="+retval);
+ }
+ //System.out.println("readNBytes(byte[] b, int off="+off+", int len="+len+")="+retval);
+ return retval;
+ }
+
+ /*
+ *
+ */
+ public byte[] readAllBytes()
+ throws IOException
+ {
+ //System.out.println("readAllBytes()");
+ ZeroTierIoctlArg ztarg = new ZeroTierIoctlArg();
+ int err = ZeroTier.ioctl(zfd, ZeroTier.FIONREAD, ztarg);
+ byte[] buf = new byte[ztarg.integer];
+ int retval = ZeroTier.read(zfd, buf);
+ if ((retval == 0) | (retval == -104) /* EINTR, from SO_RCVTIMEO */) {
+ // No action needed
+ }
+ if (retval < 0) {
+ throw new IOException("readAllBytes(b,off,len), errno="+retval);
+ }
+ return buf;
+ }
+
+ /*
+ *
+ */
+ public int readNBytes(byte[] b, int off, int len)
+ throws IOException
+ {
+ Objects.requireNonNull(b, "input byte array must not be null");
+ if ((off < 0) | (len < 0) | (len > b.length - off)) {
+ throw new IndexOutOfBoundsException("invalid argument");
+ }
+ if (len == 0) {
+ return 0;
+ }
+ int retval = ZeroTier.read_offset(zfd, b, off, len);
+ if ((retval == 0) | (retval == -104) /* EINTR, from SO_RCVTIMEO */) {
+ // No action needed
+ }
+ if (retval < 0) {
+ throw new IOException("readNBytes(b,off,len), errno="+retval);
+ }
+ //System.out.println("readNBytes(byte[] b, int off="+off+", int len="+len+")="+retval);
+ return retval;
+ }
+
+ /*
+ *
+ */
+ public long skip(long n)
+ throws IOException
+ {
+ //System.out.println("skip()");
+ if (n <= 0) {
+ return 0;
+ }
+ long bytesRemaining = n, bytesRead;
+ int bufSize = (int)Math.min(MAX_SKIP_BUFFER_SIZE, bytesRemaining);
+ byte[] buf = new byte[bufSize];
+ while (bytesRemaining > 0) {
+ if ((bytesRead = ZeroTier.read_length(zfd, buf, (int)Math.min(bufSize, bytesRemaining))) < 0) {
+ break;
+ }
+ bytesRemaining -= bytesRead;
+ }
+ return n - bytesRemaining; // skipped
+ }
+}
\ No newline at end of file
diff --git a/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierIoctlArg.java b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierIoctlArg.java
new file mode 100644
index 0000000..54c6131
--- /dev/null
+++ b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierIoctlArg.java
@@ -0,0 +1,6 @@
+package com.zerotier.libzt;
+
+public class ZeroTierIoctlArg
+{
+ int integer; // General integer to be used or updated by the zts_ioctl() call
+}
\ No newline at end of file
diff --git a/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierOutputStream.java b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierOutputStream.java
new file mode 100644
index 0000000..dd39a8a
--- /dev/null
+++ b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierOutputStream.java
@@ -0,0 +1,81 @@
+package com.zerotier.libzt;
+
+import com.zerotier.libzt.ZeroTier;
+
+import java.io.*;
+import java.util.Arrays;
+import java.util.Objects;
+
+class ZeroTierOutputStream extends OutputStream
+{
+ /*
+ * File descriptor used by lower native layer
+ */
+ int zfd;
+
+ /*
+ *
+ */
+ public void flush()
+ throws IOException
+ {
+ // System.err.println("flush: ZeroTierOutputStream does not currently support this feature");
+ // Not fully supported since we don't maintain a buffer
+ }
+
+ /*
+ *
+ */
+ public void close()
+ throws IOException
+ {
+ /* Note: this operation currently only stops RX on a socket that is shared
+ between both I/OStreams. This means that closing this stream will only shutdown
+ that aspect of the socket but not actually close it and free resources. Resources
+ will be properly freed when the socket implementation's close() is called or if
+ both I/OStreams are closed separately */
+ ZeroTier.shutdown(zfd, ZeroTier.SHUT_WR);
+ zfd = -1;
+ }
+
+ /*
+ *
+ */
+ public void write(byte[] b)
+ throws IOException
+ {
+ int err = ZeroTier.write(zfd, b);
+ if (err < 0) {
+ throw new IOException("write(b[]), errno="+err);
+ }
+ }
+
+ /*
+ *
+ */
+ public void write(byte[] b, int off, int len)
+ throws IOException
+ {
+ Objects.requireNonNull(b, "input byte array must not be null");
+ if ((off < 0) | (len < 0) | (off+len > b.length)) {
+ throw new IndexOutOfBoundsException("write(b,off,len)");
+ }
+ int err = ZeroTier.write_offset(zfd, b, off, len);
+ if (err < 0) {
+ throw new IOException("write(b[],off,len), errno="+err);
+ }
+ }
+
+ /*
+ *
+ */
+ public void write(int b)
+ throws IOException
+ {
+ byte lowByte = (byte)(b & 0xFF);
+ int err = ZeroTier.write_byte(zfd, lowByte);
+ if (err < 0) {
+ throw new IOException("write(b), errno="+err);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSSLSocketFactory.java b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSSLSocketFactory.java
new file mode 100644
index 0000000..2930f7a
--- /dev/null
+++ b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSSLSocketFactory.java
@@ -0,0 +1,101 @@
+package com.zerotier.libzt;
+
+import com.zerotier.libzt.ZeroTierSocket;
+
+import java.net.*;
+import javax.net.SocketFactory;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.*;
+import java.util.Locale;
+
+import javax.net.ssl.SSLSocketFactory;
+
+public class ZeroTierSSLSocketFactory extends SSLSocketFactory
+{
+ private final SSLSocketFactory delegate;
+
+ /*
+ *
+ */
+ public ZeroTierSSLSocketFactory(SSLSocketFactory delegate)
+ {
+ this.delegate = delegate;
+ }
+
+ /*
+ *
+ */
+ public Socket createSocket(Socket s, String host, int port, boolean autoClose)
+ throws IOException
+ {
+ ZeroTierSocket zs = new ZeroTierSocket();
+ zs.connect((SocketAddress)new InetSocketAddress(host, port), 10);
+ return delegate.createSocket(zs, host, port, autoClose);
+ }
+
+ /*
+ *
+ */
+ public Socket createSocket(Socket s, InputStream consumed, boolean autoClose)
+ throws IOException
+ {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ *
+ */
+ public Socket createSocket(InetAddress a,int b,InetAddress c,int d)
+ throws IOException
+ {
+ ZeroTierSocket s = new ZeroTierSocket();
+ return delegate.createSocket(a, b, c, d);
+ }
+
+ /*
+ *
+ */
+ public Socket createSocket(InetAddress a,int b)
+ throws IOException
+ {
+ ZeroTierSocket s = new ZeroTierSocket();
+ return delegate.createSocket(a, b);
+ }
+
+ /*
+ *
+ */
+ public Socket createSocket(String a,int b,InetAddress c,int d)
+ throws IOException
+ {
+ ZeroTierSocket s = new ZeroTierSocket();
+ return delegate.createSocket(a, b, c, d);
+ }
+
+ /*
+ *
+ */
+ public Socket createSocket(String a,int b)
+ throws IOException
+ {
+ ZeroTierSocket s = new ZeroTierSocket();
+ return delegate.createSocket(a, b);
+ }
+
+ /*
+ *
+ */
+ public String [] getSupportedCipherSuites()
+ {
+ return new String[0];
+ }
+
+ /*
+ *
+ */
+ public String [] getDefaultCipherSuites()
+ {
+ return new String[0];
+ }
+}
diff --git a/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocket.java b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocket.java
new file mode 100644
index 0000000..d89e324
--- /dev/null
+++ b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocket.java
@@ -0,0 +1,746 @@
+package com.zerotier.libzt;
+
+import com.zerotier.libzt.ZeroTier;
+import com.zerotier.libzt.ZeroTierSocketAddress;
+import com.zerotier.libzt.ZeroTierSocketImpl;
+import com.zerotier.libzt.ZeroTierSocketImplFactory;
+import com.zerotier.libzt.ZeroTierInputStream;
+import com.zerotier.libzt.ZeroTierOutputStream;
+
+import java.io.*;
+import java.net.*;
+import java.util.Objects;
+import java.nio.channels.SocketChannel;
+import java.net.InetAddress;
+
+public class ZeroTierSocket extends Socket
+{
+ /*
+ * Factory designated to create the underlying ZeroTierSocket implementation
+ */
+ static ZeroTierSocketImplFactory factory = new ZeroTierSocketImplFactory();
+
+ /*
+ * Underlying implementation of this ZeroTierSocket
+ */
+ ZeroTierSocketImpl impl;
+
+ /*
+ * Misc. state flags
+ */
+ private boolean created = false;
+ private boolean closed = false;
+ private boolean connected = false;
+ private boolean bound = false;
+ private boolean inputShutdown = false;
+ private boolean outputShutdown = false;
+
+ /*
+ * Creates and sets the implementation
+ */
+ void setImpl()
+ {
+ if (factory != null) {
+ impl = factory.createSocketImpl();
+ }
+ if (impl != null) {
+ impl.setSocket(this);
+ }
+ }
+
+ /*
+ * Returns the underlying socket implementation
+ */
+ private ZeroTierSocketImpl getImpl()
+ throws SocketException
+ {
+ if (!created) {
+ try {
+ impl.create(true);
+ }
+ catch (IOException ex) {
+ throw (SocketException) new SocketException().initCause(ex);
+ }
+ created = true;
+ }
+ return impl;
+ }
+
+ /*
+ * Create the underlying socket implementation
+ */
+ void createImpl(boolean stream)
+ throws SocketException
+ {
+ if (impl == null) {
+ setImpl();
+ }
+ try {
+ impl.create(stream);
+ created = true;
+ }
+ catch (IOException ex) {
+ throw new SocketException(e.getMessage());
+ }
+ }
+
+ /*
+ * Constructor for ZeroTierSocket
+ */
+ public ZeroTierSocket()
+ throws IOException
+ {
+ this((InetAddress)null, 0, null, 0);
+ }
+
+ /*
+ * Creates an unconnected socket
+ */
+ protected ZeroTierSocket(ZeroTierSocketImpl impl)
+ throws SocketException
+ {
+ this.impl = impl;
+ if (impl != null) {
+ this.impl.setSocket(this);
+ }
+ }
+
+ /*
+ * Constructor for ZeroTierSocket
+ */
+ public ZeroTierSocket(InetAddress raddr, int rport, InetAddress laddr, int lport)
+ throws IOException
+ {
+ setImpl();
+
+ try {
+ if (laddr != null) {
+ bind(new InetSocketAddress(laddr, lport));
+ }
+ if (raddr != null) {
+ connect(new InetSocketAddress(raddr, rport));
+ }
+ }
+ catch (Exception ex)
+ {
+ try {
+ close();
+ }
+ catch (IOException _ex) {
+ ex.addSuppressed(_ex);
+ }
+ throw ex;
+ }
+ }
+
+ /*
+ * Constructor for ZeroTierSocket
+ */
+ public ZeroTierSocket(InetAddress address, int port)
+ throws IOException
+ {
+ this(address, port, null, 0);
+ }
+
+ /*
+ * Constructor for ZeroTierSocket
+ */
+ public ZeroTierSocket(String address, int port)
+ throws IOException
+ {
+ this(InetAddress.getByName(address), port, null, 0);
+ }
+
+ /*
+ * Constructor for ZeroTierSocket
+ */
+ public ZeroTierSocket(String address, int port, InetAddress localHost, int localPort)
+ throws IOException
+ {
+ this(InetAddress.getByName(address), port, localHost, localPort);
+ }
+
+ /*
+ * Binds the socket to a local address
+ */
+ public void bind(SocketAddress localAddr)
+ throws IOException
+ {
+ if (isSocketBound()) {
+ throw new SocketException("bind: ZeroTierSocket is already bound");
+ }
+ if (isSocketClosed()) {
+ throw new SocketException("bind: ZeroTierSocket is closed");
+ }
+ if (localAddr != null && (!(localAddr instanceof InetSocketAddress))) {
+ throw new IllegalArgumentException("bind: Unsupported address type");
+ }
+ InetSocketAddress addr = (InetSocketAddress)localAddr;
+ if (addr != null && addr.isUnresolved()) {
+ throw new SocketException("bind: Unresolved address");
+ }
+ if (addr == null) {
+ addr = new InetSocketAddress(0);
+ }
+ getImpl().bind(addr.getAddress(), addr.getPort());
+ bound = true;
+ }
+
+ /*
+ * Closes the socket
+ */
+ public synchronized void close()
+ throws IOException
+ {
+ if (isSocketClosed()) {
+ return;
+ }
+ getOutputStream().flush();
+ impl.close();
+ closed = true;
+ }
+
+ /*
+ * Connects the socket to a remote address
+ */
+ public void connect(SocketAddress remoteAddr)
+ throws IOException
+ {
+ connect(remoteAddr, 0);
+ }
+
+ /*
+ * Connects the socket to a remote address
+ */
+ public void connect(SocketAddress remoteAddr, int timeout)
+ throws IOException
+ {
+ if (isSocketClosed()) {
+ throw new SocketException("connect: ZeroTierSocket is closed");
+ }
+ if (isSocketConnected()) {
+ throw new SocketException("connect: already connected");
+ }
+ if (remoteAddr == null) {
+ throw new IllegalArgumentException("connect: The address can't be null");
+ }
+ if (!(remoteAddr instanceof InetSocketAddress)) {
+ throw new IllegalArgumentException("connect: Unsupported address type");
+ }
+ if (timeout < 0) {
+ throw new IllegalArgumentException("connect: timeout cannot be negative");
+ }
+ if (!created) {
+ createImpl(true);
+ }
+ getImpl().connect(remoteAddr, 0);
+ bound = true;
+ connected = true;
+ }
+
+ /*
+ * Returns the associated Channel
+ */
+ public SocketChannel getChannel()
+ {
+ System.err.println("getChannel: ZeroTierSocket does not currently support this feature");
+ return null;
+ }
+
+ /*
+ * Returns the address to which the socket is connected
+ */
+ public InetAddress getInetAddress()
+ {
+ if (!isSocketConnected()) {
+ return null;
+ }
+ try {
+ return getImpl().getInetAddress();
+ }
+ catch (SocketException ex) {
+ // Not Reachable
+ }
+ return null;
+ }
+
+ /*
+ * Returns the input stream
+ */
+ public ZeroTierInputStream getInputStream()
+ throws IOException
+ {
+ if (!isSocketConnected()) {
+ throw new SocketException("ZeroTierSocket is not connected");
+ }
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ if (isInputStreamShutdown()) {
+ throw new SocketException("ZeroTierSocket input is shutdown");
+ }
+ return getImpl().getInputStream();
+ }
+
+ /*
+ * Returns whether SO_KEEPALIVE is enabled
+ */
+ public boolean getKeepAlive()
+ throws SocketException
+ {
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ return ((Boolean) getImpl().getOption(ZeroTier.SO_KEEPALIVE)).booleanValue();
+ }
+
+ /*
+ * Returns the local address to which the socket is bound
+ */
+ public InetAddress getLocalAddress()
+ {
+ System.err.println("getLocalAddress: ZeroTierSocket does not currently support this feature");
+ /*
+ // This is for backward compatibility
+ if (!isSocketBound()) {
+ return InetAddress.anyLocalAddress();
+ }
+ InetAddress inAddr = null;
+ try {
+ inAddr = (InetAddress) getImpl().getOption(ZeroTier.SO_BINDADDR);
+ if (inAddr.isAnyLocalAddress()) {
+ inAddr = InetAddress.anyLocalAddress();
+ }
+ }
+ catch (Exception ex) {
+ // "0.0.0.0"
+ inAddr = InetAddress.anyLocalAddress();
+ }
+ return inAddr;
+ */
+ return null;
+ }
+
+ /*
+ * Return the local port to which the socket is bound
+ */
+ public int getLocalPort()
+ {
+ if (!isSocketBound()) {
+ return -1;
+ }
+ try {
+ return getImpl().getLocalPort();
+ }
+ catch(SocketException ex) {
+ // Unreachable
+ }
+ return -1;
+ }
+
+ /*
+ * Returns the address of the endpoint that the socket is bound to.
+ */
+ public SocketAddress getLocalSocketAddress()
+ {
+ if (!isSocketBound()) {
+ return null;
+ }
+ return new InetSocketAddress(getLocalAddress(), getLocalPort());
+ }
+
+ /*
+ * Returns whether SO_OOBINLINE is enabled.
+ */
+ public boolean getOOBInline()
+ throws SocketException
+ {
+ System.err.println("getOOBInline: ZeroTierSocket does not currently support this feature");
+ return false;
+ }
+
+ /*
+ * Returns the output stream.
+ */
+ public ZeroTierOutputStream getOutputStream()
+ throws IOException
+ {
+ if (!isSocketConnected()) {
+ throw new SocketException("ZeroTierSocket is not connected");
+ }
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ if (isOutputStreamShutdown()) {
+ throw new SocketException("ZeroTierSocket output is shutdown");
+ }
+ return getImpl().getOutputStream();
+ }
+
+ /*
+ * Return the remote port to which the socket is connected
+ */
+ public int getPort()
+ {
+ if (!isSocketConnected()) {
+ return 0;
+ }
+ try {
+ return getImpl().getPort();
+ }
+ catch (SocketException ex) {
+ // Not reachable
+ }
+ return -1;
+ }
+
+ /*
+ * Returns SO_RCVBUF
+ */
+ public synchronized int getReceiveBufferSize()
+ throws SocketException
+ {
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ Object opt = getImpl().getOption(ZeroTier.SO_RCVBUF);
+ int sz = 0;
+ if (opt instanceof Integer) {
+ sz = ((Integer)opt).intValue();
+ }
+ return sz;
+ }
+
+ /*
+ * Returns the remote address to which this socket is connected
+ */
+ public SocketAddress getRemoteSocketAddress()
+ {
+ if (!isSocketConnected()) {
+ return null;
+ }
+ return new InetSocketAddress(getInetAddress(), getPort());
+ }
+
+ /*
+ * Checks whether SO_REUSEADDR is enabled.
+ */
+ public boolean getReuseAddress()
+ throws SocketException
+ {
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ return ((Boolean)(getImpl().getOption(ZeroTier.SO_REUSEADDR))).booleanValue();
+ }
+
+ /*
+ * Returns SO_SNDBUF.
+ */
+ public synchronized int getSendBufferSize()
+ throws SocketException
+ {
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ int sz = 0;
+ Object opt = getImpl().getOption(ZeroTier.SO_SNDBUF);
+ if (opt instanceof Integer) {
+ sz = ((Integer)opt).intValue();
+ }
+ return sz;
+ }
+
+ /*
+ * Returns SO_LINGER.
+ */
+ public int getSoLinger()
+ throws SocketException
+ {
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ Object opt = getImpl().getOption(ZeroTier.SO_LINGER);
+ if (opt instanceof Integer) {
+ return ((Integer)opt).intValue();
+ }
+ return -1;
+ }
+
+ /*
+ * Returns SO_TIMEOUT.
+ */
+ public synchronized int getSoTimeout()
+ throws SocketException
+ {
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ Object opt = getImpl().getOption(ZeroTier.SO_RCVTIMEO);
+ if (opt instanceof Integer) {
+ return ((Integer)opt).intValue();
+ }
+ else {
+ return 0;
+ }
+ }
+
+ /*
+ * Checks whether TCP_NODELAY is enabled.
+ */
+ public boolean getTcpNoDelay()
+ throws SocketException
+ {
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ return ((Boolean)getImpl().getOption(ZeroTier.TCP_NODELAY)).booleanValue();
+ }
+
+ /*
+ * Gets traffic class or type-of-service in the IP header for packets sent from this Socket
+ */
+ public int getTrafficClass()
+ throws SocketException
+ {
+ System.err.println("getTrafficClass: ZeroTierSocket does not currently support this feature");
+ return 0;
+ }
+
+ /*
+ * Returns whether or not the socket is bound to a local interface.
+ */
+ public boolean isSocketBound()
+ {
+ return bound;
+ }
+
+ /*
+ * Returns whether or not the socket has been closed.
+ */
+ public boolean isSocketClosed()()
+ {
+ return closed;
+ }
+
+ /*
+ * Returns whether or not the socket is connected to a remote host.
+ */
+ public boolean isSocketConnected()
+ {
+ return connected;
+ }
+
+ /*
+ * Returns whether the input aspect of the socket has been disabled.
+ */
+ public boolean isInputStreamShutdown()
+ {
+ return inputShutdown;
+ }
+
+ /*
+ * Returns whether the output aspect of the socket has been disabled.
+ */
+ public boolean isOutputStreamShutdown()
+ {
+ return outputShutdown;
+ }
+
+ /*
+ * Send a byte of urgent data on the socket.
+ */
+ public void sendUrgentData(int data)
+ throws IOException
+ {
+ System.err.println("sendUrgentData: ZeroTierSocket does not currently support this feature");
+ }
+
+ /*
+ * Enable or disable SO_KEEPALIVE.
+ */
+ public void setKeepAlive(boolean on)
+ throws SocketException
+ {
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ getImpl().setOption(ZeroTier.SO_KEEPALIVE, Boolean.valueOf(on));
+ }
+
+ /*
+ * Enable or disable SO_OOBINLINE.
+ */
+ public void setOOBInline(boolean on)
+ throws SocketException
+ {
+ System.err.println("setOOBInline: ZeroTierSocket does not currently support this feature");
+ }
+
+ /*
+ * Set performance preferences.
+ */
+ public void setPerformancePreferences(int connectionTime, int latency, int bandwidth)
+ {
+ System.err.println("setPerformancePreferences: ZeroTierSocket does not currently support this feature");
+ }
+
+ /*
+ * Set SO_RCVBUF.
+ */
+ public synchronized void setReceiveBufferSize(int size)
+ throws SocketException
+ {
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ if (size <= 0) {
+ throw new IllegalArgumentException("invalid receive buffer size argument");
+ }
+ getImpl().setOption(ZeroTier.SO_RCVBUF, new Integer(size));
+ }
+
+ /*
+ * Enable or disable SO_REUSEADDR.
+ */
+ public void setReuseAddress(boolean on)
+ throws SocketException
+ {
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ getImpl().setOption(ZeroTier.SO_REUSEADDR, Boolean.valueOf(on));
+ }
+
+ /*
+ * Set SO_SNDBUF.
+ */
+ public synchronized void setSendBufferSize(int size)
+ throws SocketException
+ {
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ if (size < 0) {
+ throw new IllegalArgumentException("size argument cannot be negative");
+ }
+ getImpl().setOption(ZeroTier.SO_SNDBUF, new Integer(size));
+ }
+
+ /*
+ * Set Socket implementation factory for all clients.
+ */
+ public static void setSocketImplFactory(ZeroTierSocketImplFactory fact)
+ throws IOException
+ {
+ if (factory != null) {
+ throw new SocketException("ZeroTierSocket factory is already defined");
+ }
+ factory = fact;
+ }
+
+ /*
+ * Enable or disable SO_LINGER time (seconds).
+ */
+ public void setSoLinger(boolean on, int linger)
+ throws SocketException
+ {
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ if (!on) {
+ getImpl().setOption(ZeroTier.SO_LINGER, new Boolean(on));
+ }
+ else {
+ if (linger < 0) {
+ throw new IllegalArgumentException("linger argument is invalid");
+ }
+ if (linger > 0xFFFF) {
+ linger = 0xFFFF;
+ }
+ getImpl().setOption(ZeroTier.SO_LINGER, new Integer(linger));
+ }
+ }
+
+ /*
+ * Enable or disable SO_TIMEOUT with the specified timeout, in milliseconds.
+ */
+ public void setSoTimeout(int timeout)
+ throws SocketException
+ {
+ if (timeout < 0) {
+ throw new IllegalArgumentException("timeout argument cannot be negative");
+ }
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ getImpl().setOption(ZeroTier.SO_RCVTIMEO, new Integer(timeout));
+ }
+
+ /*
+ * Enable or disable TCP_NODELAY (Nagle's algorithm).
+ */
+ public void setTcpNoDelay(boolean on)
+ throws SocketException
+ {
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ getImpl().setOption(ZeroTier.TCP_NODELAY, Boolean.valueOf(on));
+ }
+
+ /*
+ * Sets traffic class or ToS.
+ */
+ public void setTrafficClass(int tc)
+ throws SocketException
+ {
+ System.err.println("setTrafficClass: ZeroTierSocket does not currently support this feature");
+ }
+
+ /*
+ * Disable the input stream for this socket.
+ */
+ public void shutdownInput()
+ throws IOException
+ {
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ if (isInputStreamShutdown()) {
+ throw new SocketException("ZeroTierSocket input is already shutdown");
+ }
+ if (!isSocketConnected()) {
+ throw new SocketException("ZeroTierSocket is not connected");
+ }
+ getImpl().shutdownInput();
+ inputShutdown = true;
+ }
+
+ /*
+ * Disable the output stream for this socket.
+ */
+ public void shutdownOutput()
+ throws IOException
+ {
+ if (isSocketClosed()) {
+ throw new SocketException("ZeroTierSocket is closed");
+ }
+ if (isOutputStreamShutdown()) {
+ throw new SocketException("ZeroTierSocket output is already shutdown");
+ }
+ if (!isSocketConnected()) {
+ throw new SocketException("ZeroTierSocket is not connected");
+ }
+ getImpl().shutdownOutput();
+ outputShutdown = true;
+ }
+
+ /*
+ * Gets the underlying implementation's file descriptor.
+ */
+ public FileDescriptor getFileDescriptor()
+ {
+ return impl.getFileDescriptor();
+ }
+}
\ No newline at end of file
diff --git a/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocketAddress.java b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocketAddress.java
new file mode 100644
index 0000000..e773c09
--- /dev/null
+++ b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocketAddress.java
@@ -0,0 +1,66 @@
+package com.zerotier.libzt;
+
+import com.zerotier.libzt.ZeroTier;
+
+import java.net.InetAddress;
+
+// Designed to transport address information across the JNI boundary
+public class ZeroTierSocketAddress
+{
+ public byte[] _ip6 = new byte[16];
+ public byte[] _ip4 = new byte[4];
+
+ public int _family;
+ public int _port; // Also reused for netmask or prefix
+
+ public ZeroTierSocketAddress() {}
+
+ public ZeroTierSocketAddress(String ipStr, int port)
+ {
+ if(ipStr.contains(":")) {
+ _family = ZeroTier.AF_INET6;
+ try {
+ InetAddress ip = InetAddress.getByName(ipStr);
+ _ip6 = ip.getAddress();
+ }
+ catch (Exception e) { }
+ }
+ else if(ipStr.contains(".")) {
+ _family = ZeroTier.AF_INET;
+ try {
+ InetAddress ip = InetAddress.getByName(ipStr);
+ _ip4 = ip.getAddress();
+ }
+ catch (Exception e) { }
+ }
+ _port = port;
+ }
+
+ public int getPort() { return _port; }
+ public int getNetmask() { return _port; }
+ public int getPrefix() { return _port; }
+
+ public String ipString()
+ {
+ if (_family == ZeroTier.AF_INET) {
+ try {
+ InetAddress inet = InetAddress.getByAddress(_ip4);
+ return "" + inet.getHostAddress();
+ } catch (Exception e) {
+ System.out.println(e);
+ }
+ }
+ if (_family == ZeroTier.AF_INET6) {
+ try {
+ InetAddress inet = InetAddress.getByAddress(_ip6);
+ return "" + inet.getHostAddress();
+ } catch (Exception e) {
+ System.out.println(e);
+ }
+ }
+ return "";
+ }
+
+ public String toString() { return ipString() + ":" + _port; }
+ public String toCIDR() { return ipString() + "/" + _port; }
+}
diff --git a/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocketFactory.java b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocketFactory.java
new file mode 100644
index 0000000..0e79a65
--- /dev/null
+++ b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocketFactory.java
@@ -0,0 +1,51 @@
+package com.zerotier.libzt;
+
+import com.zerotier.libzt.ZeroTier;
+import com.zerotier.libzt.ZeroTierSocket;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import javax.net.SocketFactory;
+import java.util.Objects;
+
+public class ZeroTierSocketFactory extends SocketFactory
+{
+ public ZeroTierSocketFactory() { }
+
+ public static SocketFactory getDefault()
+ {
+ return null;
+ }
+
+ public Socket createSocket()
+ throws IOException, UnknownHostException
+ {
+ return new ZeroTierSocket();
+ }
+
+ public Socket createSocket(String host, int port)
+ throws IOException, UnknownHostException
+ {
+ return new ZeroTierSocket(host, port);
+ }
+
+ public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
+ throws IOException, UnknownHostException
+ {
+ return new ZeroTierSocket(host, port, localHost, localPort);
+ }
+
+ public Socket createSocket(InetAddress host, int port)
+ throws IOException
+ {
+ return new ZeroTierSocket(host, port);
+ }
+
+ public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort)
+ throws IOException
+ {
+ return new ZeroTierSocket(address, port, localAddress, localPort);
+ }
+}
diff --git a/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocketImpl.java b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocketImpl.java
new file mode 100644
index 0000000..cfbd72b
--- /dev/null
+++ b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocketImpl.java
@@ -0,0 +1,772 @@
+package com.zerotier.libzt;
+
+import com.zerotier.libzt.ZeroTier;
+import com.zerotier.libzt.ZeroTierSocketAddress;
+import com.zerotier.libzt.ZeroTierInputStream;
+import com.zerotier.libzt.ZeroTierOutputStream;
+import com.zerotier.libzt.ZeroTierSocketOptionValue;
+
+import java.io.*;
+import java.net.*;
+import java.util.Objects;
+import java.lang.Object;
+import java.net.SocketImpl;
+import java.net.InetSocketAddress;
+import java.util.Set;
+import java.lang.Boolean;
+
+import com.zerotier.libzt.ZeroTier;
+
+import android.os.ParcelFileDescriptor;
+
+public class ZeroTierSocketImpl extends SocketImpl
+{
+ private int defaultProtocol = 0;
+
+ /*
+ * File descriptor from lower native layer
+ */
+ private int zfd = -1;
+ private int zfd4 = -1;
+ private int zfd6 = -1;
+
+ /*
+ * Input and Output streams
+ */
+ private ZeroTierInputStream in = new ZeroTierInputStream();
+ private ZeroTierOutputStream out = new ZeroTierOutputStream();
+
+ Socket socket = null;
+ ServerSocket serverSocket = null;
+
+ /*
+ * The remote address the socket is connected to
+ */
+ protected InetAddress address;
+
+ /*
+ * Sets the underlying file descriptor valud for the SocketImpl as well as the Input/OutputStream
+ */
+ private void setNativeFileDescriptor(int fd)
+ {
+ zfd = fd;
+ in.zfd = fd;
+ out.zfd = fd;
+ }
+
+ /*
+ * Various socket options that are cached from calls to setOption() before
+ * the socket exists.
+ */
+ private int _so_rcvtimeo;
+ private boolean _so_keepalive;
+ private int _so_sndbuf;
+ private int _so_rcvbuf;
+ private boolean _so_reuseaddr;
+ private int _so_linger;
+ private int _so_tos;
+ private boolean _so_nodelay;
+
+ private void setCachedSocketOptions(int fd)
+ {
+ if (fd < 0) {
+ return;
+ }
+
+ // If we previously received a setSoTimeout() call but were unable to process it due
+ // to the fact that the underlying socket didn't even exist yet, do so now.
+ int err = 0;
+ ZeroTierSocketOptionValue optval = new ZeroTierSocketOptionValue();
+
+ try {
+ // SO_TIMEOUT
+ if (_so_rcvtimeo > 0) {
+ optval.isInteger = true;
+ optval.isBoolean = false;
+ optval.integerValue = ((Integer)_so_rcvtimeo).intValue();
+ if ((err = ZeroTier.setsockopt(fd, ZeroTier.SOL_SOCKET, ZeroTier.SO_RCVTIMEO, optval)) < 0) {
+ throw new IOException("socket("+fd+"), errno="+err+", unable to set previously cached SO_RCVTIMEO");
+ }
+ }
+ // SO_KEEPALIVE
+ if (_so_keepalive == true) {
+ optval.isInteger = false;
+ optval.isBoolean = true;
+ optval.booleanValue = ((Boolean)_so_keepalive).booleanValue() ? true : false;
+ if ((err = ZeroTier.setsockopt(fd, ZeroTier.SOL_SOCKET, ZeroTier.SO_KEEPALIVE, optval)) < 0) {
+ throw new IOException("socket("+fd+"), errno="+err+", unable to set previously cached SO_KEEPALIVE");
+ }
+ }
+ // SO_SNDBUF
+ if (_so_sndbuf > 0) {
+ optval.isInteger = true;
+ optval.isBoolean = false;
+ optval.integerValue = ((Integer)_so_sndbuf).intValue();
+ if ((err = ZeroTier.setsockopt(fd, ZeroTier.SOL_SOCKET, ZeroTier.SO_SNDBUF, optval)) < 0) {
+ throw new IOException("socket("+fd+"), errno="+err+", unable to set previously cached SO_SNDBUF");
+ }
+ }
+ // SO_RCVBUF
+ if (_so_rcvbuf > 0) {
+ optval.isInteger = true;
+ optval.isBoolean = false;
+ optval.integerValue = ((Integer)_so_rcvbuf).intValue();
+ if ((err = ZeroTier.setsockopt(fd, ZeroTier.SOL_SOCKET, ZeroTier.SO_RCVBUF, optval)) < 0) {
+ throw new IOException("socket("+fd+"), errno="+err+", unable to set previously cached SO_RCVBUF");
+ }
+ }
+ // SO_REUSEADDR
+ if (_so_reuseaddr == true) {
+ optval.isInteger = false;
+ optval.isBoolean = true;
+ optval.booleanValue = ((Boolean)_so_reuseaddr).booleanValue() ? true : false;
+ if ((err = ZeroTier.setsockopt(fd, ZeroTier.SOL_SOCKET, ZeroTier.SO_REUSEADDR, optval)) < 0) {
+ throw new IOException("socket("+fd+"), errno="+err+", unable to set previously cached SO_REUSEADDR");
+ }
+ }
+ // SO_LINGER
+ if (_so_linger > 0) {
+ optval.isInteger = true;
+ optval.isBoolean = false;
+ optval.integerValue = ((Integer)_so_linger).intValue();
+ if ((err = ZeroTier.setsockopt(fd, ZeroTier.SOL_SOCKET, ZeroTier.SO_LINGER, optval)) < 0) {
+ throw new IOException("socket("+fd+"), errno="+err+", unable to set previously cached SO_LINGER");
+ }
+ }
+ // IP_TOS
+ if (_so_tos > 0) {
+ optval.isInteger = true;
+ optval.isBoolean = false;
+ optval.integerValue = ((Integer)_so_tos).intValue();
+ if ((err = ZeroTier.setsockopt(fd, ZeroTier.SOL_SOCKET, ZeroTier.IP_TOS, optval)) < 0) {
+ throw new IOException("socket("+fd+"), errno="+err+", unable to set previously cached IP_TOS");
+ }
+ }
+ // TCP_NODELAY
+ if (_so_nodelay == true) {
+ optval.isInteger = false;
+ optval.isBoolean = true;
+ optval.booleanValue = ((Boolean)_so_nodelay).booleanValue() ? true : false;
+ if ((err = ZeroTier.setsockopt(fd, ZeroTier.SOL_SOCKET, ZeroTier.TCP_NODELAY, optval)) < 0) {
+ throw new IOException("socket("+fd+"), errno="+err+", unable to set previously cached TCP_NODELAY");
+ }
+ }
+ }
+ catch (Exception e) {
+ System.err.println(e);
+ }
+ }
+
+ /*
+ * Constructor which creates a new ZeroTierSocketImpl
+ */
+ public ZeroTierSocketImpl()
+ {
+ if ((zfd > -1) | (zfd4 > -1) | (zfd6 > -1)) { return; }
+ try {
+ create(true);
+ } catch (Exception x) {
+ System.err.println("error creating ZeroTierSocketImpl instance: " + x);
+ }
+ in.zfd = zfd;
+ out.zfd = zfd;
+ }
+
+ /*
+ * Constructor to be called when an underlying ZeroTier socket already exists (does not create a new ZeroTierSocketImpl)
+ */
+ public ZeroTierSocketImpl(int fd) {
+ setNativeFileDescriptor(fd);
+ }
+
+ /*
+ * Creates a new ZeroTier socket in the native layer
+ */
+ protected void create(boolean stream)
+ throws IOException
+ {
+ /*
+ * The native-layer socket is only created once a connect/bind call is made, this is due to the fact
+ * that beforehand we have no way to determine whether we should create an AF_INET or AF_INET6 socket,
+ * as a result, this method intentionally does nothing.
+ */
+ }
+
+ /*
+ * Creates the underlying libzt socket.
+ *
+ * This does the real work that can't be done in the constructor. This is because the socket address type
+ * isn't known until a connect() or bind() request is given. Additionally we cache the value provided by any
+ * setSoTimeout() calls and implement it immediately after creation.
+ */
+ private void createAppropriateSocketImpl(InetAddress addr)
+ throws IOException
+ {
+ if ((zfd > -1) | (zfd4 > -1) | (zfd6 > -1)) {
+ return; // Socket already created
+ }
+ if(addr instanceof Inet4Address) {
+ if ((zfd4 = ZeroTier.socket(ZeroTier.AF_INET, ZeroTier.SOCK_STREAM, defaultProtocol)) < 0) {
+ throw new IOException("socket(), errno="+zfd4+", see: libzt/ext/lwip/src/include/errno.h");
+ }
+ setCachedSocketOptions(zfd4);
+ }
+ /*
+ * Since Java creates sockets capable of handling IPV4 and IPV6, we must simulate this. We do this by
+ * creating two sockets (one of each type)
+ */
+ if(addr instanceof Inet6Address) {
+ if ((zfd4 = ZeroTier.socket(ZeroTier.AF_INET, ZeroTier.SOCK_STREAM, defaultProtocol)) < 0) {
+ throw new IOException("socket(), errno="+zfd4+", see: libzt/ext/lwip/src/include/errno.h");
+ }
+ if ((zfd6 = ZeroTier.socket(ZeroTier.AF_INET6, ZeroTier.SOCK_STREAM, defaultProtocol)) < 0) {
+ throw new IOException("socket(), errno="+zfd6+", see: libzt/ext/lwip/src/include/errno.h");
+ }
+ setCachedSocketOptions(zfd4);
+ setCachedSocketOptions(zfd6);
+ }
+ }
+
+ /*
+ * Return the remote address the socket is connected to
+ */
+ protected InetAddress getInetAddress()
+ {
+ return address;
+ }
+
+ /*
+ * Connects the socket to a remote address
+ */
+ protected void connect(String host, int port)
+ throws IOException
+ {
+ // TODO: Refactor and consolidate the connect() logic for all three methods
+ createAppropriateSocketImpl(InetAddress.getByName(host));
+ if ((zfd4 < 0) & (zfd6 < 0)) {
+ throw new IOException("invalid fd");
+ }
+ int err;
+ InetAddress address = InetAddress.getByName(host);
+ ZeroTierSocketAddress zt_addr = new ZeroTierSocketAddress(host, port);
+ if (address instanceof Inet4Address) {
+ if ((err = ZeroTier.connect(zfd4, zt_addr)) < 0) {
+ throw new IOException("connect(), errno="+err+", see: libzt/ext/lwip/src/include/errno.h");
+ }
+ setNativeFileDescriptor(zfd4);
+ }
+ if (address instanceof Inet6Address) {
+ if ((err = ZeroTier.connect(zfd6, zt_addr)) < 0) {
+ throw new IOException("connect(), errno="+err+", see: libzt/ext/lwip/src/include/errno.h");
+ }
+ setNativeFileDescriptor(zfd6);
+ }
+ super.port = port;
+ }
+
+ /*
+ * Connects the socket to a remote address
+ */
+ protected void connect(InetAddress address, int port)
+ throws IOException
+ {
+ // TODO: Refactor and consolidate the connect() logic for all three methods
+ createAppropriateSocketImpl(address);
+ if ((zfd4 < 0) & (zfd6 < 0)) {
+ throw new IOException("invalid fd");
+ }
+ int err;
+ ZeroTierSocketAddress zt_addr = new ZeroTierSocketAddress(address.getHostAddress(), port);
+ if (address instanceof Inet4Address) {
+ if ((err = ZeroTier.connect(zfd4, zt_addr)) < 0) {
+ throw new IOException("connect(), errno="+err+", see: libzt/ext/lwip/src/include/errno.h");
+ }
+ setNativeFileDescriptor(zfd4);
+ }
+ if (address instanceof Inet6Address) {
+ if ((err = ZeroTier.connect(zfd6, zt_addr)) < 0) {
+ throw new IOException("connect(), errno="+err+", see: libzt/ext/lwip/src/include/errno.h");
+ }
+ setNativeFileDescriptor(zfd6);
+ }
+ super.port = port;
+ }
+
+ /*
+ * Connects the socket to a remote address
+ */
+ protected void connect(SocketAddress address, int timeout)
+ throws IOException
+ {
+ // TODO: Refactor and consolidate the connect() logic for all three methods
+ //System.out.println("host="+((InetSocketAddress)address).getHostString()+", port="+((InetSocketAddress)address).getPort() + ", timeout="+timeout);
+ createAppropriateSocketImpl(((InetSocketAddress)address).getAddress());
+ if ((zfd4 < 0) & (zfd6 < 0)) {
+ throw new IOException("invalid fd");
+ }
+ ZeroTierSocketAddress zt_addr = null;
+ int err;
+ int port = ((InetSocketAddress)address).getPort();
+ if (((InetSocketAddress)address).getAddress() instanceof Inet4Address) {
+ zt_addr = new ZeroTierSocketAddress(((InetSocketAddress)address).getHostString(), ((InetSocketAddress)address).getPort());
+ if ((err = ZeroTier.connect(zfd4, zt_addr)) < 0) {
+ throw new IOException("connect("+zfd4+"), errno="+err+", see: libzt/ext/lwip/src/include/errno.h");
+ }
+ setNativeFileDescriptor(zfd4);
+ }
+ if (((InetSocketAddress)address).getAddress() instanceof Inet6Address) {
+ zt_addr = new ZeroTierSocketAddress(((InetSocketAddress)address).getHostString(), ((InetSocketAddress)address).getPort());
+ if ((err = ZeroTier.connect(zfd6, zt_addr)) < 0) {
+ throw new IOException("connect("+zfd6+"), errno="+err+", see: libzt/ext/lwip/src/include/errno.h");
+ }
+ setNativeFileDescriptor(zfd6);
+ }
+ super.port = port;
+ }
+
+ /*
+ * Binds the socket to a local address.
+ *
+ * If this gets a bind() request on [::] it will create a both an IPv4 and an IPv6
+ * socket. This is because we might receive a subsequent listen() and accept() request
+ * and want to accept an IPv6 connection. (or) we may get a connect() request with
+ * an IPv4 address. In the latter case we must abandon the IPv6 socket and use the IPv4
+ * socket exclusively.
+ */
+ protected void bind(InetAddress host, int port)
+ throws IOException
+ {
+ createAppropriateSocketImpl(host);
+ /*
+ After this point we may have either a) created a single IPv4 socket, or b) created
+ an IPv4 and IPv6 socket in anticipation of either verion being used
+ */
+ //System.out.println("host="+host.toString()+", port="+port);
+ int err;
+ if ((zfd < 0) & (zfd4 < 0) & (zfd6 < 0)) {
+ throw new IOException("invalid fd");
+ }
+ ZeroTierSocketAddress zt_addr = new ZeroTierSocketAddress(host.getHostAddress(), port);
+
+ if (zfd6 > -1) {
+ // Since an IPv6 socket and accept IPv4 connections we will only bind to this address
+ if ((err = ZeroTier.bind(zfd6, zt_addr)) < 0) {
+ throw new IOException("bind("+zfd6+"), errno="+err+", see: libzt/ext/lwip/src/include/errno.h");
+ }
+ super.localport = port;
+ return;
+ }
+ if (zfd4 > -1) {
+ // Otherwise, just bind to the regular IPv4 address
+ if ((err = ZeroTier.bind(zfd4, zt_addr)) < 0) {
+ throw new IOException("bind("+zfd4+"), errno="+err+", see: libzt/ext/lwip/src/include/errno.h");
+ }
+ super.localport = port;
+ return;
+ }
+
+ }
+
+ /*
+ * Puts the socket into a listening state.
+ *
+ * We listen on the IPv6 socket since it can listen for IPv4 connections
+ */
+ protected void listen(int backlog)
+ throws IOException
+ {
+ int err;
+ if ((zfd6 < 0) | (backlog < 0)) {
+ throw new IOException("invalid fd and/or backlog");
+ }
+ if ((err = ZeroTier.listen(zfd6, backlog)) < 0) {
+ throw new IOException("listen("+zfd6+"), errno="+err+", see: libzt/ext/lwip/src/include/errno.h");
+ }
+ }
+
+ /*
+ * Accepts an incoming connection.
+ *
+ * We accept on the IPv6 socket since it can accept IPv4 connections
+ */
+ protected void accept(SocketImpl si)
+ throws IOException
+ {
+ if (zfd6 < 0) {
+ throw new IOException("invalid fd");
+ }
+ int accetpedFd = -1;
+ ZeroTierSocketAddress addr = new ZeroTierSocketAddress();
+ if ((accetpedFd = ZeroTier.accept(zfd6, addr)) < 0) {
+ throw new IOException("accept("+zfd6+"), errno="+accetpedFd+", see: libzt/ext/lwip/src/include/errno.h");
+ }
+ // Give the new socket fd from the native layer to the new unconnected ZeroTierSocketImpl
+ ((ZeroTierSocketImpl)si).setFileDescriptor(accetpedFd);
+ }
+
+ /*
+ * Returns the input stream for this socket
+ */
+ protected ZeroTierInputStream getInputStream()
+ throws IOException
+ {
+ if (in == null) {
+ throw new IOException();
+ }
+ return in;
+ }
+
+ /*
+ * Returns the output stream for this socket
+ */
+ protected ZeroTierOutputStream getOutputStream()
+ throws IOException
+ {
+ if (out == null) {
+ throw new IOException();
+ }
+ return out;
+ }
+
+ /*
+ * Returns the remote port to which this socket is connected
+ */
+ protected int getPort()
+ {
+ return super.port;
+ }
+
+ /*
+ * Returns the local port to which this socket is bound
+ */
+ protected int getLocalPort()
+ {
+ return super.localport;
+ }
+
+ /*
+ * Returns whether this socket implementation supports urgent data (hint: it doesn't)
+ */
+ protected boolean supportsUrgentData()
+ {
+ return false;
+ }
+
+ /*
+ *
+ */
+ void setSocket(ZeroTierSocket soc)
+ {
+ this.socket = soc;
+ }
+
+ /*
+ *
+ */
+ Socket getSocket()
+ {
+ return socket;
+ }
+
+ /*
+ *
+ */
+ /*
+ void setServerSocket(ZeroTierServerSocket soc)
+ {
+ this.serverSocket = soc;
+ }
+ */
+
+ /*
+ *
+ */
+ ServerSocket getServerSocket()
+ {
+ return serverSocket;
+ }
+
+ /*
+ * Return the number of bytes that can be read from the socket without blocking
+ */
+ protected int available()
+ throws IOException
+ {
+ // TODO
+ return 0;
+ }
+
+ /*
+ * Closes the socket
+ */
+ protected void close()
+ throws IOException
+ {
+ if (zfd > -1) {
+ ZeroTier.close(zfd);
+ }
+ if (zfd4 > -1) {
+ ZeroTier.close(zfd4);
+ }
+ if (zfd6 > -1) {
+ ZeroTier.close(zfd6);
+ }
+ }
+
+ /*
+ * Send one byte of urgent data on the socket
+ */
+ protected void sendUrgentData(int data)
+ throws IOException
+ {
+ System.err.println("sendUrgentData: ZeroTierSocketImpl does not currently support this feature");
+ }
+
+ /*
+ * Gets some specified socket option
+ */
+ public Object getOption(int optID)
+ throws SocketException
+ {
+ // Call native layer
+ ZeroTierSocketOptionValue optval = new ZeroTierSocketOptionValue();
+ int option = -1;
+ int level = -1;
+
+ if (zfd < 0) { // If we haven't committed to a socket version yet, cache the value
+ if (optID == SocketOptions.SO_TIMEOUT || optID == ZeroTier.SO_RCVTIMEO) {
+ return Integer.valueOf(_so_rcvtimeo);
+ }
+ if (optID == SocketOptions.SO_KEEPALIVE || optID == ZeroTier.SO_KEEPALIVE) {
+ return Boolean.valueOf(_so_keepalive);
+ }
+ if (optID == SocketOptions.SO_SNDBUF || optID == ZeroTier.SO_SNDBUF) {
+ return Integer.valueOf(_so_sndbuf);
+ }
+ if (optID == SocketOptions.SO_RCVBUF || optID == ZeroTier.SO_RCVBUF) {
+ return Integer.valueOf(_so_rcvbuf);
+ }
+ if (optID == SocketOptions.SO_REUSEADDR || optID == ZeroTier.SO_REUSEADDR) {
+ return Boolean.valueOf(_so_reuseaddr);
+ }
+ if (optID == SocketOptions.SO_LINGER || optID == ZeroTier.SO_LINGER) {
+ return Integer.valueOf(_so_linger);
+ }
+ if (optID == SocketOptions.IP_TOS || optID == ZeroTier.IP_TOS) {
+ return Integer.valueOf(_so_tos);
+ }
+ if (optID == SocketOptions.TCP_NODELAY || optID == ZeroTier.TCP_NODELAY) {
+ return Boolean.valueOf(_so_nodelay);
+ }
+ }
+ else {
+ if (optID == SocketOptions.SO_TIMEOUT || optID == ZeroTier.SO_RCVTIMEO) {
+ option = ZeroTier.SO_RCVTIMEO;
+ level = ZeroTier.SOL_SOCKET;
+ }
+ if (optID == SocketOptions.SO_KEEPALIVE || optID == ZeroTier.SO_KEEPALIVE) {
+ option = ZeroTier.SO_KEEPALIVE;
+ level = ZeroTier.SOL_SOCKET;
+ }
+ if (optID == SocketOptions.SO_SNDBUF || optID == ZeroTier.SO_SNDBUF) {
+ option = ZeroTier.SO_SNDBUF;
+ level = ZeroTier.SOL_SOCKET;
+ }
+ if (optID == SocketOptions.SO_RCVBUF || optID == ZeroTier.SO_RCVBUF) {
+ option = ZeroTier.SO_RCVBUF;
+ level = ZeroTier.SOL_SOCKET;
+ }
+ if (optID == SocketOptions.SO_REUSEADDR || optID == ZeroTier.SO_REUSEADDR) {
+ option = ZeroTier.SO_REUSEADDR;
+ level = ZeroTier.SOL_SOCKET;
+ }
+ if (optID == SocketOptions.SO_LINGER || optID == ZeroTier.SO_LINGER) {
+ option = ZeroTier.SO_LINGER;
+ level = ZeroTier.SOL_SOCKET;
+ }
+ // IP
+ if (optID == SocketOptions.IP_TOS || optID == ZeroTier.IP_TOS) {
+ option = ZeroTier.IP_TOS;
+ level = ZeroTier.IPPROTO_IP;
+ }
+ // TCP
+ if (optID == SocketOptions.TCP_NODELAY || optID == ZeroTier.TCP_NODELAY) {
+ option = ZeroTier.TCP_NODELAY;
+ level = ZeroTier.IPPROTO_TCP;
+ }
+ ZeroTier.getsockopt(zfd, level, option, optval);
+ // Convert native layer's response into Java object of some sort
+ if (optval.isBoolean) {
+ return Boolean.valueOf(optval.booleanValue);
+ }
+ if (optval.isInteger) {
+ return Integer.valueOf(optval.integerValue);
+ }
+ }
+ return null;
+ }
+
+ /*
+ * Sets a socket option to a specified value. This method should be able to handle SocketOptions values
+ * as well as native ZeroTier.* options
+ */
+ public void setOption(int optID, Object value)
+ throws SocketException
+ {
+ if (value == null) {
+ throw new UnsupportedOperationException();
+ }
+
+ int option = -1;
+ int level = -1;
+
+ ZeroTierSocketOptionValue optval = new ZeroTierSocketOptionValue();
+
+ if (zfd < 0) { // If we haven't committed to a socket version yet, cache the value
+ if (optID == SocketOptions.SO_TIMEOUT || optID == ZeroTier.SO_RCVTIMEO) {
+ _so_rcvtimeo = ((Integer)value).intValue(); return;
+ }
+ if (optID == SocketOptions.SO_KEEPALIVE || optID == ZeroTier.SO_KEEPALIVE) {
+ _so_keepalive = ((Boolean)value).booleanValue() ? true : false; return;
+ }
+ if (optID == SocketOptions.SO_SNDBUF || optID == ZeroTier.SO_SNDBUF) {
+ _so_sndbuf = ((Integer)value).intValue(); return;
+ }
+ if (optID == SocketOptions.SO_RCVBUF || optID == ZeroTier.SO_RCVBUF) {
+ _so_rcvbuf = ((Integer)value).intValue(); return;
+ }
+ if (optID == SocketOptions.SO_REUSEADDR || optID == ZeroTier.SO_REUSEADDR) {
+ _so_reuseaddr = ((Boolean)value).booleanValue() ? true : false; return;
+ }
+ if (optID == SocketOptions.SO_LINGER || optID == ZeroTier.SO_LINGER) {
+ _so_linger = ((Integer)value).intValue(); return;
+ }
+ if (optID == SocketOptions.IP_TOS || optID == ZeroTier.IP_TOS) {
+ _so_tos = ((Integer)value).intValue(); return;
+ }
+ if (optID == SocketOptions.TCP_NODELAY || optID == ZeroTier.TCP_NODELAY) {
+ _so_nodelay = ((Boolean)value).booleanValue() ? true : false; return;
+ }
+ }
+ else {
+ // SOL
+ if (optID == SocketOptions.SO_TIMEOUT || optID == ZeroTier.SO_RCVTIMEO) {
+ option = ZeroTier.SO_RCVTIMEO;
+ level = ZeroTier.SOL_SOCKET;
+ if (value instanceof Integer) {
+ optval.isInteger = true;
+ optval.integerValue = ((Integer)value).intValue();
+ }
+ }
+
+ if (optID == SocketOptions.SO_KEEPALIVE || optID == ZeroTier.SO_KEEPALIVE) {
+ option = ZeroTier.SO_KEEPALIVE;
+ level = ZeroTier.SOL_SOCKET;
+ if (value instanceof Integer) {
+ optval.isBoolean = true;
+ optval.booleanValue = ((Boolean)value).booleanValue() ? true : false;
+ }
+ }
+ if (optID == SocketOptions.SO_SNDBUF || optID == ZeroTier.SO_SNDBUF) {
+ option = ZeroTier.SO_SNDBUF;
+ level = ZeroTier.SOL_SOCKET;
+ if (value instanceof Integer) {
+ optval.isInteger = true;
+ optval.integerValue = ((Integer)value).intValue();
+ }
+ }
+ if (optID == SocketOptions.SO_RCVBUF || optID == ZeroTier.SO_RCVBUF) {
+ option = ZeroTier.SO_RCVBUF;
+ level = ZeroTier.SOL_SOCKET;
+ if (value instanceof Integer) {
+ optval.isInteger = true;
+ optval.integerValue = ((Integer)value).intValue();
+ }
+ }
+ if (optID == SocketOptions.SO_REUSEADDR || optID == ZeroTier.SO_REUSEADDR) {
+ option = ZeroTier.SO_REUSEADDR;
+ level = ZeroTier.SOL_SOCKET;
+ if (value instanceof Integer) {
+ optval.isBoolean = true;
+ optval.booleanValue = ((Boolean)value).booleanValue() ? true : false;
+ }
+ }
+ if (optID == SocketOptions.SO_LINGER || optID == ZeroTier.SO_LINGER) {
+ option = ZeroTier.SO_LINGER;
+ level = ZeroTier.SOL_SOCKET;
+ if (value instanceof Integer) {
+ optval.isInteger = true;
+ optval.integerValue = ((Integer)value).intValue();
+ }
+ }
+ // IP
+ if (optID == SocketOptions.IP_TOS || optID == ZeroTier.IP_TOS) {
+ option = ZeroTier.IP_TOS;
+ level = ZeroTier.IPPROTO_IP;
+ if (value instanceof Integer) {
+ optval.isInteger = true;
+ optval.integerValue = ((Integer)value).intValue();
+ }
+ }
+ // TCP
+ if (optID == SocketOptions.TCP_NODELAY || optID == ZeroTier.TCP_NODELAY) {
+ option = ZeroTier.TCP_NODELAY;
+ level = ZeroTier.IPPROTO_TCP;
+ if (value instanceof Integer) {
+ optval.isBoolean = true;
+ optval.booleanValue = ((Boolean)value).booleanValue() ? true : false;
+ }
+ }
+
+ if (option < 0) { // No option was properly set
+ //throw new UnsupportedOperationException();
+ }
+ ZeroTier.setsockopt(zfd, level, option, optval);
+ }
+ }
+
+ /*
+ * Disables the input aspect of the socket
+ */
+ public void shutdownInput()
+ {
+ ZeroTier.shutdown(zfd, ZeroTier.SHUT_RD);
+ // Alternatively: getInputStream().close();
+ }
+
+ /*
+ * Disables the output aspect of the socket
+ */
+ public void shutdownOutput()
+ {
+ ZeroTier.shutdown(zfd, ZeroTier.SHUT_WR);
+ // Alternatively: getOutputStream().close();
+ }
+
+ /*
+ * Sets the file descriptor
+ */
+ public void setFileDescriptor(int fd)
+ {
+ zfd = fd;
+ }
+
+ /*
+ * Resets the socket
+ */
+ void reset()
+ throws IOException
+ {
+ localport = 0;
+ address = null;
+ port = 0;
+ }
+
+ public FileDescriptor getFileDescriptor()
+ {
+ // TODO: Should probably remove this for production
+ System.out.println("getFileDescriptor(), zfd="+zfd);
+ ParcelFileDescriptor pfd = ParcelFileDescriptor.adoptFd(zfd);
+ return pfd.getFileDescriptor();
+ }
+}
\ No newline at end of file
diff --git a/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocketImplFactory.java b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocketImplFactory.java
new file mode 100644
index 0000000..f04bf95
--- /dev/null
+++ b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocketImplFactory.java
@@ -0,0 +1,29 @@
+package com.zerotier.libzt;
+
+import com.zerotier.libzt.ZeroTier;
+import com.zerotier.libzt.ZeroTierSocketImpl;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import javax.net.SocketFactory;
+import java.net.SocketImplFactory;
+import java.util.Objects;
+
+
+public class ZeroTierSocketImplFactory implements SocketImplFactory
+{
+ /*
+ * Does nothing
+ */
+ public ZeroTierSocketImplFactory() { }
+
+ /*
+ * Produces a ZeroTierSocketImpl
+ */
+ public ZeroTierSocketImpl createSocketImpl()
+ {
+ return new ZeroTierSocketImpl();
+ }
+}
diff --git a/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocketOptionValue.java b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocketOptionValue.java
new file mode 100644
index 0000000..15bbfef
--- /dev/null
+++ b/ports/android/app/src/main/java/com/zerotier/libzt/ZeroTierSocketOptionValue.java
@@ -0,0 +1,14 @@
+package com.zerotier.libzt;
+
+public class ZeroTierSocketOptionValue
+{
+ public boolean isBoolean = false;
+ public boolean booleanValue;
+
+ public boolean isInteger = false;
+ public int integerValue;
+
+ public boolean isTimeval = false;
+ public int tv_sec; // seconds
+ public int tv_usec; // microseconds
+}
\ No newline at end of file
diff --git a/ports/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/ports/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..c7bd21d
--- /dev/null
+++ b/ports/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ports/android/app/src/main/res/drawable/ic_launcher_background.xml b/ports/android/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..d5fccc5
--- /dev/null
+++ b/ports/android/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ports/android/app/src/main/res/layout/activity_main.xml b/ports/android/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..7d8d24e
--- /dev/null
+++ b/ports/android/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ports/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/ports/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/ports/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/ports/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/ports/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/ports/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/ports/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/ports/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a2f5908
Binary files /dev/null and b/ports/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/ports/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/ports/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1b52399
Binary files /dev/null and b/ports/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/ports/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/ports/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..ff10afd
Binary files /dev/null and b/ports/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/ports/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/ports/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..115a4c7
Binary files /dev/null and b/ports/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/ports/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/ports/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..dcd3cd8
Binary files /dev/null and b/ports/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/ports/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/ports/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..459ca60
Binary files /dev/null and b/ports/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/ports/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/ports/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..8ca12fe
Binary files /dev/null and b/ports/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/ports/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/ports/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8e19b41
Binary files /dev/null and b/ports/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/ports/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/ports/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..b824ebd
Binary files /dev/null and b/ports/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/ports/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/ports/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..4c19a13
Binary files /dev/null and b/ports/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/ports/android/app/src/main/res/values/colors.xml b/ports/android/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..3ab3e9c
--- /dev/null
+++ b/ports/android/app/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+
+
+ #3F51B5
+ #303F9F
+ #FF4081
+
diff --git a/ports/android/app/src/main/res/values/strings.xml b/ports/android/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..87c6cc8
--- /dev/null
+++ b/ports/android/app/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ ZeroTier
+
diff --git a/ports/android/app/src/main/res/values/styles.xml b/ports/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..5885930
--- /dev/null
+++ b/ports/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/ports/android/app/src/test/java/com/example/zerotier/ExampleUnitTest.java b/ports/android/app/src/test/java/com/example/zerotier/ExampleUnitTest.java
new file mode 100644
index 0000000..f0aee30
--- /dev/null
+++ b/ports/android/app/src/test/java/com/example/zerotier/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.example.zerotier;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/ports/android/build.gradle b/ports/android/build.gradle
new file mode 100644
index 0000000..43c0708
--- /dev/null
+++ b/ports/android/build.gradle
@@ -0,0 +1,27 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+
+ repositories {
+ google()
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.1.3'
+
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/ports/android/gradle.properties b/ports/android/gradle.properties
new file mode 100644
index 0000000..b2b3bb2
--- /dev/null
+++ b/ports/android/gradle.properties
@@ -0,0 +1,13 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx1536m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
\ No newline at end of file
diff --git a/ports/android/gradle/wrapper/gradle-wrapper.jar b/ports/android/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..7a3265e
Binary files /dev/null and b/ports/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/ports/android/gradle/wrapper/gradle-wrapper.properties b/ports/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..3705bc5
--- /dev/null
+++ b/ports/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Wed Jul 18 09:57:38 PDT 2018
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip
diff --git a/ports/android/gradlew b/ports/android/gradlew
new file mode 100755
index 0000000..cccdd3d
--- /dev/null
+++ b/ports/android/gradlew
@@ -0,0 +1,172 @@
+#!/usr/bin/env sh
+
+##############################################################################
+##
+## 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=""
+
+# 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, switch paths to Windows format before running java
+if $cygwin ; 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=$((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"
+
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
+ cd "$(dirname "$0")"
+fi
+
+exec "$JAVACMD" "$@"
diff --git a/ports/android/gradlew.bat b/ports/android/gradlew.bat
new file mode 100644
index 0000000..e95643d
--- /dev/null
+++ b/ports/android/gradlew.bat
@@ -0,0 +1,84 @@
+@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 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=
+
+@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 init
+
+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 init
+
+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
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+: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 %CMD_LINE_ARGS%
+
+: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/ports/android/settings.gradle b/ports/android/settings.gradle
new file mode 100644
index 0000000..e7b4def
--- /dev/null
+++ b/ports/android/settings.gradle
@@ -0,0 +1 @@
+include ':app'
diff --git a/ports/clean.bat b/ports/clean.bat
new file mode 100644
index 0000000..6e337a8
--- /dev/null
+++ b/ports/clean.bat
@@ -0,0 +1,4 @@
+rd /S /Q bin
+rd /S /Q build
+rd /S /Q WinBuild32
+rd /S /Q WinBuild64
\ No newline at end of file
diff --git a/ports/clean.sh b/ports/clean.sh
new file mode 100755
index 0000000..65e8421
--- /dev/null
+++ b/ports/clean.sh
@@ -0,0 +1,7 @@
+#!/bin/bash
+
+rm -rf bin build products tmp
+rm -f *.o *.s *.exp *.lib .depend* *.core core
+rm -rf .depend
+find . -type f \( -name '*.o' -o -name '*.o.d' -o -name \
+ '*.out' -o -name '*.log' -o -name '*.dSYM' -o -name '*.class' \) -delete
diff --git a/ports/dist.bat b/ports/dist.bat
new file mode 100644
index 0000000..b82f883
--- /dev/null
+++ b/ports/dist.bat
@@ -0,0 +1,88 @@
+REM Build all target configurations and copy results into "prebuilt"
+
+set PrebuiltDebugWin32Dir=staging\debug\win32
+set PrebuiltDebugWin64Dir=staging\debug\win64
+set PrebuiltReleaseWin32Dir=staging\release\win32
+set PrebuiltReleaseWin64Dir=staging\release\win64
+
+mkdir %PrebuiltDebugWin32Dir%
+mkdir %PrebuiltDebugWin64Dir%
+mkdir %PrebuiltReleaseWin32Dir%
+mkdir %PrebuiltReleaseWin64Dir%
+
+set DebugWinBuildDir=bin\lib\Debug
+set ReleaseWinBuildDir=bin\lib\Release
+
+mkdir WinBuild32 & pushd WinBuild32
+cmake -G "Visual Studio 15 2017" ../
+popd
+mkdir WinBuild64 & pushd WinBuild64
+cmake -G "Visual Studio 15 2017 Win64" ../
+popd
+
+cmake --build WinBuild32 --config Release
+cmake --build WinBuild32 --config Debug
+
+copy %DebugWinBuildDir%\zt-static.lib %PrebuiltDebugWin32Dir%\zt.lib
+copy %DebugWinBuildDir%\zt-shared.dll %PrebuiltDebugWin32Dir%\zt.dll
+copy %ReleaseWinBuildDir%\zt-static.lib %PrebuiltReleaseWin32Dir%\zt.lib
+copy %ReleaseWinBuildDir%\zt-shared.dll %PrebuiltReleaseWin32Dir%\zt.dll
+
+cmake --build WinBuild64 --config Release
+cmake --build WinBuild64 --config Debug
+
+copy %DebugWinBuildDir%\zt-static.lib %PrebuiltDebugWin64Dir%\zt.lib
+copy %DebugWinBuildDir%\zt-shared.dll %PrebuiltDebugWin64Dir%\zt.dll
+copy %ReleaseWinBuildDir%\zt-static.lib %PrebuiltReleaseWin64Dir%\zt.lib
+copy %ReleaseWinBuildDir%\zt-shared.dll %PrebuiltReleaseWin64Dir%\zt.dll
+
+rd /S /Q bin
+
+# Build with JNI
+
+mkdir WinBuild32 & pushd WinBuild32
+cmake -D JNI:BOOL=ON -G "Visual Studio 15 2017" ../
+popd
+mkdir WinBuild64 & pushd WinBuild64
+cmake -D JNI:BOOL=ON -G "Visual Studio 15 2017 Win64" ../
+popd
+
+cmake --build WinBuild32 --config Release
+cmake --build WinBuild32 --config Debug
+
+REM Build JAR file
+REM release variant
+cd packages\java
+del com/zerotier/libzt/*.class
+move ..\..\%ReleaseWinBuildDir%\zt-shared.dll zt.dll
+javac com/zerotier/libzt/*.java
+jar cf zt.jar zt.dll com/zerotier/libzt/*.class
+move zt.jar ..\..\%PrebuiltReleaseWin32Dir%
+REM debug variant
+del com/zerotier/libzt/*.class
+move ..\..\%DebugWinBuildDir%\zt-shared.dll zt.dll
+javac com/zerotier/libzt/*.java
+jar cf zt.jar zt.dll com/zerotier/libzt/*.class
+move zt.jar ..\..\%PrebuiltDebugWin32Dir%
+popd
+popd
+
+cmake --build WinBuild64 --config Release
+cmake --build WinBuild64 --config Debug
+
+REM Build JAR file
+REM release variant
+cd packages\java
+del com/zerotier/libzt/*.class
+move ..\..\%ReleaseWinBuildDir%\zt-shared.dll zt.dll
+javac com/zerotier/libzt/*.java
+jar cf zt.jar zt.dll com/zerotier/libzt/*.class
+move zt.jar ..\..\%PrebuiltReleaseWin64Dir%
+REM debug variant
+del com/zerotier/libzt/*.class
+move ..\..\%DebugWinBuildDir%\zt-shared.dll zt.dll
+javac com/zerotier/libzt/*.java
+jar cf zt.jar zt.dll com/zerotier/libzt/*.class
+move zt.jar ..\..\%PrebuiltDebugWin64Dir%
+popd
+popd
\ No newline at end of file
diff --git a/ports/dist.sh b/ports/dist.sh
new file mode 100755
index 0000000..14fe9a1
--- /dev/null
+++ b/ports/dist.sh
@@ -0,0 +1,184 @@
+#!/bin/bash
+
+# Call this script from the root project directory via `make dist`
+# - submodules will be recursively initialized and updated
+# - patches will be applied to submodules if needed
+# - this script will call CMake to generate library-building packages if necessary
+# - once projects have been generated, this script will use their tooling to build the libraries/packages
+# - when all products have been built and moved to `tmp`, they will be compressed and moved to `products`
+
+OSNAME=$(uname | tr '[A-Z]' '[a-z]')
+BUILD_CONCURRENCY=4
+PROJROOT=$(pwd)
+BUILD_PRODUCTS_DIR=$(pwd)/bin
+LIB_PRODUCTS_DIR=$BUILD_PRODUCTS_DIR/lib
+FINISHED_PRODUCTS_DIR=$(pwd)/products
+STAGING_DIR=$(pwd)/staging
+# Windows (previously built)
+WIN_PREBUILT_DIR=$PROJROOT/staging/win
+WIN_RELEASE_PRODUCTS_DIR=$WIN_PREBUILT_DIR/release
+WIN_DEBUG_PRODUCTS_DIR=$WIN_PREBUILT_DIR/debug
+WIN32_RELEASE_PRODUCTS_DIR=$WIN_RELEASE_PRODUCTS_DIR/win32
+WIN64_RELEASE_PRODUCTS_DIR=$WIN_RELEASE_PRODUCTS_DIR/win64
+WIN32_DEBUG_PRODUCTS_DIR=$WIN_DEBUG_PRODUCTS_DIR/win32
+WIN64_DEBUG_PRODUCTS_DIR=$WIN_DEBUG_PRODUCTS_DIR/win64
+# Linux
+LINUX_PROD_DIR=$PROJROOT/staging/linux
+# macOS
+MACOS_PROD_DIR=$PROJROOT/staging/macos
+MACOS_RELEASE_PROD_DIR=$MACOS_PROD_DIR/release
+MACOS_DEBUG_PROD_DIR=$MACOS_PROD_DIR/debug
+# iOS
+IOS_PROD_DIR=$PROJROOT/staging/ios
+# Android
+ANDROID_PROJ_DIR=$(pwd)/"ports/android"
+ANDROID_ARCHIVE_FILENAME="zt.aar"
+# Xcode
+XCODE_IOS_PROJ_DIR=$(pwd)/"ports/xcode_ios"
+XCODE_MACOS_PROJ_DIR=$(pwd)/"ports/xcode_macos"
+
+mkdir $FINISHED_PRODUCTS_DIR
+mkdir $STAGING_DIR
+
+# Check that projects exist, generate them and exit if they don't exist
+generate_projects_if_necessary()
+{
+ if [[ $OSNAME = *"darwin"* ]]; then
+ # iOS
+ if [ ! -d "$XCODE_IOS_PROJ_DIR" ]; then
+ echo "BUILDING: iOS project"
+ should_exit=1
+ mkdir -p $XCODE_IOS_PROJ_DIR
+ cd $XCODE_IOS_PROJ_DIR
+ cmake -G Xcode ../../
+ # Bug in CMake requires us to manually replace architecture strings in project file
+ sed -i '' 's/x86_64/$(CURRENT_ARCH)/g' $PROJNAME.xcodeproj/project.pbxproj
+ cd -
+ fi
+ # macOS
+ if [ ! -d "$XCODE_MACOS_PROJ_DIR" ]; then
+ echo "BUILDING: macOS project"
+ should_exit=1
+ mkdir -p $XCODE_MACOS_PROJ_DIR
+ cd $XCODE_MACOS_PROJ_DIR
+ cmake -G Xcode ../../
+ cd -
+ fi
+ # android?
+ if [[ $should_exit = 1 ]]; then
+ echo "Generated projects. Perform necessary modifications and then re-run this script"
+ echo "Please place previously built windows binaries in $WIN_PREBUILT_DIR before running again."
+ exit 0
+ else
+ echo "Projects detected, going to build stage next"
+ fi
+ fi
+}
+
+build_all_products()
+{
+ CONFIG=$1
+ UPPERCASE_CONFIG="$(tr '[:lower:]' '[:upper:]' <<< ${1:0:1})${1:1}"
+
+ # Targets to build on and for darwin
+ if [[ $OSNAME = *"darwin"* ]]; then
+ # Xcode Frameworks --- Builds targets from a CMake-generated Xcode project
+ if false; then
+ if [[ $2 != *"JNI"* ]]; then
+ CURR_BUILD_PRODUCTS_DIR=$LIB_PRODUCTS_DIR/$UPPERCASE_CONFIG
+ # (iOS)
+ echo "BUILDING: iOS"
+ cd $XCODE_IOS_PROJ_DIR
+ xcodebuild -target zt -configuration "$UPPERCASE_CONFIG" -sdk "iphoneos"
+ xcodebuild -target zt-static -configuration "$UPPERCASE_CONFIG" -sdk "iphoneos"
+ cd -
+ CURR_ARCH="arm64" # spoof this architecture since HOSTTYPE is likely x86_64
+ CURR_TMP_PRODUCT_DIR=$STAGING_DIR/$CONFIG/ios-$CURR_ARCH
+ mkdir -p $CURR_TMP_PRODUCT_DIR
+ mv $CURR_BUILD_PRODUCTS_DIR/*.framework $CURR_TMP_PRODUCT_DIR
+ mv $CURR_BUILD_PRODUCTS_DIR/libzt.* $CURR_TMP_PRODUCT_DIR
+
+ # (macOS)
+ echo "BUILDING: macOS"
+ cd $XCODE_MACOS_PROJ_DIR
+ xcodebuild -target zt -configuration "$UPPERCASE_CONFIG" -sdk "macosx"
+ xcodebuild -target zt-static -configuration "$UPPERCASE_CONFIG" -sdk "macosx"
+ xcodebuild -target zt-shared -configuration "$UPPERCASE_CONFIG" -sdk "macosx"
+ cd -
+ CURR_TMP_PRODUCT_DIR=$STAGING_DIR/$CONFIG/macos-$(uname -m)
+ mkdir -p $CURR_TMP_PRODUCT_DIR
+ mv $CURR_BUILD_PRODUCTS_DIR/*.framework $CURR_TMP_PRODUCT_DIR
+ mv $CURR_BUILD_PRODUCTS_DIR/libzt.* $CURR_TMP_PRODUCT_DIR
+ fi
+ fi
+ # Android Archive (AAR) --- Executes a Gradle task
+ if true; then
+ CMAKE_FLAGS=$CMAKE_FLAGS" -DSDK_JNI=1"
+ CURR_ARCH="armeabi-v7a" # spoof this architecture since HOSTTYPE is likely x86_64
+ CURR_TMP_PRODUCT_DIR=$STAGING_DIR/$CONFIG/android-$CURR_ARCH
+ mkdir -p $CURR_TMP_PRODUCT_DIR
+ echo "BUILDING: AAR"
+ cd $ANDROID_PROJ_DIR
+ ./gradlew assemble$UPPERCASE_CONFIG # e.g. assembleRelease
+ mv $ANDROID_PROJ_DIR/app/build/outputs/aar/app-$CONFIG.aar $CURR_TMP_PRODUCT_DIR/$ANDROID_ARCHIVE_FILENAME
+ cd -
+ fi
+ # Java Archive (JAR)
+ if false; then
+ CURR_BUILD_PRODUCTS_DIR=$LIB_PRODUCTS_DIR
+ CMAKE_FLAGS=$CMAKE_FLAGS" -DJNI=1"
+ CURR_TMP_PRODUCT_DIR=$STAGING_DIR/$CONFIG/macos-$(uname -m)
+ mkdir -p $CURR_TMP_PRODUCT_DIR
+ echo "BUILDING: JAR"
+ rm -rf $LIB_PRODUCTS_DIR # clean-lite
+ cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=$CONFIG "-DJNI=1 -DBUILD_TESTS=0"
+ cmake --build build
+ cd $PROJROOT/ports/java
+ cp $CURR_BUILD_PRODUCTS_DIR/libzt.dylib .
+ javac com/zerotier/libzt/*.java
+ jar cf zt.jar libzt.dylib com/zerotier/libzt/*.class
+ rm libzt.dylib
+ mv zt.jar $CURR_TMP_PRODUCT_DIR
+ cd -
+ fi
+ fi
+ # Linux targets
+ if [[ $OSNAME = *"linux"* ]]; then
+ CURR_BUILD_PRODUCTS_DIR=$LIB_PRODUCTS_DIR
+ # Ordinary libraries
+ if true; then
+ rm -rf $LIB_PRODUCTS_DIR
+ cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=$CONFIG "-DBUILD_TESTS=0"
+ cmake --build build
+ # -j $BUILD_CONCURRENCY
+ CURR_TMP_PRODUCT_DIR=$STAGING_DIR/$CONFIG/linux-$(uname -m)
+ mkdir -p $CURR_TMP_PRODUCT_DIR
+ mv $CURR_BUILD_PRODUCTS_DIR/libzt.* $CURR_TMP_PRODUCT_DIR
+ fi
+ # Java JAR file
+ if true; then
+ rm -rf $LIB_PRODUCTS_DIR
+ cmake -H. -Bbuild -DCMAKE_BUILD_TYPE=$CONFIG "-DJNI=1 -DBUILD_TESTS=0"
+ cmake --build build
+ # -j $BUILD_CONCURRENCY
+ CURR_TMP_PRODUCT_DIR=$STAGING_DIR/$CONFIG/linux-$(uname -m)
+ mkdir -p $CURR_TMP_PRODUCT_DIR
+ cd $PROJROOT/ports/java
+ cp $CURR_BUILD_PRODUCTS_DIR/libzt.so .
+ javac com/zerotier/libzt/*.java
+ jar cf zt.jar libzt.dylib com/zerotier/libzt/*.class
+ rm libzt.dylib
+ mv zt.jar $CURR_TMP_PRODUCT_DIR
+ cd -
+ fi
+ fi
+}
+
+main()
+{
+ #generate_projects_if_necessary
+ build_all_products "release"
+ build_all_products "debug"
+}
+
+main "$@"
\ No newline at end of file
diff --git a/ports/module.modulemap b/ports/module.modulemap
new file mode 100644
index 0000000..2fd95d7
--- /dev/null
+++ b/ports/module.modulemap
@@ -0,0 +1,6 @@
+framework module zt {
+ umbrella header "Xcode-Bridging-Header.h"
+
+ export *
+ module * { export * }
+}
diff --git a/ports/package.sh b/ports/package.sh
new file mode 100755
index 0000000..011c29b
--- /dev/null
+++ b/ports/package.sh
@@ -0,0 +1,43 @@
+#!/bin/bash
+
+PROJNAME="zt"
+LIBNAME="lib"$PROJNAME
+LIBZT_VERSION="1.2.0"
+LIBZT_REVISION="1"
+ZT_CORE_VERSION="1.2.12"
+FILENAME_PREFIX=${LIBNAME}
+
+STAGING_DIR=$(pwd)/staging
+STAGING_DEBUG_DIR=$(pwd)/staging/debug
+STAGING_RELEASE_DIR=$(pwd)/staging/release
+FINISHED_PRODUCTS_DIR=$(pwd)/products
+
+# Clean before zipping
+find . -type f \( -name '*.DS_Store' -o -name 'thumbs.db' \) -delete
+
+# Emit a README file
+echo $'* libzt version: '${LIBZT_VERSION}$'r'${LIBZT_REVISION}$'\n* Core ZeroTier version: '${ZT_CORE_VERSION}$'\n* Date: '$(date)$'\n\nZeroTier Manual: https://www.zerotier.com/manual.shtml\n
+Other Downloads: https://www.zerotier.com/download.shtml
+\nlibzt Repo: https://github.com/zerotier/libzt\n\nFor more assistance, visit https://my.zerotier.com and ask your question in our Community section' > ${STAGING_DIR}/README.md
+
+cp ${STAGING_DIR}/README.md ${STAGING_DIR}/debug/README.md
+cp ${STAGING_DIR}/README.md ${STAGING_DIR}/release/README.md
+
+# Package everything together
+# (debug)
+PRODUCT_FILENAME=${FILENAME_PREFIX}-debug.tar.gz
+echo "Making: " ${FINISHED_PRODUCTS_DIR}/${PRODUCT_FILENAME}
+cd ${STAGING_DEBUG_DIR}
+tar --exclude=${PRODUCT_FILENAME} -zcvf ${PRODUCT_FILENAME} .
+md5 $PRODUCT_FILENAME
+mv *.tar.gz ${FINISHED_PRODUCTS_DIR}
+cd -
+
+# (release)
+PRODUCT_FILENAME=${FILENAME_PREFIX}-release.tar.gz
+echo "Making: " ${FINISHED_PRODUCTS_DIR}/${PRODUCT_FILENAME}
+cd ${STAGING_RELEASE_DIR}
+tar --exclude=${PRODUCT_FILENAME} -zcvf ${PRODUCT_FILENAME} .
+md5 $PRODUCT_FILENAME
+mv *.tar.gz ${FINISHED_PRODUCTS_DIR}
+cd -
\ No newline at end of file