Repository: kmahyyg/ztncui-aio (language: go)

Issue:
TODO: Auto Generate Planet File
Using `zerotier-idtool`, ask user for input as the controller server IP address and ZT port. Then put to the directory.


Discussion / hints:
done by https://github.com/kmahyyg/go-libzt

Fix patch:
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..0ae8275
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,4 @@
+.env
+.vscode/
+.idea/
+.DS_Store
\ No newline at end of file
diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml
new file mode 100644
index 0000000..a718d92
--- /dev/null
+++ b/.github/workflows/docker-publish.yml
@@ -0,0 +1,99 @@
+name: Docker
+
+# This workflow uses actions that are not certified by GitHub.
+# They are provided by a third-party and are governed by
+# separate terms of service, privacy policy, and support
+# documentation.
+
+on:
+  push:
+    branches: [ "master" ]
+    # Publish semver tags as releases.
+    tags: [ 'v*.*.*' ]
+
+env:
+  # Use docker.io for Docker Hub if empty
+  REGISTRY: ghcr.io
+  # github.repository as <account>/<repo>
+  IMAGE_NAME: ${{ github.repository }}
+
+
+jobs:
+  build:
+    runs-on: ubuntu-22.04
+    strategy:
+      fail-fast: false
+      matrix:
+        platform: [
+          {name: "linux/amd64", tag: "amd64"},
+          {name: "linux/arm64/v8", tag: "aarch64"},
+          {name: "linux/arm/v7", tag: "arm32v7"}
+        ]
+    permissions:
+      contents: read
+      packages: write
+      # This is used to complete the identity challenge
+      # with sigstore/fulcio when running outside of PRs.
+      id-token: write
+
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v3
+
+      # Install the cosign tool except on PR
+      # https://github.com/sigstore/cosign-installer
+      - name: Install cosign
+        if: github.event_name != 'pull_request'
+        uses: sigstore/cosign-installer@f3c664df7af409cb4873aa5068053ba9d61a57b6 #v2.6.0
+        with:
+          cosign-release: 'v1.13.1'
+
+
+      # Workaround: https://github.com/docker/build-push-action/issues/461
+      - name: Setup Docker buildx
+        uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf
+
+      # Login against a Docker registry except on PR
+      # https://github.com/docker/login-action
+      - name: Log into registry ${{ env.REGISTRY }}
+        if: github.event_name != 'pull_request'
+        uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
+        with:
+          registry: ${{ env.REGISTRY }}
+          username: ${{ github.actor }}
+          password: ${{ secrets.GITHUB_TOKEN }}
+
+      # Extract metadata (tags, labels) for Docker
+      # https://github.com/docker/metadata-action
+      - name: Extract Docker metadata
+        id: meta
+        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
+        with:
+          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+
+      # Build and push Docker image with Buildx (don't push on PR)
+      # https://github.com/docker/build-push-action
+      - name: Build and push Docker image
+        id: build-and-push
+        uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
+        with:
+          context: .
+          push: ${{ github.event_name != 'pull_request' }}
+          tags: ${{ steps.meta.outputs.tags }}
+          labels: ${{ steps.meta.outputs.labels }}
+          cache-from: type=gha
+          cache-to: type=gha,mode=max
+
+
+      # Sign the resulting Docker image digest except on PRs.
+      # This will only write to the public Rekor transparency log when the Docker
+      # repository is public to avoid leaking data.  If you would like to publish
+      # transparency data even for private images, pass --force to cosign below.
+      # https://github.com/sigstore/cosign
+      - name: Sign the published Docker image
+        if: ${{ github.event_name != 'pull_request' }}
+        env:
+          COSIGN_EXPERIMENTAL: "true"
+        # This step uses the identity token to provision an ephemeral certificate
+        # against the sigstore community Fulcio instance.
+        run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }}
diff --git a/.gitignore b/.gitignore
index 600d2d3..0fa043c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,1 +1,3 @@
-.vscode
\ No newline at end of file
+.vscode/
+.DS_Store
+.idea
\ No newline at end of file
diff --git a/AIO_VERSION b/AIO_VERSION
new file mode 100644
index 0000000..c81b1cf
--- /dev/null
+++ b/AIO_VERSION
@@ -0,0 +1,1 @@
+v20230430-1-1.10.6-0.8.6
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index b07bd04..c74211d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,11 +1,12 @@
-FROM debian:bullseye-slim AS builder
+FROM debian:bullseye-slim AS jsbuilder
 ENV NODEJS_MAJOR=18
+ENV DEBIAN_FRONTEND=noninteractive
 
-ARG DEBIAN_FRONTEND=noninteractive
 LABEL org.opencontainers.image.source="https://github.com/kmahyyg/ztncui-aio"
 LABEL MAINTAINER="Key Networks https://key-networks.com"
 LABEL Description="ztncui (a ZeroTier network controller user interface) + ZeroTier network controller"
 ADD VERSION .
+ADD AIO_VERSION .
 
 # BUILD ZTNCUI IN FIRST STAGE
 WORKDIR /build
@@ -14,78 +15,66 @@ RUN apt update -y && \
     curl -sL -o node_inst.sh https://deb.nodesource.com/setup_${NODEJS_MAJOR}.x && \
     bash node_inst.sh && \
     apt install -y nodejs --no-install-recommends && \
-    rm -f node_inst.sh && \
-    git clone https://github.com/key-networks/ztncui && \
-    npm install -g node-gyp pkg && \
-    cd ztncui/src && \
-    npm install && \
-    pkg -c ./package.json -t "node${NODEJS_MAJOR}-linux-x64" bin/www -o ztncui && \
-    zip -r /build/artifact.zip ztncui node_modules/argon2/build/Release
+    rm -f node_inst.sh
+COPY build-ztncui.sh /build/
+RUN bash /build/build-ztncui.sh
 
 # BUILD GO UTILS
-FROM golang:bullseye AS argong
+FROM golang:bullseye AS gobuilder
 WORKDIR /buildsrc
 COPY argon2g /buildsrc/argon2g
 COPY fileserv /buildsrc/fileserv
+COPY ztnodeid /buildsrc/ztnodeid
+COPY build-gobinaries.sh /buildsrc/build-gobinaries.sh
 ENV CGO_ENABLED=0
-RUN mkdir -p binaries && \
-    cd argon2g && \
-    go mod download && \
-    go build -ldflags='-s -w' -trimpath -o ../binaries/argon2g && \
-    cd .. && \
-    git clone https://github.com/jsha/minica && \
-    cd minica && \
-    go mod download && \
-    go build -ldflags='-s -w' -trimpath -o ../binaries/minica && \
-    cd .. && \
-    git clone https://github.com/tianon/gosu && \
-    cd gosu && \
-    go mod download && \
-    go build -o ../binaries/gosu -ldflags='-s -w' -trimpath && \
-    cd .. && \
-    cd fileserv && \
-    go build -ldflags='-s -w' -trimpath -o ../binaries/fileserv main.go
-
+RUN apt update -y && \ 
+    apt install zip -y && \
+    bash /buildsrc/build-gobinaries.sh
 
 # START RUNNER
 FROM debian:bullseye-slim AS runner
+ENV DEBIAN_FRONTEND=noninteractive
+ENV AUTOGEN_PLANET=0
+WORKDIR /tmp
 RUN apt update -y && \
-    apt install curl gnupg2 ca-certificates unzip supervisor net-tools procps --no-install-recommends -y && \
+    apt install curl gnupg2 ca-certificates gzip xz-utils iproute2 unzip net-tools procps --no-install-recommends -y && \
+    curl -L -O https://github.com/just-containers/s6-overlay/releases/download/v3.1.3.0/s6-overlay-noarch.tar.xz && \
+    tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz && rm /tmp/s6-overlay-noarch.tar.xz && \
+    curl -L -O https://github.com/just-containers/s6-overlay/releases/download/v3.1.3.0/s6-overlay-x86_64.tar.xz && \
+    tar -C / -Jxpf /tmp/s6-overlay-x86_64.tar.xz && rm /tmp/s6-overlay-x86_64.tar.xz && \
     groupadd -g 2222 zerotier-one && \
     useradd -u 2222 -g 2222 zerotier-one && \
-    curl -sL -o ztone.sh https://install.zerotier.com && \
-    bash ztone.sh && \
-    rm -f ztone.sh && \
+    usermod -aG zerotier-one zerotier-one && \
+    usermod -aG zerotier-one root && \
+    curl -sL -o zt-one.sh https://install.zerotier.com && \
+    bash zt-one.sh && \
+    rm -f zt-one.sh && \
     apt clean -y && \
     rm -rf /var/lib/zerotier-one && \
     rm -rf /var/lib/apt/lists/*
 
 WORKDIR /opt/key-networks/ztncui
-COPY --from=builder /build/artifact.zip .
+COPY --from=jsbuilder /build/artifact.zip .
 RUN unzip ./artifact.zip && \
     rm -f ./artifact.zip
 
-COPY --from=argong /buildsrc/binaries/gosu /bin/gosu
-COPY --from=argong /buildsrc/binaries/minica /usr/local/bin/minica
-COPY --from=argong /buildsrc/binaries/argon2g /usr/local/bin/argon2g
-COPY --from=argong /buildsrc/binaries/fileserv /usr/local/bin/gfileserv
-
+WORKDIR /
+COPY start_firsttime_init.sh /start_firsttime_init.sh
 COPY start_zt1.sh /start_zt1.sh
 COPY start_ztncui.sh /start_ztncui.sh
-COPY supervisord.conf /etc/supervisord.conf
 
-RUN chmod 0755 /bin/gosu && \
-    chmod 0755 /usr/local/bin/minica && \
-    chmod 0755 /usr/local/bin/argon2g && \
-    chmod 0755 /usr/local/bin/gfileserv && \
+COPY --from=gobuilder /buildsrc/artifact-go.zip /tmp/
+RUN unzip -d /usr/local/bin /tmp/artifact-go.zip && \
+    rm -rf /tmp/artifact-go.zip && \
+    chmod 0755 /usr/local/bin/* && \
     chmod 0755 /start_*.sh
 
+COPY s6-rc.d/ /etc/s6-overlay/s6-rc.d/
+
 EXPOSE 3000/tcp
 EXPOSE 3180/tcp
 EXPOSE 8000/tcp
 EXPOSE 3443/tcp
 
-WORKDIR /
-VOLUME ["/opt/key-networks/ztncui/etc"]
-VOLUME [ "/var/lib/zerotier-one" ]
-ENTRYPOINT [ "/usr/bin/supervisord" ]
+VOLUME ["/opt/key-networks/ztncui/etc", "/etc/zt-mkworld", "/var/lib/zerotier-one"]
+ENTRYPOINT [ "/init" ]
diff --git a/README.md b/README.md
index 435e449..62f2ae2 100644
--- a/README.md
+++ b/README.md
@@ -8,13 +8,17 @@ Follow us on [![alt @key_networks on Twitter](https://i.imgur.com/wWzX9uB.png)](
 Licensed Under GNU GPLv3
 
 ## Credit
+
 Thanks to @kmahyyg for https://github.com/kmahyyg/ztncui-aio from which this build process is forked.
 
 ## Further information
+
 Refer to https://github.com/key-networks/ztncui-containerized for the original documentation.
 
 ## Build yourself
 
+We support arm64, amd64. armv7 might work, but is not tested. Others are unsupported.
+
 ```bash
 $ git clone https://github.com/key-networks/ztncui-aio
 $ docker build . -t keynetworks/ztncui:latest
@@ -26,12 +30,29 @@ Never use `node_lts.x` as your installation script of nodejs whose version might
 
 ## Usage
 
+### Golang auto-mkworld
+
+This feature allows you to generate a planet file without using C code and compiler.
+
+Also, due to limitation of IPC of Zerotier-One UI and multiple issues, we do NOT support customized port, you can ONLY use port 9993/udp here.
+
+| MANDATORY | Name | Explanation | Default Value |
+|:--------:|:--------:|:--------:|:--------:|
+| no | AUTOGEN_PLANET | If set to 1, will use this node identity to generate a `planet` file and put to `httpfs` folder to serve it outside. If set to 2, will use config in `/etc/zt-mkworld/mkworld.config.json`. If set to 0, will do nothing. | 0 |
+
+The reference config file can be found on `ztnodeid/assets/mkworld.conf.json`. `plRecommend` is used to mark to generate planetID and planetBirth timestamp.
+
+You could also define yourself, and check the output to get C header of customized planet. After that, you will find the custom planet file under http file server root and also ca certificate.
+
+### Docker image
+
 ```bash
 $ git clone https://github.com/key-networks/ztncui-aio # to get a copy of denv file, otherwise make your own
 $ docker pull keynetworks/ztncui
 $ docker run -d -p3443:3443 -p3180:3180 \
     -v /mydata/ztncui:/opt/key-networks/ztncui/etc \
     -v /mydata/zt1:/var/lib/zerotier-one \
+    -v /mydata/zt-mkworld-conf:/etc/zt-mkworld \
     --env-file ./denv <CHANGE HERE ACCORDING TO NEXT PART> \
     --name ztncui \
     keynetworks/ztncui
@@ -43,25 +64,26 @@ If their one is not updated, try `docker pull ghcr.io/kmahyyg/ztncui-aio:latest`
 
 For ZTNCUI: https://github.com/key-networks/ztncui
 
-| REQUIRED | Name | Explanation | Default Value |
+| MANDATORY | Name | Explanation | Default Value |
 |:--------:|:--------:|:--------:|:--------:|
 | YES | NODE_ENV | https://pugjs.org/api/express.html | production |
-|  no  | HTTPS_HOST | Only Listen on HTTPS_HOST:HTTPS_PORT | NO DEFAULT |
+| no | HTTPS_HOST | Only Listen on HTTPS_HOST:HTTPS_PORT | NO DEFAULT |
 | no | HTTPS_PORT | HTTPS_PORT | 3443 |
 | no | HTTP_PORT | HTTP_PORT | 3000 |
 | no | HTTP_ALL_INTERFACES | Listen on all interfaces, useful for reverse proxy, HTTP only | NO DEFAULT |
 
-Note: If you do not set `HTTP_ALL_INTERFACES`, the 3000 port will only get listened inside container.
+Note: If you do NOT set `HTTP_ALL_INTERFACES`, the 3000 port will only get listened inside container.
 
 This image additional specified details:
 
-| REQUIRED | Name | Explanation | Default Value |
+| MANDATORY | Name | Explanation | Default Value |
 |:--------:|:--------:|:--------:|:--------:|
 | no | MYDOMAIN | generate TLS certs on the fly (if not exists) | ztncui.docker.test |
 | no | ZTNCUI_PASSWD | generate admin password on the fly (if not exists) | password |
 | YES | MYADDR | your ip address, public ip address preferred | NO DEFAULT |
 
-Also, this image exposed an http server at port 3180, you could save file in `/mydata/ztncui/myfs/` to serve it. (You could use this to build your own root server and distribute planet file)
+Also, this image exposed an http server at port 3180, you could save file in `/mydata/ztncui/httpfs/` to serve it. 
+(You could use this to build your own root server and distribute planet file, even though, that won't hurt you, I still suggest to set a protection for this http server in front)
 
 **WARNING: IF YOU DO NOT SET PASSWORD, YOU HAVE TO USE `docker exec -it <CONTAINER NAME> bash`, and then `cat /var/log/docker-ztncui.log` to get your random password. This is gatekeeper.**
 
diff --git a/build-gobinaries.sh b/build-gobinaries.sh
new file mode 100755
index 0000000..b1451c1
--- /dev/null
+++ b/build-gobinaries.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+set -xe
+cd /buildsrc
+mkdir -p binaries 
+cd argon2g 
+go mod download 
+go build -ldflags='-s -w' -trimpath -o ../binaries/argon2g 
+cd .. 
+git clone https://github.com/jsha/minica 
+cd minica 
+go mod download 
+go build -ldflags='-s -w' -trimpath -o ../binaries/minica 
+cd .. 
+git clone https://github.com/tianon/gosu 
+cd gosu 
+go mod download 
+go build -o ../binaries/gosu -ldflags='-s -w' -trimpath 
+cd .. 
+cd fileserv 
+go build -ldflags='-s -w' -trimpath -o ../binaries/fileserv main.go 
+cd .. 
+cd ztnodeid
+go build -ldflags='-s -w' -trimpath -o ../binaries/ztmkworld cmd/mkworld/main.go
+cd ..
+cd binaries
+zip -r /buildsrc/artifact-go.zip ./*
diff --git a/build-ztncui.sh b/build-ztncui.sh
new file mode 100755
index 0000000..ca3c4ed
--- /dev/null
+++ b/build-ztncui.sh
@@ -0,0 +1,30 @@
+#!/bin/bash
+set -xe
+git clone https://github.com/key-networks/ztncui
+npm install -g node-gyp pkg
+cd ztncui/src 
+npm install
+
+MACHINE_ARCHITECTURE=$(uname -m)
+export NODEJS_PACK_ARCH=""
+case ${MACHINE_ARCHITECTURE} in 
+    "arm")
+        # armv6 is too old, and unsupported by pkg, and should be deprecated, so we won't support
+        # armv7 is unsupported by pkg, you could try build yourself, but no guarantee here
+        echo "Upstream unsupported architecture: ${MACHINE_ARCHITECTURE}. Treat as armv7. But will still try to build."
+        NODEJS_PACK_ARCH="armv7"
+        ;;
+    "aarch64")
+        NODEJS_PACK_ARCH="arm64"
+        ;;
+    "x86_64")
+        NODEJS_PACK_ARCH="x64"
+        ;;
+    *)
+        echo "Unsupported Architecture: ${MACHINE_ARCHITECTURE}. Exit now."
+        exit 127
+        ;;
+esac
+
+pkg -c ./package.json -C Brotli --no-bytecode --public -t "node${NODEJS_MAJOR}-linux-${NODEJS_PACK_ARCH}" bin/www -o ztncui
+zip -r /build/artifact.zip ztncui node_modules/argon2/build/Release
diff --git a/fileserv/main.go b/fileserv/main.go
index d424208..87e2d6d 100644
--- a/fileserv/main.go
+++ b/fileserv/main.go
@@ -6,7 +6,7 @@ import (
 )
 
 func main() {
-	var fs http.FileSystem = http.Dir("/opt/key-networks/ztncui/etc/myfs")
+	var fs http.FileSystem = http.Dir("/opt/key-networks/ztncui/etc/httpfs")
 	var fsHandler = http.FileServer(fs)
 	log.Fatal(http.ListenAndServe(":3180", fsHandler))
 }
diff --git a/release.sh b/release.sh
index 7765e2a..19fd358 100755
--- a/release.sh
+++ b/release.sh
@@ -1,5 +1,11 @@
 #!/bin/bash
+#
 # Derived from https://medium.com/travis-on-docker/how-to-version-your-docker-images-1d5c577ebf54
+#
+# This script has only been used by docker.io/key-networks/ztncui
+# 
+# In kmahyyg/ztncui-aio, the publishing process is done by github-action
+#
 
 set -ex
 
diff --git a/s6-rc.d/entryinit/type b/s6-rc.d/entryinit/type
new file mode 100644
index 0000000..3d92b15
--- /dev/null
+++ b/s6-rc.d/entryinit/type
@@ -0,0 +1,1 @@
+oneshot
\ No newline at end of file
diff --git a/s6-rc.d/entryinit/up b/s6-rc.d/entryinit/up
new file mode 100755
index 0000000..a498413
--- /dev/null
+++ b/s6-rc.d/entryinit/up
@@ -0,0 +1,2 @@
+#!/command/execlineb -P
+exec /command/with-contenv bash /start_firsttime_init.sh
\ No newline at end of file
diff --git a/s6-rc.d/fileserv/dependencies.d/ztncui b/s6-rc.d/fileserv/dependencies.d/ztncui
new file mode 100644
index 0000000..e69de29
diff --git a/s6-rc.d/fileserv/run b/s6-rc.d/fileserv/run
new file mode 100755
index 0000000..66f6b41
--- /dev/null
+++ b/s6-rc.d/fileserv/run
@@ -0,0 +1,4 @@
+#!/command/with-contenv bash
+# make sure we've got it chowned
+/bin/chown -R zerotier-one:zerotier-one /opt/key-networks/ztncui
+/usr/local/bin/gosu zerotier-one:zerotier-one /usr/local/bin/fileserv
\ No newline at end of file
diff --git a/s6-rc.d/fileserv/type b/s6-rc.d/fileserv/type
new file mode 100644
index 0000000..1780f9f
--- /dev/null
+++ b/s6-rc.d/fileserv/type
@@ -0,0 +1,1 @@
+longrun
\ No newline at end of file
diff --git a/s6-rc.d/user/contents.d/entryinit b/s6-rc.d/user/contents.d/entryinit
new file mode 100644
index 0000000..e69de29
diff --git a/s6-rc.d/user/contents.d/fileserv b/s6-rc.d/user/contents.d/fileserv
new file mode 100644
index 0000000..e69de29
diff --git a/s6-rc.d/user/contents.d/ztncui b/s6-rc.d/user/contents.d/ztncui
new file mode 100644
index 0000000..e69de29
diff --git a/s6-rc.d/user/contents.d/ztone b/s6-rc.d/user/contents.d/ztone
new file mode 100644
index 0000000..e69de29
diff --git a/s6-rc.d/user/type b/s6-rc.d/user/type
new file mode 100644
index 0000000..3a8819b
--- /dev/null
+++ b/s6-rc.d/user/type
@@ -0,0 +1,1 @@
+bundle
\ No newline at end of file
diff --git a/s6-rc.d/ztncui/dependencies.d/ztone b/s6-rc.d/ztncui/dependencies.d/ztone
new file mode 100644
index 0000000..e69de29
diff --git a/s6-rc.d/ztncui/run b/s6-rc.d/ztncui/run
new file mode 100755
index 0000000..75bd85f
--- /dev/null
+++ b/s6-rc.d/ztncui/run
@@ -0,0 +1,2 @@
+#!/command/with-contenv bash
+/start_ztncui.sh
\ No newline at end of file
diff --git a/s6-rc.d/ztncui/type b/s6-rc.d/ztncui/type
new file mode 100644
index 0000000..1780f9f
--- /dev/null
+++ b/s6-rc.d/ztncui/type
@@ -0,0 +1,1 @@
+longrun
\ No newline at end of file
diff --git a/s6-rc.d/ztone/dependencies.d/entryinit b/s6-rc.d/ztone/dependencies.d/entryinit
new file mode 100644
index 0000000..e69de29
diff --git a/s6-rc.d/ztone/run b/s6-rc.d/ztone/run
new file mode 100755
index 0000000..7aedb80
--- /dev/null
+++ b/s6-rc.d/ztone/run
@@ -0,0 +1,2 @@
+#!/command/with-contenv bash
+/start_zt1.sh
\ No newline at end of file
diff --git a/s6-rc.d/ztone/type b/s6-rc.d/ztone/type
new file mode 100644
index 0000000..1780f9f
--- /dev/null
+++ b/s6-rc.d/ztone/type
@@ -0,0 +1,1 @@
+longrun
\ No newline at end of file
diff --git a/start_firsttime_init.sh b/start_firsttime_init.sh
new file mode 100755
index 0000000..5551444
--- /dev/null
+++ b/start_firsttime_init.sh
@@ -0,0 +1,147 @@
+#!/bin/bash
+
+# debugging purpose
+if [ ! -z $DEBUG_ENV ]; then
+    echo "Debugging: Print EnvVar"
+    printenv
+fi
+
+# create dest folder
+mkdir -p /etc/zt-mkworld
+
+# make sure we've got it chowned
+chown -R zerotier-one:zerotier-one /opt/key-networks/ztncui
+chown -R zerotier-one:zerotier-one /var/lib/zerotier-one 
+
+# detect if identity folder exists
+if [ ! -d /var/lib/zerotier-one ]; then
+    mkdir -p /var/lib/zerotier-one
+fi
+
+# detect if identity private key exists
+if [ ! -f /var/lib/zerotier-one/identity.secret ]; then
+    cd /var/lib/zerotier-one
+    /usr/sbin/zerotier-idtool generate identity.secret identity.public
+fi
+
+# always make httpfs folder
+mkdir -p /opt/key-networks/ztncui/etc/httpfs
+
+# detect public ip
+if [ -z $MYADDR ]; then
+    echo "Set Your IP Address to continue."
+    echo "If you don't do that, I will automatically detect."
+    MYEXTADDR=$(curl --connect-timeout 5 ip.sb)
+    if [ -z $MYEXTADDR ]; then
+        MYINTADDR=$(ifconfig eth0 | grep -Eo 'inet (addr:)?([0-9]*\.){3}[0-9]*' | grep -Eo '([0-9]*\.){3}[0-9]*' | grep -v '127.0.0.1')
+        MYADDR=${MYINTADDR}
+    else
+        MYADDR=${MYEXTADDR}
+    fi
+fi
+
+MYDOMAIN=${MYDOMAIN:-ztncui.docker.test}   # Used for planet comment
+
+echo "YOUR IP: ${MYADDR}"
+echo "YOUR DOMAIN: ${MYDOMAIN}"
+
+cd /etc/zt-mkworld
+# detect if ALREADY_INITED
+if [ -f /etc/zt-mkworld/ALREADY_INITED ]; then
+    echo "ALREADY_INITED detected."
+    exit 0
+    # if not exist, goto planet file generate.
+else
+    # if set to 0, won't do anything.
+    if [[ $AUTOGEN_PLANET -eq 0 ]]; then
+        # finally create ALREADY_INITED flag file
+        echo "AUTOGEN_PLANET is 0. Set to inited and exit."
+        touch /etc/zt-mkworld/ALREADY_INITED
+        exit 0
+    fi
+    # AUTOGEN_PLANET is not 0, backup now.
+    if [[ -f /var/lib/zerotier-one/planet ]]; then
+        cp /var/lib/zerotier-one/planet /var/lib/zerotier-one/planet.bak.$(date +%s)
+    fi
+
+    # if AUTOGEN_PLANET is set to 1, check if identity.public exists,
+    # then generate mkworld.config.json on the fly
+    if [[ $AUTOGEN_PLANET -eq 1 ]]; then
+        # check if identity.public exists
+        if [ -f /var/lib/zerotier-one/identity.public ]; then
+            # generate json config file
+            rm -f /etc/zt-mkworld/mkworld.config.json
+            # now heredoc
+            cat << EOF > /etc/zt-mkworld/mkworld.config.json
+{
+    "rootNodes": [
+      {
+        "comments": "custom planet - ${MYDOMAIN} - ${MYADDR}",
+        "identity": "$(cat /var/lib/zerotier-one/identity.public)",
+        "endpoints": [
+          "${MYADDR}/9993"
+        ]
+      }
+    ],
+    "signing": ["previous.c25519", "current.c25519"],
+    "output": "planet.custom",
+    "plID": 0,
+    "plBirth": 0,
+    "plRecommend": true
+}
+EOF
+            # run program under corresponding workdir, check exit code is 0.
+            cd /etc/zt-mkworld
+            /usr/local/bin/ztmkworld -c /etc/zt-mkworld/mkworld.config.json
+            # copy custom planet to /var/lib/zerotier-one and httpfs
+            if [[ $? -eq 0 ]]; then
+                cp -f ./planet.custom /var/lib/zerotier-one/planet
+                cp -f ./planet.custom /opt/key-networks/ztncui/etc/httpfs
+                chown -R zerotier-one:zerotier-one /var/lib/zerotier-one
+                echo "planet successfully generated."
+            else
+                echo "planet generator failed. exit now."
+                /run/s6/basedir/bin/halt
+                exit 1
+            fi
+            # finally create ALREADY_INITED flag file
+            echo "mkworld successfully ran."
+            touch /etc/zt-mkworld/ALREADY_INITED
+            exit 0
+        else
+            echo "identity.public does NOT exit, cannot generate planet file."
+            /run/s6/basedir/bin/halt
+            exit 1
+        fi
+    fi
+
+    # if set to 2, only use mkworld.config.json provided
+    # check if mkworld.config.json exists
+    if [[ $AUTOGEN_PLANET -eq 2 ]]; then
+        cd /etc/zt-mkworld
+        if [ ! -f /etc/zt-mkworld/mkworld.config.json ]; then
+            echo "/etc/zt-mkworld/mkworld.config.json not exists. exit now."
+            /run/s6/basedir/bin/halt
+            exit 1
+        fi
+        /usr/local/bin/ztmkworld -c /etc/zt-mkworld/mkworld.config.json
+        # check if successfully exit
+        # copy custom planet to /var/lib/zerotier-one and httpfs
+        if [[ $? -eq 0 ]]; then
+            cp -f ./planet.custom /var/lib/zerotier-one/planet
+            cp -f ./planet.custom /opt/key-networks/ztncui/etc/httpfs
+            chown -R zerotier-one:zerotier-one /var/lib/zerotier-one
+            echo "planet successfully generated."
+        else
+            echo "planet generator failed. exit now."
+            /run/s6/basedir/bin/halt
+            exit 1
+        fi
+        # finally create ALREADY_INITED flag file
+        echo "mkworld successfully ran."
+        touch /etc/zt-mkworld/ALREADY_INITED
+        exit 0
+    fi
+    # after generate, copy to httpfs folder, do not directly expose the mkworld config folder.
+    # the config folder contains secret keys!
+fi
diff --git a/start_zt1.sh b/start_zt1.sh
index 82fa9f3..e6b9900 100755
--- a/start_zt1.sh
+++ b/start_zt1.sh
@@ -1,21 +1,8 @@
 #!/bin/bash
 
-cd /tmp
-
-# If the file not exists, mean we need to initialize
-if [ ! -f /var/lib/zerotier-one/identity.secret ] ; then 
-    echo "Zerotier-One Configuration is **NOT** initialized."
-    usermod -aG zerotier-one root
-    mkdir -p /var/lib/zerotier-one
-    rm -rf /var/lib/zerotier-one/*
-    ln -sf /usr/sbin/zerotier-one /var/lib/zerotier-one/zerotier-cli
-    ln -sf /usr/sbin/zerotier-one /var/lib/zerotier-one/zerotier-idtool
-    ln -sf /usr/sbin/zerotier-one /var/lib/zerotier-one/zerotier-one
-    chown zerotier-one:zerotier-one /var/lib/zerotier-one    # zerotier-one user home
-    #chown -R zerotier-one:zerotier-one /var/lib/zerotier-one  # zerotier-one will change this at runtime. 
-else
-    echo "Zerotier-One Configuration is initialized."
-fi
-
-# zt1 must run as root.
+# Always do this to make sure the directory permissions are correct.
+chown -R zerotier-one:zerotier-one /var/lib/zerotier-one
+# remove secrets
+unset ZTNCUI_PASSWD
+# ZT1 must run as root.
 /usr/sbin/zerotier-one
diff --git a/start_ztncui.sh b/start_ztncui.sh
index e128383..5c78089 100755
--- a/start_ztncui.sh
+++ b/start_ztncui.sh
@@ -22,11 +22,13 @@ HTTP_ALL_INTERFACES=${HTTP_ALL_INTERFACES}
 HTTP_PORT=${HTTP_PORT:-3000}
 HTTPS_PORT=${HTTPS_PORT:-3443}
 
+echo "YOUR DOMAIN: ${MYDOMAIN}"
+
 while [ ! -f /var/lib/zerotier-one/authtoken.secret ]; do
     echo "ZT1 AuthToken is not found... Wait for ZT1 to start..."
     sleep 2
 done
-chown zerotier-one.zerotier-one /var/lib/zerotier-one/authtoken.secret
+chown zerotier-one:zerotier-one /var/lib/zerotier-one/authtoken.secret
 chmod 640 /var/lib/zerotier-one/authtoken.secret
 
 cd /opt/key-networks/ztncui
@@ -45,7 +47,7 @@ cat /opt/key-networks/ztncui/.env
 
 mkdir -p /opt/key-networks/ztncui/etc/storage 
 mkdir -p /opt/key-networks/ztncui/etc/tls
-mkdir -p /opt/key-networks/ztncui/etc/myfs # for planet files
+mkdir -p /opt/key-networks/ztncui/etc/httpfs # for planet files
 
 if [ ! -f /opt/key-networks/ztncui/etc/passwd ]; then
     echo "Default Password File Not Exists... Generating..."
@@ -58,8 +60,9 @@ if [ ! -f /opt/key-networks/ztncui/etc/tls/fullchain.pem ] || [ ! -f /opt/key-ne
     echo "Cannot detect TLS Certs, Generating..."
     cd /opt/key-networks/ztncui/etc/tls
     /usr/local/bin/minica -domains "$MYDOMAIN"
-    cp -f "$MYDOMAIN/cert.pem" fullchain.pem
+    cat "$MYDOMAIN/cert.pem" minica.pem > fullchain.pem
     cp -f "$MYDOMAIN/key.pem" privkey.pem
+    cp -f minica.pem /opt/key-networks/ztncui/etc/httpfs/ca.pem
     cd ../../
 fi
 
@@ -67,4 +70,4 @@ chown -R zerotier-one:zerotier-one /opt/key-networks/ztncui
 chmod 0755 /opt/key-networks/ztncui/ztncui
 
 unset ZTNCUI_PASSWD
-gosu zerotier-one:zerotier-one /opt/key-networks/ztncui/ztncui
+/usr/local/bin/gosu zerotier-one:zerotier-one /opt/key-networks/ztncui/ztncui
diff --git a/supervisord.conf b/supervisord.conf
deleted file mode 100644
index 6e8d28d..0000000
--- a/supervisord.conf
+++ /dev/null
@@ -1,32 +0,0 @@
-[program:ztone]
-command=/start_zt1.sh
-autostart=true
-startsecs=10
-startretries=3
-priority=5
-redirect_stderr=true
-directory=/tmp
-stdout_logfile=/var/log/docker-ztone.log
-
-[program:ztncui]
-command=/start_ztncui.sh
-autostart=true
-startsecs=10
-startretries=3
-priority=10
-redirect_stderr=true
-directory=/opt/key-networks/ztncui
-stdout_logfile=/var/log/docker-ztncui.log
-
-[program:ztplaserv]
-command=/usr/local/bin/gfileserv
-autostart=true
-startsecs=10
-startretries=3
-priority=15
-redirect_stderr=true
-stdout_logfile=/var/log/docker-ztplaserv.log
-
-[supervisord]
-logfile=/var/log/docker-supervisord.log
-nodaemon=true
diff --git a/ztnodeid/assets/mkworld.config.json b/ztnodeid/assets/mkworld.config.json
new file mode 100644
index 0000000..ff8e8e7
--- /dev/null
+++ b/ztnodeid/assets/mkworld.config.json
@@ -0,0 +1,20 @@
+{
+    "rootNodes": [
+        {
+            "comments": "amsterdam official",
+            "identity": "992fcf1db7:0:206ed59350b31916f749a1f85dffb3a8787dcbf83b8c6e9448d4e3ea0e3369301be716c3609344a9d1533850fb4460c50af43322bcfc8e13d3301a1f1003ceb6",
+            "endpoints": [
+                "195.181.173.159/443",
+                "2a02:6ea0:c024::/443"
+            ]
+        }
+    ],
+    "signing": [
+        "previous.c25519",
+        "current.c25519"
+    ],
+    "output": "planet.custom",
+    "plID": 0,
+    "plBirth": 0,
+    "plRecommend": true
+}
\ No newline at end of file
diff --git a/ztnodeid/cmd/mkworld/main.go b/ztnodeid/cmd/mkworld/main.go
new file mode 100644
index 0000000..2cc0420
--- /dev/null
+++ b/ztnodeid/cmd/mkworld/main.go
@@ -0,0 +1,268 @@
+package main
+
+import (
+	"encoding/json"
+	"errors"
+	"flag"
+	"fmt"
+	"log"
+	"math/rand"
+	"os"
+	"time"
+	"ztnodeid/pkg/node"
+	"ztnodeid/pkg/ztcrypto"
+)
+
+var (
+	ErrWorldSigningKeyIllegal = errors.New("world signing key current.c25519 / previous.c25519 is illegal")
+	ErrPreflightCheckFailed   = errors.New("preflight check failed, internal requirement cannot be satisfied")
+	errUseRecommendValue      = errors.New("potential risk of failed execution, use recommendation if possible")
+)
+
+var (
+	prevkp     = make([]byte, node.ZT_C25519_PUBLIC_KEY_LEN+node.ZT_C25519_PRIVATE_KEY_LEN)
+	curkp      = make([]byte, node.ZT_C25519_PUBLIC_KEY_LEN+node.ZT_C25519_PRIVATE_KEY_LEN)
+	mConf      = &MkWorldConfig{}
+	gConfFile  = flag.String("c", "mkworld.config.json", "program config")
+	alreadyMod = false
+)
+
+func init() {
+	flag.Parse()
+	log.Println("startup flag parsed: ", flag.Parsed())
+}
+
+func main() {
+	/**
+	// current.c25519: public key 64 bytes, private key 64 bytes
+	// signature: must be signed by previous,
+	// message is world after serialized, internal public key is current
+	// if initial, previous=current
+	// elliptic curve crypt operation are copied from NaCl
+	**/
+	// Now Start Preflight Check
+	// Check Config Number limit and legal or not
+	if err := Preflight(); err != nil {
+		switch err {
+		case ErrPreflightCheckFailed:
+			panic(err)
+		case errUseRecommendValue:
+			log.Println("!You've been warned! WARN! WARN! WARN!")
+			if mConf.PlanetRecommend {
+				log.Println("since you've set plRecommend to true, we will automatically choose a new value.")
+				log.Println("which might be much suitable for you.")
+				mConf.PlanetID = (uint64)(rand.Uint32())
+				mConf.PlanetBirth = (uint64)(time.Now().UnixMilli())
+				log.Printf("Generated Planet ID: %d, Birth TimeStamp: %d . \n", mConf.PlanetID, mConf.PlanetBirth)
+				alreadyMod = true
+			} else {
+				log.Println("planet ID and planet birth might not be suitable for unofficial world. unexpected things might happen.")
+			}
+		default:
+			log.Println("unknown error occured, preflight check failed.")
+			panic(err)
+		}
+		log.Println("!You've been warned! WARN! WARN! WARN!")
+	}
+	// check signing key
+	if err := PreFlightSigningKeyCheck(); err != nil {
+		// if signing key is illegal, generate new
+		switch err {
+		case ErrWorldSigningKeyIllegal:
+			log.Println("preflight check error occurred, but still can proceed.")
+			pub1, priv1 := ztcrypto.GenerateDualPair()
+			copy(prevkp[:node.ZT_C25519_PUBLIC_KEY_LEN], pub1[:])
+			copy(prevkp[node.ZT_C25519_PUBLIC_KEY_LEN:], priv1[:])
+			copy(curkp, prevkp)
+			err = os.WriteFile("current.c25519", prevkp, 0640)
+			if err != nil {
+				log.Println("failed to write generate c25519 key pair to disk.")
+				panic(err)
+			}
+			err = os.WriteFile("previous.c25519", curkp, 0640)
+			if err != nil {
+				log.Println("failed to write generate c25519 key pair to disk.")
+				panic(err)
+			}
+			log.Println("new world signing key generated.")
+		default:
+			log.Println("preflight check failed.")
+			// else panic
+			panic(err)
+		}
+	}
+	log.Println("preflight check successfully complete.")
+	// Preflight check successfully completed
+	// Start to build a world
+	ztW := &node.ZtWorld{
+		Type:      node.ZT_WORLD_TYPE_PLANET,
+		ID:        mConf.PlanetID,
+		Timestamp: mConf.PlanetBirth,
+	}
+	// fill the node
+	var err2 error
+	ztW.Nodes, err2 = buildPlanetNodeFromConfig()
+	if err2 != nil {
+		panic(err2)
+	}
+
+	// prepare for signing
+	var futurePubK = [node.ZT_C25519_PUBLIC_KEY_LEN]byte{}
+	copy(futurePubK[:], curkp[:node.ZT_C25519_PUBLIC_KEY_LEN])
+	ztW.PublicKeyMustBeSignedByNextTime = futurePubK
+	log.Println("generating pre-sign message.")
+	toSignZtW, err := ztW.Serialize(true, [node.ZT_C25519_SIGNATURE_LEN]byte{})
+	if err != nil {
+		panic(err)
+	}
+	log.Println("pre-sign world generated and serialized successfully.")
+
+	var sigPubK [node.ZT_C25519_PUBLIC_KEY_LEN]byte
+	var sigPrivK [node.ZT_C25519_PRIVATE_KEY_LEN]byte
+	copy(sigPubK[:], prevkp[:node.ZT_C25519_PUBLIC_KEY_LEN])
+	copy(sigPrivK[:], prevkp[node.ZT_C25519_PUBLIC_KEY_LEN:])
+
+	sig4NewWorld, err := ztcrypto.SignMessage(sigPubK, sigPrivK, toSignZtW)
+	if err != nil {
+		panic(err)
+	}
+	log.Println("world has been signed.")
+
+	// pack and serialize
+	finalWorld, err := ztW.Serialize(false, sig4NewWorld)
+	if err != nil {
+		panic(err)
+	}
+	log.Println("new signed world are packed.")
+	err = os.WriteFile(mConf.OutputFile, finalWorld, 0644)
+	if err != nil {
+		panic(err)
+	}
+	log.Println("packed new signed world has been written to file.")
+	log.Println(" ")
+
+	// if params are recommended, save
+	if alreadyMod {
+		mDt, err := json.Marshal(mConf)
+		if err != nil {
+			log.Println("err when trying to save modified mkworld json, err: ", err)
+		} else {
+			err2 := os.WriteFile("mkworld.new.json", mDt, 0644)
+			if err2 != nil {
+				log.Println("write file to disk failed, err:", err2)
+			}
+			log.Println("write modified json successfully.")
+		}
+	}
+	log.Println(" ")
+
+	// get c output
+	log.Println("now c language output: ")
+	fmt.Println(" ")
+	fmt.Println("#define ZT_DEFAULT_WORLD_LENGTH ", len(finalWorld))
+	fmt.Printf("static const unsigned char ZT_DEFAULT_WORLD[ZT_DEFAULT_WORLD_LENGTH] = {")
+	for i, v := range finalWorld {
+		if i > 0 {
+			fmt.Printf(",")
+		}
+		fmt.Printf("0x%02x", v)
+	}
+	fmt.Printf("};\n")
+	fmt.Println(" ")
+}
+
+type MkWorldConfig struct {
+	SigningKeyFiles []string      `json:"signing"`
+	OutputFile      string        `json:"output"`
+	RootNodes       []MkWorldNode `json:"rootNodes"`
+	PlanetID        uint64        `json:"plID"`
+	PlanetBirth     uint64        `json:"plBirth"`
+	PlanetRecommend bool          `json:"plRecommend"`
+}
+
+type MkWorldNode struct {
+	Comments    string   `json:"comments,omitempty"`
+	IdentityStr string   `json:"identity"`
+	Endpoints   []string `json:"endpoints"`
+}
+
+func Preflight() error {
+	gcfdata, err := os.ReadFile(*gConfFile)
+	if err != nil {
+		return err
+	}
+	log.Println("config file read.")
+	err = json.Unmarshal(gcfdata, mConf)
+	if err != nil {
+		return err
+	}
+	log.Println("config file unmarshalled.")
+	if len(mConf.SigningKeyFiles) != 2 {
+		log.Println("signing key must have 2 files.")
+		return ErrPreflightCheckFailed
+	}
+	if len(mConf.RootNodes) > node.ZT_WORLD_MAX_ROOTS {
+		log.Println("root nodes are too many.")
+		return ErrPreflightCheckFailed
+	}
+	for _, v := range mConf.RootNodes {
+		if len(v.Endpoints) > node.ZT_WORLD_MAX_STABLE_ENDPOINTS_PER_ROOT {
+			log.Println("stable endpoints for current root node are too many.")
+			return ErrPreflightCheckFailed
+		}
+	}
+	if mConf.PlanetID == node.ZT_WORLD_ID_EARTH || mConf.PlanetID == node.ZT_WORLD_ID_MARS || mConf.PlanetBirth == 1567191349589 {
+		log.Println("!WARNING! You've specified a Planet ID / Birth that is currently in use.")
+		return errUseRecommendValue
+	}
+	if mConf.PlanetBirth <= 1567191349589 {
+		log.Println("!WARNING! You've been created a world older than official, timestamp should be larger than 1567191349589.")
+		return errUseRecommendValue
+	}
+	return nil
+}
+
+func PreFlightSigningKeyCheck() error {
+	var err1, err2 error
+	// "signing": ["previous.c25519", "current.c25519"]
+	tPrevkp, err1 := os.ReadFile(mConf.SigningKeyFiles[0])
+	tCurkp, err2 := os.ReadFile(mConf.SigningKeyFiles[1])
+	if err1 != nil || err2 != nil {
+		log.Println("read world signing key failed: ", err1, " , ", err2)
+		return ErrWorldSigningKeyIllegal
+	}
+	preqLen := node.ZT_C25519_PRIVATE_KEY_LEN + node.ZT_C25519_PUBLIC_KEY_LEN
+	if len(prevkp) != preqLen || len(curkp) != preqLen {
+		log.Println("existing world signing key does not satisfy required length.")
+		return ErrWorldSigningKeyIllegal
+	} else {
+		curkp = tCurkp
+		prevkp = tPrevkp
+	}
+	return nil
+}
+
+func buildPlanetNodeFromConfig() ([]*node.ZtWorldPlanetNode, error) {
+	res := []*node.ZtWorldPlanetNode{}
+	for _, v := range mConf.RootNodes {
+		n1 := &node.ZtWorldPlanetNode{}
+		n1ep := make([]*node.ZtNodeInetAddr, 0)
+		n1id := &node.ZtWorldPlanetNodeIdentity{}
+		err := n1id.FromString(v.IdentityStr, false)
+		if err != nil {
+			return nil, err
+		}
+		for _, v2 := range v.Endpoints {
+			n1addr := &node.ZtNodeInetAddr{}
+			err := n1addr.FromString(v2)
+			if err != nil {
+				return nil, err
+			}
+			n1ep = append(n1ep, n1addr)
+		}
+		n1.Identity = n1id
+		n1.Endpoints = n1ep
+		res = append(res, n1)
+	}
+	return res, nil
+}
diff --git a/ztnodeid/go.mod b/ztnodeid/go.mod
new file mode 100644
index 0000000..03538d6
--- /dev/null
+++ b/ztnodeid/go.mod
@@ -0,0 +1,5 @@
+module ztnodeid
+
+go 1.20
+
+require golang.org/x/crypto v0.8.0
diff --git a/ztnodeid/go.sum b/ztnodeid/go.sum
new file mode 100644
index 0000000..66fe607
--- /dev/null
+++ b/ztnodeid/go.sum
@@ -0,0 +1,2 @@
+golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
+golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
diff --git a/ztnodeid/pkg/node/errs.go b/ztnodeid/pkg/node/errs.go
new file mode 100644
index 0000000..3a1b1f9
--- /dev/null
+++ b/ztnodeid/pkg/node/errs.go
@@ -0,0 +1,16 @@
+/*
+ *  SPDX-License-Identifier: AGPL-3.0-only
+ *  Copyright (C) 2023 by kmahyyg in Patmeow Limited
+ */
+
+package node
+
+import "errors"
+
+var (
+	ErrMaxEndpointsExceeded   = errors.New("zerotier node has too many endpoints")
+	ErrMaxRootsExceeded       = errors.New("zerotier root exceeds limits")
+	ErrSerializedDataTooLarge = errors.New("serialized data longer than restriction")
+	ErrInvalidData            = errors.New("data input invalid")
+	ErrUnknown                = errors.New("unknown error")
+)
diff --git a/ztnodeid/pkg/node/identity.go b/ztnodeid/pkg/node/identity.go
new file mode 100644
index 0000000..7c1132e
--- /dev/null
+++ b/ztnodeid/pkg/node/identity.go
@@ -0,0 +1,116 @@
+// Copyright (c) 2021, ZeroTier, Inc.
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+//    list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+//    this list of conditions and the following disclaimer in the documentation
+//    and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+//    contributors may be used to endorse or promote products derived from
+//    this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+package node
+
+/**
+ * A ZeroTier identity
+ *
+ * An identity consists of a public key, a 40-bit ZeroTier address computed
+ * from that key in a collision-resistant fashion, and a self-signature.
+ *
+ * The address derivation algorithm makes it computationally very expensive to
+ * search for a different public key that duplicates an existing address. (See
+ * code for deriveAddress() for this algorithm.)
+ */
+
+import (
+	"fmt"
+	"ztnodeid/pkg/ztcrypto"
+)
+
+const ztIdentityHashCashFirstByteLessThan = 17
+
+// ZeroTierIdentity contains a public key, a private key, and a string representation of the identity.
+type ZeroTierIdentity struct {
+	address    uint64 // ZeroTier address, only least significant 40 bits are used
+	publicKey  [64]byte
+	privateKey *[64]byte
+}
+
+// NewZeroTierIdentity creates a new ZeroTier Identity.
+// This can be a little bit time-consuming due to one way proof of work requirements (usually a few hundred milliseconds).
+func NewZeroTierIdentity() (id ZeroTierIdentity) {
+	for {
+		pub, priv := ztcrypto.GenerateDualPair()
+		dig := ztcrypto.ComputeZeroTierIdentityMemoryHardHash(pub[:])
+		if dig[0] < ztIdentityHashCashFirstByteLessThan && dig[59] != 0xff {
+			id.address = uint64(dig[59])
+			id.address <<= 8
+			id.address |= uint64(dig[60])
+			id.address <<= 8
+			id.address |= uint64(dig[61])
+			id.address <<= 8
+			id.address |= uint64(dig[62])
+			id.address <<= 8
+			id.address |= uint64(dig[63])
+			if id.address != 0 {
+				id.publicKey = pub
+				id.privateKey = &priv
+				break
+			}
+		}
+	}
+	return
+}
+
+// PrivateKeyString returns the full identity.secret if the private key is set, or an empty string if no private key is set.
+func (id *ZeroTierIdentity) PrivateKeyString() string {
+	if id.privateKey != nil {
+		s := fmt.Sprintf("%.10x:0:%x:%x", id.address, id.publicKey, *id.privateKey)
+		return s
+	}
+	return ""
+}
+
+// PublicKeyString returns identity.public contents.
+func (id *ZeroTierIdentity) PublicKeyString() string {
+	s := fmt.Sprintf("%.10x:0:%x", id.address, id.publicKey)
+	return s
+}
+
+// IDString returns the NodeID as a 10-digit hex string
+func (id *ZeroTierIdentity) IDString() string {
+	s := fmt.Sprintf("%.10x", id.address)
+	return s
+}
+
+// ID returns the ZeroTier address as a uint64
+func (id *ZeroTierIdentity) ID() uint64 {
+	return id.address
+}
+
+// PrivateKey returns the bytes of the private key (or nil if not set)
+func (id *ZeroTierIdentity) PrivateKey() *[64]byte {
+	return id.privateKey
+}
+
+// PublicKey returns the public key bytes
+func (id *ZeroTierIdentity) PublicKey() [64]byte {
+	return id.publicKey
+}
diff --git a/ztnodeid/pkg/node/node.go b/ztnodeid/pkg/node/node.go
new file mode 100644
index 0000000..96fb8e2
--- /dev/null
+++ b/ztnodeid/pkg/node/node.go
@@ -0,0 +1,74 @@
+/*
+ *  SPDX-License-Identifier: AGPL-3.0-only
+ *  Copyright (C) 2023 by kmahyyg in Patmeow Limited
+ */
+
+package node
+
+import (
+	"bytes"
+	"encoding/hex"
+	"fmt"
+	"strings"
+)
+
+const (
+	ZT_C25519_PUBLIC_KEY_LEN  = 64
+	ZT_C25519_PRIVATE_KEY_LEN = 64
+	ZT_C25519_SIGNATURE_LEN   = 96
+)
+
+type ZtNormalNode struct {
+	ZtNodeAddress [5]byte // but only use big-endian high 40 bits
+	PublicKey     [ZT_C25519_PUBLIC_KEY_LEN]byte
+	privateKey    [ZT_C25519_PRIVATE_KEY_LEN]byte
+}
+
+// HasPrivateKey return true if first 4 bytes are all null
+func (ztn ZtNormalNode) HasPrivateKey() bool {
+	return !bytes.Equal(ztn.privateKey[0:4], []byte{0, 0, 0, 0})
+}
+
+// ExposePrivateKey returns internal private key
+func (ztn ZtNormalNode) ExposePrivateKey() []byte {
+	return ztn.privateKey[:]
+}
+
+// FromString import node identity using "identity.public", to use content of "identity.secret" with private key
+// imported at the same time, set hasPrivateKey to true
+func (ztn *ZtNormalNode) FromString(data string, hasPrivateKey bool) (err error) {
+	tmpDt := strings.Split(data, ":")
+	if len(tmpDt) != 3 && hasPrivateKey {
+		return ErrInvalidData
+	}
+	if len(tmpDt) < 2 {
+		return ErrInvalidData
+	}
+	if hasPrivateKey {
+		tmpPrivk, err := hex.DecodeString(tmpDt[2])
+		if err != nil {
+			return err
+		}
+		copy(ztn.privateKey[:], tmpPrivk)
+	}
+	tmpAddr, err := hex.DecodeString(tmpDt[0])
+	if err != nil {
+		return err
+	}
+	copy(ztn.ZtNodeAddress[:], tmpAddr)
+	tmpPubk, err := hex.DecodeString(tmpDt[2])
+	if err != nil {
+		return err
+	}
+	copy(ztn.PublicKey[:], tmpPubk)
+	return nil
+}
+
+func (ztn ZtNormalNode) ToString(exportPrivateKey bool) string {
+	// the second field, its value 0 indicates Curve25519/Ed25519 identity type
+	pub := fmt.Sprintf("%02x:0:%02x", ztn.ZtNodeAddress, ztn.PublicKey)
+	if exportPrivateKey && ztn.HasPrivateKey() {
+		return pub + fmt.Sprintf(":%02x", ztn.privateKey)
+	}
+	return pub
+}
diff --git a/ztnodeid/pkg/node/world.go b/ztnodeid/pkg/node/world.go
new file mode 100644
index 0000000..08d5b0b
--- /dev/null
+++ b/ztnodeid/pkg/node/world.go
@@ -0,0 +1,204 @@
+/*
+ *  SPDX-License-Identifier: AGPL-3.0-only
+ *  Copyright (C) 2023 by kmahyyg in Patmeow Limited
+ */
+
+package node
+
+import (
+	"bytes"
+	"encoding/binary"
+	"net"
+	"strconv"
+	"strings"
+	"syscall"
+)
+
+// Code reproduced from https://github.com/zerotier/ZeroTierOne/blob/e0a3291235230352148d5d30e51b341bfd9ad458/node/World.hpp
+
+const (
+	// ZT_WORLD_MAX_ROOTS is the Maximum number of roots (sanity limit, okay to increase)
+	//
+	// A given root can (through multi-homing) be distributed across any number of
+	// physical endpoints, but having more than one is good to permit total failure
+	// of one root or its withdrawal due to compromise without taking the whole net
+	// down.
+	ZT_WORLD_MAX_ROOTS = 4
+	// ZT_WORLD_MAX_STABLE_ENDPOINTS_PER_ROOT is the Maximum number of stable endpoints per root (sanity limit, okay to increase)
+	ZT_WORLD_MAX_STABLE_ENDPOINTS_PER_ROOT = 32
+	// ZT_WORLD_MAX_SERIALIZED_LENGTH is the (more than) maximum length of a serialized World
+	ZT_WORLD_MAX_SERIALIZED_LENGTH = ((1024 + (32 * ZT_WORLD_MAX_STABLE_ENDPOINTS_PER_ROOT)) *
+		ZT_WORLD_MAX_ROOTS) + ZT_C25519_PUBLIC_KEY_LEN + ZT_C25519_SIGNATURE_LEN + 128
+)
+
+type ZtWorldID = uint64
+
+const (
+	// ZT_WORLD_ID_EARTH is the official world in production ZeroTier Cloud
+	ZT_WORLD_ID_EARTH ZtWorldID = 149604618
+	// ZT_WORLD_ID_MARS is reserved world for future
+	ZT_WORLD_ID_MARS = 227883110
+)
+
+type ZtWorldType = uint8
+
+const (
+	// ZT_WORLD_TYPE_NULL should never be used in real world, but it's not invalid
+	ZT_WORLD_TYPE_NULL ZtWorldType = iota
+	// ZT_WORLD_TYPE_PLANET Planets, of which there is currently one (Earth)
+	ZT_WORLD_TYPE_PLANET
+	// ZT_WORLD_TYPE_MOON are user-created and many
+	ZT_WORLD_TYPE_MOON = 127
+)
+
+type ZtWorld struct {
+	Type                            ZtWorldType
+	ID                              ZtWorldID
+	Timestamp                       uint64
+	PublicKeyMustBeSignedByNextTime [ZT_C25519_PUBLIC_KEY_LEN]byte
+	Nodes                           []*ZtWorldPlanetNode
+}
+
+type ZtNodeInetAddr struct {
+	IP   *net.IP // net.IP == []byte
+	Port uint16
+}
+
+func (a *ZtNodeInetAddr) Family() int {
+	if a == nil || len(*a.IP) <= net.IPv4len {
+		return syscall.AF_INET
+	}
+	if a.IP.To4() != nil {
+		return syscall.AF_INET
+	}
+	return syscall.AF_INET6
+}
+
+func (a *ZtNodeInetAddr) FromString(ipport string) error {
+	// endpoint address use specific format: <IPADDR>/<PORT>
+	// identity address use contents from identity.public
+	tIpPort := strings.Split(ipport, "/")
+	if len(tIpPort) != 2 {
+		return ErrInvalidData
+	}
+	tPort, err := strconv.ParseUint(tIpPort[1], 10, 16)
+	if err != nil {
+		return err
+	}
+	tIp := net.ParseIP(tIpPort[0])
+	if tIp == nil {
+		return ErrInvalidData
+	}
+	a.IP = &tIp
+	a.Port = (uint16)(tPort)
+	return nil
+}
+
+func (ztniaddr *ZtNodeInetAddr) Serialize() ([]byte, error) {
+	var buf = make([]byte, 0)
+	switch ztniaddr.Family() {
+	case syscall.AF_INET:
+		buf = append(buf, (uint8)(4))
+		// uint32_t in_addr_t in_addr = sin_addr.s_addr (4bytes)
+		buf = append(buf, ztniaddr.IP.To4()...)
+		// in case we have port, append port. (reserved, but not used)
+		// port -> ntoh conversion, net-byteorder is always big-endian
+		buf = binary.BigEndian.AppendUint16(buf, ztniaddr.Port)
+		return buf, nil
+	case syscall.AF_INET6:
+		buf = append(buf, (uint8)(6))
+		// uint32_t in_addr_t in_addr = sin_addr.s_addr (4bytes)
+		buf = append(buf, ztniaddr.IP.To16()...)
+		// in case we have port, append port. (reserved, but not used)
+		// port -> ntoh conversion, net-byteorder is always big-endian
+		buf = binary.BigEndian.AppendUint16(buf, ztniaddr.Port)
+		return buf, nil
+	default:
+		buf = []byte{0}
+		return buf, ErrInvalidData
+	}
+}
+
+type ZtWorldPlanetNodeIdentity = ZtNormalNode
+
+func (ztpnid ZtWorldPlanetNodeIdentity) Serialize(inclPrivKey bool) ([]byte, error) {
+	var buf = make([]byte, 0)
+	buf = append(buf, ztpnid.ZtNodeAddress[:]...)
+	buf = append(buf, 0)
+	buf = append(buf, ztpnid.PublicKey[:]...)
+	if ztpnid.HasPrivateKey() && inclPrivKey {
+		buf = append(buf, ZT_C25519_PRIVATE_KEY_LEN)
+		buf = append(buf, ztpnid.privateKey[:]...)
+	} else {
+		buf = append(buf, 0)
+	}
+	return buf, nil
+}
+
+type ZtWorldPlanetNode struct {
+	Identity  *ZtWorldPlanetNodeIdentity
+	Endpoints []*ZtNodeInetAddr
+}
+
+func (ztpn ZtWorldPlanetNode) Serialize() ([]byte, error) {
+	var buf = make([]byte, 0)
+	// node->identity.serialize(), then append
+	idData, err := ztpn.Identity.Serialize(false)
+	if err != nil {
+		return nil, err
+	}
+	buf = append(buf, idData...)
+	// node->endpoints.size()
+	buf = append(buf, (uint8)(len(ztpn.Endpoints)))
+	//    for each endpoint, endpoint.serialize(), then append
+	for _, ep := range ztpn.Endpoints {
+		epData, err := ep.Serialize()
+		if err != nil {
+			return nil, err
+		}
+		buf = append(buf, epData...)
+	}
+	// check if exceed limit then return result
+	if len(ztpn.Endpoints) > ZT_WORLD_MAX_STABLE_ENDPOINTS_PER_ROOT {
+		return nil, ErrMaxEndpointsExceeded
+	}
+	return buf, nil
+}
+
+func (ztw ZtWorld) Serialize(forSign bool, c25519sig [ZT_C25519_SIGNATURE_LEN]byte) ([]byte, error) {
+	var buf = make([]byte, 0)
+	// by default, forSign = false
+	if forSign {
+		buf = binary.BigEndian.AppendUint64(buf, 0x7f7f7f7f7f7f7f7f)
+	}
+	buf = append(buf, ztw.Type)
+	buf = binary.BigEndian.AppendUint64(buf, ztw.ID)
+	buf = binary.BigEndian.AppendUint64(buf, ztw.Timestamp)
+	buf = append(buf, ztw.PublicKeyMustBeSignedByNextTime[:]...)
+	// make sure sig is not 0
+	if !forSign && !bytes.Equal(c25519sig[:4], []byte{0, 0, 0, 0}) {
+		buf = append(buf, c25519sig[:]...)
+	}
+	buf = append(buf, (uint8)(len(ztw.Nodes)))
+	if len(ztw.Nodes) > ZT_WORLD_MAX_ROOTS {
+		return nil, ErrMaxRootsExceeded
+	}
+	for _, n := range ztw.Nodes {
+		nBytes, err := n.Serialize()
+		if err != nil {
+			return nil, err
+		}
+		buf = append(buf, nBytes...)
+	}
+	if ztw.Type == ZT_WORLD_TYPE_MOON {
+		// official comments: no attached dictionary (for future use)
+		buf = binary.BigEndian.AppendUint16(buf, 0)
+	}
+	if forSign {
+		buf = binary.BigEndian.AppendUint64(buf, 0xf7f7f7f7f7f7f7f7)
+	}
+	if len(buf) > ZT_WORLD_MAX_SERIALIZED_LENGTH {
+		return nil, ErrSerializedDataTooLarge
+	}
+	return buf, nil
+}
diff --git a/ztnodeid/pkg/ztcrypto/identity.go b/ztnodeid/pkg/ztcrypto/identity.go
new file mode 100644
index 0000000..be9463e
--- /dev/null
+++ b/ztnodeid/pkg/ztcrypto/identity.go
@@ -0,0 +1,108 @@
+/*
+ *  SPDX-License-Identifier: AGPL-3.0-only
+ *  Copyright (C) 2023 by kmahyyg in Patmeow Limited
+ */
+
+package ztcrypto
+
+import (
+	secrand "crypto/rand"
+	"crypto/sha512"
+	"encoding/binary"
+	"golang.org/x/crypto/curve25519"
+	"golang.org/x/crypto/ed25519"
+	"golang.org/x/crypto/salsa20/salsa"
+)
+
+const ztIdentityGenMemory = 2097152
+
+func ComputeZeroTierIdentityMemoryHardHash(publicKey []byte) []byte {
+	s512 := sha512.Sum512(publicKey)
+
+	var genmem [ztIdentityGenMemory]byte
+	var s20key [32]byte
+	var s20ctr [16]byte
+	var s20ctri uint64
+	copy(s20key[:], s512[0:32])
+	copy(s20ctr[0:8], s512[32:40])
+	salsa.XORKeyStream(genmem[0:64], genmem[0:64], &s20ctr, &s20key)
+	s20ctri++
+	for i := 64; i < ztIdentityGenMemory; i += 64 {
+		binary.LittleEndian.PutUint64(s20ctr[8:16], s20ctri)
+		salsa.XORKeyStream(genmem[i:i+64], genmem[i-64:i], &s20ctr, &s20key)
+		s20ctri++
+	}
+
+	var tmp [8]byte
+	for i := 0; i < ztIdentityGenMemory; {
+		idx1 := uint(binary.BigEndian.Uint64(genmem[i:])&7) * 8
+		i += 8
+		idx2 := (uint(binary.BigEndian.Uint64(genmem[i:])) % uint(ztIdentityGenMemory/8)) * 8
+		i += 8
+		gm := genmem[idx2 : idx2+8]
+		d := s512[idx1 : idx1+8]
+		copy(tmp[:], gm)
+		copy(gm, d)
+		copy(d, tmp[:])
+		binary.LittleEndian.PutUint64(s20ctr[8:16], s20ctri)
+		salsa.XORKeyStream(s512[:], s512[:], &s20ctr, &s20key)
+		s20ctri++
+	}
+
+	return s512[:]
+}
+
+// GenerateDualPair generates a key pair containing two pairs: one for curve25519 and one for ed25519.
+func GenerateDualPair() (pub [64]byte, priv [64]byte) {
+	k0pub, k0priv, _ := ed25519.GenerateKey(secrand.Reader)
+	var k1pub, k1priv [32]byte
+	secrand.Read(k1priv[:])
+	curve25519.ScalarBaseMult(&k1pub, &k1priv)
+	// https://www.eiken.dev/blog/2020/11/code-spotlight-the-reference-implementation-of-ed25519-part-1/
+	// First 32 bytes of pub and priv are the keys for ECDH key
+	// agreement. This generates the public portion from the private.
+	copy(pub[0:32], k1pub[:])
+	copy(priv[0:32], k1priv[:])
+	// Second 32 bytes of pub and priv are the keys for ed25519
+	// signing and verification.
+	copy(pub[32:64], k0pub[0:32])
+	copy(priv[32:64], k0priv[0:32])
+	// same as node/C25519.cpp
+	return
+}
+
+func SignMessage(pub [64]byte, priv [64]byte, msg []byte) ([96]byte, error) {
+	var sigBuf = make([]byte, 96)
+	var finalSig = [96]byte{}
+	// Zerotier Official: we sign the first 32 bytes of SHA-512(msg)
+	h := sha512.New()
+	h.Write(msg)
+	s512 := h.Sum(nil)
+	copy(sigBuf[64:], s512[:32])
+	//
+	// in fact, it's ed25519 sign.
+	//
+	/**
+	 * This takes the SHA-512 of msg[] and then signs the first 32 bytes of this
+	 * digest, returning it and the 64-byte ed25519 signature in signature[].
+	 * This results in a signature that verifies both the signer's authenticity
+	 * and the integrity of the message.
+	 *
+	 * This is based on the original ed25519 code from NaCl and the SUPERCOP
+	 * cipher benchmark suite, but with the modification that it always
+	 * produces a signature of fixed 96-byte length based on the hash of an
+	 * arbitrary-length message.
+	 *
+	 */
+	//
+	// Signature = (R,S,Sha512-Of-First32-Msg)
+	//
+	goPrivK := make([]byte, 64)
+	copy(goPrivK[:32], priv[32:64])
+	copy(goPrivK[32:], pub[32:64])
+	sigData := ed25519.Sign(goPrivK, s512[:32])
+	copy(sigBuf[:64], sigData)
+	// finalize and return
+	copy(finalSig[:], sigBuf)
+	return finalSig, nil
+}


Test patch: