Browse Source

Merge branch 'dev' into redisrection

Grant Limberg 3 years ago
parent
commit
c6fc3560f2
100 changed files with 7954 additions and 837 deletions
  1. 68 0
      .drone.jsonnet
  2. 2634 246
      .drone.yml
  3. 5 5
      .gitignore
  4. 4 0
      Makefile
  5. 11 5
      README.md
  6. 33 4
      RELEASE-NOTES.md
  7. 10 0
      ci/Dockerfile.deb
  8. 36 0
      ci/Dockerfile.el6
  9. 9 0
      ci/Dockerfile.rpm
  10. 0 7
      ci/Dockerfile.sid
  11. 101 13
      ci/scripts/build.sh
  12. 37 0
      ci/scripts/munge_debian_changelog.sh
  13. 36 0
      ci/scripts/munge_rpm_spec.sh
  14. 0 8
      controller/DB.cpp
  15. 1 2
      controller/DB.hpp
  16. 0 51
      controller/DBMirrorSet.cpp
  17. 0 6
      controller/DBMirrorSet.hpp
  18. 51 77
      controller/EmbeddedNetworkController.cpp
  19. 9 3
      controller/EmbeddedNetworkController.hpp
  20. 16 12
      controller/PostgreSQL.cpp
  21. 24 0
      debian/changelog
  22. 1 1
      debian/control
  23. 1 1
      dockerbuild/Dockerfile.alpine
  24. 40 10
      entrypoint.sh.release
  25. 21 0
      ext/inja/LICENSE
  26. 391 0
      ext/inja/README.md
  27. 2949 0
      ext/inja/inja.hpp
  28. 1 1
      ext/installfiles/mac/ZeroTier One.pkgproj
  29. BIN
      ext/installfiles/windows/Prerequisites/Visual C++ Redistributable for Visual Studio 2015-2022/VC_redist.x64.exe
  30. BIN
      ext/installfiles/windows/Prerequisites/Visual C++ Redistributable for Visual Studio 2015-2022/VC_redist.x86.exe
  31. 83 46
      ext/installfiles/windows/ZeroTier One.aip
  32. 0 0
      ext/nlohmann/LICENSE.MIT
  33. 0 0
      ext/nlohmann/README.md
  34. 0 0
      ext/nlohmann/json.hpp
  35. 2 6
      include/ZeroTierOne.h
  36. 2 2
      make-bsd.mk
  37. 73 25
      make-linux.mk
  38. 7 3
      make-mac.mk
  39. 295 143
      node/Bond.cpp
  40. 68 13
      node/Bond.hpp
  41. 25 15
      node/Constants.hpp
  42. 34 30
      node/IncomingPacket.cpp
  43. 8 2
      node/IncomingPacket.hpp
  44. 2 1
      node/Membership.cpp
  45. 4 10
      node/Membership.hpp
  46. 8 0
      node/Network.cpp
  47. 8 4
      node/Network.hpp
  48. 9 10
      node/NetworkConfig.hpp
  49. 1 2
      node/Node.cpp
  50. 17 0
      node/Path.hpp
  51. 4 4
      node/Peer.cpp
  52. 9 20
      node/Peer.hpp
  53. 2 2
      node/Revocation.hpp
  54. 1 1
      node/Salsa20.hpp
  55. 15 0
      node/SelfAwareness.cpp
  56. 7 0
      node/SelfAwareness.hpp
  57. 9 31
      one.cpp
  58. 4 4
      osdep/Binder.hpp
  59. 1 1
      osdep/OSUtils.hpp
  60. 30 10
      osdep/Phy.hpp
  61. 4 0
      pkg/README.md
  62. 10 0
      pkg/asustor/Dockerfile
  63. 13 0
      pkg/asustor/build.sh
  64. 29 0
      pkg/asustor/entrypoint.sh
  65. 15 0
      pkg/asustor/zerotier/CONTROL/config.json
  66. 1 0
      pkg/asustor/zerotier/CONTROL/description.txt
  67. BIN
      pkg/asustor/zerotier/CONTROL/icon.png
  68. 149 0
      pkg/asustor/zerotier/CONTROL/license.txt
  69. 24 0
      pkg/asustor/zerotier/CONTROL/post-install.sh
  70. 3 0
      pkg/asustor/zerotier/CONTROL/post-uninstall.sh
  71. 0 0
      pkg/asustor/zerotier/CONTROL/pre-install.sh
  72. 0 0
      pkg/asustor/zerotier/CONTROL/pre-uninstall.sh
  73. 26 0
      pkg/asustor/zerotier/CONTROL/start-stop.sh
  74. 1 0
      pkg/asustor/zerotier/apkg-version
  75. 58 0
      pkg/asustor/zerotier/www/index.html
  76. 6 0
      pkg/config.json
  77. 8 0
      pkg/qnap/Dockerfile
  78. 13 0
      pkg/qnap/build.sh
  79. 76 0
      pkg/qnap/entrypoint.sh
  80. 3 0
      pkg/qnap/qdk.conf
  81. 7 0
      pkg/qnap/zerotier/Makefile
  82. 0 0
      pkg/qnap/zerotier/arm_64/.gitkeep
  83. 0 0
      pkg/qnap/zerotier/arm_x09/.gitkeep
  84. 0 0
      pkg/qnap/zerotier/arm_x10/.gitkeep
  85. 0 0
      pkg/qnap/zerotier/arm_x12/.gitkeep
  86. 0 0
      pkg/qnap/zerotier/arm_x19/.gitkeep
  87. 0 0
      pkg/qnap/zerotier/arm_x31/.gitkeep
  88. 0 0
      pkg/qnap/zerotier/arm_x41/.gitkeep
  89. 0 0
      pkg/qnap/zerotier/config/.gitkeep
  90. 0 0
      pkg/qnap/zerotier/icons/.gitkeep
  91. BIN
      pkg/qnap/zerotier/icons/ZeroTier.gif
  92. BIN
      pkg/qnap/zerotier/icons/ZeroTier_80.gif
  93. BIN
      pkg/qnap/zerotier/icons/ZeroTier_gray.gif
  94. 158 0
      pkg/qnap/zerotier/package_routines
  95. 99 0
      pkg/qnap/zerotier/qpkg.cfg
  96. 0 0
      pkg/qnap/zerotier/shared/.gitkeep
  97. BIN
      pkg/qnap/zerotier/shared/.qpkg_icon.gif
  98. BIN
      pkg/qnap/zerotier/shared/.qpkg_icon_80.gif
  99. BIN
      pkg/qnap/zerotier/shared/.qpkg_icon_gray.gif
  100. 34 0
      pkg/qnap/zerotier/shared/zerotier.sh

+ 68 - 0
.drone.jsonnet

@@ -0,0 +1,68 @@
+
+local targets = [
+      //
+      // Render these into .drone.yaml by running "make drone"
+      //      
+      { "os": "linux", "name": "el9", "isas": [ "amd64", "arm64", "ppc64le", "s390x" ], "events": [ "tag" ] },
+      { "os": "linux", "name": "el8", "isas": [ "amd64", "arm64", "ppc64le", "s390x" ], "events": [ "tag" ] },
+      { "os": "linux", "name": "el7", "isas": [ "amd64", "ppc64le"], "events": [ "tag" ] },
+      { "os": "linux", "name": "el6", "isas": [ "amd64" ], "events": [ "tag" ] },
+      { "os": "linux", "name": "amzn2", "isas": [ "amd64", "arm64" ], "events": [ "tag" ] },
+      { "os": "linux", "name": "fc37", "isas": [ "amd64", "arm64", "ppc64le", "s390x" ], "events": [ "tag" ] },
+      { "os": "linux", "name": "fc36", "isas": [ "amd64", "arm64", "ppc64le", "s390x" ], "events": [ "tag" ] },
+      { "os": "linux", "name": "fc35", "isas": [ "amd64", "arm64", "ppc64le", "s390x" ], "events": [ "tag" ] },
+      { "os": "linux", "name": "jammy", "isas": [ "amd64", "arm64", "armv7", "riscv64", "ppc64le", "s390x" ], "events": [ "tag" ] },
+      { "os": "linux", "name": "focal", "isas": [ "amd64", "arm64", "armv7", "riscv64", "ppc64le"  ], "events": [ "tag" ] },
+      { "os": "linux", "name": "bionic", "isas": [ "amd64", "arm64", "386", "ppc64le", "s390x" ], "events": ["tag" ] },
+      { "os": "linux", "name": "xenial", "isas": [ "amd64", "arm64", "386" ], "events": [ "tag" ] },
+      { "os": "linux", "name": "sid", "isas": [ "386", "amd64", "arm64", "armv7", "riscv64", "mips64le", "ppc64le", "s390x" ], "events": [ "push", "tag" ] },
+      { "os": "linux", "name": "bookworm", "isas": [ "amd64", "arm64", "armv7", "386", "mips64le", "ppc64le", "s390x" ], "events": [ "tag" ] },
+      { "os": "linux", "name": "bullseye", "isas": [ "amd64", "arm64", "armv7", "386", "mips64le", "ppc64le", "s390x" ], "events": [ "tag" ] },
+      { "os": "linux", "name": "buster", "isas": [ "amd64", "arm64", "armv7", "386", "mips64le", "ppc64le", "s390x" ], "events": [ "tag" ] },
+      { "os": "linux", "name": "stretch", "isas": [ "amd64", "arm64", "386" ], "events": [ "tag" ] },
+      // { "os": "windows", "name": "win2k19", "isas": [ "amd64" ], "events": ["push", "tag" ] }
+];
+
+local Build(platform, os, isa, events) = {
+  "kind": "pipeline",
+  "type": "docker",
+  "pull": "always",
+  "name": platform + " " + isa + " " + "build",
+  "clone": { "depth": 1 },
+  "steps": [
+    {
+      "name": "build",
+      "image": "registry.sean.farm/honda-builder",
+      "commands": [ "./ci/scripts/build.sh " + platform + " " + isa + " " + "100.0.0+${DRONE_COMMIT_SHA:0:8}" + " " + "${DRONE_BUILD_EVENT}" ]
+    },
+    {
+      "name": "list",
+      "image": "registry.sean.farm/honda-builder",
+      "commands": [ "ls -la " + platform ]
+    },
+    {
+      "name": "notify-mattermost",
+      "image": "registry.sean.farm/mattermost-notify",
+      "environment": {
+        "token": { "from_secret": "mattermost-token" },
+        "host": { "from_secret": "mattermost-host" },
+        "channel": { "from_secret": "mattermost-channel" },
+        "maxRetry": 3,
+      },
+      "when": { "status": [ "failure" ] }
+    }
+  ],  
+  "image_pull_secrets": [ "dockerconfigjson" ],
+  [ if isa == "arm64" || isa == "armv7" then "platform" ]: { os: os, arch: "arm64" },
+  "trigger": { "event": events }
+};
+
+// puttin on the bits
+
+std.flattenArrays([
+  [
+     Build(p.name, p.os, isa, p.events)
+      for isa in p.isas
+  ]
+  for p in targets
+])

+ 2634 - 246
.drone.yml

@@ -1,267 +1,2655 @@
 ---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: el9 amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh el9 amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la el9
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: el9 arm64 build
+platform:
+  arch: arm64
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh el9 arm64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la el9
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: el9 ppc64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh el9 ppc64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la el9
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: el9 s390x build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh el9 s390x 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la el9
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: el8 amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh el8 amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la el8
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: el8 arm64 build
+platform:
+  arch: arm64
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh el8 arm64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la el8
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: el8 ppc64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh el8 ppc64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la el8
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: el8 s390x build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh el8 s390x 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la el8
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: el7 amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh el7 amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la el7
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: el7 ppc64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh el7 ppc64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la el7
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: el6 amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh el6 amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la el6
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: amzn2 amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh amzn2 amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la amzn2
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: amzn2 arm64 build
+platform:
+  arch: arm64
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh amzn2 arm64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la amzn2
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: fc37 amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh fc37 amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la fc37
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: fc37 arm64 build
+platform:
+  arch: arm64
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh fc37 arm64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la fc37
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: fc37 ppc64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh fc37 ppc64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la fc37
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: fc37 s390x build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh fc37 s390x 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la fc37
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: fc36 amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh fc36 amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la fc36
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: fc36 arm64 build
+platform:
+  arch: arm64
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh fc36 arm64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la fc36
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: fc36 ppc64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh fc36 ppc64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la fc36
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: fc36 s390x build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh fc36 s390x 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la fc36
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: fc35 amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh fc35 amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la fc35
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: fc35 arm64 build
+platform:
+  arch: arm64
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh fc35 arm64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la fc35
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: fc35 ppc64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh fc35 ppc64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la fc35
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: fc35 s390x build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh fc35 s390x 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la fc35
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: jammy amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh jammy amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la jammy
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: jammy arm64 build
+platform:
+  arch: arm64
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh jammy arm64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la jammy
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: jammy armv7 build
+platform:
+  arch: arm64
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh jammy armv7 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la jammy
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: jammy riscv64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh jammy riscv64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la jammy
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: jammy ppc64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh jammy ppc64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la jammy
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: jammy s390x build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh jammy s390x 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la jammy
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: focal amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh focal amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la focal
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: focal arm64 build
+platform:
+  arch: arm64
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh focal arm64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la focal
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: focal armv7 build
+platform:
+  arch: arm64
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh focal armv7 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la focal
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: focal riscv64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh focal riscv64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la focal
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: focal ppc64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh focal ppc64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la focal
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: bionic amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bionic amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bionic
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: bionic arm64 build
+platform:
+  arch: arm64
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bionic arm64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bionic
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: bionic 386 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bionic 386 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bionic
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: bionic ppc64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bionic ppc64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bionic
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: bionic s390x build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bionic s390x 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bionic
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: xenial amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh xenial amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la xenial
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: xenial arm64 build
+platform:
+  arch: arm64
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh xenial arm64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la xenial
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: xenial 386 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh xenial 386 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la xenial
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: sid 386 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh sid 386 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la sid
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - push
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: sid amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh sid amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la sid
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - push
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: sid arm64 build
+platform:
+  arch: arm64
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh sid arm64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la sid
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - push
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: sid armv7 build
+platform:
+  arch: arm64
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh sid armv7 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la sid
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - push
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: sid riscv64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh sid riscv64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la sid
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - push
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: sid mips64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh sid mips64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la sid
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - push
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: sid ppc64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh sid ppc64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la sid
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - push
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: sid s390x build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh sid s390x 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la sid
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - push
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
 kind: pipeline
+name: bookworm amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bookworm amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bookworm
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
 type: docker
-name: build 386
-
-clone:
-  depth: 1
-
-steps:
-  - name: build 386
-    image: registry.sean.farm/honda-builder
-    commands:
-      - ./ci/scripts/build.sh linux 386 $${DRONE_COMMIT_SHA}
-  - name: notify-mattermost
-    pull: always
-    image: registry.sean.farm/mattermost-notify
-    environment:
-      token:
-        from_secret: mattermost-token
-      host:
-        from_secret: mattermost-host
-      channel:
-        from_secret: mattermost-channel
-      maxRetry: 3
-    when:
-      status:
-        - failure
-        - success
-
-image_pull_secrets:
-  - dockerconfigjson
-
----
-kind: pipeline
-type: docker
-name: build amd64
-
-clone:
-  depth: 1
-
-steps:
-  - name: build amd64
-    image: registry.sean.farm/honda-builder
-    commands:
-      - ./ci/scripts/build.sh linux amd64 $${DRONE_COMMIT_SHA}
-  - name: notify-mattermost
-    pull: always
-    image: registry.sean.farm/mattermost-notify
-    environment:
-      token:
-        from_secret: mattermost-token
-      host:
-        from_secret: mattermost-host
-      channel:
-        from_secret: mattermost-channel
-      maxRetry: 3
-    when:
-      status:
-        - failure
-        - success
-
-image_pull_secrets:
-  - dockerconfigjson
-
----
-kind: pipeline
-type: docker
-name: build arm64
-
-clone:
-  depth: 1
-
-steps:
-  - name: build arm64
-    image: registry.sean.farm/honda-builder
-    commands:
-      - ./ci/scripts/build.sh linux arm64 $${DRONE_COMMIT_SHA}
-  - name: notify-mattermost
-    pull: always
-    image: registry.sean.farm/mattermost-notify
-    environment:
-      token:
-        from_secret: mattermost-token
-      host:
-        from_secret: mattermost-host
-      channel:
-        from_secret: mattermost-channel
-      maxRetry: 3
-    when:
-      status:
-        - failure
-        - success
-
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: bookworm arm64 build
 platform:
+  arch: arm64
   os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bookworm arm64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bookworm
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: bookworm armv7 build
+platform:
   arch: arm64
-
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bookworm armv7 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bookworm
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: bookworm 386 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bookworm 386 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bookworm
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: bookworm mips64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bookworm mips64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bookworm
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: bookworm ppc64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bookworm ppc64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bookworm
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
 image_pull_secrets:
-  - dockerconfigjson
-
+- dockerconfigjson
+kind: pipeline
+name: bookworm s390x build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bookworm s390x 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bookworm
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
 ---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
 kind: pipeline
+name: bullseye amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bullseye amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bullseye
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
 type: docker
-name: build armv7
-
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: bullseye arm64 build
 platform:
+  arch: arm64
   os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bullseye arm64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bullseye
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: bullseye armv7 build
+platform:
   arch: arm64
-
-clone:
-  depth: 1
-
-steps:
-  - name: build armv7
-    image: registry.sean.farm/honda-builder
-    commands:
-      - ./ci/scripts/build.sh linux armv7 $${DRONE_COMMIT_SHA}
-  - name: notify-mattermost
-    pull: always
-    image: registry.sean.farm/mattermost-notify
-    environment:
-      token:
-        from_secret: mattermost-token
-      host:
-        from_secret: mattermost-host
-      channel:
-        from_secret: mattermost-channel
-      maxRetry: 3
-    when:
-      status:
-        - failure
-        - success
-
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bullseye armv7 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bullseye
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: bullseye 386 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bullseye 386 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bullseye
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: bullseye mips64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bullseye mips64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bullseye
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: bullseye ppc64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bullseye ppc64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bullseye
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: bullseye s390x build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh bullseye s390x 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la bullseye
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: buster amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh buster amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la buster
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: buster arm64 build
 platform:
+  arch: arm64
   os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh buster arm64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la buster
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: buster armv7 build
+platform:
   arch: arm64
-  
-image_pull_secrets:
-  - dockerconfigjson
-
----
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh buster armv7 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la buster
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: buster 386 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh buster 386 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la buster
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: buster mips64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh buster mips64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la buster
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: buster ppc64le build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh buster ppc64le 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la buster
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: buster s390x build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh buster s390x 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la buster
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: stretch amd64 build
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh stretch amd64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la stretch
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker
+---
+clone:
+  depth: 1
+image_pull_secrets:
+- dockerconfigjson
 kind: pipeline
+name: stretch arm64 build
+platform:
+  arch: arm64
+  os: linux
+pull: always
+steps:
+- commands:
+  - ./ci/scripts/build.sh stretch arm64 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la stretch
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
 type: docker
-name: build riscv64
-
+---
 clone:
   depth: 1
-
+image_pull_secrets:
+- dockerconfigjson
+kind: pipeline
+name: stretch 386 build
+pull: always
 steps:
-  - name: build riscv64
-    image: registry.sean.farm/honda-builder
-    commands:
-      - ./ci/scripts/build.sh linux riscv64 $${DRONE_COMMIT_SHA}
-  - name: notify-mattermost
-    pull: always
-    image: registry.sean.farm/mattermost-notify
-    environment:
-      token:
-        from_secret: mattermost-token
-      host:
-        from_secret: mattermost-host
-      channel:
-        from_secret: mattermost-channel
-      maxRetry: 3
-    when:
-      status:
-        - failure
-        - success
-
-image_pull_secrets:
-  - dockerconfigjson
-
----
-kind: pipeline
-type: docker
-name: build mips64le
-
-clone:
-  depth: 1
-
-steps:
-  - name: build mips64le
-    image: registry.sean.farm/honda-builder
-    commands:
-      - ./ci/scripts/build.sh linux mips64le $${DRONE_COMMIT_SHA}
-  - name: notify-mattermost
-    pull: always
-    image: registry.sean.farm/mattermost-notify
-    environment:
-      token:
-        from_secret: mattermost-token
-      host:
-        from_secret: mattermost-host
-      channel:
-        from_secret: mattermost-channel
-      maxRetry: 3
-    when:
-      status:
-        - failure
-        - success
-
-image_pull_secrets:
-  - dockerconfigjson
-
----
-kind: pipeline
-type: docker
-name: build ppc64le
-
-clone:
-  depth: 1
-
-steps:
-  - name: build ppc64le
-    image: registry.sean.farm/honda-builder
-    commands:
-      - ./ci/scripts/build.sh linux ppc64le $${DRONE_COMMIT_SHA}
-  - name: notify-mattermost
-    pull: always
-    image: registry.sean.farm/mattermost-notify
-    environment:
-      token:
-        from_secret: mattermost-token
-      host:
-        from_secret: mattermost-host
-      channel:
-        from_secret: mattermost-channel
-      maxRetry: 3
-    when:
-      status:
-        - failure
-        - success
-
-image_pull_secrets:
-  - dockerconfigjson
-
----
-kind: pipeline
-type: docker
-name: build s390x
-
-clone:
-  depth: 1
-
-steps:
-  - name: build s390x
-    image: registry.sean.farm/honda-builder
-    commands:
-      - ./ci/scripts/build.sh linux s390x $${DRONE_COMMIT_SHA}
-  - name: notify-mattermost
-    pull: always
-    image: registry.sean.farm/mattermost-notify
-    environment:
-      token:
-        from_secret: mattermost-token
-      host:
-        from_secret: mattermost-host
-      channel:
-        from_secret: mattermost-channel
-      maxRetry: 3
-    when:
-      status:
-        - failure
-        - success
-
-image_pull_secrets:
-  - dockerconfigjson
+- commands:
+  - ./ci/scripts/build.sh stretch 386 100.0.0+${DRONE_COMMIT_SHA:0:8} ${DRONE_BUILD_EVENT}
+  image: registry.sean.farm/honda-builder
+  name: build
+- commands:
+  - ls -la stretch
+  image: registry.sean.farm/honda-builder
+  name: list
+- environment:
+    channel:
+      from_secret: mattermost-channel
+    host:
+      from_secret: mattermost-host
+    maxRetry: 3
+    token:
+      from_secret: mattermost-token
+  image: registry.sean.farm/mattermost-notify
+  name: notify-mattermost
+  when:
+    status:
+    - failure
+trigger:
+  event:
+  - tag
+type: docker

+ 5 - 5
.gitignore

@@ -6,6 +6,11 @@
 /zerotier
 /nltest
 
+# IDE stuff
+/.idea
+/.nova
+/compile_commands.json
+
 # OS-created garbage files from various platforms
 .DS_Store
 .Apple*
@@ -30,7 +35,6 @@ Thumbs.db
 /windows/WebUIWrapper/obj
 /windows/lib
 /ext/installfiles/windows/ZeroTier One-SetupFiles
-/ext/installfiles/windows/Prerequisites
 /ext/installfiles/windows/*-cache
 /ZeroTier One.msi
 *.vcxproj.backup
@@ -58,7 +62,6 @@ zt1-src.tar.gz
 *.opensdf
 *.user
 *.cache
-*.obj
 *.tlog
 *.pid
 *.pkg
@@ -103,7 +106,6 @@ windows/ZeroTierOne/Debug/
 *.swp
 *~.nib
 DerivedData/
-build/
 *.pbxuser
 *.mode1v3
 *.mode2v3
@@ -114,7 +116,6 @@ build/
 !default.perspectivev3
 *.xccheckout
 xcuserdata/
-ext/librethinkdbxx/build
 .vscode
 __pycache__
 *~
@@ -123,7 +124,6 @@ attic/world/mkworld
 workspace/
 workspace2/
 zeroidc/target/
-tmp/
 
 #snapcraft specifics
 /parts/

+ 4 - 0
Makefile

@@ -26,3 +26,7 @@ endif
 ifeq ($(OSTYPE),NetBSD)
 	include make-netbsd.mk
 endif
+
+drone:
+	@echo "rendering .drone.yaml from .drone.jsonnet"
+	drone jsonnet --format --stream

+ 11 - 5
README.md

@@ -1,6 +1,7 @@
 ZeroTier - Global Area Networking
 ======
-This document is written for a software developer audience. For information on using ZeroTier, see the: [Website](https://www.zerotier.com), [Documentation Site](https://docs.zerotier.com), and [Discussion Forum](https://discuss.zerotier.com)
+
+*This document is written for a software developer audience. For information on using ZeroTier, see the: [Website](https://www.zerotier.com), [Documentation Site](https://docs.zerotier.com), and [Discussion Forum](https://discuss.zerotier.com).*
 
 ZeroTier is a smart programmable Ethernet switch for planet Earth. It allows all networked devices, VMs, containers, and applications to communicate as if they all reside in the same physical data center or cloud region.
 
@@ -42,24 +43,29 @@ The base path contains the ZeroTier One service main entry point (`one.cpp`), se
  - `rule-compiler/`: JavaScript rules language compiler for defining network-level rules.
  - `service/`: the ZeroTier One service, which wraps the ZeroTier core and provides VPN-like connectivity to virtual networks for desktops, laptops, servers, VMs, and containers.
  - `windows/`: Visual Studio solution files, Windows service code, and the Windows task bar app UI.
+ - `zeroidc/`: OIDC implementation used by ZeroTier service to log into SSO-enabled networks. (This part is written in Rust, and more Rust will be appearing in this repository in the future.)
 
 ### Build and Platform Notes
 
 To build on Mac and Linux just type `make`. On FreeBSD and OpenBSD `gmake` (GNU make) is required and can be installed from packages or ports. For Windows there is a Visual Studio solution in `windows/`.
 
  - **Mac**
-   - Xcode command line tools for OSX 10.8 or newer are required.
+   - Xcode command line tools for macOS 10.13 or newer are required.
+   - Rust for x86_64 and ARM64 targets *if SSO is enabled in the build*.
  - **Linux**
    - The minimum compiler versions required are GCC/G++ 4.9.3 or CLANG/CLANG++ 3.4.2. (Install `clang` on CentOS 7 as G++ is too old.)
    - Linux makefiles automatically detect and prefer clang/clang++ if present as it produces smaller and slightly faster binaries in most cases. You can override by supplying CC and CXX variables on the make command line.
+   - Rust for x86_64 and ARM64 targets *if SSO is enabled in the build*.
  - **Windows**
-   - Windows 7 or newer is supported. This *may* work on Vista but isn't officially supported there. It will not work on Windows XP.
-   - We build with Visual Studio 2017. Older versions may not work. Clang or MinGW will also probably work but may require some makefile hacking.
+   - Visual Studio 2022 on Windows 10 or newer.
+   - Rust for x86_64 and ARM64 targets *if SSO is enabled in the build*.
  - **FreeBSD**
    - GNU make is required. Type `gmake` to build.
+   - Rust for x86_64 and ARM64 targets *if SSO is enabled in the build*.
  - **OpenBSD**
    - There is a limit of four network memberships on OpenBSD as there are only four tap devices (`/dev/tap0` through `/dev/tap3`).
    - GNU make is required. Type `gmake` to build.
+   - Rust for x86_64 and ARM64 targets *if SSO is enabled in the build*.
 
 Typing `make selftest` will build a *zerotier-selftest* binary which unit tests various internals and reports on a few aspects of the build environment. It's a good idea to try this on novel platforms or architectures.
 
@@ -82,7 +88,7 @@ Here's where home folders live (by default) on each OS:
  * **Linux**: `/var/lib/zerotier-one`
  * **FreeBSD** / **OpenBSD**: `/var/db/zerotier-one`
  * **Mac**: `/Library/Application Support/ZeroTier/One`
- * **Windows**: `\ProgramData\ZeroTier\One` (That's for Windows 7. The base 'shared app data' folder might be different on different Windows versions.)
+ * **Windows**: `\ProgramData\ZeroTier\One` (That's the default. The base 'shared app data' folder might be different if Windows is installed with a non-standard drive letter assignment or layout.)
 
 ### Basic Troubleshooting
 

+ 33 - 4
RELEASE-NOTES.md

@@ -1,11 +1,40 @@
 ZeroTier Release Notes
 ======
 
-# 2022-03-21 -- Version 1.8.7
+# 2022-06-07 -- Version 1.10.0
 
- * Fix for dependency installations in Windows MSI package
- * Fix for privilege escalation in desktop UI when the user is not a current super-user
- * Bug fix in local OIDC / SSO support
+ * Fix formatting problem in `zerotier-cli` when using SSO networks.
+ * Fix a few other minor bugs in SSO signin to prepare for general availability.
+ * Remove requirement for webview in desktop UI and instead just make everything available via the tray pulldown/menu. Use [libui-ng](https://github.com/libui-ng/libui-ng) for minor prompt dialogs. Saves space and eliminates installation headaches on Windows.
+ * Fix SSO "spam" bug in desktop UI.
+ * Use system default browser for SSO login so all your plugins, MFA devices, password managers, etc. will work as you have them configured.
+ * Minor fix for bonding/multipath.
+
+# 2022-05-10 -- Version 1.8.10
+
+ * Fixed a bug preventing SSO sign-on on Windows.
+
+# 2022-04-25 -- Version 1.8.9
+
+ * Fixed a long-standing and strange bug that was causing sporadic "phantom" packet authentication failures. Not a security problem but could be behind spordaic reports of link failures under some conditions.
+ * Fized a memory leak in SSO/OIDC support.
+ * Fixed SSO/OIDC display error on CLI.
+ * Fixed a bug causing nodes to sometimes fail to push certs to each other (primarily affects SSO/OIDC use cases).
+ * Fixed a deadlock bug on leaving SSO/OIDC managed networks.
+ * Added some new Linux distributions to the build subsystem.
+
+# 2022-04-11 -- Version 1.8.8
+
+ * Fix a local privilege escalation bug in the Windows installer.
+ * Dependency fix for some Ubuntu versions.
+ * No changes for other platforms. Windows upgrade recommended, everyone else optional.
+
+# 2022-03-30 -- Version 1.8.7
+
+ * Fix for dependency installations in Windows MSI package.
+ * Fix for desktop UI setup when run by a non-super-user.
+ * Bug fix in local OIDC / SSO support for auth0 and other providers.
+ * Other minor fixes for e.g. old Linux distributions.
 
 # 2022-03-04 -- Version 1.8.6
 

+ 10 - 0
ci/Dockerfile.deb

@@ -0,0 +1,10 @@
+ARG PLATFORM
+FROM registry.sean.farm/${PLATFORM}-builder as stage
+WORKDIR /work/build
+COPY . .
+RUN make debian
+RUN ls -ls /work
+
+FROM scratch AS export
+ARG PLATFORM
+COPY --from=stage /work/*.deb ./${PLATFORM}/

+ 36 - 0
ci/Dockerfile.el6

@@ -0,0 +1,36 @@
+ARG DOCKER_ARCH
+FROM --platform=linux/${DOCKER_ARCH} alpine:edge AS builder
+
+RUN apk update
+RUN apk add curl
+RUN apk add bash
+RUN apk add file
+RUN apk add rust
+RUN apk add cargo
+RUN apk add make
+RUN apk add cmake
+RUN apk add clang
+RUN apk add openssl-dev
+RUN apk add linux-headers
+RUN apk add build-base
+RUN apk add openssl-libs-static
+
+COPY . .
+RUN ZT_STATIC=1 make one
+RUN ls -la
+
+ARG DOCKER_ARCH
+FROM --platform=linux/${DOCKER_ARCH} centos:6 AS stage
+WORKDIR /root/rpmbuild/BUILD
+COPY . .
+COPY --from=builder zerotier-one ./
+RUN curl https://gist.githubusercontent.com/someara/b363002ba6e57b3c474dd027d4daef85/raw/4ac5534139752fc92fbe1a53599a390214f69615/el6%2520vault --output /etc/yum.repos.d/CentOS-Base.repo
+RUN uname -a
+RUN yum -y install make gcc rpm-build
+RUN pwd
+RUN ls -la
+RUN make redhat
+
+FROM scratch AS export
+ARG PLATFORM
+COPY --from=stage /root/rpmbuild/RPMS/*/*.rpm ./${PLATFORM}/

+ 9 - 0
ci/Dockerfile.rpm

@@ -0,0 +1,9 @@
+ARG PLATFORM
+FROM registry.sean.farm/${PLATFORM}-builder as stage
+WORKDIR /root/rpmbuild/BUILD
+COPY . .
+RUN make redhat
+
+FROM scratch AS export
+ARG PLATFORM
+COPY --from=stage /root/rpmbuild/RPMS/*/*.rpm ./${PLATFORM}/

+ 0 - 7
ci/Dockerfile.sid

@@ -1,7 +0,0 @@
-FROM registry.sean.farm/sid-builder as stage
-COPY . .
-RUN /usr/bin/make -j 8
-
-FROM scratch AS export
-COPY --from=stage /zerotier-one .
-COPY --from=stage /zerotier-cli .

+ 101 - 13
ci/scripts/build.sh

@@ -2,27 +2,115 @@
 set -euo pipefail
 IFS=$'\n\t'
 
-export GOOS=$1
-export GOARCH=$2
+export PLATFORM=$1
+export ZT_ISA=$2
 export VERSION=$3
-export DOCKER_BUILDKIT=1
+export EVENT=$4
 
-echo "nproc: $(nproc)"
+case $PLATFORM in
+    el*|fc*|amzn*)
+        export PKGFMT=rpm
+        ;;
+    *)
+        export PKGFMT=deb
+esac
+
+# OSX
+# x86_64-apple-darwin
+# aarch64-apple-darwin
 
-case $GOARCH in
-    armv5)
-        export ARCH=arm/v5
+# Windows
+# x86_64-pc-windows-msvc
+# i686-pc-windows-msvc
+# aarch64-pc-windows-msvc
+
+# Linux
+# i686-unknown-linux-gnu
+# x86_64-unknown-linux-gnu
+# arm-unknown-linux-gnueabi       ?
+# arm-unknown-linux-gnueabihf     ?
+# armv7-unknown-linux-gnueabihf
+# 
+
+case $ZT_ISA in
+    386)
+        export DOCKER_ARCH=386
+        export RUST_TRIPLET=i686-unknown-linux-gnu
+        ;;
+    amd64)
+        export DOCKER_ARCH=amd64
+        export RUST_TRIPLET=x86_64-unknown-linux-gnu
         ;;
-    armv7)
-        export ARCH=arm/v7
+    armv6)
+        export DOCKER_ARCH=arm/v6
+        export RUST_TRIPLET=arm-unknown-linux-gnueabi
+        ;;
+    armv7)        
+        export DOCKER_ARCH=arm/v7
+        export RUST_TRIPLET=arm-unknown-linux-gnueabihf
         ;;
     arm64)
-        export ARCH=arm64/v8
+        export DOCKER_ARCH=arm64/v8
+        export RUST_TRIPLET=aarch64-unknown-linux-gnu
         ;;
-    *)
-        export ARCH=$GOARCH
+    riscv64)
+        export DOCKER_ARCH=riscv64
+        export RUST_TRIPLET=riscv64gc-unknown-linux-gnu
+        ;;
+    ppc64le)
+        export DOCKER_ARCH=ppc64le
+        export RUST_TRIPLET=powerpc64le-unknown-linux-gnu
+        ;;    
+    mips64le)
+        export DOCKER_ARCH=mips64le
+        export RUST_TRIPLET=mips64el-unknown-linux-gnuabi64
+        ;;
+    s390x)
+        export DOCKER_ARCH=s390x
+        export RUST_TRIPLET=s390x-unknown-linux-gnu
+        ;;
+    *)        
+        echo "ERROR: could not determine architecture settings. PLEASE FIX ME"
+        exit 1
         ;;
 esac
 
+if [ -f "ci/Dockerfile.${PLATFORM}" ]; then
+    export DOCKERFILE="ci/Dockerfile.${PLATFORM}"
+else
+    export DOCKERFILE="ci/Dockerfile.${PKGFMT}"
+fi
+
+echo "#~~~~~~~~~~~~~~~~~~~~"
+echo "$0 variables:"
+echo "nproc: $(nproc)"
+echo "ZT_ISA: ${ZT_ISA}"
+echo "DOCKER_ARCH: ${DOCKER_ARCH}"
+echo "RUST_TRIPLET: ${RUST_TRIPLET}"
+echo "VERSION: ${VERSION}"
+echo "EVENT: ${EVENT}"
+echo "PKGFMT: ${PKGFMT}"
+echo "PWD: ${PWD}"
+echo "DOCKERFILE: ${DOCKERFILE}"
+echo "#~~~~~~~~~~~~~~~~~~~~"
+
+if [ ${EVENT} == "push" ]; then
+make munge_rpm zerotier-one.spec VERSION=${VERSION}
+make munge_deb debian/changelog VERSION=${VERSION}
+fi
+
+export DOCKER_BUILDKIT=1
 docker run --privileged --rm tonistiigi/binfmt --install all
-docker buildx build --platform ${GOOS}/${ARCH} -f ci/Dockerfile.sid --target export -t test . --output out/${GOOS}/${GOARCH}
+
+# docker pull --platform linux/${DOCKER_ARCH} registry.sean.farm/${PLATFORM}-builder
+
+docker buildx build \
+       --build-arg PLATFORM="${PLATFORM}" \
+       --build-arg RUST_TRIPLET="${RUST_TRIPLET}" \
+       --build-arg DOCKER_ARCH="${DOCKER_ARCH}" \
+       --platform linux/${DOCKER_ARCH} \
+       -f ${DOCKERFILE} \
+       -t build \
+       . \
+       --output type=local,dest=. \
+       --target export

+ 37 - 0
ci/scripts/munge_debian_changelog.sh

@@ -0,0 +1,37 @@
+#!/bin/bash
+set -euo pipefail
+IFS=$'\n\t'
+
+export FILE=$1
+export VERSION=$2
+export NAME=$3
+export MESSAGE=$4
+export DATE=$(date "+%a, %d %b %Y %T %z")
+# export DATE=$(date "+%a %b %d %Y")
+
+set +e
+grep --version | grep BSD &> /dev/null
+if [ $? == 0 ]; then BSDGREP=true ; else BSDGREP=false ; fi
+set -e
+
+# echo "#~~~~~~~~~~~~~~~~~~~~"
+# echo "$0 variables:"
+# echo "VERSION: ${VERSION}"
+# echo "NAME: ${NAME}"
+# echo "MESSAGE: ${MESSAGE}"
+# echo "DATE: ${DATE}"
+# echo "BSDGREP: ${BSDGREP}"
+# echo "#~~~~~~~~~~~~~~~~~~~~"
+# echo
+
+if $BSDGREP ; then    
+    sed -i '' s/^Version:.*/"Version:        ${VERSION}"/ ${FILE}
+else
+    sed -i s/^Version:.*/"Version:        ${VERSION}"/ ${FILE}
+fi
+
+awk -v version=${VERSION} -v date=${DATE} -v name=${NAME} -v message=${MESSAGE} \
+    'BEGIN{print "zerotier-one (" version ") unstable; urgency=medium\n\n  * " message "\n\n -- " name "  " date "\n" }{ print }' \
+    ${FILE} > ${FILE}.new
+
+mv ${FILE}.new ${FILE}

+ 36 - 0
ci/scripts/munge_rpm_spec.sh

@@ -0,0 +1,36 @@
+#!/bin/bash
+set -euo pipefail
+IFS=$'\n\t'
+
+export FILE=$1
+export VERSION=$2
+export NAME=$3
+export MESSAGE=$4
+export DATE=$(date "+%a %b %d %Y")
+
+set +e
+grep --version | grep BSD &> /dev/null
+if [ $? == 0 ]; then BSDGREP=true ; else BSDGREP=false ; fi
+set -e
+
+# echo "#~~~~~~~~~~~~~~~~~~~~"
+# echo "$0 variables:"
+# echo "VERSION: ${VERSION}"
+# echo "NAME: ${NAME}"
+# echo "MESSAGE: ${MESSAGE}"
+# echo "DATE: ${DATE}"
+# echo "BSDGREP: ${BSDGREP}"
+# echo "#~~~~~~~~~~~~~~~~~~~~"
+# echo
+
+if $BSDGREP ; then    
+    sed -i '' s/^Version:.*/"Version:        ${VERSION}"/ ${FILE}
+else
+    sed -i s/^Version:.*/"Version:        ${VERSION}"/ ${FILE}
+fi
+
+awk -v version=${VERSION} -v date=${DATE} -v name=${NAME} -v message=${MESSAGE} \
+    'FNR==NR{ if (/%changelog/) p=NR; next} 1; FNR==p{ print "* " date  " " name " - " version "\n- " message "\n" }' \
+    ${FILE} ${FILE} > ${FILE}.new
+
+mv ${FILE}.new ${FILE}

+ 0 - 8
controller/DB.cpp

@@ -196,14 +196,6 @@ void DB::networks(std::set<uint64_t> &networks)
 		networks.insert(n->first);
 }
 
-void DB::networkMemberSSOHasExpired(uint64_t nwid, int64_t now) {
-	std::lock_guard<std::mutex> l(_networks_l);
-	auto nw = _networks.find(nwid);
-	if (nw != _networks.end()) {
-		nw->second->mostRecentDeauthTime = now;
-	}
-}
-
 void DB::_memberChanged(nlohmann::json &old,nlohmann::json &memberConfig,bool notifyListeners)
 {
 	uint64_t memberId = 0;

+ 1 - 2
controller/DB.hpp

@@ -33,7 +33,7 @@
 #include <set>
 #include <map>
 
-#include "../ext/json/json.hpp"
+#include <nlohmann/json.hpp>
 
 #define ZT_MEMBER_AUTH_TIMEOUT_NOTIFY_BEFORE 25000
 
@@ -135,7 +135,6 @@ public:
 	virtual void nodeIsOnline(const uint64_t networkId,const uint64_t memberId,const InetAddress &physicalAddress) = 0;
 
 	virtual AuthInfo getSSOAuthInfo(const nlohmann::json &member, const std::string &redirectURL) { return AuthInfo(); }
-	virtual void networkMemberSSOHasExpired(uint64_t nwid, int64_t ts);
 
 	inline void addListener(DB::ChangeListener *const listener)
 	{

+ 0 - 51
controller/DBMirrorSet.cpp

@@ -137,14 +137,6 @@ AuthInfo DBMirrorSet::getSSOAuthInfo(const nlohmann::json &member, const std::st
 	return AuthInfo();
 }
 
-void DBMirrorSet::networkMemberSSOHasExpired(uint64_t nwid, int64_t ts)
-{
-	std::lock_guard<std::mutex> l(_dbs_l);
-	for(auto d=_dbs.begin();d!=_dbs.end();++d) { 
-		(*d)->networkMemberSSOHasExpired(nwid, ts);
-	}
-}
-
 void DBMirrorSet::networks(std::set<uint64_t> &networks)
 {
 	std::lock_guard<std::mutex> l(_dbs_l);
@@ -248,47 +240,4 @@ void DBMirrorSet::onNetworkMemberDeauthorize(const void *db,uint64_t networkId,u
 	_listener->onNetworkMemberDeauthorize(this,networkId,memberId);
 }
 
-void DBMirrorSet::membersExpiring(std::set< std::pair<uint64_t, uint64_t> > &soon, std::set< std::pair<uint64_t, uint64_t> > &expired)
-{
-	std::unique_lock<std::mutex> l(_membersExpiringSoon_l);
-	int64_t now = OSUtils::now();
-	for(auto next=_membersExpiringSoon.begin();next!=_membersExpiringSoon.end();) {
-		if (next->first > now) {
-			const uint64_t nwid = next->second.first;
-			const uint64_t memberId = next->second.second;
-			nlohmann::json network, member;
-			if (this->get(nwid, network, memberId, member)) {
-				try {
-					const bool authorized = member["authorized"];
-					const bool ssoExempt = member["ssoExempt"];
-					const int64_t authenticationExpiryTime = member["authenticationExpiryTime"];
-					if ((authenticationExpiryTime == next->first)&&(authorized)&&(!ssoExempt)) {
-						if ((authenticationExpiryTime - now) > ZT_MEMBER_AUTH_TIMEOUT_NOTIFY_BEFORE) {
-							// Stop when we get to entries too far in the future.
-							break;
-						} else {
-							const bool ssoEnabled = network["ssoEnabled"];
-							if (ssoEnabled)
-								soon.insert(std::pair<uint64_t, uint64_t>(nwid, memberId));
-						}
-					} else {
-						// Obsolete entry, no longer authorized, or SSO exempt.
-					}
-				} catch ( ... ) {
-					// Invalid member object, erase.
-				}
-			} else {
-				// Not found.
-			}
-		}
-		_membersExpiringSoon.erase(next++);
-	}
-}
-
-void DBMirrorSet::memberWillExpire(int64_t expTime, uint64_t nwid, uint64_t memberId)
-{
-	std::unique_lock<std::mutex> l(_membersExpiringSoon_l);
-	_membersExpiringSoon.insert(std::pair< int64_t, std::pair< uint64_t, uint64_t > >(expTime, std::pair< uint64_t, uint64_t >(nwid, memberId)));
-}
-
 } // namespace ZeroTier

+ 0 - 6
controller/DBMirrorSet.hpp

@@ -52,7 +52,6 @@ public:
 	virtual void onNetworkMemberDeauthorize(const void *db,uint64_t networkId,uint64_t memberId);
 
 	AuthInfo getSSOAuthInfo(const nlohmann::json &member, const std::string &redirectURL);
-	void networkMemberSSOHasExpired(uint64_t nwid, int64_t ts);
 
 	inline void addDB(const std::shared_ptr<DB> &db)
 	{
@@ -61,17 +60,12 @@ public:
 		_dbs.push_back(db);
 	}
 
-	void membersExpiring(std::set< std::pair<uint64_t, uint64_t> > &soon, std::set< std::pair<uint64_t, uint64_t> > &expired);
-	void memberWillExpire(int64_t expTime, uint64_t nwid, uint64_t memberId);
-
 private:
 	DB::ChangeListener *const _listener;
 	std::atomic_bool _running;
 	std::thread _syncCheckerThread;
 	std::vector< std::shared_ptr< DB > > _dbs;
 	mutable std::mutex _dbs_l;
-	std::set< std::pair< int64_t, std::pair<uint64_t, uint64_t> > > _membersExpiringSoon;
-	mutable std::mutex _membersExpiringSoon_l;
 };
 
 } // namespace ZeroTier

+ 51 - 77
controller/EmbeddedNetworkController.cpp

@@ -1262,6 +1262,7 @@ void EmbeddedNetworkController::_request(
 	}
 	const bool newMember = ((!member.is_object())||(member.empty()));
 	DB::initMember(member);
+	_MemberStatusKey msk(nwid,identity.address().toInt());
 
 	{
 		const std::string haveIdStr(OSUtils::jsonString(member["identity"],""));
@@ -1335,43 +1336,21 @@ void EmbeddedNetworkController::_request(
 	// Should we check SSO Stuff?
 	// If network is configured with SSO, and the member is not marked exempt: yes
 	// Otherwise no, we use standard auth logic.
+	AuthInfo info;
+	int64_t authenticationExpiryTime = -1;
 	bool networkSSOEnabled = OSUtils::jsonBool(network["ssoEnabled"], false);
 	bool memberSSOExempt = OSUtils::jsonBool(member["ssoExempt"], false);
-	AuthInfo info;
 	if (networkSSOEnabled && !memberSSOExempt) {
-		// TODO:  Get expiry time if auth is still valid
-
-		// else get new auth info & stuff
+		authenticationExpiryTime = (int64_t)OSUtils::jsonInt(member["authenticationExpiryTime"], 0);
 		info = _db.getSSOAuthInfo(member, _ssoRedirectURL);
 		assert(info.enabled == networkSSOEnabled);
-
-		std::string memberId = member["id"];
-		//fprintf(stderr, "ssoEnabled && !ssoExempt %s-%s\n", nwids, memberId.c_str());
-		uint64_t authenticationExpiryTime = (int64_t)OSUtils::jsonInt(member["authenticationExpiryTime"], 0);
-		fprintf(stderr, "authExpiryTime: %lld\n", authenticationExpiryTime);
-		if (authenticationExpiryTime < now) {
-			fprintf(stderr, "Handling expired member\n");
+		if (authenticationExpiryTime <= now) {
 			if (info.version == 0) {
-				if (!info.authenticationURL.empty()) {
-					_db.networkMemberSSOHasExpired(nwid, now);
-					onNetworkMemberDeauthorize(&_db, nwid, identity.address().toInt());
-
-					Dictionary<4096> authInfo;
-					authInfo.add(ZT_AUTHINFO_DICT_KEY_VERSION, (uint64_t)0ULL);
-					authInfo.add(ZT_AUTHINFO_DICT_KEY_AUTHENTICATION_URL, info.authenticationURL.c_str());
-					//fprintf(stderr, "sending auth URL: %s\n", authenticationURL.c_str());
-
-					DB::cleanMember(member);
-					_db.save(member,true);
-
-					_sender->ncSendError(nwid,requestPacketId,identity.address(),NetworkController::NC_ERROR_AUTHENTICATION_REQUIRED, authInfo.data(), authInfo.sizeBytes());
-					return;
-				}
-			}
-			else if (info.version == 1) {
-				_db.networkMemberSSOHasExpired(nwid, now);
-				onNetworkMemberDeauthorize(&_db, nwid, identity.address().toInt());
-
+				Dictionary<4096> authInfo;
+				authInfo.add(ZT_AUTHINFO_DICT_KEY_VERSION, (uint64_t)0ULL);
+				authInfo.add(ZT_AUTHINFO_DICT_KEY_AUTHENTICATION_URL, info.authenticationURL.c_str());
+				_sender->ncSendError(nwid,requestPacketId,identity.address(),NetworkController::NC_ERROR_AUTHENTICATION_REQUIRED, authInfo.data(), authInfo.sizeBytes());
+			} else if (info.version == 1) {
 				Dictionary<8192> authInfo;
 				authInfo.add(ZT_AUTHINFO_DICT_KEY_VERSION, info.version);
 				authInfo.add(ZT_AUTHINFO_DICT_KEY_ISSUER_URL, info.issuerURL.c_str());
@@ -1379,20 +1358,11 @@ void EmbeddedNetworkController::_request(
 				authInfo.add(ZT_AUTHINFO_DICT_KEY_NONCE, info.ssoNonce.c_str());
 				authInfo.add(ZT_AUTHINFO_DICT_KEY_STATE, info.ssoState.c_str());
 				authInfo.add(ZT_AUTHINFO_DICT_KEY_CLIENT_ID, info.ssoClientID.c_str());
-
-				DB::cleanMember(member);
-				_db.save(member, true);
-
-				fprintf(stderr, "Sending NC_ERROR_AUTHENTICATION_REQUIRED\n");
 				_sender->ncSendError(nwid,requestPacketId,identity.address(),NetworkController::NC_ERROR_AUTHENTICATION_REQUIRED, authInfo.data(), authInfo.sizeBytes());
-				return;
 			}
-			else {
-				fprintf(stderr, "invalid sso info.version %llu\n", info.version);
-			}
-		} else if (authorized) {
-			fprintf(stderr, "Setting member will expire to: %lld\n", authenticationExpiryTime);
-			_db.memberWillExpire(authenticationExpiryTime, nwid, identity.address().toInt());
+			DB::cleanMember(member);
+			_db.save(member,true);
+			return;
 		}
 	}
 
@@ -1411,8 +1381,8 @@ void EmbeddedNetworkController::_request(
 
 			{
 				std::lock_guard<std::mutex> l(_memberStatus_l);
-				_MemberStatus &ms = _memberStatus[_MemberStatusKey(nwid,identity.address().toInt())];
-
+				_MemberStatus &ms = _memberStatus[msk];
+				ms.authenticationExpiryTime = authenticationExpiryTime;
 				ms.vMajor = (int)vMajor;
 				ms.vMinor = (int)vMinor;
 				ms.vRev = (int)vRev;
@@ -1420,9 +1390,13 @@ void EmbeddedNetworkController::_request(
 				ms.lastRequestMetaData = metaData;
 				ms.identity = identity;
 			}
-		}		
+
+			if (authenticationExpiryTime > 0) {
+				std::lock_guard<std::mutex> l(_expiringSoon_l);
+				_expiringSoon.insert(std::pair<int64_t, _MemberStatusKey>(authenticationExpiryTime, msk));
+			}
+		}
 	} else {
-		
 		// If they are not authorized, STOP!
 		DB::cleanMember(member);
 		_db.save(member,true);
@@ -1434,18 +1408,13 @@ void EmbeddedNetworkController::_request(
 	// If we made it this far, they are authorized (and authenticated).
 	// -------------------------------------------------------------------------
 
-	int64_t credentialtmd = ZT_NETWORKCONFIG_DEFAULT_CREDENTIAL_TIME_MAX_MAX_DELTA;
-	if (now > ns.mostRecentDeauthTime) {
-		// If we recently de-authorized a member, shrink credential TTL/max delta to
-		// be below the threshold required to exclude it. Cap this to a min/max to
-		// prevent jitter or absurdly large values.
-		const uint64_t deauthWindow = now - ns.mostRecentDeauthTime;
-		if (deauthWindow < ZT_NETWORKCONFIG_DEFAULT_CREDENTIAL_TIME_MIN_MAX_DELTA) {
-			credentialtmd = ZT_NETWORKCONFIG_DEFAULT_CREDENTIAL_TIME_MIN_MAX_DELTA;
-		} else if (deauthWindow < (ZT_NETWORKCONFIG_DEFAULT_CREDENTIAL_TIME_MAX_MAX_DELTA + 5000ULL)) {
-			credentialtmd = deauthWindow - 5000ULL;
-		}
+	// Default timeout: 15 minutes. Maximum: two hours. Can be specified by an optional field in the network config
+	// if something longer than 15 minutes is desired. Minimum is 5 minutes since shorter than that would be flaky.
+	int64_t credentialtmd = ZT_NETWORKCONFIG_DEFAULT_CREDENTIAL_TIME_DFL_MAX_DELTA;
+	if (network.contains("certificateTimeoutWindowSize")) {
+		credentialtmd = (int64_t)network["certificateTimeoutWindowSize"];
 	}
+	credentialtmd = std::max(std::min(credentialtmd, ZT_NETWORKCONFIG_DEFAULT_CREDENTIAL_TIME_MAX_MAX_DELTA), ZT_NETWORKCONFIG_DEFAULT_CREDENTIAL_TIME_MIN_MAX_DELTA);
 
 	std::unique_ptr<NetworkConfig> nc(new NetworkConfig());
 
@@ -1460,7 +1429,7 @@ void EmbeddedNetworkController::_request(
 	nc->mtu = std::max(std::min((unsigned int)OSUtils::jsonInt(network["mtu"],ZT_DEFAULT_MTU),(unsigned int)ZT_MAX_MTU),(unsigned int)ZT_MIN_MTU);
 	nc->multicastLimit = (unsigned int)OSUtils::jsonInt(network["multicastLimit"],32ULL);
 
-	nc->ssoEnabled = OSUtils::jsonBool(network["ssoEnabled"], false);
+	nc->ssoEnabled = networkSSOEnabled; //OSUtils::jsonBool(network["ssoEnabled"], false);
 	nc->ssoVersion = info.version;
 
 	if (info.version == 0) {
@@ -1858,6 +1827,8 @@ void EmbeddedNetworkController::_startThreads()
 	const long hwc = std::max((long)std::thread::hardware_concurrency(),(long)1);
 	for(long t=0;t<hwc;++t) {
 		_threads.emplace_back([this]() {
+			std::vector<_MemberStatusKey> expired;
+			nlohmann::json network, member;
 			for(;;) {
 				_RQEntry *qe = (_RQEntry *)0;
 				auto timedWaitResult = _queue.get(qe, 1000);
@@ -1876,28 +1847,31 @@ void EmbeddedNetworkController::_startThreads()
 					}
 				}
 
-				std::set< std::pair<uint64_t, uint64_t> > soon;
-				std::set< std::pair<uint64_t, uint64_t> > expired;
-				_db.membersExpiring(soon, expired);
-
-				for(auto s=soon.begin();s!=soon.end();++s) {
-					Identity identity;
-					Dictionary<ZT_NETWORKCONFIG_METADATA_DICT_CAPACITY> lastMetaData;
-					{
-						std::unique_lock<std::mutex> ll(_memberStatus_l);
-						auto ms = _memberStatus.find(_MemberStatusKey(s->first, s->second));
-						if (ms != _memberStatus.end()) {
-							lastMetaData = ms->second.lastRequestMetaData;
-							identity = ms->second.identity;
+				expired.clear();
+				int64_t now = OSUtils::now();
+				{
+					std::lock_guard<std::mutex> l(_expiringSoon_l);
+					for(auto s=_expiringSoon.begin();s!=_expiringSoon.end();) {
+						const int64_t when = s->first;
+						if (when <= now) {
+							// The user may have re-authorized, so we must actually look it up and check.
+							network.clear();
+							member.clear();
+							if (_db.get(s->second.networkId, network, s->second.nodeId, member)) {
+								int64_t authenticationExpiryTime = (int64_t)OSUtils::jsonInt(member["authenticationExpiryTime"], 0);
+								if (authenticationExpiryTime <= now) {
+									expired.push_back(s->second);
+								}
+							}
+							_expiringSoon.erase(s++);
+						} else {
+							// Don't bother going further into the future than necessary.
+							break;
 						}
 					}
-					if (identity) {
-						request(s->first,InetAddress(),0,identity,lastMetaData);
-					}
 				}
-
 				for(auto e=expired.begin();e!=expired.end();++e) {
-					onNetworkMemberDeauthorize(nullptr, e->first, e->second);
+					onNetworkMemberDeauthorize(nullptr, e->networkId, e->nodeId);
 				}
 			}
 		});

+ 9 - 3
controller/EmbeddedNetworkController.hpp

@@ -35,7 +35,7 @@
 #include "../osdep/Thread.hpp"
 #include "../osdep/BlockingQueue.hpp"
 
-#include "../ext/json/json.hpp"
+#include <nlohmann/json.hpp>
 
 #include "DB.hpp"
 #include "DBMirrorSet.hpp"
@@ -109,6 +109,7 @@ private:
 			RQENTRY_TYPE_REQUEST = 0
 		} type;
 	};
+
 	struct _MemberStatusKey
 	{
 		_MemberStatusKey() : networkId(0),nodeId(0) {}
@@ -116,11 +117,13 @@ private:
 		uint64_t networkId;
 		uint64_t nodeId;
 		inline bool operator==(const _MemberStatusKey &k) const { return ((k.networkId == networkId)&&(k.nodeId == nodeId)); }
+		inline bool operator<(const _MemberStatusKey &k) const { return (k.networkId < networkId) || ((k.networkId == networkId)&&(k.nodeId < nodeId)); }
 	};
 	struct _MemberStatus
 	{
-		_MemberStatus() : lastRequestTime(0),vMajor(-1),vMinor(-1),vRev(-1),vProto(-1) {}
-		uint64_t lastRequestTime;
+		_MemberStatus() : lastRequestTime(0),authenticationExpiryTime(-1),vMajor(-1),vMinor(-1),vRev(-1),vProto(-1) {}
+		int64_t lastRequestTime;
+		int64_t authenticationExpiryTime;
 		int vMajor,vMinor,vRev,vProto;
 		Dictionary<ZT_NETWORKCONFIG_METADATA_DICT_CAPACITY> lastRequestMetaData;
 		Identity identity;
@@ -152,6 +155,9 @@ private:
 	std::unordered_map< _MemberStatusKey,_MemberStatus,_MemberStatusHash > _memberStatus;
 	std::mutex _memberStatus_l;
 
+	std::set< std::pair<int64_t, _MemberStatusKey> > _expiringSoon;
+	std::mutex _expiringSoon_l;
+
 	RedisConfig *_rc;
 	std::string _ssoRedirectURL;
 };

+ 16 - 12
controller/PostgreSQL.cpp

@@ -28,7 +28,7 @@
 #include <chrono>
 
 
-// #define ZT_TRACE 1
+// #define REDIS_TRACE 1
 
 using json = nlohmann::json;
 
@@ -783,6 +783,7 @@ void PostgreSQL::initializeMembers()
 			std::string assignedAddresses = std::get<20>(row);
 
 			config["id"] = memberId;
+			config["address"] = memberId;
 			config["nwid"] = networkId;
 			config["activeBridge"] = activeBridge.value_or(false);
 			config["authorized"] = authorized.value_or(false);
@@ -942,30 +943,31 @@ void PostgreSQL::_membersWatcher_Postgres() {
 void PostgreSQL::_membersWatcher_Redis() {
 	char buf[11] = {0};
 	std::string key = "member-stream:{" + std::string(_myAddress.toString(buf)) + "}";
+	std::string lastID = "0";
 	fprintf(stderr, "Listening to member stream: %s\n", key.c_str());
 	while (_run == 1) {
 		try {
 			json tmp;
 			std::unordered_map<std::string, ItemStream> result;
 			if (_rc->clusterMode) {
-				_cluster->xread(key, "$", std::chrono::seconds(1), 0, std::inserter(result, result.end()));
+				_cluster->xread(key, lastID, std::chrono::seconds(1), 0, std::inserter(result, result.end()));
 			} else {
-				_redis->xread(key, "$", std::chrono::seconds(1), 0, std::inserter(result, result.end()));
+				_redis->xread(key, lastID, std::chrono::seconds(1), 0, std::inserter(result, result.end()));
 			}
 			if (!result.empty()) {
 				for (auto element : result) {
-	#ifdef ZT_TRACE
+	#ifdef REDIS_TRACE
 					fprintf(stdout, "Received notification from: %s\n", element.first.c_str());
 	#endif
 					for (auto rec : element.second) {
 						std::string id = rec.first;
 						auto attrs = rec.second;
-	#ifdef ZT_TRACE
+	#ifdef REDIS_TRACE
 						fprintf(stdout, "Record ID: %s\n", id.c_str());
 						fprintf(stdout, "attrs len: %lu\n", attrs.size());
 	#endif
 						for (auto a : attrs) {
-	#ifdef ZT_TRACE
+	#ifdef REDIS_TRACE
 							fprintf(stdout, "key: %s\nvalue: %s\n", a.first.c_str(), a.second.c_str());
 	#endif
 							try {
@@ -987,6 +989,7 @@ void PostgreSQL::_membersWatcher_Redis() {
 						} else {
 							_redis->xdel(key, id);
 						}
+						lastID = id;
 					}
 				}
 			}
@@ -1029,31 +1032,31 @@ void PostgreSQL::_networksWatcher_Postgres() {
 void PostgreSQL::_networksWatcher_Redis() {
 	char buf[11] = {0};
 	std::string key = "network-stream:{" + std::string(_myAddress.toString(buf)) + "}";
-	
+	std::string lastID = "0";
 	while (_run == 1) {
 		try {
 			json tmp;
 			std::unordered_map<std::string, ItemStream> result;
 			if (_rc->clusterMode) {
-				_cluster->xread(key, "$", std::chrono::seconds(1), 0, std::inserter(result, result.end()));
+				_cluster->xread(key, lastID, std::chrono::seconds(1), 0, std::inserter(result, result.end()));
 			} else {
-				_redis->xread(key, "$", std::chrono::seconds(1), 0, std::inserter(result, result.end()));
+				_redis->xread(key, lastID, std::chrono::seconds(1), 0, std::inserter(result, result.end()));
 			}
 			
 			if (!result.empty()) {
 				for (auto element : result) {
-#ifdef ZT_TRACE
+#ifdef REDIS_TRACE
 					fprintf(stdout, "Received notification from: %s\n", element.first.c_str());
 #endif
 					for (auto rec : element.second) {
 						std::string id = rec.first;
 						auto attrs = rec.second;
-#ifdef ZT_TRACE
+#ifdef REDIS_TRACE
 						fprintf(stdout, "Record ID: %s\n", id.c_str());
 						fprintf(stdout, "attrs len: %lu\n", attrs.size());
 #endif
 						for (auto a : attrs) {
-#ifdef ZT_TRACE
+#ifdef REDIS_TRACE
 							fprintf(stdout, "key: %s\nvalue: %s\n", a.first.c_str(), a.second.c_str());
 #endif
 							try {
@@ -1075,6 +1078,7 @@ void PostgreSQL::_networksWatcher_Redis() {
 						} else {
 							_redis->xdel(key, id);
 						}
+						lastID = id;
 					}
 				}
 			}

+ 24 - 0
debian/changelog

@@ -1,3 +1,27 @@
+zerotier-one (1.10.0) unstable; urgency=medium
+
+  * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko <[email protected]>  Fri, 03 Jun 2022 01:00:00 -0700
+
+zerotier-one (1.8.10) unstable; urgency=medium
+
+  * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko <[email protected]>  Tue, 10 May 2022 01:00:00 -0700
+
+zerotier-one (1.8.9) unstable; urgency=medium
+
+  * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko <[email protected]>  Mon, 25 Apr 2022 01:00:00 -0700
+
+zerotier-one (1.8.8) unstable; urgency=medium
+
+  * See RELEASE-NOTES.md for release notes.
+
+ -- Adam Ierymenko <[email protected]>  Mon, 11 Apr 2022 01:00:00 -0700
+
 zerotier-one (1.8.7) unstable; urgency=medium
 
   * See RELEASE-NOTES.md for release notes.

+ 1 - 1
debian/control

@@ -10,7 +10,7 @@ Homepage: https://www.zerotier.com/
 
 Package: zerotier-one
 Architecture: any
-Depends:  ${shlibs:Depends}, ${misc:Depends}, iproute2, adduser, libstdc++6, openssl
+Depends:  iproute2, adduser, libstdc++6 (>= 5), openssl
 Homepage: https://www.zerotier.com/
 Description: ZeroTier network virtualization service
  ZeroTier One lets you join ZeroTier virtual networks and

+ 1 - 1
dockerbuild/Dockerfile.alpine

@@ -1,4 +1,4 @@
-FROM alpine:3.11.3
+FROM alpine:3.15
 
 ARG go_pkg_url
 

+ 40 - 10
entrypoint.sh.release

@@ -1,7 +1,7 @@
 #!/bin/sh
 
 grepzt() {
-  [ -f /var/lib/zerotier-one/zerotier-one.pid -a -n "$(cat /var/lib/zerotier-one/zerotier-one.pid)" -a -d "/proc/$(cat /var/lib/zerotier-one/zerotier-one.pid)" ]
+  [ -f /var/lib/zerotier-one/zerotier-one.pid -a -n "$(cat /var/lib/zerotier-one/zerotier-one.pid 2>/dev/null)" -a -d "/proc/$(cat /var/lib/zerotier-one/zerotier-one.pid 2>/dev/null)" ]
   return $?
 }
 
@@ -33,29 +33,57 @@ fi
 mkztfile zerotier-one.port 0600 "9993"
 
 killzerotier() {
-  echo "Killing zerotier"
-  kill $(cat /var/lib/zerotier-one/zerotier-one.pid)  
+  log "Killing zerotier"
+  kill $(cat /var/lib/zerotier-one/zerotier-one.pid 2>/dev/null)
   exit 0
 }
 
+log_header() {
+  echo -n "\r=>"
+}
+
+log_detail_header() {
+  echo -n "\r===>"
+}
+
+log() {
+  echo "$(log_header)" "$@"
+}
+
+log_params() {
+  title=$1
+  shift
+  log "$title" "[$@]"
+}
+
+log_detail() {
+  echo "$(log_detail_header)" "$@"
+}
+
+log_detail_params() {
+  title=$1
+  shift
+  log_detail "$title" "[$@]"
+}
+
 trap killzerotier INT TERM
 
-echo "Configuring networks to join"
+log "Configuring networks to join"
 mkdir -p /var/lib/zerotier-one/networks.d
 
-echo "joining networks: $@"
+log_params "Joining networks:" $@
 for i in "$@"
 do
-  echo "Configuring join for $i"
+  log_detail_params "Configuring join:" "$i"
   touch "/var/lib/zerotier-one/networks.d/${i}.conf"
 done
 
-echo "starting zerotier"
+log "Starting ZeroTier"
 nohup /usr/sbin/zerotier-one &
 
 while ! grepzt
 do
-  echo "zerotier hasn't started, waiting a second"
+  log_detail "ZeroTier hasn't started, waiting a second"
 
   if [ -f nohup.out ]
   then
@@ -65,7 +93,7 @@ do
   sleep 1
 done
 
-echo "Writing healthcheck for networks: $@"
+log_params "Writing healthcheck for networks:" $@
 
 cat >/healthcheck.sh <<EOF
 #!/bin/bash
@@ -77,7 +105,9 @@ EOF
 
 chmod +x /healthcheck.sh
 
-echo "Sleeping infinitely"
+log_params "zerotier-cli info:" "$(zerotier-cli info)"
+
+log "Sleeping infinitely"
 while true
 do
   sleep 1

+ 21 - 0
ext/inja/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018-2021 Berscheid
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 391 - 0
ext/inja/README.md

@@ -0,0 +1,391 @@
+[<div align="center"><img width="500" src="https://raw.githubusercontent.com/pantor/inja/master/doc/logo.svg?sanitize=true"></div>](https://github.com/pantor/inja/releases)
+
+<p align="center">
+  <a href="https://github.com/pantor/inja/actions">
+    <img src="https://github.com/pantor/inja/workflows/CI/badge.svg" alt="CI Status">
+  </a>
+
+  <a href="https://github.com/pantor/inja/actions">
+    <img src="https://github.com/pantor/inja/workflows/Documentation/badge.svg" alt="Documentation Status">
+  </a>
+
+  <a href="https://www.codacy.com/manual/pantor/inja?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=pantor/inja&amp;utm_campaign=Badge_Grade">
+    <img src="https://app.codacy.com/project/badge/Grade/211718f7a36541819d1244c0e2ee6f08"/>
+  </a>
+
+  <a href="https://github.com/pantor/inja/releases">
+    <img src="https://img.shields.io/github/release/pantor/inja.svg" alt="Github Releases">
+  </a>
+
+  <a href="http://github.com/pantor/inja/issues">
+    <img src="https://img.shields.io/github/issues/pantor/inja.svg" alt="Github Issues">
+  </a>
+
+  <a href="https://raw.githubusercontent.com/pantor/inja/master/LICENSE">
+    <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="GitHub License">
+  </a>
+</p>
+
+Inja is a template engine for modern C++, loosely inspired by [jinja](http://jinja.pocoo.org) for python. It has an easy and yet powerful template syntax with all variables, loops, conditions, includes, callbacks, and comments you need, nested and combined as you like. Inja uses the wonderful [json](https://github.com/nlohmann/json) library by nlohmann for data input. Most importantly, inja needs only two header files, which is (nearly) as trivial as integration in C++ can get. Of course, everything is tested on all relevant compilers. Here is what it looks like:
+
+```.cpp
+json data;
+data["name"] = "world";
+
+inja::render("Hello {{ name }}!", data); // Returns "Hello world!"
+```
+
+## Integration
+
+Inja is a headers only library, which can be downloaded from the [releases](https://github.com/pantor/inja/releases) or directly from the `include/` or `single_include/` folder. Inja uses `nlohmann/json.hpp` (>= v3.8.0) as its single dependency, so make sure it can be included from `inja.hpp`. json can be downloaded [here](https://github.com/nlohmann/json/releases). Then integration is as easy as:
+
+```.cpp
+#include <inja.hpp>
+
+// Just for convenience
+using namespace inja;
+```
+
+If you are using the [Meson Build System](http://mesonbuild.com), then you can wrap this repository as a subproject.
+
+If you are using [Conan](https://conan.io) to manage your dependencies, have a look at [this repository](https://github.com/DEGoodmanWilson/conan-inja). Please file issues [here](https://github.com/DEGoodmanWilson/conan-inja/issues) if you experience problems with the packages.
+
+You can also integrate inja in your project using [Hunter](https://github.com/cpp-pm/hunter), a package manager for C++.
+
+If you are using [vcpkg](https://github.com/Microsoft/vcpkg) on your project for external dependencies, then you can use the [inja package](https://github.com/Microsoft/vcpkg/tree/master/ports/inja). Please see the vcpkg project for any issues regarding the packaging.
+
+If you are using [cget](https://cget.readthedocs.io/en/latest/), you can install the latest development version with `cget install pantor/inja`. A specific version can be installed with `cget install pantor/[email protected]`.
+
+On macOS, you can install inja via [Homebrew](https://formulae.brew.sh/formula/inja#default) and `brew install inja`.
+
+If you are using [conda](https://docs.conda.io/en/latest/), you can install the latest version from [conda-forge](https://anaconda.org/conda-forge/inja) with `conda install -c conda-forge inja`.
+
+## Tutorial
+
+This tutorial will give you an idea how to use inja. It will explain the most important concepts and give practical advices using examples and executable code. Beside this tutorial, you may check out the [documentation](https://pantor.github.io/inja).
+
+### Template Rendering
+
+The basic template rendering takes a template as a `std::string` and a `json` object for all data. It returns the rendered template as an `std::string`.
+
+```.cpp
+json data;
+data["name"] = "world";
+
+render("Hello {{ name }}!", data); // Returns std::string "Hello world!"
+render_to(std::cout, "Hello {{ name }}!", data); // Writes "Hello world!" to stream
+```
+
+For more advanced usage, an environment is recommended.
+```.cpp
+Environment env;
+
+// Render a string with json data
+std::string result = env.render("Hello {{ name }}!", data); // "Hello world!"
+
+// Or directly read a template file
+Template temp = env.parse_template("./templates/greeting.txt");
+std::string result = env.render(temp, data); // "Hello world!"
+
+data["name"] = "Inja";
+std::string result = env.render(temp, data); // "Hello Inja!"
+
+// Or read the template file (and/or the json file) directly from the environment
+result = env.render_file("./templates/greeting.txt", data);
+result = env.render_file_with_json_file("./templates/greeting.txt", "./data.json");
+
+// Or write a rendered template file
+env.write(temp, data, "./result.txt");
+env.write_with_json_file("./templates/greeting.txt", "./data.json", "./result.txt");
+```
+
+The environment class can be configured to your needs.
+```.cpp
+// With default settings
+Environment env_default;
+
+// With global path to template files and where files will be saved
+Environment env_1 {"../path/templates/"};
+
+// With separate input and output path
+Environment env_2 {"../path/templates/", "../path/results/"};
+
+// With other opening and closing strings (here the defaults)
+env.set_expression("{{", "}}"); // Expressions
+env.set_comment("{#", "#}"); // Comments
+env.set_statement("{%", "%}"); // Statements {% %} for many things, see below
+env.set_line_statement("##"); // Line statements ## (just an opener)
+```
+
+### Variables
+
+Variables are rendered within the `{{ ... }}` expressions.
+```.cpp
+json data;
+data["neighbour"] = "Peter";
+data["guests"] = {"Jeff", "Tom", "Patrick"};
+data["time"]["start"] = 16;
+data["time"]["end"] = 22;
+
+// Indexing in array
+render("{{ guests.1 }}", data); // "Tom"
+
+// Objects
+render("{{ time.start }} to {{ time.end + 1 }}pm", data); // "16 to 23pm"
+```
+If no variable is found, valid JSON is printed directly, otherwise an `inja::RenderError` is thrown.
+
+### Statements
+
+Statements can be written either with the `{% ... %}` syntax or the `##` syntax for entire lines. Note that `##` needs to start the line without indentation. The most important statements are loops, conditions and file includes. All statements can be nested.
+
+#### Loops
+
+```.cpp
+// Combining loops and line statements
+render(R"(Guest List:
+## for guest in guests
+	{{ loop.index1 }}: {{ guest }}
+## endfor )", data)
+
+/* Guest List:
+	1: Jeff
+	2: Tom
+	3: Patrick */
+```
+In a loop, the special variables `loop.index (number)`, `loop.index1 (number)`, `loop.is_first (boolean)` and `loop.is_last (boolean)` are defined. In nested loops, the parent loop variables are available e.g. via `loop.parent.index`. You can also iterate over objects like `{% for key, value in time %}`.
+
+#### Conditions
+
+Conditions support the typical if, else if and else statements. Following conditions are for example possible:
+```.cpp
+// Standard comparisons with a variable
+render("{% if time.hour >= 20 %}Serve{% else if time.hour >= 18 %}Make{% endif %} dinner.", data); // Serve dinner.
+
+// Variable in list
+render("{% if neighbour in guests %}Turn up the music!{% endif %}", data); // Turn up the music!
+
+// Logical operations
+render("{% if guest_count < (3+2) and all_tired %}Sleepy...{% else %}Keep going...{% endif %}", data); // Sleepy...
+
+// Negations
+render("{% if not guest_count %}The End{% endif %}", data); // The End
+```
+
+#### Includes
+
+You can either include other in-memory templates or from the file system.
+```.cpp
+// To include in-memory templates, add them to the environment first
+inja::Template content_template = env.parse("Hello {{ neighbour }}!");
+env.include_template("content", content_template);
+env.render("Content: {% include \"content\" %}", data); // "Content: Hello Peter!"
+
+// Other template files are included relative from the current file location
+render("{% include \"footer.html\" %}", data);
+```
+If a corresponding template could not be found in the file system, the *include callback* is called:
+```.cpp
+// The callback takes the current path and the wanted include name and returns a template
+env.set_include_callback([&env](const std::string& path, const std::string& template_name) {
+  return env.parse("Hello {{ neighbour }} from " + template_name);
+});
+
+// You can disable to search for templates in the file system via
+env.set_search_included_templates_in_files(false);
+```
+
+Inja will throw an `inja::RenderError` if an included file is not found and no callback is specified. To disable this error, you can call `env.set_throw_at_missing_includes(false)`.
+
+#### Assignments
+
+Variables can also be defined within the template using the set statment.
+```.cpp
+render("{% set new_hour=23 %}{{ new_hour }}pm", data); // "23pm"
+render("{% set time.start=18 %}{{ time.start }}pm", data); // using json pointers
+```
+
+Assignments only set the value within the rendering context; they do not modify the json object passed into the `render` call.
+
+### Functions
+
+A few functions are implemented within the inja template syntax. They can be called with
+```.cpp
+// Upper and lower function, for string cases
+render("Hello {{ upper(neighbour) }}!", data); // "Hello PETER!"
+render("Hello {{ lower(neighbour) }}!", data); // "Hello peter!"
+
+// Range function, useful for loops
+render("{% for i in range(4) %}{{ loop.index1 }}{% endfor %}", data); // "1234"
+render("{% for i in range(3) %}{{ at(guests, i) }} {% endfor %}", data); // "Jeff Tom Patrick "
+
+// Length function (please don't combine with range, use list directly...)
+render("I count {{ length(guests) }} guests.", data); // "I count 3 guests."
+
+// Get first and last element in a list
+render("{{ first(guests) }} was first.", data); // "Jeff was first."
+render("{{ last(guests) }} was last.", data); // "Patir was last."
+
+// Sort a list
+render("{{ sort([3,2,1]) }}", data); // "[1,2,3]"
+render("{{ sort(guests) }}", data); // "[\"Jeff\", \"Patrick\", \"Tom\"]"
+
+// Join a list with a separator
+render("{{ join([1,2,3], \" + \") }}", data); // "1 + 2 + 3"
+render("{{ join(guests, \", \") }}", data); // "Jeff, Patrick, Tom"
+
+// Round numbers to a given precision
+render("{{ round(3.1415, 0) }}", data); // 3
+render("{{ round(3.1415, 3) }}", data); // 3.142
+
+// Check if a value is odd, even or divisible by a number
+render("{{ odd(42) }}", data); // false
+render("{{ even(42) }}", data); // true
+render("{{ divisibleBy(42, 7) }}", data); // true
+
+// Maximum and minimum values from a list
+render("{{ max([1, 2, 3]) }}", data); // 3
+render("{{ min([-2.4, -1.2, 4.5]) }}", data); // -2.4
+
+// Convert strings to numbers
+render("{{ int(\"2\") == 2 }}", data); // true
+render("{{ float(\"1.8\") > 2 }}", data); // false
+
+// Set default values if variables are not defined
+render("Hello {{ default(neighbour, \"my friend\") }}!", data); // "Hello Peter!"
+render("Hello {{ default(colleague, \"my friend\") }}!", data); // "Hello my friend!"
+
+// Access an objects value dynamically
+render("{{ at(time, \"start\") }} to {{ time.end }}", data); // "16 to 22"
+
+// Check if a key exists in an object
+render("{{ exists(\"guests\") }}", data); // "true"
+render("{{ exists(\"city\") }}", data); // "false"
+render("{{ existsIn(time, \"start\") }}", data); // "true"
+render("{{ existsIn(time, neighbour) }}", data); // "false"
+
+// Check if a key is a specific type
+render("{{ isString(neighbour) }}", data); // "true"
+render("{{ isArray(guests) }}", data); // "true"
+// Implemented type checks: isArray, isBoolean, isFloat, isInteger, isNumber, isObject, isString,
+```
+
+### Callbacks
+
+You can create your own and more complex functions with callbacks. These are implemented with `std::function`, so you can for example use C++ lambdas. Inja `Arguments` are a vector of json pointers.
+```.cpp
+Environment env;
+
+/*
+ * Callbacks are defined by its:
+ * - name,
+ * - (optional) number of arguments,
+ * - callback function.
+ */
+env.add_callback("double", 1, [](Arguments& args) {
+	int number = args.at(0)->get<int>(); // Adapt the index and type of the argument
+	return 2 * number;
+});
+
+// You can then use a callback like a regular function
+env.render("{{ double(16) }}", data); // "32"
+
+// Inja falls back to variadic callbacks if the number of expected arguments is omitted.
+env.add_callback("argmax", [](Arguments& args) {
+  auto result = std::max_element(args.begin(), args.end(), [](const json* a, const json* b) { return *a < *b;});
+  return std::distance(args.begin(), result);
+});
+env.render("{{ argmax(4, 2, 6) }}", data); // "2"
+env.render("{{ argmax(0, 2, 6, 8, 3) }}", data); // "3"
+
+// A callback without argument can be used like a dynamic variable:
+std::string greet = "Hello";
+env.add_callback("double-greetings", 0, [greet](Arguments args) {
+	return greet + " " + greet + "!";
+});
+env.render("{{ double-greetings }}", data); // "Hello Hello!"
+```
+You can also add a void callback without return variable, e.g. for debugging:
+```.cpp
+env.add_void_callback("log", 1, [greet](Arguments args) {
+	std::cout << "logging: " << args[0] << std::endl;
+});
+env.render("{{ log(neighbour) }}", data); // Prints nothing to result, only to cout...
+```
+
+### Template Inheritance
+
+Template inheritance allows you to build a base *skeleton* template that contains all the common elements and defines blocks that child templates can override. Lets show an example: The base template
+```.html
+<!DOCTYPE html>
+<html>
+<head>
+  {% block head %}
+  <link rel="stylesheet" href="style.css" />
+  <title>{% block title %}{% endblock %} - My Webpage</title>
+  {% endblock %}
+</head>
+<body>
+  <div id="content">{% block content %}{% endblock %}</div>
+</body>
+</html>
+```
+contains three `blocks` that child templates can fill in. The child template
+```.html
+{% extends "base.html" %}
+{% block title %}Index{% endblock %}
+{% block head %}
+  {{ super() }}
+  <style type="text/css">
+    .important { color: #336699; }
+  </style>
+{% endblock %}
+{% block content %}
+  <h1>Index</h1>
+  <p class="important">
+    Welcome to my blog!
+  </p>
+{% endblock %}
+```
+calls a parent template with the `extends` keyword; it should be the first element in the template. It is possible to render the contents of the parent block by calling `super()`. In the case of multiple levels of `{% extends %}`, super references may be called with an argument (e.g. `super(2)`) to skip levels in the inheritance tree.
+
+### Whitespace Control
+
+In the default configuration, no whitespace is removed while rendering the file. To support a more readable template style, you can configure the environment to control whitespaces before and after a statement automatically. While enabling `set_trim_blocks` removes the first newline after a statement, `set_lstrip_blocks` strips tabs and spaces from the beginning of a line to the start of a block.
+
+```.cpp
+Environment env;
+env.set_trim_blocks(true);
+env.set_lstrip_blocks(true);
+```
+
+With both `trim_blocks` and `lstrip_blocks` enabled, you can put statements on their own lines. Furthermore, you can also strip whitespaces for both statements and expressions by hand. If you add a minus sign (`-`) to the start or end, the whitespaces before or after that block will be removed:
+
+```.cpp
+render("Hello       {{- name -}}     !", data); // "Hello Inja!"
+render("{% if neighbour in guests -%}   I was there{% endif -%}   !", data); // Renders without any whitespaces
+```
+
+Stripping behind a statement or expression also removes any newlines.
+
+### Comments
+
+Comments can be written with the `{# ... #}` syntax.
+```.cpp
+render("Hello{# Todo #}!", data); // "Hello!"
+```
+
+### Exceptions
+
+Inja uses exceptions to handle ill-formed template input. However, exceptions can be switched off with either using the compiler flag `-fno-exceptions` or by defining the symbol `INJA_NOEXCEPTION`. In this case, exceptions are replaced by `abort()` calls.
+
+
+## Supported compilers
+
+Inja uses the `string_view` feature of the C++17 STL. Currently, the following compilers are tested:
+
+- GCC 7 - 11 (and possibly later)
+- Clang 5 - 12 (and possibly later)
+- Microsoft Visual C++ 2017 15.0 - 2022 (and possibly later)
+
+A list of supported compiler / os versions can be found in the [CI definition](https://github.com/pantor/inja/blob/master/.github/workflows/ci.yml).

+ 2949 - 0
ext/inja/inja.hpp

@@ -0,0 +1,2949 @@
+/*
+  ___        _          Version 3.3
+ |_ _|_ __  (_) __ _    https://github.com/pantor/inja
+  | || '_ \ | |/ _` |   Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+  | || | | || | (_| |
+ |___|_| |_|/ |\__,_|   Copyright (c) 2018-2021 Lars Berscheid
+          |__/
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+#ifndef INCLUDE_INJA_INJA_HPP_
+#define INCLUDE_INJA_INJA_HPP_
+
+#include <nlohmann/json.hpp>
+
+namespace inja {
+#ifndef INJA_DATA_TYPE
+using json = nlohmann::json;
+#else
+using json = INJA_DATA_TYPE;
+#endif
+} // namespace inja
+
+#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(INJA_NOEXCEPTION)
+#ifndef INJA_THROW
+#define INJA_THROW(exception) throw exception
+#endif
+#else
+#include <cstdlib>
+#ifndef INJA_THROW
+#define INJA_THROW(exception)                                                                                                                                  \
+  std::abort();                                                                                                                                                \
+  std::ignore = exception
+#endif
+#ifndef INJA_NOEXCEPTION
+#define INJA_NOEXCEPTION
+#endif
+#endif
+
+// #include "environment.hpp"
+#ifndef INCLUDE_INJA_ENVIRONMENT_HPP_
+#define INCLUDE_INJA_ENVIRONMENT_HPP_
+
+#include <fstream>
+#include <iostream>
+#include <memory>
+#include <sstream>
+#include <string>
+#include <string_view>
+
+// #include "config.hpp"
+#ifndef INCLUDE_INJA_CONFIG_HPP_
+#define INCLUDE_INJA_CONFIG_HPP_
+
+#include <functional>
+#include <string>
+
+// #include "template.hpp"
+#ifndef INCLUDE_INJA_TEMPLATE_HPP_
+#define INCLUDE_INJA_TEMPLATE_HPP_
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+// #include "node.hpp"
+#ifndef INCLUDE_INJA_NODE_HPP_
+#define INCLUDE_INJA_NODE_HPP_
+
+#include <string>
+#include <string_view>
+#include <utility>
+
+// #include "function_storage.hpp"
+#ifndef INCLUDE_INJA_FUNCTION_STORAGE_HPP_
+#define INCLUDE_INJA_FUNCTION_STORAGE_HPP_
+
+#include <string_view>
+#include <vector>
+
+namespace inja {
+
+using Arguments = std::vector<const json*>;
+using CallbackFunction = std::function<json(Arguments& args)>;
+using VoidCallbackFunction = std::function<void(Arguments& args)>;
+
+/*!
+ * \brief Class for builtin functions and user-defined callbacks.
+ */
+class FunctionStorage {
+public:
+  enum class Operation {
+    Not,
+    And,
+    Or,
+    In,
+    Equal,
+    NotEqual,
+    Greater,
+    GreaterEqual,
+    Less,
+    LessEqual,
+    Add,
+    Subtract,
+    Multiplication,
+    Division,
+    Power,
+    Modulo,
+    AtId,
+    At,
+    Default,
+    DivisibleBy,
+    Even,
+    Exists,
+    ExistsInObject,
+    First,
+    Float,
+    Int,
+    IsArray,
+    IsBoolean,
+    IsFloat,
+    IsInteger,
+    IsNumber,
+    IsObject,
+    IsString,
+    Last,
+    Length,
+    Lower,
+    Max,
+    Min,
+    Odd,
+    Range,
+    Round,
+    Sort,
+    Upper,
+    Super,
+    Join,
+    Callback,
+    ParenLeft,
+    ParenRight,
+    None,
+  };
+
+  struct FunctionData {
+    explicit FunctionData(const Operation& op, const CallbackFunction& cb = CallbackFunction {}): operation(op), callback(cb) {}
+    const Operation operation;
+    const CallbackFunction callback;
+  };
+
+private:
+  const int VARIADIC {-1};
+
+  std::map<std::pair<std::string, int>, FunctionData> function_storage = {
+      {std::make_pair("at", 2), FunctionData {Operation::At}},
+      {std::make_pair("default", 2), FunctionData {Operation::Default}},
+      {std::make_pair("divisibleBy", 2), FunctionData {Operation::DivisibleBy}},
+      {std::make_pair("even", 1), FunctionData {Operation::Even}},
+      {std::make_pair("exists", 1), FunctionData {Operation::Exists}},
+      {std::make_pair("existsIn", 2), FunctionData {Operation::ExistsInObject}},
+      {std::make_pair("first", 1), FunctionData {Operation::First}},
+      {std::make_pair("float", 1), FunctionData {Operation::Float}},
+      {std::make_pair("int", 1), FunctionData {Operation::Int}},
+      {std::make_pair("isArray", 1), FunctionData {Operation::IsArray}},
+      {std::make_pair("isBoolean", 1), FunctionData {Operation::IsBoolean}},
+      {std::make_pair("isFloat", 1), FunctionData {Operation::IsFloat}},
+      {std::make_pair("isInteger", 1), FunctionData {Operation::IsInteger}},
+      {std::make_pair("isNumber", 1), FunctionData {Operation::IsNumber}},
+      {std::make_pair("isObject", 1), FunctionData {Operation::IsObject}},
+      {std::make_pair("isString", 1), FunctionData {Operation::IsString}},
+      {std::make_pair("last", 1), FunctionData {Operation::Last}},
+      {std::make_pair("length", 1), FunctionData {Operation::Length}},
+      {std::make_pair("lower", 1), FunctionData {Operation::Lower}},
+      {std::make_pair("max", 1), FunctionData {Operation::Max}},
+      {std::make_pair("min", 1), FunctionData {Operation::Min}},
+      {std::make_pair("odd", 1), FunctionData {Operation::Odd}},
+      {std::make_pair("range", 1), FunctionData {Operation::Range}},
+      {std::make_pair("round", 2), FunctionData {Operation::Round}},
+      {std::make_pair("sort", 1), FunctionData {Operation::Sort}},
+      {std::make_pair("upper", 1), FunctionData {Operation::Upper}},
+      {std::make_pair("super", 0), FunctionData {Operation::Super}},
+      {std::make_pair("super", 1), FunctionData {Operation::Super}},
+      {std::make_pair("join", 2), FunctionData {Operation::Join}},
+  };
+
+public:
+  void add_builtin(std::string_view name, int num_args, Operation op) {
+    function_storage.emplace(std::make_pair(static_cast<std::string>(name), num_args), FunctionData {op});
+  }
+
+  void add_callback(std::string_view name, int num_args, const CallbackFunction& callback) {
+    function_storage.emplace(std::make_pair(static_cast<std::string>(name), num_args), FunctionData {Operation::Callback, callback});
+  }
+
+  FunctionData find_function(std::string_view name, int num_args) const {
+    auto it = function_storage.find(std::make_pair(static_cast<std::string>(name), num_args));
+    if (it != function_storage.end()) {
+      return it->second;
+
+      // Find variadic function
+    } else if (num_args > 0) {
+      it = function_storage.find(std::make_pair(static_cast<std::string>(name), VARIADIC));
+      if (it != function_storage.end()) {
+        return it->second;
+      }
+    }
+
+    return FunctionData {Operation::None};
+  }
+};
+
+} // namespace inja
+
+#endif // INCLUDE_INJA_FUNCTION_STORAGE_HPP_
+
+// #include "utils.hpp"
+#ifndef INCLUDE_INJA_UTILS_HPP_
+#define INCLUDE_INJA_UTILS_HPP_
+
+#include <algorithm>
+#include <fstream>
+#include <string>
+#include <string_view>
+#include <utility>
+
+// #include "exceptions.hpp"
+#ifndef INCLUDE_INJA_EXCEPTIONS_HPP_
+#define INCLUDE_INJA_EXCEPTIONS_HPP_
+
+#include <stdexcept>
+#include <string>
+
+namespace inja {
+
+struct SourceLocation {
+  size_t line;
+  size_t column;
+};
+
+struct InjaError : public std::runtime_error {
+  const std::string type;
+  const std::string message;
+
+  const SourceLocation location;
+
+  explicit InjaError(const std::string& type, const std::string& message)
+      : std::runtime_error("[inja.exception." + type + "] " + message), type(type), message(message), location({0, 0}) {}
+
+  explicit InjaError(const std::string& type, const std::string& message, SourceLocation location)
+      : std::runtime_error("[inja.exception." + type + "] (at " + std::to_string(location.line) + ":" + std::to_string(location.column) + ") " + message),
+        type(type), message(message), location(location) {}
+};
+
+struct ParserError : public InjaError {
+  explicit ParserError(const std::string& message, SourceLocation location): InjaError("parser_error", message, location) {}
+};
+
+struct RenderError : public InjaError {
+  explicit RenderError(const std::string& message, SourceLocation location): InjaError("render_error", message, location) {}
+};
+
+struct FileError : public InjaError {
+  explicit FileError(const std::string& message): InjaError("file_error", message) {}
+  explicit FileError(const std::string& message, SourceLocation location): InjaError("file_error", message, location) {}
+};
+
+struct DataError : public InjaError {
+  explicit DataError(const std::string& message, SourceLocation location): InjaError("data_error", message, location) {}
+};
+
+} // namespace inja
+
+#endif // INCLUDE_INJA_EXCEPTIONS_HPP_
+
+
+namespace inja {
+
+namespace string_view {
+inline std::string_view slice(std::string_view view, size_t start, size_t end) {
+  start = std::min(start, view.size());
+  end = std::min(std::max(start, end), view.size());
+  return view.substr(start, end - start);
+}
+
+inline std::pair<std::string_view, std::string_view> split(std::string_view view, char Separator) {
+  size_t idx = view.find(Separator);
+  if (idx == std::string_view::npos) {
+    return std::make_pair(view, std::string_view());
+  }
+  return std::make_pair(slice(view, 0, idx), slice(view, idx + 1, std::string_view::npos));
+}
+
+inline bool starts_with(std::string_view view, std::string_view prefix) {
+  return (view.size() >= prefix.size() && view.compare(0, prefix.size(), prefix) == 0);
+}
+} // namespace string_view
+
+inline SourceLocation get_source_location(std::string_view content, size_t pos) {
+  // Get line and offset position (starts at 1:1)
+  auto sliced = string_view::slice(content, 0, pos);
+  std::size_t last_newline = sliced.rfind("\n");
+
+  if (last_newline == std::string_view::npos) {
+    return {1, sliced.length() + 1};
+  }
+
+  // Count newlines
+  size_t count_lines = 0;
+  size_t search_start = 0;
+  while (search_start <= sliced.size()) {
+    search_start = sliced.find("\n", search_start) + 1;
+    if (search_start == 0) {
+      break;
+    }
+    count_lines += 1;
+  }
+
+  return {count_lines + 1, sliced.length() - last_newline};
+}
+
+inline void replace_substring(std::string& s, const std::string& f, const std::string& t) {
+  if (f.empty()) {
+    return;
+  }
+  for (auto pos = s.find(f);            // find first occurrence of f
+       pos != std::string::npos;        // make sure f was found
+       s.replace(pos, f.size(), t),     // replace with t, and
+       pos = s.find(f, pos + t.size())) // find next occurrence of f
+  {}
+}
+
+} // namespace inja
+
+#endif // INCLUDE_INJA_UTILS_HPP_
+
+
+namespace inja {
+
+class NodeVisitor;
+class BlockNode;
+class TextNode;
+class ExpressionNode;
+class LiteralNode;
+class DataNode;
+class FunctionNode;
+class ExpressionListNode;
+class StatementNode;
+class ForStatementNode;
+class ForArrayStatementNode;
+class ForObjectStatementNode;
+class IfStatementNode;
+class IncludeStatementNode;
+class ExtendsStatementNode;
+class BlockStatementNode;
+class SetStatementNode;
+
+class NodeVisitor {
+public:
+  virtual ~NodeVisitor() = default;
+
+  virtual void visit(const BlockNode& node) = 0;
+  virtual void visit(const TextNode& node) = 0;
+  virtual void visit(const ExpressionNode& node) = 0;
+  virtual void visit(const LiteralNode& node) = 0;
+  virtual void visit(const DataNode& node) = 0;
+  virtual void visit(const FunctionNode& node) = 0;
+  virtual void visit(const ExpressionListNode& node) = 0;
+  virtual void visit(const StatementNode& node) = 0;
+  virtual void visit(const ForStatementNode& node) = 0;
+  virtual void visit(const ForArrayStatementNode& node) = 0;
+  virtual void visit(const ForObjectStatementNode& node) = 0;
+  virtual void visit(const IfStatementNode& node) = 0;
+  virtual void visit(const IncludeStatementNode& node) = 0;
+  virtual void visit(const ExtendsStatementNode& node) = 0;
+  virtual void visit(const BlockStatementNode& node) = 0;
+  virtual void visit(const SetStatementNode& node) = 0;
+};
+
+/*!
+ * \brief Base node class for the abstract syntax tree (AST).
+ */
+class AstNode {
+public:
+  virtual void accept(NodeVisitor& v) const = 0;
+
+  size_t pos;
+
+  AstNode(size_t pos): pos(pos) {}
+  virtual ~AstNode() {}
+};
+
+class BlockNode : public AstNode {
+public:
+  std::vector<std::shared_ptr<AstNode>> nodes;
+
+  explicit BlockNode(): AstNode(0) {}
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  }
+};
+
+class TextNode : public AstNode {
+public:
+  const size_t length;
+
+  explicit TextNode(size_t pos, size_t length): AstNode(pos), length(length) {}
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  }
+};
+
+class ExpressionNode : public AstNode {
+public:
+  explicit ExpressionNode(size_t pos): AstNode(pos) {}
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  }
+};
+
+class LiteralNode : public ExpressionNode {
+public:
+  const json value;
+
+  explicit LiteralNode(std::string_view data_text, size_t pos): ExpressionNode(pos), value(json::parse(data_text)) {}
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  }
+};
+
+class DataNode : public ExpressionNode {
+public:
+  const std::string name;
+  const json::json_pointer ptr;
+
+  static std::string convert_dot_to_ptr(std::string_view ptr_name) {
+    std::string result;
+    do {
+      std::string_view part;
+      std::tie(part, ptr_name) = string_view::split(ptr_name, '.');
+      result.push_back('/');
+      result.append(part.begin(), part.end());
+    } while (!ptr_name.empty());
+    return result;
+  }
+
+  explicit DataNode(std::string_view ptr_name, size_t pos): ExpressionNode(pos), name(ptr_name), ptr(json::json_pointer(convert_dot_to_ptr(ptr_name))) {}
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  }
+};
+
+class FunctionNode : public ExpressionNode {
+  using Op = FunctionStorage::Operation;
+
+public:
+  enum class Associativity {
+    Left,
+    Right,
+  };
+
+  unsigned int precedence;
+  Associativity associativity;
+
+  Op operation;
+
+  std::string name;
+  int number_args; // Should also be negative -> -1 for unknown number
+  std::vector<std::shared_ptr<ExpressionNode>> arguments;
+  CallbackFunction callback;
+
+  explicit FunctionNode(std::string_view name, size_t pos)
+      : ExpressionNode(pos), precedence(8), associativity(Associativity::Left), operation(Op::Callback), name(name), number_args(1) {}
+  explicit FunctionNode(Op operation, size_t pos): ExpressionNode(pos), operation(operation), number_args(1) {
+    switch (operation) {
+    case Op::Not: {
+      number_args = 1;
+      precedence = 4;
+      associativity = Associativity::Left;
+    } break;
+    case Op::And: {
+      number_args = 2;
+      precedence = 1;
+      associativity = Associativity::Left;
+    } break;
+    case Op::Or: {
+      number_args = 2;
+      precedence = 1;
+      associativity = Associativity::Left;
+    } break;
+    case Op::In: {
+      number_args = 2;
+      precedence = 2;
+      associativity = Associativity::Left;
+    } break;
+    case Op::Equal: {
+      number_args = 2;
+      precedence = 2;
+      associativity = Associativity::Left;
+    } break;
+    case Op::NotEqual: {
+      number_args = 2;
+      precedence = 2;
+      associativity = Associativity::Left;
+    } break;
+    case Op::Greater: {
+      number_args = 2;
+      precedence = 2;
+      associativity = Associativity::Left;
+    } break;
+    case Op::GreaterEqual: {
+      number_args = 2;
+      precedence = 2;
+      associativity = Associativity::Left;
+    } break;
+    case Op::Less: {
+      number_args = 2;
+      precedence = 2;
+      associativity = Associativity::Left;
+    } break;
+    case Op::LessEqual: {
+      number_args = 2;
+      precedence = 2;
+      associativity = Associativity::Left;
+    } break;
+    case Op::Add: {
+      number_args = 2;
+      precedence = 3;
+      associativity = Associativity::Left;
+    } break;
+    case Op::Subtract: {
+      number_args = 2;
+      precedence = 3;
+      associativity = Associativity::Left;
+    } break;
+    case Op::Multiplication: {
+      number_args = 2;
+      precedence = 4;
+      associativity = Associativity::Left;
+    } break;
+    case Op::Division: {
+      number_args = 2;
+      precedence = 4;
+      associativity = Associativity::Left;
+    } break;
+    case Op::Power: {
+      number_args = 2;
+      precedence = 5;
+      associativity = Associativity::Right;
+    } break;
+    case Op::Modulo: {
+      number_args = 2;
+      precedence = 4;
+      associativity = Associativity::Left;
+    } break;
+    case Op::AtId: {
+      number_args = 2;
+      precedence = 8;
+      associativity = Associativity::Left;
+    } break;
+    default: {
+      precedence = 1;
+      associativity = Associativity::Left;
+    }
+    }
+  }
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  }
+};
+
+class ExpressionListNode : public AstNode {
+public:
+  std::shared_ptr<ExpressionNode> root;
+
+  explicit ExpressionListNode(): AstNode(0) {}
+  explicit ExpressionListNode(size_t pos): AstNode(pos) {}
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  }
+};
+
+class StatementNode : public AstNode {
+public:
+  StatementNode(size_t pos): AstNode(pos) {}
+
+  virtual void accept(NodeVisitor& v) const = 0;
+};
+
+class ForStatementNode : public StatementNode {
+public:
+  ExpressionListNode condition;
+  BlockNode body;
+  BlockNode* const parent;
+
+  ForStatementNode(BlockNode* const parent, size_t pos): StatementNode(pos), parent(parent) {}
+
+  virtual void accept(NodeVisitor& v) const = 0;
+};
+
+class ForArrayStatementNode : public ForStatementNode {
+public:
+  const std::string value;
+
+  explicit ForArrayStatementNode(const std::string& value, BlockNode* const parent, size_t pos): ForStatementNode(parent, pos), value(value) {}
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  }
+};
+
+class ForObjectStatementNode : public ForStatementNode {
+public:
+  const std::string key;
+  const std::string value;
+
+  explicit ForObjectStatementNode(const std::string& key, const std::string& value, BlockNode* const parent, size_t pos)
+      : ForStatementNode(parent, pos), key(key), value(value) {}
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  }
+};
+
+class IfStatementNode : public StatementNode {
+public:
+  ExpressionListNode condition;
+  BlockNode true_statement;
+  BlockNode false_statement;
+  BlockNode* const parent;
+
+  const bool is_nested;
+  bool has_false_statement {false};
+
+  explicit IfStatementNode(BlockNode* const parent, size_t pos): StatementNode(pos), parent(parent), is_nested(false) {}
+  explicit IfStatementNode(bool is_nested, BlockNode* const parent, size_t pos): StatementNode(pos), parent(parent), is_nested(is_nested) {}
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  }
+};
+
+class IncludeStatementNode : public StatementNode {
+public:
+  const std::string file;
+
+  explicit IncludeStatementNode(const std::string& file, size_t pos): StatementNode(pos), file(file) {}
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  }
+};
+
+class ExtendsStatementNode : public StatementNode {
+public:
+  const std::string file;
+
+  explicit ExtendsStatementNode(const std::string& file, size_t pos): StatementNode(pos), file(file) {}
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  };
+};
+
+class BlockStatementNode : public StatementNode {
+public:
+  const std::string name;
+  BlockNode block;
+  BlockNode* const parent;
+
+  explicit BlockStatementNode(BlockNode* const parent, const std::string& name, size_t pos): StatementNode(pos), name(name), parent(parent) {}
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  };
+};
+
+class SetStatementNode : public StatementNode {
+public:
+  const std::string key;
+  ExpressionListNode expression;
+
+  explicit SetStatementNode(const std::string& key, size_t pos): StatementNode(pos), key(key) {}
+
+  void accept(NodeVisitor& v) const {
+    v.visit(*this);
+  }
+};
+
+} // namespace inja
+
+#endif // INCLUDE_INJA_NODE_HPP_
+
+// #include "statistics.hpp"
+#ifndef INCLUDE_INJA_STATISTICS_HPP_
+#define INCLUDE_INJA_STATISTICS_HPP_
+
+// #include "node.hpp"
+
+
+namespace inja {
+
+/*!
+ * \brief A class for counting statistics on a Template.
+ */
+class StatisticsVisitor : public NodeVisitor {
+  void visit(const BlockNode& node) {
+    for (auto& n : node.nodes) {
+      n->accept(*this);
+    }
+  }
+
+  void visit(const TextNode&) {}
+  void visit(const ExpressionNode&) {}
+  void visit(const LiteralNode&) {}
+
+  void visit(const DataNode&) {
+    variable_counter += 1;
+  }
+
+  void visit(const FunctionNode& node) {
+    for (auto& n : node.arguments) {
+      n->accept(*this);
+    }
+  }
+
+  void visit(const ExpressionListNode& node) {
+    node.root->accept(*this);
+  }
+
+  void visit(const StatementNode&) {}
+  void visit(const ForStatementNode&) {}
+
+  void visit(const ForArrayStatementNode& node) {
+    node.condition.accept(*this);
+    node.body.accept(*this);
+  }
+
+  void visit(const ForObjectStatementNode& node) {
+    node.condition.accept(*this);
+    node.body.accept(*this);
+  }
+
+  void visit(const IfStatementNode& node) {
+    node.condition.accept(*this);
+    node.true_statement.accept(*this);
+    node.false_statement.accept(*this);
+  }
+
+  void visit(const IncludeStatementNode&) {}
+
+  void visit(const ExtendsStatementNode&) {}
+
+  void visit(const BlockStatementNode& node) {
+    node.block.accept(*this);
+  }
+
+  void visit(const SetStatementNode&) {}
+
+public:
+  unsigned int variable_counter;
+
+  explicit StatisticsVisitor(): variable_counter(0) {}
+};
+
+} // namespace inja
+
+#endif // INCLUDE_INJA_STATISTICS_HPP_
+
+
+namespace inja {
+
+/*!
+ * \brief The main inja Template.
+ */
+struct Template {
+  BlockNode root;
+  std::string content;
+  std::map<std::string, std::shared_ptr<BlockStatementNode>> block_storage;
+
+  explicit Template() {}
+  explicit Template(const std::string& content): content(content) {}
+
+  /// Return number of variables (total number, not distinct ones) in the template
+  int count_variables() {
+    auto statistic_visitor = StatisticsVisitor();
+    root.accept(statistic_visitor);
+    return statistic_visitor.variable_counter;
+  }
+};
+
+using TemplateStorage = std::map<std::string, Template>;
+
+} // namespace inja
+
+#endif // INCLUDE_INJA_TEMPLATE_HPP_
+
+
+namespace inja {
+
+/*!
+ * \brief Class for lexer configuration.
+ */
+struct LexerConfig {
+  std::string statement_open {"{%"};
+  std::string statement_open_no_lstrip {"{%+"};
+  std::string statement_open_force_lstrip {"{%-"};
+  std::string statement_close {"%}"};
+  std::string statement_close_force_rstrip {"-%}"};
+  std::string line_statement {"##"};
+  std::string expression_open {"{{"};
+  std::string expression_open_force_lstrip {"{{-"};
+  std::string expression_close {"}}"};
+  std::string expression_close_force_rstrip {"-}}"};
+  std::string comment_open {"{#"};
+  std::string comment_open_force_lstrip {"{#-"};
+  std::string comment_close {"#}"};
+  std::string comment_close_force_rstrip {"-#}"};
+  std::string open_chars {"#{"};
+
+  bool trim_blocks {false};
+  bool lstrip_blocks {false};
+
+  void update_open_chars() {
+    open_chars = "";
+    if (open_chars.find(line_statement[0]) == std::string::npos) {
+      open_chars += line_statement[0];
+    }
+    if (open_chars.find(statement_open[0]) == std::string::npos) {
+      open_chars += statement_open[0];
+    }
+    if (open_chars.find(statement_open_no_lstrip[0]) == std::string::npos) {
+      open_chars += statement_open_no_lstrip[0];
+    }
+    if (open_chars.find(statement_open_force_lstrip[0]) == std::string::npos) {
+      open_chars += statement_open_force_lstrip[0];
+    }
+    if (open_chars.find(expression_open[0]) == std::string::npos) {
+      open_chars += expression_open[0];
+    }
+    if (open_chars.find(expression_open_force_lstrip[0]) == std::string::npos) {
+      open_chars += expression_open_force_lstrip[0];
+    }
+    if (open_chars.find(comment_open[0]) == std::string::npos) {
+      open_chars += comment_open[0];
+    }
+    if (open_chars.find(comment_open_force_lstrip[0]) == std::string::npos) {
+      open_chars += comment_open_force_lstrip[0];
+    }
+  }
+};
+
+/*!
+ * \brief Class for parser configuration.
+ */
+struct ParserConfig {
+  bool search_included_templates_in_files {true};
+
+  std::function<Template(const std::string&, const std::string&)> include_callback;
+};
+
+/*!
+ * \brief Class for render configuration.
+ */
+struct RenderConfig {
+  bool throw_at_missing_includes {true};
+};
+
+} // namespace inja
+
+#endif // INCLUDE_INJA_CONFIG_HPP_
+
+// #include "function_storage.hpp"
+
+// #include "parser.hpp"
+#ifndef INCLUDE_INJA_PARSER_HPP_
+#define INCLUDE_INJA_PARSER_HPP_
+
+#include <limits>
+#include <stack>
+#include <string>
+#include <utility>
+#include <vector>
+
+// #include "config.hpp"
+
+// #include "exceptions.hpp"
+
+// #include "function_storage.hpp"
+
+// #include "lexer.hpp"
+#ifndef INCLUDE_INJA_LEXER_HPP_
+#define INCLUDE_INJA_LEXER_HPP_
+
+#include <cctype>
+#include <locale>
+
+// #include "config.hpp"
+
+// #include "token.hpp"
+#ifndef INCLUDE_INJA_TOKEN_HPP_
+#define INCLUDE_INJA_TOKEN_HPP_
+
+#include <string>
+#include <string_view>
+
+namespace inja {
+
+/*!
+ * \brief Helper-class for the inja Lexer.
+ */
+struct Token {
+  enum class Kind {
+    Text,
+    ExpressionOpen,     // {{
+    ExpressionClose,    // }}
+    LineStatementOpen,  // ##
+    LineStatementClose, // \n
+    StatementOpen,      // {%
+    StatementClose,     // %}
+    CommentOpen,        // {#
+    CommentClose,       // #}
+    Id,                 // this, this.foo
+    Number,             // 1, 2, -1, 5.2, -5.3
+    String,             // "this"
+    Plus,               // +
+    Minus,              // -
+    Times,              // *
+    Slash,              // /
+    Percent,            // %
+    Power,              // ^
+    Comma,              // ,
+    Dot,                // .
+    Colon,              // :
+    LeftParen,          // (
+    RightParen,         // )
+    LeftBracket,        // [
+    RightBracket,       // ]
+    LeftBrace,          // {
+    RightBrace,         // }
+    Equal,              // ==
+    NotEqual,           // !=
+    GreaterThan,        // >
+    GreaterEqual,       // >=
+    LessThan,           // <
+    LessEqual,          // <=
+    Unknown,
+    Eof,
+  };
+
+  Kind kind {Kind::Unknown};
+  std::string_view text;
+
+  explicit constexpr Token() = default;
+  explicit constexpr Token(Kind kind, std::string_view text): kind(kind), text(text) {}
+
+  std::string describe() const {
+    switch (kind) {
+    case Kind::Text:
+      return "<text>";
+    case Kind::LineStatementClose:
+      return "<eol>";
+    case Kind::Eof:
+      return "<eof>";
+    default:
+      return static_cast<std::string>(text);
+    }
+  }
+};
+
+} // namespace inja
+
+#endif // INCLUDE_INJA_TOKEN_HPP_
+
+// #include "utils.hpp"
+
+
+namespace inja {
+
+/*!
+ * \brief Class for lexing an inja Template.
+ */
+class Lexer {
+  enum class State {
+    Text,
+    ExpressionStart,
+    ExpressionStartForceLstrip,
+    ExpressionBody,
+    LineStart,
+    LineBody,
+    StatementStart,
+    StatementStartNoLstrip,
+    StatementStartForceLstrip,
+    StatementBody,
+    CommentStart,
+    CommentStartForceLstrip,
+    CommentBody,
+  };
+
+  enum class MinusState {
+    Operator,
+    Number,
+  };
+
+  const LexerConfig& config;
+
+  State state;
+  MinusState minus_state;
+  std::string_view m_in;
+  size_t tok_start;
+  size_t pos;
+
+  Token scan_body(std::string_view close, Token::Kind closeKind, std::string_view close_trim = std::string_view(), bool trim = false) {
+  again:
+    // skip whitespace (except for \n as it might be a close)
+    if (tok_start >= m_in.size()) {
+      return make_token(Token::Kind::Eof);
+    }
+    const char ch = m_in[tok_start];
+    if (ch == ' ' || ch == '\t' || ch == '\r') {
+      tok_start += 1;
+      goto again;
+    }
+
+    // check for close
+    if (!close_trim.empty() && inja::string_view::starts_with(m_in.substr(tok_start), close_trim)) {
+      state = State::Text;
+      pos = tok_start + close_trim.size();
+      const Token tok = make_token(closeKind);
+      skip_whitespaces_and_newlines();
+      return tok;
+    }
+
+    if (inja::string_view::starts_with(m_in.substr(tok_start), close)) {
+      state = State::Text;
+      pos = tok_start + close.size();
+      const Token tok = make_token(closeKind);
+      if (trim) {
+        skip_whitespaces_and_first_newline();
+      }
+      return tok;
+    }
+
+    // skip \n
+    if (ch == '\n') {
+      tok_start += 1;
+      goto again;
+    }
+
+    pos = tok_start + 1;
+    if (std::isalpha(ch)) {
+      minus_state = MinusState::Operator;
+      return scan_id();
+    }
+
+    const MinusState current_minus_state = minus_state;
+    if (minus_state == MinusState::Operator) {
+      minus_state = MinusState::Number;
+    }
+
+    switch (ch) {
+    case '+':
+      return make_token(Token::Kind::Plus);
+    case '-':
+      if (current_minus_state == MinusState::Operator) {
+        return make_token(Token::Kind::Minus);
+      }
+      return scan_number();
+    case '*':
+      return make_token(Token::Kind::Times);
+    case '/':
+      return make_token(Token::Kind::Slash);
+    case '^':
+      return make_token(Token::Kind::Power);
+    case '%':
+      return make_token(Token::Kind::Percent);
+    case '.':
+      return make_token(Token::Kind::Dot);
+    case ',':
+      return make_token(Token::Kind::Comma);
+    case ':':
+      return make_token(Token::Kind::Colon);
+    case '(':
+      return make_token(Token::Kind::LeftParen);
+    case ')':
+      minus_state = MinusState::Operator;
+      return make_token(Token::Kind::RightParen);
+    case '[':
+      return make_token(Token::Kind::LeftBracket);
+    case ']':
+      minus_state = MinusState::Operator;
+      return make_token(Token::Kind::RightBracket);
+    case '{':
+      return make_token(Token::Kind::LeftBrace);
+    case '}':
+      minus_state = MinusState::Operator;
+      return make_token(Token::Kind::RightBrace);
+    case '>':
+      if (pos < m_in.size() && m_in[pos] == '=') {
+        pos += 1;
+        return make_token(Token::Kind::GreaterEqual);
+      }
+      return make_token(Token::Kind::GreaterThan);
+    case '<':
+      if (pos < m_in.size() && m_in[pos] == '=') {
+        pos += 1;
+        return make_token(Token::Kind::LessEqual);
+      }
+      return make_token(Token::Kind::LessThan);
+    case '=':
+      if (pos < m_in.size() && m_in[pos] == '=') {
+        pos += 1;
+        return make_token(Token::Kind::Equal);
+      }
+      return make_token(Token::Kind::Unknown);
+    case '!':
+      if (pos < m_in.size() && m_in[pos] == '=') {
+        pos += 1;
+        return make_token(Token::Kind::NotEqual);
+      }
+      return make_token(Token::Kind::Unknown);
+    case '\"':
+      return scan_string();
+    case '0':
+    case '1':
+    case '2':
+    case '3':
+    case '4':
+    case '5':
+    case '6':
+    case '7':
+    case '8':
+    case '9':
+      minus_state = MinusState::Operator;
+      return scan_number();
+    case '_':
+    case '@':
+    case '$':
+      minus_state = MinusState::Operator;
+      return scan_id();
+    default:
+      return make_token(Token::Kind::Unknown);
+    }
+  }
+
+  Token scan_id() {
+    for (;;) {
+      if (pos >= m_in.size()) {
+        break;
+      }
+      const char ch = m_in[pos];
+      if (!std::isalnum(ch) && ch != '.' && ch != '/' && ch != '_' && ch != '-') {
+        break;
+      }
+      pos += 1;
+    }
+    return make_token(Token::Kind::Id);
+  }
+
+  Token scan_number() {
+    for (;;) {
+      if (pos >= m_in.size()) {
+        break;
+      }
+      const char ch = m_in[pos];
+      // be very permissive in lexer (we'll catch errors when conversion happens)
+      if (!(std::isdigit(ch) || ch == '.' || ch == 'e' || ch == 'E' || (ch == '+' && (pos == 0 || m_in[pos-1] == 'e' || m_in[pos-1] == 'E')) || (ch == '-' && (pos == 0 || m_in[pos-1] == 'e' || m_in[pos-1] == 'E')))) {
+        break;
+      }
+      pos += 1;
+    }
+    return make_token(Token::Kind::Number);
+  }
+
+  Token scan_string() {
+    bool escape {false};
+    for (;;) {
+      if (pos >= m_in.size()) {
+        break;
+      }
+      const char ch = m_in[pos++];
+      if (ch == '\\') {
+        escape = true;
+      } else if (!escape && ch == m_in[tok_start]) {
+        break;
+      } else {
+        escape = false;
+      }
+    }
+    return make_token(Token::Kind::String);
+  }
+
+  Token make_token(Token::Kind kind) const {
+    return Token(kind, string_view::slice(m_in, tok_start, pos));
+  }
+
+  void skip_whitespaces_and_newlines() {
+    if (pos < m_in.size()) {
+      while (pos < m_in.size() && (m_in[pos] == ' ' || m_in[pos] == '\t' || m_in[pos] == '\n' || m_in[pos] == '\r')) {
+        pos += 1;
+      }
+    }
+  }
+
+  void skip_whitespaces_and_first_newline() {
+    if (pos < m_in.size()) {
+      while (pos < m_in.size() && (m_in[pos] == ' ' || m_in[pos] == '\t')) {
+        pos += 1;
+      }
+    }
+
+    if (pos < m_in.size()) {
+      const char ch = m_in[pos];
+      if (ch == '\n') {
+        pos += 1;
+      } else if (ch == '\r') {
+        pos += 1;
+        if (pos < m_in.size() && m_in[pos] == '\n') {
+          pos += 1;
+        }
+      }
+    }
+  }
+
+  static std::string_view clear_final_line_if_whitespace(std::string_view text) {
+    std::string_view result = text;
+    while (!result.empty()) {
+      const char ch = result.back();
+      if (ch == ' ' || ch == '\t') {
+        result.remove_suffix(1);
+      } else if (ch == '\n' || ch == '\r') {
+        break;
+      } else {
+        return text;
+      }
+    }
+    return result;
+  }
+
+public:
+  explicit Lexer(const LexerConfig& config): config(config), state(State::Text), minus_state(MinusState::Number) {}
+
+  SourceLocation current_position() const {
+    return get_source_location(m_in, tok_start);
+  }
+
+  void start(std::string_view input) {
+    m_in = input;
+    tok_start = 0;
+    pos = 0;
+    state = State::Text;
+    minus_state = MinusState::Number;
+
+    // Consume byte order mark (BOM) for UTF-8
+    if (inja::string_view::starts_with(m_in, "\xEF\xBB\xBF")) {
+      m_in = m_in.substr(3);
+    }
+  }
+
+  Token scan() {
+    tok_start = pos;
+
+  again:
+    if (tok_start >= m_in.size()) {
+      return make_token(Token::Kind::Eof);
+    }
+
+    switch (state) {
+    default:
+    case State::Text: {
+      // fast-scan to first open character
+      const size_t open_start = m_in.substr(pos).find_first_of(config.open_chars);
+      if (open_start == std::string_view::npos) {
+        // didn't find open, return remaining text as text token
+        pos = m_in.size();
+        return make_token(Token::Kind::Text);
+      }
+      pos += open_start;
+
+      // try to match one of the opening sequences, and get the close
+      std::string_view open_str = m_in.substr(pos);
+      bool must_lstrip = false;
+      if (inja::string_view::starts_with(open_str, config.expression_open)) {
+        if (inja::string_view::starts_with(open_str, config.expression_open_force_lstrip)) {
+          state = State::ExpressionStartForceLstrip;
+          must_lstrip = true;
+        } else {
+          state = State::ExpressionStart;
+        }
+      } else if (inja::string_view::starts_with(open_str, config.statement_open)) {
+        if (inja::string_view::starts_with(open_str, config.statement_open_no_lstrip)) {
+          state = State::StatementStartNoLstrip;
+        } else if (inja::string_view::starts_with(open_str, config.statement_open_force_lstrip)) {
+          state = State::StatementStartForceLstrip;
+          must_lstrip = true;
+        } else {
+          state = State::StatementStart;
+          must_lstrip = config.lstrip_blocks;
+        }
+      } else if (inja::string_view::starts_with(open_str, config.comment_open)) {
+        if (inja::string_view::starts_with(open_str, config.comment_open_force_lstrip)) {
+          state = State::CommentStartForceLstrip;
+          must_lstrip = true;
+        } else {
+          state = State::CommentStart;
+          must_lstrip = config.lstrip_blocks;
+        }
+      } else if ((pos == 0 || m_in[pos - 1] == '\n') && inja::string_view::starts_with(open_str, config.line_statement)) {
+        state = State::LineStart;
+      } else {
+        pos += 1; // wasn't actually an opening sequence
+        goto again;
+      }
+
+      std::string_view text = string_view::slice(m_in, tok_start, pos);
+      if (must_lstrip) {
+        text = clear_final_line_if_whitespace(text);
+      }
+
+      if (text.empty()) {
+        goto again; // don't generate empty token
+      }
+      return Token(Token::Kind::Text, text);
+    }
+    case State::ExpressionStart: {
+      state = State::ExpressionBody;
+      pos += config.expression_open.size();
+      return make_token(Token::Kind::ExpressionOpen);
+    }
+    case State::ExpressionStartForceLstrip: {
+      state = State::ExpressionBody;
+      pos += config.expression_open_force_lstrip.size();
+      return make_token(Token::Kind::ExpressionOpen);
+    }
+    case State::LineStart: {
+      state = State::LineBody;
+      pos += config.line_statement.size();
+      return make_token(Token::Kind::LineStatementOpen);
+    }
+    case State::StatementStart: {
+      state = State::StatementBody;
+      pos += config.statement_open.size();
+      return make_token(Token::Kind::StatementOpen);
+    }
+    case State::StatementStartNoLstrip: {
+      state = State::StatementBody;
+      pos += config.statement_open_no_lstrip.size();
+      return make_token(Token::Kind::StatementOpen);
+    }
+    case State::StatementStartForceLstrip: {
+      state = State::StatementBody;
+      pos += config.statement_open_force_lstrip.size();
+      return make_token(Token::Kind::StatementOpen);
+    }
+    case State::CommentStart: {
+      state = State::CommentBody;
+      pos += config.comment_open.size();
+      return make_token(Token::Kind::CommentOpen);
+    }
+    case State::CommentStartForceLstrip: {
+      state = State::CommentBody;
+      pos += config.comment_open_force_lstrip.size();
+      return make_token(Token::Kind::CommentOpen);
+    }
+    case State::ExpressionBody:
+      return scan_body(config.expression_close, Token::Kind::ExpressionClose, config.expression_close_force_rstrip);
+    case State::LineBody:
+      return scan_body("\n", Token::Kind::LineStatementClose);
+    case State::StatementBody:
+      return scan_body(config.statement_close, Token::Kind::StatementClose, config.statement_close_force_rstrip, config.trim_blocks);
+    case State::CommentBody: {
+      // fast-scan to comment close
+      const size_t end = m_in.substr(pos).find(config.comment_close);
+      if (end == std::string_view::npos) {
+        pos = m_in.size();
+        return make_token(Token::Kind::Eof);
+      }
+
+      // Check for trim pattern
+      const bool must_rstrip = inja::string_view::starts_with(m_in.substr(pos + end - 1), config.comment_close_force_rstrip);
+
+      // return the entire comment in the close token
+      state = State::Text;
+      pos += end + config.comment_close.size();
+      Token tok = make_token(Token::Kind::CommentClose);
+
+      if (must_rstrip || config.trim_blocks) {
+        skip_whitespaces_and_first_newline();
+      }
+      return tok;
+    }
+    }
+  }
+
+  const LexerConfig& get_config() const {
+    return config;
+  }
+};
+
+} // namespace inja
+
+#endif // INCLUDE_INJA_LEXER_HPP_
+
+// #include "node.hpp"
+
+// #include "template.hpp"
+
+// #include "token.hpp"
+
+// #include "utils.hpp"
+
+
+namespace inja {
+
+/*!
+ * \brief Class for parsing an inja Template.
+ */
+class Parser {
+  const ParserConfig& config;
+
+  Lexer lexer;
+  TemplateStorage& template_storage;
+  const FunctionStorage& function_storage;
+
+  Token tok, peek_tok;
+  bool have_peek_tok {false};
+
+  size_t current_paren_level {0};
+  size_t current_bracket_level {0};
+  size_t current_brace_level {0};
+
+  std::string_view literal_start;
+
+  BlockNode* current_block {nullptr};
+  ExpressionListNode* current_expression_list {nullptr};
+  std::stack<std::pair<FunctionNode*, size_t>> function_stack;
+  std::vector<std::shared_ptr<ExpressionNode>> arguments;
+
+  std::stack<std::shared_ptr<FunctionNode>> operator_stack;
+  std::stack<IfStatementNode*> if_statement_stack;
+  std::stack<ForStatementNode*> for_statement_stack;
+  std::stack<BlockStatementNode*> block_statement_stack;
+
+  inline void throw_parser_error(const std::string& message) const {
+    INJA_THROW(ParserError(message, lexer.current_position()));
+  }
+
+  inline void get_next_token() {
+    if (have_peek_tok) {
+      tok = peek_tok;
+      have_peek_tok = false;
+    } else {
+      tok = lexer.scan();
+    }
+  }
+
+  inline void get_peek_token() {
+    if (!have_peek_tok) {
+      peek_tok = lexer.scan();
+      have_peek_tok = true;
+    }
+  }
+
+  inline void add_literal(const char* content_ptr) {
+    std::string_view data_text(literal_start.data(), tok.text.data() - literal_start.data() + tok.text.size());
+    arguments.emplace_back(std::make_shared<LiteralNode>(data_text, data_text.data() - content_ptr));
+  }
+
+  inline void add_operator() {
+    auto function = operator_stack.top();
+    operator_stack.pop();
+
+    for (int i = 0; i < function->number_args; ++i) {
+      function->arguments.insert(function->arguments.begin(), arguments.back());
+      arguments.pop_back();
+    }
+    arguments.emplace_back(function);
+  }
+
+  void add_to_template_storage(std::string_view path, std::string& template_name) {
+    if (template_storage.find(template_name) != template_storage.end()) {
+      return;
+    }
+
+    std::string original_path = static_cast<std::string>(path);
+    std::string original_name = template_name;
+
+    if (config.search_included_templates_in_files) {
+      // Build the relative path
+      template_name = original_path + original_name;
+      if (template_name.compare(0, 2, "./") == 0) {
+        template_name.erase(0, 2);
+      }
+
+      if (template_storage.find(template_name) == template_storage.end()) {
+        // Load file
+        std::ifstream file;
+        file.open(template_name);
+        if (!file.fail()) {
+          std::string text((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
+
+          auto include_template = Template(text);
+          template_storage.emplace(template_name, include_template);
+          parse_into_template(template_storage[template_name], template_name);
+          return;
+        } else if (!config.include_callback) {
+          INJA_THROW(FileError("failed accessing file at '" + template_name + "'"));
+        }
+      }
+    }
+
+    // Try include callback
+    if (config.include_callback) {
+      auto include_template = config.include_callback(original_path, original_name);
+      template_storage.emplace(template_name, include_template);
+    }
+  }
+
+  std::string parse_filename(const Token& tok) const {
+    if (tok.kind != Token::Kind::String) {
+      throw_parser_error("expected string, got '" + tok.describe() + "'");
+    }
+
+    if (tok.text.length() < 2) {
+      throw_parser_error("expected filename, got '" + static_cast<std::string>(tok.text) + "'");
+    }
+
+    // Remove first and last character ""
+    return std::string {tok.text.substr(1, tok.text.length() - 2)};
+  }
+
+  bool parse_expression(Template& tmpl, Token::Kind closing) {
+    while (tok.kind != closing && tok.kind != Token::Kind::Eof) {
+      // Literals
+      switch (tok.kind) {
+      case Token::Kind::String: {
+        if (current_brace_level == 0 && current_bracket_level == 0) {
+          literal_start = tok.text;
+          add_literal(tmpl.content.c_str());
+        }
+      } break;
+      case Token::Kind::Number: {
+        if (current_brace_level == 0 && current_bracket_level == 0) {
+          literal_start = tok.text;
+          add_literal(tmpl.content.c_str());
+        }
+      } break;
+      case Token::Kind::LeftBracket: {
+        if (current_brace_level == 0 && current_bracket_level == 0) {
+          literal_start = tok.text;
+        }
+        current_bracket_level += 1;
+      } break;
+      case Token::Kind::LeftBrace: {
+        if (current_brace_level == 0 && current_bracket_level == 0) {
+          literal_start = tok.text;
+        }
+        current_brace_level += 1;
+      } break;
+      case Token::Kind::RightBracket: {
+        if (current_bracket_level == 0) {
+          throw_parser_error("unexpected ']'");
+        }
+
+        current_bracket_level -= 1;
+        if (current_brace_level == 0 && current_bracket_level == 0) {
+          add_literal(tmpl.content.c_str());
+        }
+      } break;
+      case Token::Kind::RightBrace: {
+        if (current_brace_level == 0) {
+          throw_parser_error("unexpected '}'");
+        }
+
+        current_brace_level -= 1;
+        if (current_brace_level == 0 && current_bracket_level == 0) {
+          add_literal(tmpl.content.c_str());
+        }
+      } break;
+      case Token::Kind::Id: {
+        get_peek_token();
+
+        // Data Literal
+        if (tok.text == static_cast<decltype(tok.text)>("true") || tok.text == static_cast<decltype(tok.text)>("false") ||
+            tok.text == static_cast<decltype(tok.text)>("null")) {
+          if (current_brace_level == 0 && current_bracket_level == 0) {
+            literal_start = tok.text;
+            add_literal(tmpl.content.c_str());
+          }
+
+          // Operator
+        } else if (tok.text == "and" || tok.text == "or" || tok.text == "in" || tok.text == "not") {
+          goto parse_operator;
+
+          // Functions
+        } else if (peek_tok.kind == Token::Kind::LeftParen) {
+          operator_stack.emplace(std::make_shared<FunctionNode>(static_cast<std::string>(tok.text), tok.text.data() - tmpl.content.c_str()));
+          function_stack.emplace(operator_stack.top().get(), current_paren_level);
+
+          // Variables
+        } else {
+          arguments.emplace_back(std::make_shared<DataNode>(static_cast<std::string>(tok.text), tok.text.data() - tmpl.content.c_str()));
+        }
+
+        // Operators
+      } break;
+      case Token::Kind::Equal:
+      case Token::Kind::NotEqual:
+      case Token::Kind::GreaterThan:
+      case Token::Kind::GreaterEqual:
+      case Token::Kind::LessThan:
+      case Token::Kind::LessEqual:
+      case Token::Kind::Plus:
+      case Token::Kind::Minus:
+      case Token::Kind::Times:
+      case Token::Kind::Slash:
+      case Token::Kind::Power:
+      case Token::Kind::Percent:
+      case Token::Kind::Dot: {
+
+      parse_operator:
+        FunctionStorage::Operation operation;
+        switch (tok.kind) {
+        case Token::Kind::Id: {
+          if (tok.text == "and") {
+            operation = FunctionStorage::Operation::And;
+          } else if (tok.text == "or") {
+            operation = FunctionStorage::Operation::Or;
+          } else if (tok.text == "in") {
+            operation = FunctionStorage::Operation::In;
+          } else if (tok.text == "not") {
+            operation = FunctionStorage::Operation::Not;
+          } else {
+            throw_parser_error("unknown operator in parser.");
+          }
+        } break;
+        case Token::Kind::Equal: {
+          operation = FunctionStorage::Operation::Equal;
+        } break;
+        case Token::Kind::NotEqual: {
+          operation = FunctionStorage::Operation::NotEqual;
+        } break;
+        case Token::Kind::GreaterThan: {
+          operation = FunctionStorage::Operation::Greater;
+        } break;
+        case Token::Kind::GreaterEqual: {
+          operation = FunctionStorage::Operation::GreaterEqual;
+        } break;
+        case Token::Kind::LessThan: {
+          operation = FunctionStorage::Operation::Less;
+        } break;
+        case Token::Kind::LessEqual: {
+          operation = FunctionStorage::Operation::LessEqual;
+        } break;
+        case Token::Kind::Plus: {
+          operation = FunctionStorage::Operation::Add;
+        } break;
+        case Token::Kind::Minus: {
+          operation = FunctionStorage::Operation::Subtract;
+        } break;
+        case Token::Kind::Times: {
+          operation = FunctionStorage::Operation::Multiplication;
+        } break;
+        case Token::Kind::Slash: {
+          operation = FunctionStorage::Operation::Division;
+        } break;
+        case Token::Kind::Power: {
+          operation = FunctionStorage::Operation::Power;
+        } break;
+        case Token::Kind::Percent: {
+          operation = FunctionStorage::Operation::Modulo;
+        } break;
+        case Token::Kind::Dot: {
+          operation = FunctionStorage::Operation::AtId;
+        } break;
+        default: {
+          throw_parser_error("unknown operator in parser.");
+        }
+        }
+        auto function_node = std::make_shared<FunctionNode>(operation, tok.text.data() - tmpl.content.c_str());
+
+        while (!operator_stack.empty() &&
+               ((operator_stack.top()->precedence > function_node->precedence) ||
+                (operator_stack.top()->precedence == function_node->precedence && function_node->associativity == FunctionNode::Associativity::Left)) &&
+               (operator_stack.top()->operation != FunctionStorage::Operation::ParenLeft)) {
+          add_operator();
+        }
+
+        operator_stack.emplace(function_node);
+      } break;
+      case Token::Kind::Comma: {
+        if (current_brace_level == 0 && current_bracket_level == 0) {
+          if (function_stack.empty()) {
+            throw_parser_error("unexpected ','");
+          }
+
+          function_stack.top().first->number_args += 1;
+        }
+      } break;
+      case Token::Kind::Colon: {
+        if (current_brace_level == 0 && current_bracket_level == 0) {
+          throw_parser_error("unexpected ':'");
+        }
+      } break;
+      case Token::Kind::LeftParen: {
+        current_paren_level += 1;
+        operator_stack.emplace(std::make_shared<FunctionNode>(FunctionStorage::Operation::ParenLeft, tok.text.data() - tmpl.content.c_str()));
+
+        get_peek_token();
+        if (peek_tok.kind == Token::Kind::RightParen) {
+          if (!function_stack.empty() && function_stack.top().second == current_paren_level - 1) {
+            function_stack.top().first->number_args = 0;
+          }
+        }
+      } break;
+      case Token::Kind::RightParen: {
+        current_paren_level -= 1;
+        while (!operator_stack.empty() && operator_stack.top()->operation != FunctionStorage::Operation::ParenLeft) {
+          add_operator();
+        }
+
+        if (!operator_stack.empty() && operator_stack.top()->operation == FunctionStorage::Operation::ParenLeft) {
+          operator_stack.pop();
+        }
+
+        if (!function_stack.empty() && function_stack.top().second == current_paren_level) {
+          auto func = function_stack.top().first;
+          auto function_data = function_storage.find_function(func->name, func->number_args);
+          if (function_data.operation == FunctionStorage::Operation::None) {
+            throw_parser_error("unknown function " + func->name);
+          }
+          func->operation = function_data.operation;
+          if (function_data.operation == FunctionStorage::Operation::Callback) {
+            func->callback = function_data.callback;
+          }
+
+          if (operator_stack.empty()) {
+            throw_parser_error("internal error at function " + func->name);
+          }
+
+          add_operator();
+          function_stack.pop();
+        }
+      }
+      default:
+        break;
+      }
+
+      get_next_token();
+    }
+
+    while (!operator_stack.empty()) {
+      add_operator();
+    }
+
+    if (arguments.size() == 1) {
+      current_expression_list->root = arguments[0];
+      arguments = {};
+    } else if (arguments.size() > 1) {
+      throw_parser_error("malformed expression");
+    }
+
+    return true;
+  }
+
+  bool parse_statement(Template& tmpl, Token::Kind closing, std::string_view path) {
+    if (tok.kind != Token::Kind::Id) {
+      return false;
+    }
+
+    if (tok.text == static_cast<decltype(tok.text)>("if")) {
+      get_next_token();
+
+      auto if_statement_node = std::make_shared<IfStatementNode>(current_block, tok.text.data() - tmpl.content.c_str());
+      current_block->nodes.emplace_back(if_statement_node);
+      if_statement_stack.emplace(if_statement_node.get());
+      current_block = &if_statement_node->true_statement;
+      current_expression_list = &if_statement_node->condition;
+
+      if (!parse_expression(tmpl, closing)) {
+        return false;
+      }
+    } else if (tok.text == static_cast<decltype(tok.text)>("else")) {
+      if (if_statement_stack.empty()) {
+        throw_parser_error("else without matching if");
+      }
+      auto& if_statement_data = if_statement_stack.top();
+      get_next_token();
+
+      if_statement_data->has_false_statement = true;
+      current_block = &if_statement_data->false_statement;
+
+      // Chained else if
+      if (tok.kind == Token::Kind::Id && tok.text == static_cast<decltype(tok.text)>("if")) {
+        get_next_token();
+
+        auto if_statement_node = std::make_shared<IfStatementNode>(true, current_block, tok.text.data() - tmpl.content.c_str());
+        current_block->nodes.emplace_back(if_statement_node);
+        if_statement_stack.emplace(if_statement_node.get());
+        current_block = &if_statement_node->true_statement;
+        current_expression_list = &if_statement_node->condition;
+
+        if (!parse_expression(tmpl, closing)) {
+          return false;
+        }
+      }
+    } else if (tok.text == static_cast<decltype(tok.text)>("endif")) {
+      if (if_statement_stack.empty()) {
+        throw_parser_error("endif without matching if");
+      }
+
+      // Nested if statements
+      while (if_statement_stack.top()->is_nested) {
+        if_statement_stack.pop();
+      }
+
+      auto& if_statement_data = if_statement_stack.top();
+      get_next_token();
+
+      current_block = if_statement_data->parent;
+      if_statement_stack.pop();
+    } else if (tok.text == static_cast<decltype(tok.text)>("block")) {
+      get_next_token();
+
+      if (tok.kind != Token::Kind::Id) {
+        throw_parser_error("expected block name, got '" + tok.describe() + "'");
+      }
+
+      const std::string block_name = static_cast<std::string>(tok.text);
+
+      auto block_statement_node = std::make_shared<BlockStatementNode>(current_block, block_name, tok.text.data() - tmpl.content.c_str());
+      current_block->nodes.emplace_back(block_statement_node);
+      block_statement_stack.emplace(block_statement_node.get());
+      current_block = &block_statement_node->block;
+      auto success = tmpl.block_storage.emplace(block_name, block_statement_node);
+      if (!success.second) {
+        throw_parser_error("block with the name '" + block_name + "' does already exist");
+      }
+
+      get_next_token();
+    } else if (tok.text == static_cast<decltype(tok.text)>("endblock")) {
+      if (block_statement_stack.empty()) {
+        throw_parser_error("endblock without matching block");
+      }
+
+      auto& block_statement_data = block_statement_stack.top();
+      get_next_token();
+
+      current_block = block_statement_data->parent;
+      block_statement_stack.pop();
+    } else if (tok.text == static_cast<decltype(tok.text)>("for")) {
+      get_next_token();
+
+      // options: for a in arr; for a, b in obj
+      if (tok.kind != Token::Kind::Id) {
+        throw_parser_error("expected id, got '" + tok.describe() + "'");
+      }
+
+      Token value_token = tok;
+      get_next_token();
+
+      // Object type
+      std::shared_ptr<ForStatementNode> for_statement_node;
+      if (tok.kind == Token::Kind::Comma) {
+        get_next_token();
+        if (tok.kind != Token::Kind::Id) {
+          throw_parser_error("expected id, got '" + tok.describe() + "'");
+        }
+
+        Token key_token = std::move(value_token);
+        value_token = tok;
+        get_next_token();
+
+        for_statement_node = std::make_shared<ForObjectStatementNode>(static_cast<std::string>(key_token.text), static_cast<std::string>(value_token.text),
+                                                                      current_block, tok.text.data() - tmpl.content.c_str());
+
+        // Array type
+      } else {
+        for_statement_node =
+            std::make_shared<ForArrayStatementNode>(static_cast<std::string>(value_token.text), current_block, tok.text.data() - tmpl.content.c_str());
+      }
+
+      current_block->nodes.emplace_back(for_statement_node);
+      for_statement_stack.emplace(for_statement_node.get());
+      current_block = &for_statement_node->body;
+      current_expression_list = &for_statement_node->condition;
+
+      if (tok.kind != Token::Kind::Id || tok.text != static_cast<decltype(tok.text)>("in")) {
+        throw_parser_error("expected 'in', got '" + tok.describe() + "'");
+      }
+      get_next_token();
+
+      if (!parse_expression(tmpl, closing)) {
+        return false;
+      }
+    } else if (tok.text == static_cast<decltype(tok.text)>("endfor")) {
+      if (for_statement_stack.empty()) {
+        throw_parser_error("endfor without matching for");
+      }
+
+      auto& for_statement_data = for_statement_stack.top();
+      get_next_token();
+
+      current_block = for_statement_data->parent;
+      for_statement_stack.pop();
+    } else if (tok.text == static_cast<decltype(tok.text)>("include")) {
+      get_next_token();
+
+      std::string template_name = parse_filename(tok);
+      add_to_template_storage(path, template_name);
+
+      current_block->nodes.emplace_back(std::make_shared<IncludeStatementNode>(template_name, tok.text.data() - tmpl.content.c_str()));
+
+      get_next_token();
+    } else if (tok.text == static_cast<decltype(tok.text)>("extends")) {
+      get_next_token();
+
+      std::string template_name = parse_filename(tok);
+      add_to_template_storage(path, template_name);
+
+      current_block->nodes.emplace_back(std::make_shared<ExtendsStatementNode>(template_name, tok.text.data() - tmpl.content.c_str()));
+
+      get_next_token();
+    } else if (tok.text == static_cast<decltype(tok.text)>("set")) {
+      get_next_token();
+
+      if (tok.kind != Token::Kind::Id) {
+        throw_parser_error("expected variable name, got '" + tok.describe() + "'");
+      }
+
+      std::string key = static_cast<std::string>(tok.text);
+      get_next_token();
+
+      auto set_statement_node = std::make_shared<SetStatementNode>(key, tok.text.data() - tmpl.content.c_str());
+      current_block->nodes.emplace_back(set_statement_node);
+      current_expression_list = &set_statement_node->expression;
+
+      if (tok.text != static_cast<decltype(tok.text)>("=")) {
+        throw_parser_error("expected '=', got '" + tok.describe() + "'");
+      }
+      get_next_token();
+
+      if (!parse_expression(tmpl, closing)) {
+        return false;
+      }
+    } else {
+      return false;
+    }
+    return true;
+  }
+
+  void parse_into(Template& tmpl, std::string_view path) {
+    lexer.start(tmpl.content);
+    current_block = &tmpl.root;
+
+    for (;;) {
+      get_next_token();
+      switch (tok.kind) {
+      case Token::Kind::Eof: {
+        if (!if_statement_stack.empty()) {
+          throw_parser_error("unmatched if");
+        }
+        if (!for_statement_stack.empty()) {
+          throw_parser_error("unmatched for");
+        }
+      }
+        return;
+      case Token::Kind::Text: {
+        current_block->nodes.emplace_back(std::make_shared<TextNode>(tok.text.data() - tmpl.content.c_str(), tok.text.size()));
+      } break;
+      case Token::Kind::StatementOpen: {
+        get_next_token();
+        if (!parse_statement(tmpl, Token::Kind::StatementClose, path)) {
+          throw_parser_error("expected statement, got '" + tok.describe() + "'");
+        }
+        if (tok.kind != Token::Kind::StatementClose) {
+          throw_parser_error("expected statement close, got '" + tok.describe() + "'");
+        }
+      } break;
+      case Token::Kind::LineStatementOpen: {
+        get_next_token();
+        if (!parse_statement(tmpl, Token::Kind::LineStatementClose, path)) {
+          throw_parser_error("expected statement, got '" + tok.describe() + "'");
+        }
+        if (tok.kind != Token::Kind::LineStatementClose && tok.kind != Token::Kind::Eof) {
+          throw_parser_error("expected line statement close, got '" + tok.describe() + "'");
+        }
+      } break;
+      case Token::Kind::ExpressionOpen: {
+        get_next_token();
+
+        auto expression_list_node = std::make_shared<ExpressionListNode>(tok.text.data() - tmpl.content.c_str());
+        current_block->nodes.emplace_back(expression_list_node);
+        current_expression_list = expression_list_node.get();
+
+        if (!parse_expression(tmpl, Token::Kind::ExpressionClose)) {
+          throw_parser_error("expected expression, got '" + tok.describe() + "'");
+        }
+
+        if (tok.kind != Token::Kind::ExpressionClose) {
+          throw_parser_error("expected expression close, got '" + tok.describe() + "'");
+        }
+      } break;
+      case Token::Kind::CommentOpen: {
+        get_next_token();
+        if (tok.kind != Token::Kind::CommentClose) {
+          throw_parser_error("expected comment close, got '" + tok.describe() + "'");
+        }
+      } break;
+      default: {
+        throw_parser_error("unexpected token '" + tok.describe() + "'");
+      } break;
+      }
+    }
+  }
+
+public:
+  explicit Parser(const ParserConfig& parser_config, const LexerConfig& lexer_config, TemplateStorage& template_storage,
+                  const FunctionStorage& function_storage)
+      : config(parser_config), lexer(lexer_config), template_storage(template_storage), function_storage(function_storage) {}
+
+  Template parse(std::string_view input, std::string_view path) {
+    auto result = Template(static_cast<std::string>(input));
+    parse_into(result, path);
+    return result;
+  }
+
+  Template parse(std::string_view input) {
+    return parse(input, "./");
+  }
+
+  void parse_into_template(Template& tmpl, std::string_view filename) {
+    std::string_view path = filename.substr(0, filename.find_last_of("/\\") + 1);
+
+    // StringRef path = sys::path::parent_path(filename);
+    auto sub_parser = Parser(config, lexer.get_config(), template_storage, function_storage);
+    sub_parser.parse_into(tmpl, path);
+  }
+
+  std::string load_file(const std::string& filename) {
+    std::ifstream file;
+    file.open(filename);
+    if (file.fail()) {
+      INJA_THROW(FileError("failed accessing file at '" + filename + "'"));
+    }
+    std::string text((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
+    return text;
+  }
+};
+
+} // namespace inja
+
+#endif // INCLUDE_INJA_PARSER_HPP_
+
+// #include "renderer.hpp"
+#ifndef INCLUDE_INJA_RENDERER_HPP_
+#define INCLUDE_INJA_RENDERER_HPP_
+
+#include <algorithm>
+#include <numeric>
+#include <string>
+#include <utility>
+#include <vector>
+
+// #include "config.hpp"
+
+// #include "exceptions.hpp"
+
+// #include "node.hpp"
+
+// #include "template.hpp"
+
+// #include "utils.hpp"
+
+
+namespace inja {
+
+/*!
+ * \brief Class for rendering a Template with data.
+ */
+class Renderer : public NodeVisitor {
+  using Op = FunctionStorage::Operation;
+
+  const RenderConfig config;
+  const TemplateStorage& template_storage;
+  const FunctionStorage& function_storage;
+
+  const Template* current_template;
+  size_t current_level {0};
+  std::vector<const Template*> template_stack;
+  std::vector<const BlockStatementNode*> block_statement_stack;
+
+  const json* data_input;
+  std::ostream* output_stream;
+
+  json additional_data;
+  json* current_loop_data = &additional_data["loop"];
+
+  std::vector<std::shared_ptr<json>> data_tmp_stack;
+  std::stack<const json*> data_eval_stack;
+  std::stack<const DataNode*> not_found_stack;
+
+  bool break_rendering {false};
+
+  static bool truthy(const json* data) {
+    if (data->is_boolean()) {
+      return data->get<bool>();
+    } else if (data->is_number()) {
+      return (*data != 0);
+    } else if (data->is_null()) {
+      return false;
+    }
+    return !data->empty();
+  }
+
+  void print_data(const std::shared_ptr<json> value) {
+    if (value->is_string()) {
+      *output_stream << value->get_ref<const json::string_t&>();
+    } else if (value->is_number_integer()) {
+      *output_stream << value->get<const json::number_integer_t>();
+    } else if (value->is_null()) {
+    } else {
+      *output_stream << value->dump();
+    }
+  }
+
+  const std::shared_ptr<json> eval_expression_list(const ExpressionListNode& expression_list) {
+    if (!expression_list.root) {
+      throw_renderer_error("empty expression", expression_list);
+    }
+
+    expression_list.root->accept(*this);
+
+    if (data_eval_stack.empty()) {
+      throw_renderer_error("empty expression", expression_list);
+    } else if (data_eval_stack.size() != 1) {
+      throw_renderer_error("malformed expression", expression_list);
+    }
+
+    const auto result = data_eval_stack.top();
+    data_eval_stack.pop();
+
+    if (!result) {
+      if (not_found_stack.empty()) {
+        throw_renderer_error("expression could not be evaluated", expression_list);
+      }
+
+      auto node = not_found_stack.top();
+      not_found_stack.pop();
+
+      throw_renderer_error("variable '" + static_cast<std::string>(node->name) + "' not found", *node);
+    }
+    return std::make_shared<json>(*result);
+  }
+
+  void throw_renderer_error(const std::string& message, const AstNode& node) {
+    SourceLocation loc = get_source_location(current_template->content, node.pos);
+    INJA_THROW(RenderError(message, loc));
+  }
+
+  void make_result(const json&& result) {
+    auto result_ptr = std::make_shared<json>(result);
+    data_tmp_stack.push_back(result_ptr);
+    data_eval_stack.push(result_ptr.get());
+  }
+
+  template <size_t N, size_t N_start = 0, bool throw_not_found = true> std::array<const json*, N> get_arguments(const FunctionNode& node) {
+    if (node.arguments.size() < N_start + N) {
+      throw_renderer_error("function needs " + std::to_string(N_start + N) + " variables, but has only found " + std::to_string(node.arguments.size()), node);
+    }
+
+    for (size_t i = N_start; i < N_start + N; i += 1) {
+      node.arguments[i]->accept(*this);
+    }
+
+    if (data_eval_stack.size() < N) {
+      throw_renderer_error("function needs " + std::to_string(N) + " variables, but has only found " + std::to_string(data_eval_stack.size()), node);
+    }
+
+    std::array<const json*, N> result;
+    for (size_t i = 0; i < N; i += 1) {
+      result[N - i - 1] = data_eval_stack.top();
+      data_eval_stack.pop();
+
+      if (!result[N - i - 1]) {
+        const auto data_node = not_found_stack.top();
+        not_found_stack.pop();
+
+        if (throw_not_found) {
+          throw_renderer_error("variable '" + static_cast<std::string>(data_node->name) + "' not found", *data_node);
+        }
+      }
+    }
+    return result;
+  }
+
+  template <bool throw_not_found = true> Arguments get_argument_vector(const FunctionNode& node) {
+    const size_t N = node.arguments.size();
+    for (auto a : node.arguments) {
+      a->accept(*this);
+    }
+
+    if (data_eval_stack.size() < N) {
+      throw_renderer_error("function needs " + std::to_string(N) + " variables, but has only found " + std::to_string(data_eval_stack.size()), node);
+    }
+
+    Arguments result {N};
+    for (size_t i = 0; i < N; i += 1) {
+      result[N - i - 1] = data_eval_stack.top();
+      data_eval_stack.pop();
+
+      if (!result[N - i - 1]) {
+        const auto data_node = not_found_stack.top();
+        not_found_stack.pop();
+
+        if (throw_not_found) {
+          throw_renderer_error("variable '" + static_cast<std::string>(data_node->name) + "' not found", *data_node);
+        }
+      }
+    }
+    return result;
+  }
+
+  void visit(const BlockNode& node) {
+    for (auto& n : node.nodes) {
+      n->accept(*this);
+
+      if (break_rendering) {
+        break;
+      }
+    }
+  }
+
+  void visit(const TextNode& node) {
+    output_stream->write(current_template->content.c_str() + node.pos, node.length);
+  }
+
+  void visit(const ExpressionNode&) {}
+
+  void visit(const LiteralNode& node) {
+    data_eval_stack.push(&node.value);
+  }
+
+  void visit(const DataNode& node) {
+    if (additional_data.contains(node.ptr)) {
+      data_eval_stack.push(&(additional_data[node.ptr]));
+    } else if (data_input->contains(node.ptr)) {
+      data_eval_stack.push(&(*data_input)[node.ptr]);
+    } else {
+      // Try to evaluate as a no-argument callback
+      const auto function_data = function_storage.find_function(node.name, 0);
+      if (function_data.operation == FunctionStorage::Operation::Callback) {
+        Arguments empty_args {};
+        const auto value = std::make_shared<json>(function_data.callback(empty_args));
+        data_tmp_stack.push_back(value);
+        data_eval_stack.push(value.get());
+      } else {
+        data_eval_stack.push(nullptr);
+        not_found_stack.emplace(&node);
+      }
+    }
+  }
+
+  void visit(const FunctionNode& node) {
+    switch (node.operation) {
+    case Op::Not: {
+      const auto args = get_arguments<1>(node);
+      make_result(!truthy(args[0]));
+    } break;
+    case Op::And: {
+      make_result(truthy(get_arguments<1, 0>(node)[0]) && truthy(get_arguments<1, 1>(node)[0]));
+    } break;
+    case Op::Or: {
+      make_result(truthy(get_arguments<1, 0>(node)[0]) || truthy(get_arguments<1, 1>(node)[0]));
+    } break;
+    case Op::In: {
+      const auto args = get_arguments<2>(node);
+      make_result(std::find(args[1]->begin(), args[1]->end(), *args[0]) != args[1]->end());
+    } break;
+    case Op::Equal: {
+      const auto args = get_arguments<2>(node);
+      make_result(*args[0] == *args[1]);
+    } break;
+    case Op::NotEqual: {
+      const auto args = get_arguments<2>(node);
+      make_result(*args[0] != *args[1]);
+    } break;
+    case Op::Greater: {
+      const auto args = get_arguments<2>(node);
+      make_result(*args[0] > *args[1]);
+    } break;
+    case Op::GreaterEqual: {
+      const auto args = get_arguments<2>(node);
+      make_result(*args[0] >= *args[1]);
+    } break;
+    case Op::Less: {
+      const auto args = get_arguments<2>(node);
+      make_result(*args[0] < *args[1]);
+    } break;
+    case Op::LessEqual: {
+      const auto args = get_arguments<2>(node);
+      make_result(*args[0] <= *args[1]);
+    } break;
+    case Op::Add: {
+      const auto args = get_arguments<2>(node);
+      if (args[0]->is_string() && args[1]->is_string()) {
+        make_result(args[0]->get_ref<const std::string&>() + args[1]->get_ref<const std::string&>());
+      } else if (args[0]->is_number_integer() && args[1]->is_number_integer()) {
+        make_result(args[0]->get<int>() + args[1]->get<int>());
+      } else {
+        make_result(args[0]->get<double>() + args[1]->get<double>());
+      }
+    } break;
+    case Op::Subtract: {
+      const auto args = get_arguments<2>(node);
+      if (args[0]->is_number_integer() && args[1]->is_number_integer()) {
+        make_result(args[0]->get<int>() - args[1]->get<int>());
+      } else {
+        make_result(args[0]->get<double>() - args[1]->get<double>());
+      }
+    } break;
+    case Op::Multiplication: {
+      const auto args = get_arguments<2>(node);
+      if (args[0]->is_number_integer() && args[1]->is_number_integer()) {
+        make_result(args[0]->get<int>() * args[1]->get<int>());
+      } else {
+        make_result(args[0]->get<double>() * args[1]->get<double>());
+      }
+    } break;
+    case Op::Division: {
+      const auto args = get_arguments<2>(node);
+      if (args[1]->get<double>() == 0) {
+        throw_renderer_error("division by zero", node);
+      }
+      make_result(args[0]->get<double>() / args[1]->get<double>());
+    } break;
+    case Op::Power: {
+      const auto args = get_arguments<2>(node);
+      if (args[0]->is_number_integer() && args[1]->get<int>() >= 0) {
+        int result = static_cast<int>(std::pow(args[0]->get<int>(), args[1]->get<int>()));
+        make_result(result);
+      } else {
+        double result = std::pow(args[0]->get<double>(), args[1]->get<int>());
+        make_result(result);
+      }
+    } break;
+    case Op::Modulo: {
+      const auto args = get_arguments<2>(node);
+      make_result(args[0]->get<int>() % args[1]->get<int>());
+    } break;
+    case Op::AtId: {
+      const auto container = get_arguments<1, 0, false>(node)[0];
+      node.arguments[1]->accept(*this);
+      if (not_found_stack.empty()) {
+        throw_renderer_error("could not find element with given name", node);
+      }
+      const auto id_node = not_found_stack.top();
+      not_found_stack.pop();
+      data_eval_stack.pop();
+      data_eval_stack.push(&container->at(id_node->name));
+    } break;
+    case Op::At: {
+      const auto args = get_arguments<2>(node);
+      if (args[0]->is_object()) {
+        data_eval_stack.push(&args[0]->at(args[1]->get<std::string>()));
+      } else {
+        data_eval_stack.push(&args[0]->at(args[1]->get<int>()));
+      }
+    } break;
+    case Op::Default: {
+      const auto test_arg = get_arguments<1, 0, false>(node)[0];
+      data_eval_stack.push(test_arg ? test_arg : get_arguments<1, 1>(node)[0]);
+    } break;
+    case Op::DivisibleBy: {
+      const auto args = get_arguments<2>(node);
+      const int divisor = args[1]->get<int>();
+      make_result((divisor != 0) && (args[0]->get<int>() % divisor == 0));
+    } break;
+    case Op::Even: {
+      make_result(get_arguments<1>(node)[0]->get<int>() % 2 == 0);
+    } break;
+    case Op::Exists: {
+      auto&& name = get_arguments<1>(node)[0]->get_ref<const std::string&>();
+      make_result(data_input->contains(json::json_pointer(DataNode::convert_dot_to_ptr(name))));
+    } break;
+    case Op::ExistsInObject: {
+      const auto args = get_arguments<2>(node);
+      auto&& name = args[1]->get_ref<const std::string&>();
+      make_result(args[0]->find(name) != args[0]->end());
+    } break;
+    case Op::First: {
+      const auto result = &get_arguments<1>(node)[0]->front();
+      data_eval_stack.push(result);
+    } break;
+    case Op::Float: {
+      make_result(std::stod(get_arguments<1>(node)[0]->get_ref<const std::string&>()));
+    } break;
+    case Op::Int: {
+      make_result(std::stoi(get_arguments<1>(node)[0]->get_ref<const std::string&>()));
+    } break;
+    case Op::Last: {
+      const auto result = &get_arguments<1>(node)[0]->back();
+      data_eval_stack.push(result);
+    } break;
+    case Op::Length: {
+      const auto val = get_arguments<1>(node)[0];
+      if (val->is_string()) {
+        make_result(val->get_ref<const std::string&>().length());
+      } else {
+        make_result(val->size());
+      }
+    } break;
+    case Op::Lower: {
+      std::string result = get_arguments<1>(node)[0]->get<std::string>();
+      std::transform(result.begin(), result.end(), result.begin(), ::tolower);
+      make_result(std::move(result));
+    } break;
+    case Op::Max: {
+      const auto args = get_arguments<1>(node);
+      const auto result = std::max_element(args[0]->begin(), args[0]->end());
+      data_eval_stack.push(&(*result));
+    } break;
+    case Op::Min: {
+      const auto args = get_arguments<1>(node);
+      const auto result = std::min_element(args[0]->begin(), args[0]->end());
+      data_eval_stack.push(&(*result));
+    } break;
+    case Op::Odd: {
+      make_result(get_arguments<1>(node)[0]->get<int>() % 2 != 0);
+    } break;
+    case Op::Range: {
+      std::vector<int> result(get_arguments<1>(node)[0]->get<int>());
+      std::iota(result.begin(), result.end(), 0);
+      make_result(std::move(result));
+    } break;
+    case Op::Round: {
+      const auto args = get_arguments<2>(node);
+      const int precision = args[1]->get<int>();
+      const double result = std::round(args[0]->get<double>() * std::pow(10.0, precision)) / std::pow(10.0, precision);
+      if (precision == 0) {
+        make_result(int(result));
+      } else {
+        make_result(result);
+      }
+    } break;
+    case Op::Sort: {
+      auto result_ptr = std::make_shared<json>(get_arguments<1>(node)[0]->get<std::vector<json>>());
+      std::sort(result_ptr->begin(), result_ptr->end());
+      data_tmp_stack.push_back(result_ptr);
+      data_eval_stack.push(result_ptr.get());
+    } break;
+    case Op::Upper: {
+      std::string result = get_arguments<1>(node)[0]->get<std::string>();
+      std::transform(result.begin(), result.end(), result.begin(), ::toupper);
+      make_result(std::move(result));
+    } break;
+    case Op::IsBoolean: {
+      make_result(get_arguments<1>(node)[0]->is_boolean());
+    } break;
+    case Op::IsNumber: {
+      make_result(get_arguments<1>(node)[0]->is_number());
+    } break;
+    case Op::IsInteger: {
+      make_result(get_arguments<1>(node)[0]->is_number_integer());
+    } break;
+    case Op::IsFloat: {
+      make_result(get_arguments<1>(node)[0]->is_number_float());
+    } break;
+    case Op::IsObject: {
+      make_result(get_arguments<1>(node)[0]->is_object());
+    } break;
+    case Op::IsArray: {
+      make_result(get_arguments<1>(node)[0]->is_array());
+    } break;
+    case Op::IsString: {
+      make_result(get_arguments<1>(node)[0]->is_string());
+    } break;
+    case Op::Callback: {
+      auto args = get_argument_vector(node);
+      make_result(node.callback(args));
+    } break;
+    case Op::Super: {
+      const auto args = get_argument_vector(node);
+      const size_t old_level = current_level;
+      const size_t level_diff = (args.size() == 1) ? args[0]->get<int>() : 1;
+      const size_t level = current_level + level_diff;
+
+      if (block_statement_stack.empty()) {
+        throw_renderer_error("super() call is not within a block", node);
+      }
+
+      if (level < 1 || level > template_stack.size() - 1) {
+        throw_renderer_error("level of super() call does not match parent templates (between 1 and " + std::to_string(template_stack.size() - 1) + ")", node);
+      }
+
+      const auto current_block_statement = block_statement_stack.back();
+      const Template* new_template = template_stack.at(level);
+      const Template* old_template = current_template;
+      const auto block_it = new_template->block_storage.find(current_block_statement->name);
+      if (block_it != new_template->block_storage.end()) {
+        current_template = new_template;
+        current_level = level;
+        block_it->second->block.accept(*this);
+        current_level = old_level;
+        current_template = old_template;
+      } else {
+        throw_renderer_error("could not find block with name '" + current_block_statement->name + "'", node);
+      }
+      make_result(nullptr);
+    } break;
+    case Op::Join: {
+      const auto args = get_arguments<2>(node);
+      const auto separator = args[1]->get<std::string>();
+      std::ostringstream os;
+      std::string sep;
+      for (const auto& value : *args[0]) {
+        os << sep;
+        if (value.is_string()) {
+          os << value.get<std::string>(); // otherwise the value is surrounded with ""
+        } else {
+          os << value.dump();
+        }
+        sep = separator;
+      }
+      make_result(os.str());
+    } break;
+    case Op::ParenLeft:
+    case Op::ParenRight:
+    case Op::None:
+      break;
+    }
+  }
+
+  void visit(const ExpressionListNode& node) {
+    print_data(eval_expression_list(node));
+  }
+
+  void visit(const StatementNode&) {}
+
+  void visit(const ForStatementNode&) {}
+
+  void visit(const ForArrayStatementNode& node) {
+    const auto result = eval_expression_list(node.condition);
+    if (!result->is_array()) {
+      throw_renderer_error("object must be an array", node);
+    }
+
+    if (!current_loop_data->empty()) {
+      auto tmp = *current_loop_data; // Because of clang-3
+      (*current_loop_data)["parent"] = std::move(tmp);
+    }
+
+    size_t index = 0;
+    (*current_loop_data)["is_first"] = true;
+    (*current_loop_data)["is_last"] = (result->size() <= 1);
+    for (auto it = result->begin(); it != result->end(); ++it) {
+      additional_data[static_cast<std::string>(node.value)] = *it;
+
+      (*current_loop_data)["index"] = index;
+      (*current_loop_data)["index1"] = index + 1;
+      if (index == 1) {
+        (*current_loop_data)["is_first"] = false;
+      }
+      if (index == result->size() - 1) {
+        (*current_loop_data)["is_last"] = true;
+      }
+
+      node.body.accept(*this);
+      ++index;
+    }
+
+    additional_data[static_cast<std::string>(node.value)].clear();
+    if (!(*current_loop_data)["parent"].empty()) {
+      const auto tmp = (*current_loop_data)["parent"];
+      *current_loop_data = std::move(tmp);
+    } else {
+      current_loop_data = &additional_data["loop"];
+    }
+  }
+
+  void visit(const ForObjectStatementNode& node) {
+    const auto result = eval_expression_list(node.condition);
+    if (!result->is_object()) {
+      throw_renderer_error("object must be an object", node);
+    }
+
+    if (!current_loop_data->empty()) {
+      (*current_loop_data)["parent"] = std::move(*current_loop_data);
+    }
+
+    size_t index = 0;
+    (*current_loop_data)["is_first"] = true;
+    (*current_loop_data)["is_last"] = (result->size() <= 1);
+    for (auto it = result->begin(); it != result->end(); ++it) {
+      additional_data[static_cast<std::string>(node.key)] = it.key();
+      additional_data[static_cast<std::string>(node.value)] = it.value();
+
+      (*current_loop_data)["index"] = index;
+      (*current_loop_data)["index1"] = index + 1;
+      if (index == 1) {
+        (*current_loop_data)["is_first"] = false;
+      }
+      if (index == result->size() - 1) {
+        (*current_loop_data)["is_last"] = true;
+      }
+
+      node.body.accept(*this);
+      ++index;
+    }
+
+    additional_data[static_cast<std::string>(node.key)].clear();
+    additional_data[static_cast<std::string>(node.value)].clear();
+    if (!(*current_loop_data)["parent"].empty()) {
+      *current_loop_data = std::move((*current_loop_data)["parent"]);
+    } else {
+      current_loop_data = &additional_data["loop"];
+    }
+  }
+
+  void visit(const IfStatementNode& node) {
+    const auto result = eval_expression_list(node.condition);
+    if (truthy(result.get())) {
+      node.true_statement.accept(*this);
+    } else if (node.has_false_statement) {
+      node.false_statement.accept(*this);
+    }
+  }
+
+  void visit(const IncludeStatementNode& node) {
+    auto sub_renderer = Renderer(config, template_storage, function_storage);
+    const auto included_template_it = template_storage.find(node.file);
+    if (included_template_it != template_storage.end()) {
+      sub_renderer.render_to(*output_stream, included_template_it->second, *data_input, &additional_data);
+    } else if (config.throw_at_missing_includes) {
+      throw_renderer_error("include '" + node.file + "' not found", node);
+    }
+  }
+
+  void visit(const ExtendsStatementNode& node) {
+    const auto included_template_it = template_storage.find(node.file);
+    if (included_template_it != template_storage.end()) {
+      const Template* parent_template = &included_template_it->second;
+      render_to(*output_stream, *parent_template, *data_input, &additional_data);
+      break_rendering = true;
+    } else if (config.throw_at_missing_includes) {
+      throw_renderer_error("extends '" + node.file + "' not found", node);
+    }
+  }
+
+  void visit(const BlockStatementNode& node) {
+    const size_t old_level = current_level;
+    current_level = 0;
+    current_template = template_stack.front();
+    const auto block_it = current_template->block_storage.find(node.name);
+    if (block_it != current_template->block_storage.end()) {
+      block_statement_stack.emplace_back(&node);
+      block_it->second->block.accept(*this);
+      block_statement_stack.pop_back();
+    }
+    current_level = old_level;
+    current_template = template_stack.back();
+  }
+
+  void visit(const SetStatementNode& node) {
+    std::string ptr = node.key;
+    replace_substring(ptr, ".", "/");
+    ptr = "/" + ptr;
+    additional_data[json::json_pointer(ptr)] = *eval_expression_list(node.expression);
+  }
+
+public:
+  Renderer(const RenderConfig& config, const TemplateStorage& template_storage, const FunctionStorage& function_storage)
+      : config(config), template_storage(template_storage), function_storage(function_storage) {}
+
+  void render_to(std::ostream& os, const Template& tmpl, const json& data, json* loop_data = nullptr) {
+    output_stream = &os;
+    current_template = &tmpl;
+    data_input = &data;
+    if (loop_data) {
+      additional_data = *loop_data;
+      current_loop_data = &additional_data["loop"];
+    }
+
+    template_stack.emplace_back(current_template);
+    current_template->root.accept(*this);
+
+    data_tmp_stack.clear();
+  }
+};
+
+} // namespace inja
+
+#endif // INCLUDE_INJA_RENDERER_HPP_
+
+// #include "template.hpp"
+
+// #include "utils.hpp"
+
+
+namespace inja {
+
+/*!
+ * \brief Class for changing the configuration.
+ */
+class Environment {
+  std::string input_path;
+  std::string output_path;
+
+  LexerConfig lexer_config;
+  ParserConfig parser_config;
+  RenderConfig render_config;
+
+  FunctionStorage function_storage;
+  TemplateStorage template_storage;
+
+public:
+  Environment(): Environment("") {}
+
+  explicit Environment(const std::string& global_path): input_path(global_path), output_path(global_path) {}
+
+  Environment(const std::string& input_path, const std::string& output_path): input_path(input_path), output_path(output_path) {}
+
+  /// Sets the opener and closer for template statements
+  void set_statement(const std::string& open, const std::string& close) {
+    lexer_config.statement_open = open;
+    lexer_config.statement_open_no_lstrip = open + "+";
+    lexer_config.statement_open_force_lstrip = open + "-";
+    lexer_config.statement_close = close;
+    lexer_config.statement_close_force_rstrip = "-" + close;
+    lexer_config.update_open_chars();
+  }
+
+  /// Sets the opener for template line statements
+  void set_line_statement(const std::string& open) {
+    lexer_config.line_statement = open;
+    lexer_config.update_open_chars();
+  }
+
+  /// Sets the opener and closer for template expressions
+  void set_expression(const std::string& open, const std::string& close) {
+    lexer_config.expression_open = open;
+    lexer_config.expression_open_force_lstrip = open + "-";
+    lexer_config.expression_close = close;
+    lexer_config.expression_close_force_rstrip = "-" + close;
+    lexer_config.update_open_chars();
+  }
+
+  /// Sets the opener and closer for template comments
+  void set_comment(const std::string& open, const std::string& close) {
+    lexer_config.comment_open = open;
+    lexer_config.comment_open_force_lstrip = open + "-";
+    lexer_config.comment_close = close;
+    lexer_config.comment_close_force_rstrip = "-" + close;
+    lexer_config.update_open_chars();
+  }
+
+  /// Sets whether to remove the first newline after a block
+  void set_trim_blocks(bool trim_blocks) {
+    lexer_config.trim_blocks = trim_blocks;
+  }
+
+  /// Sets whether to strip the spaces and tabs from the start of a line to a block
+  void set_lstrip_blocks(bool lstrip_blocks) {
+    lexer_config.lstrip_blocks = lstrip_blocks;
+  }
+
+  /// Sets the element notation syntax
+  void set_search_included_templates_in_files(bool search_in_files) {
+    parser_config.search_included_templates_in_files = search_in_files;
+  }
+
+  /// Sets whether a missing include will throw an error
+  void set_throw_at_missing_includes(bool will_throw) {
+    render_config.throw_at_missing_includes = will_throw;
+  }
+
+  Template parse(std::string_view input) {
+    Parser parser(parser_config, lexer_config, template_storage, function_storage);
+    return parser.parse(input);
+  }
+
+  Template parse_template(const std::string& filename) {
+    Parser parser(parser_config, lexer_config, template_storage, function_storage);
+    auto result = Template(parser.load_file(input_path + static_cast<std::string>(filename)));
+    parser.parse_into_template(result, input_path + static_cast<std::string>(filename));
+    return result;
+  }
+
+  Template parse_file(const std::string& filename) {
+    return parse_template(filename);
+  }
+
+  std::string render(std::string_view input, const json& data) {
+    return render(parse(input), data);
+  }
+
+  std::string render(const Template& tmpl, const json& data) {
+    std::stringstream os;
+    render_to(os, tmpl, data);
+    return os.str();
+  }
+
+  std::string render_file(const std::string& filename, const json& data) {
+    return render(parse_template(filename), data);
+  }
+
+  std::string render_file_with_json_file(const std::string& filename, const std::string& filename_data) {
+    const json data = load_json(filename_data);
+    return render_file(filename, data);
+  }
+
+  void write(const std::string& filename, const json& data, const std::string& filename_out) {
+    std::ofstream file(output_path + filename_out);
+    file << render_file(filename, data);
+    file.close();
+  }
+
+  void write(const Template& temp, const json& data, const std::string& filename_out) {
+    std::ofstream file(output_path + filename_out);
+    file << render(temp, data);
+    file.close();
+  }
+
+  void write_with_json_file(const std::string& filename, const std::string& filename_data, const std::string& filename_out) {
+    const json data = load_json(filename_data);
+    write(filename, data, filename_out);
+  }
+
+  void write_with_json_file(const Template& temp, const std::string& filename_data, const std::string& filename_out) {
+    const json data = load_json(filename_data);
+    write(temp, data, filename_out);
+  }
+
+  std::ostream& render_to(std::ostream& os, const Template& tmpl, const json& data) {
+    Renderer(render_config, template_storage, function_storage).render_to(os, tmpl, data);
+    return os;
+  }
+
+  std::string load_file(const std::string& filename) {
+    Parser parser(parser_config, lexer_config, template_storage, function_storage);
+    return parser.load_file(input_path + filename);
+  }
+
+  json load_json(const std::string& filename) {
+    std::ifstream file;
+    file.open(input_path + filename);
+    if (file.fail()) {
+      INJA_THROW(FileError("failed accessing file at '" + input_path + filename + "'"));
+    }
+
+    return json::parse(std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>());
+  }
+
+  /*!
+  @brief Adds a variadic callback
+  */
+  void add_callback(const std::string& name, const CallbackFunction& callback) {
+    add_callback(name, -1, callback);
+  }
+
+  /*!
+  @brief Adds a variadic void callback
+  */
+  void add_void_callback(const std::string& name, const VoidCallbackFunction& callback) {
+    add_void_callback(name, -1, callback);
+  }
+
+  /*!
+  @brief Adds a callback with given number or arguments
+  */
+  void add_callback(const std::string& name, int num_args, const CallbackFunction& callback) {
+    function_storage.add_callback(name, num_args, callback);
+  }
+
+  /*!
+  @brief Adds a void callback with given number or arguments
+  */
+  void add_void_callback(const std::string& name, int num_args, const VoidCallbackFunction& callback) {
+    function_storage.add_callback(name, num_args, [callback](Arguments& args) {
+      callback(args);
+      return json();
+    });
+  }
+
+  /** Includes a template with a given name into the environment.
+   * Then, a template can be rendered in another template using the
+   * include "<name>" syntax.
+   */
+  void include_template(const std::string& name, const Template& tmpl) {
+    template_storage[name] = tmpl;
+  }
+
+  /*!
+  @brief Sets a function that is called when an included file is not found
+  */
+  void set_include_callback(const std::function<Template(const std::string&, const std::string&)>& callback) {
+    parser_config.include_callback = callback;
+  }
+};
+
+/*!
+@brief render with default settings to a string
+*/
+inline std::string render(std::string_view input, const json& data) {
+  return Environment().render(input, data);
+}
+
+/*!
+@brief render with default settings to the given output stream
+*/
+inline void render_to(std::ostream& os, std::string_view input, const json& data) {
+  Environment env;
+  env.render_to(os, env.parse(input), data);
+}
+
+} // namespace inja
+
+#endif // INCLUDE_INJA_ENVIRONMENT_HPP_
+
+// #include "exceptions.hpp"
+
+// #include "parser.hpp"
+
+// #include "renderer.hpp"
+
+// #include "template.hpp"
+
+
+#endif // INCLUDE_INJA_INJA_HPP_

+ 1 - 1
ext/installfiles/mac/ZeroTier One.pkgproj

@@ -701,7 +701,7 @@
 				<key>USE_HFS+_COMPRESSION</key>
 				<false/>
 				<key>VERSION</key>
-				<string>1.8.7</string>
+				<string>1.10.0</string>
 			</dict>
 			<key>TYPE</key>
 			<integer>0</integer>

BIN
ext/installfiles/windows/Prerequisites/Visual C++ Redistributable for Visual Studio 2015-2022/VC_redist.x64.exe


BIN
ext/installfiles/windows/Prerequisites/Visual C++ Redistributable for Visual Studio 2015-2022/VC_redist.x86.exe


+ 83 - 46
ext/installfiles/windows/ZeroTier One.aip

@@ -1,12 +1,17 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<DOCUMENT Type="Advanced Installer" CreateVersion="10.9" version="19.2" Modules="enterprise" RootPath="." Language="en" Id="{DC564647-6BF0-4550-87F4-89C938D0159C}">
+<DOCUMENT Type="Advanced Installer" CreateVersion="10.9" version="19.5" Modules="enterprise" RootPath="." Language="en" Id="{DC564647-6BF0-4550-87F4-89C938D0159C}">
+  <COMPONENT cid="caphyon.advinst.msicomp.ProjectOptionsComponent">
+    <ROW Name="HiddenItems" Value="ActSyncAppComponent;CPLAppletComponent;AutorunComponent;GameUxComponent;SilverlightSlnComponent;AppXAppDetailsComponent;FixupComponent;AppXCapabilitiesComponent;AppXDependenciesComponent;AppXProductDetailsComponent;AppXVisualAssetsComponent;AppXAppDeclarationsComponent;AppXUriRulesComponent;MsiXDiffComponent;MsixManifestEditorComponent"/>
+  </COMPONENT>
   <COMPONENT cid="caphyon.advinst.msicomp.MsiPropsComponent">
     <ROW Property="AI_BITMAP_DISPLAY_MODE" Value="0"/>
     <ROW Property="AI_CURRENT_YEAR" Value="2022" ValueLocId="-"/>
     <ROW Property="AI_EMBD_MSI_EXTR_PATH" Value="[TempFolder]" ValueLocId="-"/>
     <ROW Property="AI_EXTERNALUIUNINSTALLERNAME" MultiBuildValue="DefaultBuild:aiui"/>
     <ROW Property="AI_FINDEXE_TITLE" Value="Select the installation package for [|ProductName]" ValueLocId="AI.Property.FindExeTitle"/>
+    <ROW Property="AI_PACKAGING_TOOL" Value="Advanced Installer 19.5 build 36301275" ValueLocId="-"/>
     <ROW Property="AI_PREDEF_LCONDS_PROPS" Value="AI_DETECTED_DOTNET_VERSION"/>
+    <ROW Property="AI_PREREQ_REPAIR_ENABLED" MultiBuildValue="ExeBuild:1"/>
     <ROW Property="AI_PRODUCTNAME_ARP" Value="ZeroTier One"/>
     <ROW Property="AI_REQUIRED_DOTNET_DISPLAY" MultiBuildValue="DefaultBuild:4.5" ValueLocId="-"/>
     <ROW Property="AI_REQUIRED_DOTNET_VERSION" MultiBuildValue="DefaultBuild:4.5" ValueLocId="-"/>
@@ -27,10 +32,10 @@
     <ROW Property="LIMITUI" MultiBuildValue="DefaultBuild:1"/>
     <ROW Property="MSIFASTINSTALL" MultiBuildValue="DefaultBuild:2"/>
     <ROW Property="Manufacturer" Value="ZeroTier, Inc."/>
-    <ROW Property="ProductCode" Value="1033:{1CF2BF7E-ADE7-440B-A50A-F2912D8B4D0D} " Type="16"/>
+    <ROW Property="ProductCode" Value="1033:{5FF7375F-69D4-4CAA-800B-3EA1E587AAAE} " Type="16"/>
     <ROW Property="ProductLanguage" Value="1033"/>
     <ROW Property="ProductName" Value="ZeroTier One"/>
-    <ROW Property="ProductVersion" Value="1.8.6"/>
+    <ROW Property="ProductVersion" Value="1.10.0"/>
     <ROW Property="REBOOT" MultiBuildValue="DefaultBuild:ReallySuppress"/>
     <ROW Property="SecureCustomProperties" Value="OLDPRODUCTS;AI_NEWERPRODUCTFOUND;AI_SETUPEXEPATH;SETUPEXEDIR"/>
     <ROW Property="UpgradeCode" Value="{B0E2A5F3-88B6-4E77-B922-CB4739B4C4C8}"/>
@@ -49,33 +54,38 @@
     <ROW Property="ZTHEADLESS" Value="No"/>
   </COMPONENT>
   <COMPONENT cid="caphyon.advinst.msicomp.MsiDirsComponent">
-    <ROW Directory="APPDIR" Directory_Parent="TARGETDIR" DefaultDir="APPDIR:." IsPseudoRoot="1" DirectoryOptions="3"/>
+    <ROW Directory="APPDIR" Directory_Parent="TARGETDIR" DefaultDir="APPDIR:." IsPseudoRoot="1" DirectoryOptions="15"/>
     <ROW Directory="CommonAppDataFolder" Directory_Parent="TARGETDIR" DefaultDir="COMMON~1|CommonAppDataFolder" IsPseudoRoot="1"/>
-    <ROW Directory="One_Dir" Directory_Parent="ZeroTier_Dir" DefaultDir="One"/>
+    <ROW Directory="One_Dir" Directory_Parent="ZeroTier_Dir" DefaultDir="One" DirectoryOptions="12"/>
     <ROW Directory="ProgramFilesFolder" Directory_Parent="TARGETDIR" DefaultDir="PROGRA~1|ProgramFilesFolder" IsPseudoRoot="1"/>
     <ROW Directory="ProgramMenuFolder" Directory_Parent="TARGETDIR" DefaultDir="PROGRA~2|ProgramMenuFolder" IsPseudoRoot="1"/>
     <ROW Directory="TARGETDIR" DefaultDir="SourceDir"/>
-    <ROW Directory="ZeroTier_Dir" Directory_Parent="CommonAppDataFolder" DefaultDir="ZeroTier"/>
+    <ROW Directory="ZeroTier_Dir" Directory_Parent="CommonAppDataFolder" DefaultDir="ZeroTier" DirectoryOptions="12"/>
     <ROW Directory="i686_1_Dir" Directory_Parent="ProgramMenuFolder" DefaultDir=".:i686"/>
-    <ROW Directory="i686_Dir" Directory_Parent="APPDIR" DefaultDir=".:i686" DirectoryOptions="3"/>
-    <ROW Directory="networks.d_Dir" Directory_Parent="One_Dir" DefaultDir="networks.d"/>
-    <ROW Directory="regid.201001.com.zerotier_Dir" Directory_Parent="CommonAppDataFolder" DefaultDir="REGID2~1.ZER|regid.2010-01.com.zerotier"/>
-    <ROW Directory="tapwindows_Dir" Directory_Parent="One_Dir" DefaultDir="TAP-WI~1|tap-windows"/>
-    <ROW Directory="x64_Dir" Directory_Parent="tapwindows_Dir" DefaultDir="x64"/>
-    <ROW Directory="x86_Dir" Directory_Parent="tapwindows_Dir" DefaultDir="x86"/>
+    <ROW Directory="i686_Dir" Directory_Parent="APPDIR" DefaultDir=".:i686" DirectoryOptions="15"/>
+    <ROW Directory="networks.d_Dir" Directory_Parent="One_Dir" DefaultDir="networks.d" DirectoryOptions="12"/>
+    <ROW Directory="regid.201001.com.zerotier_Dir" Directory_Parent="CommonAppDataFolder" DefaultDir="REGID2~1.ZER|regid.2010-01.com.zerotier" DirectoryOptions="12"/>
+    <ROW Directory="tapwindows_Dir" Directory_Parent="One_Dir" DefaultDir="TAP-WI~1|tap-windows" DirectoryOptions="12"/>
+    <ROW Directory="x64_Dir" Directory_Parent="tapwindows_Dir" DefaultDir="x64" DirectoryOptions="12"/>
+    <ROW Directory="x86_Dir" Directory_Parent="tapwindows_Dir" DefaultDir="x86" DirectoryOptions="12"/>
   </COMPONENT>
   <COMPONENT cid="caphyon.advinst.msicomp.MsiCompsComponent">
-    <ROW Component="A918597FE054CCCB65ABDBA0AD8F63C" ComponentId="{458A5336-4527-4409-BB9D-D074790820A6}" Directory_="APPDIR" Attributes="4" KeyPath="A918597FE054CCCB65ABDBA0AD8F63C" Options="2"/>
-    <ROW Component="AI_CustomARPName" ComponentId="{B098B41F-3C1C-4D4E-BB89-FF0C81A26E9C}" Directory_="APPDIR" Attributes="4" KeyPath="DisplayName" Options="1"/>
+    <ROW Component="A918597FE054CCCB65ABDBA0AD8F63C" ComponentId="{9EBBA2D0-7170-4C6C-9B07-9405F08DC282}" Directory_="APPDIR" Attributes="4" KeyPath="A918597FE054CCCB65ABDBA0AD8F63C" Options="2"/>
+    <ROW Component="AI_CustomARPName" ComponentId="{B1226053-207C-4922-AF29-49542B56F5FB}" Directory_="APPDIR" Attributes="4" KeyPath="DisplayName" Options="1"/>
     <ROW Component="AI_DisableModify" ComponentId="{46FFA8C5-A0CB-4E05-9AD3-911D543DE8CA}" Directory_="APPDIR" Attributes="4" KeyPath="NoModify" Options="1"/>
     <ROW Component="AI_ExePath" ComponentId="{8E02B36C-7A19-429B-A93E-77A9261AC918}" Directory_="APPDIR" Attributes="4" KeyPath="AI_ExePath"/>
     <ROW Component="APPDIR" ComponentId="{4DD7907D-D7FE-4CD6-B1A0-B5C1625F5133}" Directory_="APPDIR" Attributes="0"/>
-    <ROW Component="C4FE6FD5B7C4D07B3A313E754A9A6A8" ComponentId="{7A501107-9EEE-45A6-9609-F5E1A7372448}" Directory_="APPDIR" Attributes="260" KeyPath="C4FE6FD5B7C4D07B3A313E754A9A6A8" Options="2"/>
+    <ROW Component="C4FE6FD5B7C4D07B3A313E754A9A6A8" ComponentId="{8F2CBC66-14B3-4DF4-8F6E-4B79B080BB12}" Directory_="APPDIR" Attributes="4" KeyPath="C4FE6FD5B7C4D07B3A313E754A9A6A8" Options="2"/>
+    <ROW Component="One" ComponentId="{41AB11E7-066E-414A-96F8-F051D3D3B353}" Directory_="One_Dir" Attributes="0"/>
     <ROW Component="ProductInformation" ComponentId="{DB078D04-EA8E-4A7C-9001-89BAD932F9D9}" Directory_="APPDIR" Attributes="4" KeyPath="Version"/>
-    <ROW Component="RequiredApplication" ComponentId="{92E37C98-F8B3-4A85-AE77-3D8D9A223776}" Directory_="APPDIR" Attributes="4" Condition="VersionNT &gt;= 1000" KeyPath="RequiredApplication" Options="2"/>
+    <ROW Component="ZeroTier" ComponentId="{8864F744-9BDF-4891-88A1-6D23D76BCCB1}" Directory_="ZeroTier_Dir" Attributes="0"/>
     <ROW Component="i686" ComponentId="{6EC46014-3BFD-4017-ACBC-C4417D1D6361}" Directory_="i686_1_Dir" Attributes="0"/>
+    <ROW Component="i686_1" ComponentId="{60156BDC-31D7-47EE-A307-B62129607DD5}" Directory_="i686_Dir" Attributes="0"/>
     <ROW Component="networks.d" ComponentId="{EF54D0DF-889F-41DC-AF5C-4E7F96AB1C8B}" Directory_="networks.d_Dir" Attributes="0"/>
     <ROW Component="regid.201001.com.zerotier" ComponentId="{A39C80FC-6A8F-454F-9052-10DAC3C3B139}" Directory_="regid.201001.com.zerotier_Dir" Attributes="0"/>
+    <ROW Component="tapwindows" ComponentId="{3E9CBCCE-EC9D-4802-B8FD-DADB4CC532A2}" Directory_="tapwindows_Dir" Attributes="0"/>
+    <ROW Component="x64" ComponentId="{4DD1F90B-53F1-4390-BDF1-E6D9B39B8D80}" Directory_="x64_Dir" Attributes="0"/>
+    <ROW Component="x86" ComponentId="{8E83C577-3C22-49B7-82A8-369BE1F19224}" Directory_="x86_Dir" Attributes="0"/>
     <ROW Component="zerotier_desktop_ui.exe" ComponentId="{61A7F53C-C6C3-418D-A652-2E4D9F8173AA}" Directory_="APPDIR" Attributes="256" Condition="ZTHEADLESS = &quot;No&quot; AND VersionNT64" KeyPath="zerotier_desktop_ui.exe"/>
     <ROW Component="zerotier_desktop_ui.exe_1" ComponentId="{5CFEA823-6D17-4EAA-BBAA-810E1C89555D}" Directory_="i686_Dir" Attributes="0" Condition="ZTHEADLESS = &quot;No&quot; AND NOT VersionNT64" KeyPath="zerotier_desktop_ui.exe_1"/>
     <ROW Component="zerotierone_x64.exe" ComponentId="{DFCFB72D-B055-4E60-B6D8-81FF585C2183}" Directory_="One_Dir" Attributes="256" Condition="VersionNT64" KeyPath="zerotierone_x64.exe"/>
@@ -84,10 +94,9 @@
     <ROW Component="zttap300_x86_win10" ComponentId="{9F913E48-095B-4EA3-98DA-EDAB1593F3E3}" Directory_="x86_Dir" Attributes="0" Condition="NOT VersionNT64" KeyPath="zttap300.cat_3" Type="0"/>
   </COMPONENT>
   <COMPONENT cid="caphyon.advinst.msicomp.MsiFeatsComponent">
-    <ROW Feature="A918597FE054CCCB65ABDBA0AD8F63C" Title="Visual C++ Redistributable for Visual Studio 2015-2019 x86" Description="Visual C++ Redistributable for Visual Studio 2015-2019 x86" Display="3" Level="1" Attributes="0"/>
-    <ROW Feature="C4FE6FD5B7C4D07B3A313E754A9A6A8" Title="Visual C++ Redistributable for Visual Studio 2015-2019 x64" Description="Visual C++ Redistributable for Visual Studio 2015-2019 x64" Display="5" Level="1" Attributes="0"/>
+    <ROW Feature="A918597FE054CCCB65ABDBA0AD8F63C" Title="Visual C++ Redistributable for Visual Studio 2015-2022 x86" Description="Visual C++ Redistributable for Visual Studio 2015-2022 x86" Display="5" Level="1" Attributes="0"/>
+    <ROW Feature="C4FE6FD5B7C4D07B3A313E754A9A6A8" Title="Visual C++ Redistributable for Visual Studio 2015-2022 x64" Description="Visual C++ Redistributable for Visual Studio 2015-2022 x64" Display="3" Level="1" Attributes="0"/>
     <ROW Feature="MainFeature" Title="MainFeature" Description="Description" Display="1" Level="1" Directory_="APPDIR" Attributes="0"/>
-    <ROW Feature="RequiredApplication" Title="Microsoft Edge Webview2 Runtime" Description="Microsoft Edge Webview2 Runtime" Display="7" Level="1" Attributes="0"/>
     <ROW Feature="ZeroTierOne" Title="MainFeature" Description="ZeroTier One" Display="0" Level="1" Directory_="APPDIR" Attributes="0"/>
     <ATTRIBUTE name="CurrentFeature" value="ZeroTierOne"/>
   </COMPONENT>
@@ -242,9 +251,6 @@
     <ROW Dialog_="VerifyReadyDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_PATCH" Ordering="199"/>
     <ROW Dialog_="VerifyReadyDlg" Control_="Back" Event="NewDialog" Argument="PatchWelcomeDlg" Condition="AI_PATCH" Ordering="203"/>
     <ROW Dialog_="ResumeDlg" Control_="Install" Event="EndDialog" Argument="Return" Condition="AI_RESUME" Ordering="299"/>
-    <ROW Dialog_="ExitDialog" Control_="Finish" Event="DoAction" Argument="AI_CleanPrereq" Condition="1" Ordering="301"/>
-    <ROW Dialog_="FatalError" Control_="Finish" Event="DoAction" Argument="AI_CleanPrereq" Condition="1" Ordering="102"/>
-    <ROW Dialog_="UserExit" Control_="Finish" Event="DoAction" Argument="AI_CleanPrereq" Condition="1" Ordering="101"/>
     <ROW Dialog_="Windows7Warning" Control_="Cancel" Event="SpawnDialog" Argument="CancelDlg" Condition="1" Ordering="100"/>
     <ROW Dialog_="WelcomeDlg" Control_="Next" Event="SpawnDialog" Argument="OutOfRbDiskDlg" Condition="AI_INSTALL AND OutOfDiskSpace = 1 AND OutOfNoRbDiskSpace = 0 AND (PROMPTROLLBACKCOST=&quot;P&quot; OR NOT PROMPTROLLBACKCOST)" Ordering="5" Options="2"/>
     <ROW Dialog_="WelcomeDlg" Control_="Next" Event="EnableRollback" Argument="False" Condition="AI_INSTALL AND OutOfDiskSpace = 1 AND OutOfNoRbDiskSpace = 0 AND PROMPTROLLBACKCOST=&quot;D&quot;" Ordering="6" Options="2"/>
@@ -256,12 +262,21 @@
     <ROW Dialog_="WelcomeDlg" Control_="Back" Event="[ButtonText_Next]" Argument="[AI_ButtonText_Next_Orig]" Condition="AI_INSTALL" Ordering="0" Options="2"/>
     <ROW Dialog_="WelcomeDlg" Control_="Back" Event="[Text_Next]" Argument="[AI_Text_Next_Orig]" Condition="AI_INSTALL" Ordering="1" Options="2"/>
     <ROW Dialog_="ExitDialog" Control_="Finish" Event="DoAction" Argument="AI_ChainerScheduleReboot" Condition="Not AIEXTERNALUI" Ordering="302"/>
+    <ROW Dialog_="ExitDialog" Control_="Finish" Event="DoAction" Argument="AI_CleanPrereq" Condition="1" Ordering="303"/>
+    <ROW Dialog_="FatalError" Control_="Finish" Event="DoAction" Argument="AI_CleanPrereq" Condition="1" Ordering="102"/>
+    <ROW Dialog_="UserExit" Control_="Finish" Event="DoAction" Argument="AI_CleanPrereq" Condition="1" Ordering="101"/>
   </COMPONENT>
   <COMPONENT cid="caphyon.advinst.msicomp.MsiCreateFolderComponent">
     <ROW Directory_="networks.d_Dir" Component_="networks.d" ManualDelete="false"/>
     <ROW Directory_="regid.201001.com.zerotier_Dir" Component_="regid.201001.com.zerotier" ManualDelete="false"/>
     <ROW Directory_="APPDIR" Component_="APPDIR" ManualDelete="true"/>
     <ROW Directory_="i686_1_Dir" Component_="i686" ManualDelete="false"/>
+    <ROW Directory_="ZeroTier_Dir" Component_="ZeroTier" ManualDelete="true"/>
+    <ROW Directory_="One_Dir" Component_="One" ManualDelete="false"/>
+    <ROW Directory_="tapwindows_Dir" Component_="tapwindows" ManualDelete="false"/>
+    <ROW Directory_="x64_Dir" Component_="x64" ManualDelete="false"/>
+    <ROW Directory_="x86_Dir" Component_="x86" ManualDelete="false"/>
+    <ROW Directory_="i686_Dir" Component_="i686_1" ManualDelete="false"/>
   </COMPONENT>
   <COMPONENT cid="caphyon.advinst.msicomp.MsiCustActComponent">
     <ROW Action="AI_AppSearchEx" Type="1" Source="Prereq.dll" Target="DoAppSearchEx"/>
@@ -308,6 +323,7 @@
     <ROW Action="AI_RESTORE_LOCATION" Type="65" Source="aicustact.dll" Target="RestoreLocation"/>
     <ROW Action="AI_RemoveExternalUIStub" Type="1" Source="ExternalUICleaner.dll" Target="RemoveExternalUIStub"/>
     <ROW Action="AI_ResolveKnownFolders" Type="1" Source="aicustact.dll" Target="AI_ResolveKnownFolders"/>
+    <ROW Action="AI_ResolveLocalizedCredentials" Type="1" Source="aicustact.dll" Target="GetLocalizedCredentials"/>
     <ROW Action="AI_RollbackChainers" Type="11585" Source="chainersupport.dll" Target="RollbackChainedPackages" WithoutSeq="true"/>
     <ROW Action="AI_SHOW_LOG" Type="65" Source="aicustact.dll" Target="LaunchLogFile" WithoutSeq="true"/>
     <ROW Action="AI_STORE_LOCATION" Type="51" Source="ARPINSTALLLOCATION" Target="[APPDIR]"/>
@@ -353,11 +369,16 @@
     <ROW Feature_="ZeroTierOne" Component_="AI_DisableModify"/>
     <ROW Feature_="ZeroTierOne" Component_="zerotier_desktop_ui.exe"/>
     <ROW Feature_="ZeroTierOne" Component_="zerotier_desktop_ui.exe_1"/>
-    <ROW Feature_="A918597FE054CCCB65ABDBA0AD8F63C" Component_="A918597FE054CCCB65ABDBA0AD8F63C"/>
-    <ROW Feature_="C4FE6FD5B7C4D07B3A313E754A9A6A8" Component_="C4FE6FD5B7C4D07B3A313E754A9A6A8"/>
-    <ROW Feature_="RequiredApplication" Component_="RequiredApplication"/>
     <ROW Feature_="ZeroTierOne" Component_="AI_ExePath"/>
     <ROW Feature_="ZeroTierOne" Component_="i686"/>
+    <ROW Feature_="ZeroTierOne" Component_="ZeroTier"/>
+    <ROW Feature_="ZeroTierOne" Component_="One"/>
+    <ROW Feature_="ZeroTierOne" Component_="tapwindows"/>
+    <ROW Feature_="ZeroTierOne" Component_="x64"/>
+    <ROW Feature_="ZeroTierOne" Component_="x86"/>
+    <ROW Feature_="ZeroTierOne" Component_="i686_1"/>
+    <ROW Feature_="C4FE6FD5B7C4D07B3A313E754A9A6A8" Component_="C4FE6FD5B7C4D07B3A313E754A9A6A8"/>
+    <ROW Feature_="A918597FE054CCCB65ABDBA0AD8F63C" Component_="A918597FE054CCCB65ABDBA0AD8F63C"/>
   </COMPONENT>
   <COMPONENT cid="caphyon.advinst.msicomp.MsiIconsComponent">
     <ROW Name="ZeroTierIcon.exe" SourcePath="..\..\..\artwork\ZeroTierIcon.ico" Index="0"/>
@@ -367,12 +388,12 @@
     <ROW Action="AI_RESTORE_LOCATION" Condition="APPDIR=&quot;&quot;" Sequence="749"/>
     <ROW Action="AI_STORE_LOCATION" Condition="(Not Installed) OR REINSTALL" Sequence="1502"/>
     <ROW Action="AI_PREPARE_UPGRADE" Condition="AI_UPGRADE=&quot;No&quot; AND (Not Installed)" Sequence="1397"/>
-    <ROW Action="AI_ResolveKnownFolders" Sequence="52"/>
+    <ROW Action="AI_ResolveKnownFolders" Sequence="53"/>
     <ROW Action="AI_XmlInstall" Condition="(REMOVE &lt;&gt; &quot;ALL&quot;)" Sequence="5103"/>
     <ROW Action="AI_DATA_SETTER" Condition="(REMOVE &lt;&gt; &quot;ALL&quot;)" Sequence="5102"/>
     <ROW Action="AI_XmlUninstall" Condition="(REMOVE)" Sequence="3102"/>
     <ROW Action="AI_DATA_SETTER_1" Condition="(REMOVE)" Sequence="3101"/>
-    <ROW Action="InstallFinalize" Sequence="6602" SeqType="0" MsiKey="InstallFinalize"/>
+    <ROW Action="InstallFinalize" Sequence="6604" SeqType="0" MsiKey="InstallFinalize"/>
     <ROW Action="AI_RemoveExternalUIStub" Condition="(REMOVE=&quot;ALL&quot;) AND ((VersionNT &gt; 500) OR((VersionNT = 500) AND (ServicePackLevel &gt;= 4)))" Sequence="1501"/>
     <ROW Action="TapDeviceRemove32" Condition="( Installed AND ( REMOVE = &quot;ALL&quot; OR AI_INSTALL_MODE = &quot;Remove&quot; ) AND NOT UPGRADINGPRODUCTCODE ) AND ( NOT VersionNT64 )" Sequence="1605"/>
     <ROW Action="TapDeviceRemove64" Condition="( Installed AND ( REMOVE = &quot;ALL&quot; OR AI_INSTALL_MODE = &quot;Remove&quot; ) AND NOT UPGRADINGPRODUCTCODE ) AND ( VersionNT64 )" Sequence="1606"/>
@@ -393,7 +414,7 @@
     <ROW Action="AI_DeleteLzma" Condition="SETUPEXEDIR=&quot;&quot; AND Installed AND (REMOVE&lt;&gt;&quot;ALL&quot;) AND (AI_INSTALL_MODE&lt;&gt;&quot;Remove&quot;) AND (NOT PATCH)" Sequence="6594" Builds="ExeBuild"/>
     <ROW Action="TerminateUIOld" Sequence="1602"/>
     <ROW Action="AI_DATA_SETTER_6" Sequence="1601"/>
-    <ROW Action="AI_EnableDebugLog" Sequence="51"/>
+    <ROW Action="AI_EnableDebugLog" Sequence="52"/>
     <ROW Action="AI_AppSearchEx" Sequence="101"/>
     <ROW Action="AI_PrepareChainers" Condition="VersionMsi &gt;= &quot;4.05&quot;" Sequence="5851"/>
     <ROW Action="AI_ExtractFiles" Sequence="1399" Builds="ExeBuild"/>
@@ -401,21 +422,24 @@
     <ROW Action="AI_GetArpIconPath" Sequence="1401"/>
     <ROW Action="TerminateUINew" Sequence="1604"/>
     <ROW Action="AI_DATA_SETTER_5" Sequence="1603"/>
-    <ROW Action="AI_ConfigureChainer" Condition="((UILevel = 2) OR (UILevel = 3)) AND (NOT UPGRADINGPRODUCTCODE)" Sequence="6600"/>
-    <ROW Action="AI_LaunchChainer" Condition="AI_PREREQ_CHAINER AND (NOT UPGRADINGPRODUCTCODE)" Sequence="6601"/>
+    <ROW Action="LaunchUI" Condition="( NOT Installed ) AND ( ZTHEADLESS = &quot;No&quot; )" Sequence="6605"/>
+    <ROW Action="AI_DETECT_MODERNWIN" Condition="(VersionNT &gt;= 603)" Sequence="54" MsiKey="AI_DETECT_MODERNWIN"/>
+    <ROW Action="AI_ResolveLocalizedCredentials" Sequence="51"/>
+    <ROW Action="AI_ConfigureChainer" Condition="((UILevel = 2) OR (UILevel = 3)) AND (NOT UPGRADINGPRODUCTCODE)" Sequence="6602"/>
+    <ROW Action="AI_LaunchChainer" Condition="AI_PREREQ_CHAINER AND (NOT UPGRADINGPRODUCTCODE)" Sequence="6603"/>
     <ROW Action="AI_VerifyPrereq" Sequence="1101"/>
-    <ROW Action="LaunchUI" Condition="( NOT Installed ) AND ( ZTHEADLESS = &quot;No&quot; )" Sequence="6603"/>
   </COMPONENT>
   <COMPONENT cid="caphyon.advinst.msicomp.MsiInstallUISequenceComponent">
     <ROW Action="AI_RESTORE_LOCATION" Condition="APPDIR=&quot;&quot;" Sequence="749"/>
-    <ROW Action="AI_ResolveKnownFolders" Sequence="53"/>
-    <ROW Action="AI_DpiContentScale" Sequence="52"/>
+    <ROW Action="AI_ResolveKnownFolders" Sequence="54"/>
+    <ROW Action="AI_DpiContentScale" Sequence="53"/>
     <ROW Action="AI_BACKUP_AI_SETUPEXEPATH" Sequence="99"/>
     <ROW Action="AI_RESTORE_AI_SETUPEXEPATH" Condition="AI_SETUPEXEPATH_ORIGINAL" Sequence="103"/>
     <ROW Action="ExecuteAction" Sequence="1299" SeqType="0" MsiKey="ExecuteAction"/>
     <ROW Action="AI_DetectSoftware" Sequence="102"/>
-    <ROW Action="AI_EnableDebugLog" Sequence="51"/>
+    <ROW Action="AI_EnableDebugLog" Sequence="52"/>
     <ROW Action="AI_AppSearchEx" Sequence="101"/>
+    <ROW Action="AI_ResolveLocalizedCredentials" Sequence="51"/>
     <ROW Action="AI_DownloadPrereq" Sequence="1296"/>
     <ROW Action="AI_ExtractPrereq" Sequence="1297"/>
     <ROW Action="AI_InstallPrerequisite" Sequence="1298"/>
@@ -433,6 +457,23 @@
     <ROW Condition="SETUPEXEDIR OR (REMOVE=&quot;ALL&quot;)" Description="This package can only be run from a bootstrapper." DescriptionLocId="AI.LaunchCondition.RequireBootstrapper" IsPredefined="true" Builds="ExeBuild"/>
     <ROW Condition="VersionNT" Description="[ProductName] cannot be installed on [WindowsType9XDisplay]." DescriptionLocId="AI.LaunchCondition.No9X" IsPredefined="true" Builds="DefaultBuild;ExeBuild"/>
   </COMPONENT>
+  <COMPONENT cid="caphyon.advinst.msicomp.MsiLockPermComponent">
+    <ROW LockObject="ZeroTier_Dir" Table="CreateFolder" User="[GRP_EVERYONE]" Permission="1179817" Flags="0"/>
+    <ROW LockObject="ZeroTier_Dir" Table="CreateFolder" User="[GRP_ADMINISTRATORS]" Permission="1245631" Flags="0"/>
+    <ROW LockObject="One_Dir" Table="CreateFolder" User="[GRP_ADMINISTRATORS]" Permission="1245631" Flags="0"/>
+    <ROW LockObject="One_Dir" Table="CreateFolder" User="[GRP_EVERYONE]" Permission="1179817" Flags="0"/>
+    <ROW LockObject="networks.d_Dir" Table="CreateFolder" User="[GRP_ADMINISTRATORS]" Permission="1245631" Flags="0"/>
+    <ROW LockObject="networks.d_Dir" Table="CreateFolder" User="[GRP_EVERYONE]" Permission="1179817" Flags="0"/>
+    <ROW LockObject="tapwindows_Dir" Table="CreateFolder" User="[GRP_EVERYONE]" Permission="1179817" Flags="0"/>
+    <ROW LockObject="tapwindows_Dir" Table="CreateFolder" User="[GRP_ADMINISTRATORS]" Permission="1245631" Flags="0"/>
+    <ROW LockObject="x64_Dir" Table="CreateFolder" User="[GRP_ADMINISTRATORS]" Permission="1245631" Flags="0"/>
+    <ROW LockObject="x64_Dir" Table="CreateFolder" User="[GRP_EVERYONE]" Permission="1179817" Flags="0"/>
+    <ROW LockObject="x86_Dir" Table="CreateFolder" User="[GRP_ADMINISTRATORS]" Permission="1245631" Flags="0"/>
+    <ROW LockObject="x86_Dir" Table="CreateFolder" User="[GRP_EVERYONE]" Permission="1179817" Flags="0"/>
+    <ROW LockObject="regid.201001.com.zerotier_Dir" Table="CreateFolder" User="[GRP_EVERYONE]" Permission="1179817" Flags="0"/>
+    <ROW LockObject="APPDIR" Table="CreateFolder" User="[GRP_EVERYONE]" Permission="1179817" Flags="0"/>
+    <ROW LockObject="i686_Dir" Table="CreateFolder" User="[GRP_EVERYONE]" Permission="1179817" Flags="0"/>
+  </COMPONENT>
   <COMPONENT cid="caphyon.advinst.msicomp.MsiRegLocatorComponent">
     <ROW Signature_="AI_EXE_PATH_CU" Root="1" Key="Software\Caphyon\Advanced Installer\LZMA\[ProductCode]\[ProductVersion]" Name="AI_ExePath" Type="2"/>
     <ROW Signature_="AI_EXE_PATH_LM" Root="2" Key="Software\Caphyon\Advanced Installer\LZMA\[ProductCode]\[ProductVersion]" Name="AI_ExePath" Type="2"/>
@@ -469,7 +510,6 @@
     <ROW Registry="ProductVersion_1" Root="-1" Key="Software\Caphyon\Advanced Installer\LZMA\[ProductCode]\[ProductVersion]" Name="\"/>
     <ROW Registry="Publisher" Root="-1" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\[ProductName] [ProductVersion]" Name="Publisher" Value="[Manufacturer]" Component_="AI_CustomARPName"/>
     <ROW Registry="Readme" Root="-1" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\[ProductName] [ProductVersion]" Name="Readme" Value="[ARPREADME]" Component_="AI_CustomARPName"/>
-    <ROW Registry="RequiredApplication" Root="-1" Key="Software\Caphyon\Advanced Installer\Prereqs\[ProductCode]\[ProductVersion]" Name="RequiredApplication" Value="1" Component_="RequiredApplication"/>
     <ROW Registry="Software" Root="-1" Key="Software" Name="\"/>
     <ROW Registry="URLInfoAbout" Root="-1" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\[ProductName] [ProductVersion]" Name="URLInfoAbout" Value="[ARPURLINFOABOUT]" Component_="AI_CustomARPName"/>
     <ROW Registry="URLUpdateInfo" Root="-1" Key="Software\Microsoft\Windows\CurrentVersion\Uninstall\[ProductName] [ProductVersion]" Name="URLUpdateInfo" Value="[ARPURLUPDATEINFO]" Component_="AI_CustomARPName"/>
@@ -501,17 +541,14 @@
     <ROW UpgradeCode="[|UpgradeCode]" VersionMin="[|ProductVersion]" Attributes="2" ActionProperty="AI_NEWERPRODUCTFOUND"/>
   </COMPONENT>
   <COMPONENT cid="caphyon.advinst.msicomp.PreReqComponent">
-    <ROW PrereqKey="A918597FE054CCCB65ABDBA0AD8F63C" DisplayName="Visual C++ Redistributable for Visual Studio 2015-2019 x86" VersionMin="14.26" SetupFileUrl="https://download.visualstudio.microsoft.com/download/pr/d60aa805-26e9-47df-b4e3-cd6fcc392333/A06AAC66734A618AB33C1522920654DDFC44FC13CAFAA0F0AB85B199C3D51DC0/VC_redist.x86.exe" Location="1" ExactSize="14413048" WinNT64Versions="Windows XP/Vista/Windows 7/Windows 8 x64/Windows 8.1 x64/Windows 10 x64/Windows 11 x64" Operator="0" ComLine="/q /norestart" BasicUiComLine="/q /norestart" NoUiComLine="/q /norestart" Options="f" MD5="fe6eae1c34528d1ea224569dcdc35618" TargetName="Visual C++ Redistributable for Visual Studio 2015-2019" Feature="A918597FE054CCCB65ABDBA0AD8F63C"/>
-    <ROW PrereqKey="C4FE6FD5B7C4D07B3A313E754A9A6A8" DisplayName="Visual C++ Redistributable for Visual Studio 2015-2019 x64" VersionMin="14.26" SetupFileUrl="https://download.visualstudio.microsoft.com/download/pr/d60aa805-26e9-47df-b4e3-cd6fcc392333/7D7105C52FCD6766BEEE1AE162AA81E278686122C1E44890712326634D0B055E/VC_redist.x64.exe" Location="1" ExactSize="14974616" WinNTVersions="Windows 9x/ME/NT/2000/XP/Vista/Windows 7/Windows 8 x86/Windows 8.1 x86/Windows 10 x86" Operator="1" ComLine="/q /norestart" BasicUiComLine="/q /norestart" NoUiComLine="/q /norestart" Options="xf" MD5="264c296cc0bf00db6ba8e7bf8cc4e706" TargetName="Visual C++ Redistributable for Visual Studio 2015-2019" Feature="C4FE6FD5B7C4D07B3A313E754A9A6A8"/>
-    <ROW PrereqKey="RequiredApplication" DisplayName="Microsoft Edge Webview2 Runtime" SetupFileUrl="https://go.microsoft.com/fwlink/p/?LinkId=2124703" Location="1" ExactSize="0" Operator="1" ComLine="/silent /install" BasicUiComLine="/silent /install" NoUiComLine="/silent /install" Options="fx" TargetName="MicrosoftEdgeWebview2Setup.exe" Feature="RequiredApplication"/>
-    <ATTRIBUTE name="PrereqsOrder" value="A918597FE054CCCB65ABDBA0AD8F63C C4FE6FD5B7C4D07B3A313E754A9A6A8 RequiredApplication"/>
+    <ROW PrereqKey="A918597FE054CCCB65ABDBA0AD8F63C" DisplayName="Visual C++ Redistributable for Visual Studio 2015-2022 x86" VersionMin="14.30" SetupFileUrl="Prerequisites\Visual C++ Redistributable for Visual Studio 2015-2022\VC_redist.x86.exe" Location="0" ExactSize="0" WinNTVersions="Windows Vista RTM x86, Windows Vista SP1 x86, Windows Server 2008 RTM x86, Windows 7 RTM x86" WinNT64Versions="Windows Vista RTM x64, Windows Vista SP1 x64, Windows Server 2008 RTM x64, Windows 7 RTM x64, Windows Server 2008 R2 RTM x64" Operator="0" NoUiComLine="/q /norestart" Options="f" TargetName="Visual C++ Redistributable for Visual Studio 2015-2022\VC_redist.x86.exe" Feature="A918597FE054CCCB65ABDBA0AD8F63C"/>
+    <ROW PrereqKey="C4FE6FD5B7C4D07B3A313E754A9A6A8" DisplayName="Visual C++ Redistributable for Visual Studio 2015-2022 x64" VersionMin="14.30" SetupFileUrl="Prerequisites\Visual C++ Redistributable for Visual Studio 2015-2022\VC_redist.x64.exe" Location="0" ExactSize="0" WinNTVersions="Windows 9x/ME/NT/2000/XP/Vista/Windows 7/Windows 8 x86/Windows 8.1 x86/Windows 10 x86" WinNT64Versions="Windows Vista RTM x64, Windows Vista SP1 x64, Windows Server 2008 RTM x64, Windows 7 RTM x64, Windows Server 2008 R2 RTM x64" Operator="1" NoUiComLine="/q /norestart" Options="xf" TargetName="Visual C++ Redistributable for Visual Studio 2015-2022\VC_redist.x64.exe" Feature="C4FE6FD5B7C4D07B3A313E754A9A6A8"/>
+    <ATTRIBUTE name="PrereqsOrder" value="C4FE6FD5B7C4D07B3A313E754A9A6A8 A918597FE054CCCB65ABDBA0AD8F63C"/>
   </COMPONENT>
   <COMPONENT cid="caphyon.advinst.msicomp.PreReqSearchComponent">
-    <ROW SearchKey="A918597FE054CCCB65ABDBA0AD8F63CSyst" Prereq="A918597FE054CCCB65ABDBA0AD8F63C" SearchType="0" SearchString="[SystemFolder]vcruntime140.dll" VerMin="14.26.28720" Order="2" Property="PreReqSearch_1_A918597FE054CCCB65AB"/>
-    <ROW SearchKey="A918597FE054CCCB65ABDBA0AD8F63CVers" Prereq="A918597FE054CCCB65ABDBA0AD8F63C" SearchType="2" SearchString="HKLM\SOFTWARE\Microsoft\DevDiv\VC\Servicing\14.0\RuntimeMinimum\Version" VerMin="14.26.28720" Order="1" Property="PreReqSearch_A918597FE054CCCB65ABDB"/>
-    <ROW SearchKey="C4FE6FD5B7C4D07B3A313E754A9A6A8Vers" Prereq="C4FE6FD5B7C4D07B3A313E754A9A6A8" SearchType="2" SearchString="HKLM\SOFTWARE\Microsoft\DevDiv\VC\Servicing\14.0\RuntimeMinimum\Version" VerMin="14.26.28720" Order="1" Property="PreReqSearch_C4FE6FD5B7C4D07B3A313E" Platform="1"/>
-    <ROW SearchKey="F3017226FE2A42958BDF00C3A9A7E4C5" Prereq="RequiredApplication" SearchType="5" SearchString="HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Order="2" Property="PreReqSearch_1" Platform="1"/>
-    <ROW SearchKey="SystemFolderfile.dll" Prereq="RequiredApplication" SearchType="5" SearchString="HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Order="1" Property="PreReqSearch" Platform="1"/>
+    <ROW SearchKey="A918597FE054CCCB65ABDBA0AD8F63CSyst" Prereq="A918597FE054CCCB65ABDBA0AD8F63C" SearchType="0" SearchString="[SystemFolder]vcruntime140.dll" VerMin="14.30.30704" Order="2" Property="PreReqSearch_1_A918597FE054CCCB65AB"/>
+    <ROW SearchKey="A918597FE054CCCB65ABDBA0AD8F63CVers" Prereq="A918597FE054CCCB65ABDBA0AD8F63C" SearchType="2" SearchString="HKLM\SOFTWARE\Microsoft\DevDiv\VC\Servicing\14.0\RuntimeMinimum\Version" VerMin="14.30.30704" Order="1" Property="PreReqSearch_A918597FE054CCCB65ABDB"/>
+    <ROW SearchKey="C4FE6FD5B7C4D07B3A313E754A9A6A8Vers" Prereq="C4FE6FD5B7C4D07B3A313E754A9A6A8" SearchType="2" SearchString="HKLM\SOFTWARE\Microsoft\DevDiv\VC\Servicing\14.0\RuntimeMinimum\Version" VerMin="14.30.30704" Order="1" Property="PreReqSearch_C4FE6FD5B7C4D07B3A313E"/>
     <ROW SearchKey="UpgradeCode" SearchType="4" SearchString="{88AA80DE-14CA-4443-B024-6EC13F3EDDAD}" Order="2" Property="ZTTAP300_X86_INSTALLED"/>
     <ROW SearchKey="_" SearchType="4" SearchString="{88AA80DE-14CA-4443-B024-6EC13F3EDDAD}" Order="1" Property="ZTTAP300_X64_INSTALLED"/>
   </COMPONENT>
@@ -536,10 +573,10 @@
     <ROW XmlAttribute="xsischemaLocation" XmlElement="swidsoftware_identification_tag" Name="xsi:schemaLocation" Flags="14" Order="3" Value="http://standards.iso.org/iso/19770/-2/2008/schema.xsd software_identification_tag.xsd"/>
   </COMPONENT>
   <COMPONENT cid="caphyon.advinst.msicomp.XmlElementComponent">
-    <ROW XmlElement="swidbuild" ParentElement="swidnumeric" Name="swid:build" Condition="1" Order="2" Flags="14" Text="6" UpdateIndexInParent="0"/>
+    <ROW XmlElement="swidbuild" ParentElement="swidnumeric" Name="swid:build" Condition="1" Order="2" Flags="14" Text="0" UpdateIndexInParent="0"/>
     <ROW XmlElement="swidentitlement_required_indicator" ParentElement="swidsoftware_identification_tag" Name="swid:entitlement_required_indicator" Condition="1" Order="0" Flags="14" Text="false" UpdateIndexInParent="0"/>
     <ROW XmlElement="swidmajor" ParentElement="swidnumeric" Name="swid:major" Condition="1" Order="0" Flags="14" Text="1" UpdateIndexInParent="0"/>
-    <ROW XmlElement="swidminor" ParentElement="swidnumeric" Name="swid:minor" Condition="1" Order="1" Flags="14" Text="8" UpdateIndexInParent="0"/>
+    <ROW XmlElement="swidminor" ParentElement="swidnumeric" Name="swid:minor" Condition="1" Order="1" Flags="14" Text="10" UpdateIndexInParent="0"/>
     <ROW XmlElement="swidname" ParentElement="swidproduct_version" Name="swid:name" Condition="1" Order="0" Flags="14" Text="[ProductVersion]" UpdateIndexInParent="0"/>
     <ROW XmlElement="swidname_1" ParentElement="swidsoftware_creator" Name="swid:name" Condition="1" Order="0" Flags="14" Text="ZeroTier, Inc." UpdateIndexInParent="0"/>
     <ROW XmlElement="swidname_2" ParentElement="swidsoftware_licensor" Name="swid:name" Condition="1" Order="0" Flags="14" Text="ZeroTier, Inc." UpdateIndexInParent="0"/>

+ 0 - 0
ext/json/LICENSE.MIT → ext/nlohmann/LICENSE.MIT


+ 0 - 0
ext/json/README.md → ext/nlohmann/README.md


+ 0 - 0
ext/json/json.hpp → ext/nlohmann/json.hpp


+ 2 - 6
include/ZeroTierOne.h

@@ -1079,7 +1079,8 @@ enum ZT_Architecture
 	ZT_ARCHITECTURE_DOTNET_CLR = 13,
 	ZT_ARCHITECTURE_JAVA_JVM = 14,
 	ZT_ARCHITECTURE_WEB = 15,
-	ZT_ARCHITECTURE_S390X = 16
+	ZT_ARCHITECTURE_S390X = 16,
+	ZT_ARCHITECTURE_LOONGARCH64 = 17
 };
 
 /**
@@ -1404,11 +1405,6 @@ typedef struct
 	 */
 	int bondingPolicy;
 
-	/**
-	 * The health status of the bond to this peer
-	 */
-	bool isHealthy;
-
 	/**
 	 * The number of links that comprise the bond to this peer that are considered alive
 	 */

+ 2 - 2
make-bsd.mk

@@ -1,6 +1,6 @@
 # This requires GNU make, which is typically "gmake" on BSD systems
 
-INCLUDES=
+INCLUDES=-isystem ext
 DEFS=
 LIBS=
 
@@ -152,7 +152,7 @@ endif
 
 override DEFS+=-DZT_BUILD_PLATFORM=$(ZT_BUILD_PLATFORM) -DZT_BUILD_ARCHITECTURE=$(ZT_ARCHITECTURE) -DZT_SOFTWARE_UPDATE_DEFAULT="\"disable\""
 
-CXXFLAGS+=$(CFLAGS) -std=c++11 #-D_GLIBCXX_USE_C99 -D_GLIBCXX_USE_C99_MATH -D_GLIBCXX_USE_C99_MATH_TR1
+CXXFLAGS+=$(CFLAGS) -std=c++17 #-D_GLIBCXX_USE_C99 -D_GLIBCXX_USE_C99_MATH -D_GLIBCXX_USE_C99_MATH_TR1
 
 all:	one
 

+ 73 - 25
make-linux.mk

@@ -1,15 +1,15 @@
 # Automagically pick CLANG or RH/CentOS newer GCC if present
 # This is only done if we have not overridden these with an environment or CLI variable
 ifeq ($(origin CC),default)
-        CC:=$(shell if [ -e /usr/bin/clang ]; then echo clang; else echo gcc; fi)
-        CC:=$(shell if [ -e /opt/rh/devtoolset-8/root/usr/bin/gcc ]; then echo /opt/rh/devtoolset-8/root/usr/bin/gcc; else echo $(CC); fi)
+	CC:=$(shell if [ -e /usr/bin/clang ]; then echo clang; else echo gcc; fi)
+	CC:=$(shell if [ -e /opt/rh/devtoolset-8/root/usr/bin/gcc ]; then echo /opt/rh/devtoolset-8/root/usr/bin/gcc; else echo $(CC); fi)
 endif
 ifeq ($(origin CXX),default)
-        CXX:=$(shell if [ -e /usr/bin/clang++ ]; then echo clang++; else echo g++; fi)
-        CXX:=$(shell if [ -e /opt/rh/devtoolset-8/root/usr/bin/g++ ]; then echo /opt/rh/devtoolset-8/root/usr/bin/g++; else echo $(CXX); fi)
+	CXX:=$(shell if [ -e /usr/bin/clang++ ]; then echo clang++; else echo g++; fi)
+	CXX:=$(shell if [ -e /opt/rh/devtoolset-8/root/usr/bin/g++ ]; then echo /opt/rh/devtoolset-8/root/usr/bin/g++; else echo $(CXX); fi)
 endif
 
-INCLUDES?=-Izeroidc/target
+INCLUDES?=-Izeroidc/target -isystem ext
 DEFS?=
 LDLIBS?=
 DESTDIR?=
@@ -31,7 +31,7 @@ ifeq ($(MINIUPNPC_IS_NEW_ENOUGH),1)
 	override DEFS+=-DZT_USE_SYSTEM_MINIUPNPC
 	LDLIBS+=-lminiupnpc
 else
-	override DEFS+=-DMINIUPNP_STATICLIB -DMINIUPNPC_SET_SOCKET_TIMEOUT -DMINIUPNPC_GET_SRC_ADDR -D_BSD_SOURCE -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=600 -DOS_STRING=\"Linux\" -DMINIUPNPC_VERSION_STRING=\"2.0\" -DUPNP_VERSION_STRING=\"UPnP/1.1\" -DENABLE_STRNATPMPERR
+	override DEFS+=-DMINIUPNP_STATICLIB -DMINIUPNPC_SET_SOCKET_TIMEOUT -DMINIUPNPC_GET_SRC_ADDR -D_BSD_SOURCE -D_DEFAULT_SOURCE -D_XOPEN_SOURCE=600 -DOS_STRING="\"Linux\"" -DMINIUPNPC_VERSION_STRING="\"2.0\"" -DUPNP_VERSION_STRING="\"UPnP/1.1\"" -DENABLE_STRNATPMPERR
 	ONE_OBJS+=ext/miniupnpc/connecthostport.o ext/miniupnpc/igd_desc_parse.o ext/miniupnpc/minisoap.o ext/miniupnpc/minissdpc.o ext/miniupnpc/miniupnpc.o ext/miniupnpc/miniwget.o ext/miniupnpc/minixml.o ext/miniupnpc/portlistingparse.o ext/miniupnpc/receivedata.o ext/miniupnpc/upnpcommands.o ext/miniupnpc/upnpdev.o ext/miniupnpc/upnperrors.o ext/miniupnpc/upnpreplyparse.o
 endif
 ifeq ($(wildcard /usr/include/natpmp.h),)
@@ -59,32 +59,33 @@ ifeq ($(ZT_SANITIZE),1)
 endif
 ifeq ($(ZT_DEBUG),1)
 	override CFLAGS+=-Wall -Wno-deprecated -g -O -pthread $(INCLUDES) $(DEFS)
-	override CXXFLAGS+=-Wall -Wno-deprecated -g -O -std=c++11 -pthread $(INCLUDES) $(DEFS)
+	override CXXFLAGS+=-Wall -Wno-deprecated -g -O -std=c++17 -pthread $(INCLUDES) $(DEFS)
 	ZT_TRACE=1
 	RUSTFLAGS=
 	# The following line enables optimization for the crypto code, since
 	# C25519 in particular is almost UNUSABLE in -O0 even on a 3ghz box!
 node/Salsa20.o node/SHA512.o node/C25519.o node/Poly1305.o: CXXFLAGS=-Wall -O2 -g -pthread $(INCLUDES) $(DEFS)
 else
-	CFLAGS?=-O3 -fstack-protector -fPIE
+	CFLAGS?=-O3 -fstack-protector
 	override CFLAGS+=-Wall -Wno-deprecated -pthread $(INCLUDES) -DNDEBUG $(DEFS)
-	CXXFLAGS?=-O3 -fstack-protector -fPIE
-	override CXXFLAGS+=-Wall -Wno-deprecated -std=c++11 -pthread $(INCLUDES) -DNDEBUG $(DEFS)
+	CXXFLAGS?=-O3 -fstack-protector
+	override CXXFLAGS+=-Wall -Wno-deprecated -std=c++17 -pthread $(INCLUDES) -DNDEBUG $(DEFS)
 	LDFLAGS=-pie -Wl,-z,relro,-z,now
 	RUSTFLAGS=--release
 endif
 
 ifeq ($(ZT_QNAP), 1)
-        override DEFS+=-D__QNAP__
+	override DEFS+=-D__QNAP__
+	ZT_EMBEDDED=1
 endif
 ifeq ($(ZT_UBIQUITI), 1)
-        override DEFS+=-D__UBIQUITI__
+	override DEFS+=-D__UBIQUITI__
+	ZT_EMBEDDED=1
 endif
 
 ifeq ($(ZT_SYNOLOGY), 1)
-	override CFLAGS+=-fPIC
-	override CXXFLAGS+=-fPIC
 	override DEFS+=-D__SYNOLOGY__
+	ZT_EMBEDDED=1
 endif
 
 ifeq ($(ZT_DISABLE_COMPRESSION), 1)
@@ -213,9 +214,9 @@ ifeq ($(CC_MACH),armv7hl)
 	ZT_USE_ARM32_NEON_ASM_CRYPTO=1
 endif
 ifeq ($(CC_MACH),armv7ve)
-        ZT_ARCHITECTURE=3
-        override DEFS+=-DZT_NO_TYPE_PUNNING
-        ZT_USE_ARM32_NEON_ASM_CRYPTO=1
+	ZT_ARCHITECTURE=3
+	override DEFS+=-DZT_NO_TYPE_PUNNING
+	ZT_USE_ARM32_NEON_ASM_CRYPTO=1
 endif
 ifeq ($(CC_MACH),arm64)
 	ZT_ARCHITECTURE=4
@@ -249,6 +250,10 @@ endif
 ifeq ($(CC_MACH),riscv64)
 	ZT_ARCHITECTURE=0
 endif
+ifeq ($(CC_MACH),loongarch64)
+	ZT_ARCHITECTURE=17
+	override DEFS+=-DZT_NO_TYPE_PUNNING
+endif
 
 # Fail if system architecture could not be determined
 ifeq ($(ZT_ARCHITECTURE),999)
@@ -268,10 +273,13 @@ ifeq ($(ZT_IA32),1)
 endif
 
 ifeq ($(ZT_SSO_SUPPORTED), 1)
-	ifeq ($(ZT_DEBUG),1)
-		LDLIBS+=zeroidc/target/debug/libzeroidc.a -ldl -lssl -lcrypto
-	else
-		LDLIBS+=zeroidc/target/release/libzeroidc.a -ldl -lssl -lcrypto
+	ifeq ($(ZT_EMBEDDED),)
+		override DEFS+=-DZT_SSO_SUPPORTED=1
+		ifeq ($(ZT_DEBUG),1)
+			LDLIBS+=zeroidc/target/debug/libzeroidc.a -ldl -lssl -lcrypto
+		else
+			LDLIBS+=zeroidc/target/release/libzeroidc.a -ldl -lssl -lcrypto
+		endif
 	endif
 endif
 
@@ -306,8 +314,8 @@ ifeq ($(ZT_ARCHITECTURE),3)
 		override CXXFLAGS+=-march=armv5t -mfloat-abi=soft -msoft-float -mno-unaligned-access -marm
 		ZT_USE_ARM32_NEON_ASM_CRYPTO=0
 	else
-		override CFLAGS+=-mfloat-abi=hard -march=armv6kz -marm -mfpu=vfp -mno-unaligned-access -mtp=cp15 -mcpu=arm1176jzf-s
-		override CXXFLAGS+=-mfloat-abi=hard -march=armv6kz -marm -mfpu=vfp -fexceptions -mno-unaligned-access -mtp=cp15 -mcpu=arm1176jzf-s
+		override CFLAGS+=-mfloat-abi=hard -march=armv6zk -marm -mfpu=vfp -mno-unaligned-access -mtp=cp15 -mcpu=arm1176jzf-s
+		override CXXFLAGS+=-mfloat-abi=hard -march=armv6zk -marm -mfpu=vfp -fexceptions -mno-unaligned-access -mtp=cp15 -mcpu=arm1176jzf-s
 		ZT_USE_ARM32_NEON_ASM_CRYPTO=0
 	endif
 endif
@@ -326,12 +334,20 @@ ifeq ($(ZT_USE_ARM32_NEON_ASM_CRYPTO),1)
 	override CORE_OBJS+=ext/arm32-neon-salsa2012-asm/salsa2012.o
 endif
 
+# Position Independence
+override CFLAGS+=-fPIC -fPIE
+override CXXFLAGS+=-fPIC -fPIE
+
 .PHONY: all
 all:	one
 
 .PHONY: one
 one: zerotier-one zerotier-idtool zerotier-cli
 
+from_builder:
+	ln -sf zerotier-one zerotier-idtool
+	ln -sf zerotier-one zerotier-cli
+
 zerotier-one:	$(CORE_OBJS) $(ONE_OBJS) one.o
 	$(CXX) $(CXXFLAGS) $(LDFLAGS) -o zerotier-one $(CORE_OBJS) $(ONE_OBJS) one.o $(LDLIBS)
 
@@ -344,7 +360,7 @@ zerotier-cli: zerotier-one
 $(ONE_OBJS): zeroidc
 
 libzerotiercore.a:	FORCE
-	make CFLAGS="-O3 -fstack-protector -fPIC" CXXFLAGS="-O3 -std=c++11 -fstack-protector -fPIC" $(CORE_OBJS)
+	make CFLAGS="-O3 -fstack-protector -fPIC" CXXFLAGS="-O3 -std=c++17 -fstack-protector -fPIC" $(CORE_OBJS)
 	ar rcs libzerotiercore.a $(CORE_OBJS)
 	ranlib libzerotiercore.a
 
@@ -384,9 +400,11 @@ debug:	FORCE
 	make ZT_DEBUG=1 selftest
 
 ifeq ($(ZT_SSO_SUPPORTED), 1)
+ifeq ($(ZT_EMBEDDED),)
 zeroidc:	FORCE
 #	export PATH=/root/.cargo/bin:$$PATH; cd zeroidc && cargo build -j1 $(RUSTFLAGS)
-	export PATH=/root/.cargo/bin:$$PATH; cd zeroidc && cargo build $(RUSTFLAGS)
+	export PATH=/${HOME}/.cargo/bin:$$PATH; cd zeroidc && cargo build $(RUSTFLAGS)
+endif
 else
 zeroidc:
 endif
@@ -440,6 +458,20 @@ uninstall:	FORCE
 
 # These are just for convenience for building Linux packages
 
+echo_flags:
+	@echo "=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~"
+	@echo "echo_flags :: CC=$(CC)"
+	@echo "echo_flags :: CXX=$(CXX)"
+	@echo "echo_flags :: CFLAGS=$(CFLAGS)"
+	@echo "echo_flags :: CXXFLAGS=$(CXXFLAGS)"
+	@echo "echo_flags :: LDFLAGS=$(LDFLAGS)"
+	@echo "echo_flags :: RUSTFLAGS=$(RUSTFLAGS)"
+	@echo "=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~"
+
+# debian: echo_flags
+# 	@echo "building deb package"
+# 	debuild --no-lintian -b -uc -us
+
 debian:	FORCE
 	debuild --no-lintian -I -i -us -uc -nc -b
 
@@ -478,4 +510,20 @@ snap-upload-stable: FORCE
 		snapcraft upload --release=stable $${SNAPFILE};\
 	done
 
+synology-pkg: FORCE
+	cd pkg/synology ; ./build.sh build
+
+synology-docker: FORCE
+	cd pkg/synology/dsm7-docker/; ./build.sh build-and-push
+
+munge_rpm:
+	@:$(call check_defined, VERSION)
+	@echo "Updating rpm spec to $(VERSION)"
+	ci/scripts/munge_rpm_spec.sh zerotier-one.spec $(VERSION) "Adam Ierymenko <[email protected]>" "see https://github.com/zerotier/ZeroTierOne for release notes"
+
+munge_deb:
+	@:$(call check_defined, VERSION)
+	@echo "Updating debian/changelog to $(VERSION)"
+	ci/scripts/munge_debian_changelog.sh debian/changelog $(VERSION) "Adam Ierymenko <[email protected]>" "see https://github.com/zerotier/ZeroTierOne for release notes"
+
 FORCE:

+ 7 - 3
make-mac.mk

@@ -2,7 +2,7 @@ CC=clang
 CXX=clang++
 TOPDIR=$(shell PWD)
 
-INCLUDES=-I$(shell PWD)/zeroidc/target
+INCLUDES=-I$(shell PWD)/zeroidc/target -isystem $(TOPDIR)/ext
 DEFS=
 LIBS=
 ARCH_FLAGS=-arch x86_64 -arch arm64 
@@ -47,10 +47,10 @@ endif
 # Use fast ASM Salsa20/12 for x64 processors
 DEFS+=-DZT_USE_X64_ASM_SALSA2012
 CORE_OBJS+=ext/x64-salsa2012-asm/salsa2012.o
-CXXFLAGS=$(CFLAGS) -std=c++11 -stdlib=libc++
+CXXFLAGS=$(CFLAGS) -std=c++17 -stdlib=libc++
 
 # Build miniupnpc and nat-pmp as included libraries -- extra defs are required for these sources
-DEFS+=-DMACOSX -DZT_USE_MINIUPNPC -DMINIUPNP_STATICLIB -D_DARWIN_C_SOURCE -DMINIUPNPC_SET_SOCKET_TIMEOUT -DMINIUPNPC_GET_SRC_ADDR -D_BSD_SOURCE -D_DEFAULT_SOURCE -DOS_STRING=\"Darwin/15.0.0\" -DMINIUPNPC_VERSION_STRING=\"2.0\" -DUPNP_VERSION_STRING=\"UPnP/1.1\" -DENABLE_STRNATPMPERR
+DEFS+=-DMACOSX -DZT_SSO_SUPPORTED -DZT_USE_MINIUPNPC -DMINIUPNP_STATICLIB -D_DARWIN_C_SOURCE -DMINIUPNPC_SET_SOCKET_TIMEOUT -DMINIUPNPC_GET_SRC_ADDR -D_BSD_SOURCE -D_DEFAULT_SOURCE -DOS_STRING=\"Darwin/15.0.0\" -DMINIUPNPC_VERSION_STRING=\"2.0\" -DUPNP_VERSION_STRING=\"UPnP/1.1\" -DENABLE_STRNATPMPERR
 ONE_OBJS+=ext/libnatpmp/natpmp.o ext/libnatpmp/getgateway.o ext/miniupnpc/connecthostport.o ext/miniupnpc/igd_desc_parse.o ext/miniupnpc/minisoap.o ext/miniupnpc/minissdpc.o ext/miniupnpc/miniupnpc.o ext/miniupnpc/miniwget.o ext/miniupnpc/minixml.o ext/miniupnpc/portlistingparse.o ext/miniupnpc/receivedata.o ext/miniupnpc/upnpcommands.o ext/miniupnpc/upnpdev.o ext/miniupnpc/upnperrors.o ext/miniupnpc/upnpreplyparse.o osdep/PortMapper.o
 ifeq ($(ZT_CONTROLLER),1)
 	MACOS_VERSION_MIN=10.15
@@ -154,6 +154,10 @@ selftest: $(CORE_OBJS) $(ONE_OBJS) selftest.o
 
 zerotier-selftest: selftest
 
+# Make compile_commands.json for clangd editor extensions. Probably works on Linux too.
+compile_commands: FORCE
+	compiledb make ZT_DEBUG=1
+
 # Requires Packages: http://s.sudre.free.fr/Software/Packages/about.html
 mac-dist-pkg: FORCE
 	packagesbuild "ext/installfiles/mac/ZeroTier One.pkgproj"

+ 295 - 143
node/Bond.cpp

@@ -12,11 +12,12 @@
 /****/
 
 #include "Bond.hpp"
+
 #include "Switch.hpp"
 
 #include <cmath>
-#include <string>
 #include <cstdio>
+#include <string>
 
 namespace ZeroTier {
 
@@ -39,6 +40,9 @@ std::map<std::string, std::map<std::string, SharedPtr<Link> > > Bond::_interface
 
 bool Bond::linkAllowed(std::string& policyAlias, SharedPtr<Link> link)
 {
+	if (! link) {
+		return false;
+	}
 	bool foundInDefinitions = false;
 	if (_linkDefinitions.count(policyAlias)) {
 		auto it = _linkDefinitions[policyAlias].begin();
@@ -90,7 +94,7 @@ SharedPtr<Bond> Bond::getBondByPeerId(int64_t identity)
 	return _bonds.count(identity) ? _bonds[identity] : SharedPtr<Bond>();
 }
 
-SharedPtr<Bond> Bond::createTransportTriggeredBond(const RuntimeEnvironment* renv, const SharedPtr<Peer>& peer)
+SharedPtr<Bond> Bond::createBond(const RuntimeEnvironment* renv, const SharedPtr<Peer>& peer)
 {
 	Mutex::Lock _l(_bonds_m);
 	int64_t identity = peer->identity().address().toInt();
@@ -145,7 +149,13 @@ SharedPtr<Bond> Bond::createTransportTriggeredBond(const RuntimeEnvironment* ren
 	return SharedPtr<Bond>();
 }
 
-SharedPtr<Link> Bond::getLinkBySocket(const std::string& policyAlias, uint64_t localSocket)
+void Bond::destroyBond(uint64_t peerId)
+{
+	Mutex::Lock _l(_bonds_m);
+	_bonds.erase(peerId);
+}
+
+SharedPtr<Link> Bond::getLinkBySocket(const std::string& policyAlias, uint64_t localSocket, bool createIfNeeded = false)
 {
 	Mutex::Lock _l(_links_m);
 	char ifname[64] = { 0 };
@@ -153,10 +163,14 @@ SharedPtr<Link> Bond::getLinkBySocket(const std::string& policyAlias, uint64_t l
 	std::string ifnameStr(ifname);
 	auto search = _interfaceToLinkMap[policyAlias].find(ifnameStr);
 	if (search == _interfaceToLinkMap[policyAlias].end()) {
-		// If the link wasn't already known, add a new entry
-		SharedPtr<Link> s = new Link(ifnameStr, 0, 0, true, ZT_BOND_SLAVE_MODE_SPARE, "", 0.0);
-		_interfaceToLinkMap[policyAlias].insert(std::pair<std::string, SharedPtr<Link> >(ifnameStr, s));
-		return s;
+		if (createIfNeeded) {
+			SharedPtr<Link> s = new Link(ifnameStr, 0, 0, true, ZT_BOND_SLAVE_MODE_SPARE, "", 0.0);
+			_interfaceToLinkMap[policyAlias].insert(std::pair<std::string, SharedPtr<Link> >(ifnameStr, s));
+			return s;
+		}
+		else {
+			return SharedPtr<Link>();
+		}
 	}
 	else {
 		return search->second;
@@ -218,10 +232,12 @@ Bond::Bond(const RuntimeEnvironment* renv, SharedPtr<Bond> originalBond, const S
 void Bond::nominatePathToBond(const SharedPtr<Path>& path, int64_t now)
 {
 	Mutex::Lock _l(_paths_m);
+	debug("attempting to nominate link %s", pathToStr(path).c_str());
 	/**
 	 * Ensure the link is allowed and the path is not already present
 	 */
-	if (! RR->bc->linkAllowed(_policyAlias, getLink(path))) {
+	if (! RR->bc->linkAllowed(_policyAlias, getLinkBySocket(_policyAlias, path->localSocket(), true))) {
+		debug("link %s is not permitted according to user-specified rules", pathToStr(path).c_str());
 		return;
 	}
 	bool alreadyPresent = false;
@@ -229,6 +245,7 @@ void Bond::nominatePathToBond(const SharedPtr<Path>& path, int64_t now)
 		// Sanity check
 		if (path.ptr() == _paths[i].p.ptr()) {
 			alreadyPresent = true;
+			debug("link %s already exists", pathToStr(path).c_str());
 			break;
 		}
 	}
@@ -247,20 +264,22 @@ void Bond::nominatePathToBond(const SharedPtr<Path>& path, int64_t now)
 					// Determine if there are any other paths on this link
 					bool bFoundCommonLink = false;
 					SharedPtr<Link> commonLink = RR->bc->getLinkBySocket(_policyAlias, _paths[i].p->localSocket());
-					for (unsigned int j = 0; j < ZT_MAX_PEER_NETWORK_PATHS; ++j) {
-						if (_paths[j].p && _paths[j].p.ptr() != _paths[i].p.ptr()) {
-							if (RR->bc->getLinkBySocket(_policyAlias, _paths[j].p->localSocket()) == commonLink) {
-								bFoundCommonLink = true;
-								_paths[j].onlyPathOnLink = false;
+					if (commonLink) {
+						for (unsigned int j = 0; j < ZT_MAX_PEER_NETWORK_PATHS; ++j) {
+							if (_paths[j].p && _paths[j].p.ptr() != _paths[i].p.ptr()) {
+								if (RR->bc->getLinkBySocket(_policyAlias, _paths[j].p->localSocket()) == commonLink, true) {
+									bFoundCommonLink = true;
+									_paths[j].onlyPathOnLink = false;
+								}
 							}
 						}
+						_paths[i].ipvPref = sl->ipvPref();
+						_paths[i].mode = sl->mode();
+						_paths[i].enabled = sl->enabled();
+						_paths[i].onlyPathOnLink = ! bFoundCommonLink;
 					}
-					_paths[i].ipvPref = sl->ipvPref();
-					_paths[i].mode = sl->mode();
-					_paths[i].enabled = sl->enabled();
-					_paths[i].onlyPathOnLink = ! bFoundCommonLink;
 				}
-				log("nominate link %s", pathToStr(path).c_str());
+				log("nominated link %s", pathToStr(path).c_str());
 				break;
 			}
 		}
@@ -313,7 +332,7 @@ SharedPtr<Path> Bond::getAppropriatePath(int64_t now, int32_t flowId)
 			}
 			// Reset striping counter
 			_rrPacketsSentOnCurrLink = 0;
-			if (_numBondedPaths == 1 || _rrIdx >= (ZT_MAX_PEER_NETWORK_PATHS-1)) {
+			if (_numBondedPaths == 1 || _rrIdx >= (ZT_MAX_PEER_NETWORK_PATHS - 1)) {
 				_rrIdx = 0;
 			}
 			else {
@@ -367,7 +386,7 @@ void Bond::recordIncomingInvalidPacket(const SharedPtr<Path>& path)
 	Mutex::Lock _l(_paths_m);
 	for (int i = 0; i < ZT_MAX_PEER_NETWORK_PATHS; ++i) {
 		if (_paths[i].p == path) {
-			_paths[i].packetValiditySamples.push(false);
+			//_paths[i].packetValiditySamples.push(false);
 		}
 	}
 }
@@ -388,9 +407,10 @@ void Bond::recordOutgoingPacket(const SharedPtr<Path>& path, uint64_t packetId,
 			_lastFrame = now;
 		}
 		if (shouldRecord) {
+			//_paths[pathIdx].expectingAckAsOf = now;
+			//_paths[pathIdx].totalBytesSentSinceLastAckRecieved += payloadLength;
 			//_paths[pathIdx].unackedBytes += payloadLength;
-			// Take note that we're expecting a VERB_ACK on this path as of a specific time
-			if (_paths[pathIdx].qosStatsOut.size() < ZT_QOS_MAX_OUTSTANDING_RECORDS) {
+			if (_paths[pathIdx].qosStatsOut.size() < ZT_QOS_MAX_PENDING_RECORDS) {
 				_paths[pathIdx].qosStatsOut[packetId] = now;
 			}
 		}
@@ -423,9 +443,25 @@ void Bond::recordIncomingPacket(const SharedPtr<Path>& path, uint64_t packetId,
 				_lastFrame = now;
 			}
 			if (shouldRecord) {
-				_paths[pathIdx].qosStatsIn[packetId] = now;
-				++(_paths[pathIdx].packetsReceivedSinceLastQoS);
-				_paths[pathIdx].packetValiditySamples.push(true);
+				if (_paths[pathIdx].qosStatsIn.size() < ZT_QOS_MAX_PENDING_RECORDS) {
+					// debug("Recording QoS information (table size = %d)", _paths[pathIdx].qosStatsIn.size());
+					_paths[pathIdx].qosStatsIn[packetId] = now;
+					++(_paths[pathIdx].packetsReceivedSinceLastQoS);
+					//_paths[pathIdx].packetValiditySamples.push(true);
+				}
+				else {
+					debug("QoS buffer full, will not record information");
+				}
+				/*
+				if (_paths[pathIdx].ackStatsIn.size() < ZT_ACK_MAX_PENDING_RECORDS) {
+					//debug("Recording ACK information (table size = %d)", _paths[pathIdx].ackStatsIn.size());
+					_paths[pathIdx].ackStatsIn[packetId] = payloadLength;
+					++(_paths[pathIdx].packetsReceivedSinceLastAck);
+				}
+				else {
+					debug("ACK buffer full, will not record information");
+				}
+				*/
 			}
 		}
 	}
@@ -470,6 +506,16 @@ void Bond::receivedQoS(const SharedPtr<Path>& path, int64_t now, int count, uint
 	_paths[pathIdx].qosRecordSize.push(count);
 }
 
+void Bond::receivedAck(int pathIdx, int64_t now, int32_t ackedBytes)
+{
+	/*
+	Mutex::Lock _l(_paths_m);
+	debug("received ACK of %d bytes on path %s, there are still %d un-acked bytes", ackedBytes, pathToStr(_paths[pathIdx].p).c_str(), _paths[pathIdx].unackedBytes);
+	_paths[pathIdx].lastAckReceived = now;
+	_paths[pathIdx].unackedBytes = (ackedBytes > _paths[pathIdx].unackedBytes) ? 0 : _paths[pathIdx].unackedBytes - ackedBytes;
+	*/
+}
+
 int32_t Bond::generateQoSPacket(int pathIdx, int64_t now, char* qosBuffer)
 {
 	int32_t len = 0;
@@ -544,7 +590,6 @@ bool Bond::assignFlowToBondedPath(SharedPtr<Flow>& flow, int64_t now)
 		}
 		flow->assignPath(_abPathIdx, now);
 	}
-	SharedPtr<Link> link = RR->bc->getLinkBySocket(_policyAlias, _paths[flow->assignedPath].p->localSocket());
 	debug("assign out-flow %04x to link %s (%lu / %lu flows)", flow->id, pathToStr(_paths[flow->assignedPath].p).c_str(), _paths[flow->assignedPath].assignedFlowCount, (unsigned long)_flows.size());
 	return true;
 }
@@ -631,22 +676,24 @@ void Bond::processIncomingPathNegotiationRequest(uint64_t now, SharedPtr<Path>&
 		return;
 	}
 	SharedPtr<Link> link = RR->bc->getLinkBySocket(_policyAlias, _paths[pathIdx].p->localSocket());
-	if (remoteUtility > _localUtility) {
-		_paths[pathIdx].p->address().toString(pathStr);
-		debug("peer suggests alternate link %s/%s, remote utility (%d) greater than local utility (%d), switching to suggested link\n", link->ifname().c_str(), pathStr, remoteUtility, _localUtility);
-		_negotiatedPathIdx = pathIdx;
-	}
-	if (remoteUtility < _localUtility) {
-		debug("peer suggests alternate link %s/%s, remote utility (%d) less than local utility (%d), not switching\n", link->ifname().c_str(), pathStr, remoteUtility, _localUtility);
-	}
-	if (remoteUtility == _localUtility) {
-		debug("peer suggests alternate link %s/%s, remote utility (%d) equal to local utility (%d)\n", link->ifname().c_str(), pathStr, remoteUtility, _localUtility);
-		if (_peer->_id.address().toInt() > RR->node->identity().address().toInt()) {
-			debug("agree with peer to use alternate link %s/%s\n", link->ifname().c_str(), pathStr);
+	if (link) {
+		if (remoteUtility > _localUtility) {
+			_paths[pathIdx].p->address().toString(pathStr);
+			debug("peer suggests alternate link %s/%s, remote utility (%d) greater than local utility (%d), switching to suggested link\n", link->ifname().c_str(), pathStr, remoteUtility, _localUtility);
 			_negotiatedPathIdx = pathIdx;
 		}
-		else {
-			debug("ignore petition from peer to use alternate link %s/%s\n", link->ifname().c_str(), pathStr);
+		if (remoteUtility < _localUtility) {
+			debug("peer suggests alternate link %s/%s, remote utility (%d) less than local utility (%d), not switching\n", link->ifname().c_str(), pathStr, remoteUtility, _localUtility);
+		}
+		if (remoteUtility == _localUtility) {
+			debug("peer suggests alternate link %s/%s, remote utility (%d) equal to local utility (%d)\n", link->ifname().c_str(), pathStr, remoteUtility, _localUtility);
+			if (_peer->_id.address().toInt() > RR->node->identity().address().toInt()) {
+				debug("agree with peer to use alternate link %s/%s\n", link->ifname().c_str(), pathStr);
+				_negotiatedPathIdx = pathIdx;
+			}
+			else {
+				debug("ignore petition from peer to use alternate link %s/%s\n", link->ifname().c_str(), pathStr);
+			}
 		}
 	}
 }
@@ -717,16 +764,42 @@ void Bond::sendPATH_NEGOTIATION_REQUEST(void* tPtr, int pathIdx)
 	if (_paths[pathIdx].p->address()) {
 		outp.armor(_peer->key(), false, _peer->aesKeysIfSupported());
 		RR->node->putPacket(tPtr, _paths[pathIdx].p->localSocket(), _paths[pathIdx].p->address(), outp.data(), outp.size());
+		_overheadBytes += outp.size();
 	}
 }
 
+void Bond::sendACK(void* tPtr, int pathIdx, int64_t localSocket, const InetAddress& atAddress, int64_t now)
+{
+	/*
+	Packet outp(_peer->_id.address(), RR->identity.address(), Packet::VERB_ACK);
+	int32_t bytesToAck = 0;
+	std::map<uint64_t, uint64_t>::iterator it = _paths[pathIdx].ackStatsIn.begin();
+	while (it != _paths[pathIdx].ackStatsIn.end()) {
+		bytesToAck += it->second;
+		++it;
+	}
+	debug("sending ACK of %d bytes on path %s (table size = %d)", bytesToAck, pathToStr(_paths[pathIdx].p).c_str(), _paths[pathIdx].ackStatsIn.size());
+	outp.append<uint32_t>(bytesToAck);
+	if (atAddress) {
+		outp.armor(_peer->key(), false, _peer->aesKeysIfSupported());
+		RR->node->putPacket(tPtr, localSocket, atAddress, outp.data(), outp.size());
+	}
+	else {
+		RR->sw->send(tPtr, outp, false);
+	}
+	_paths[pathIdx].ackStatsIn.clear();
+	_paths[pathIdx].packetsReceivedSinceLastAck = 0;
+	_paths[pathIdx].lastAckSent = now;
+	*/
+}
+
 void Bond::sendQOS_MEASUREMENT(void* tPtr, int pathIdx, int64_t localSocket, const InetAddress& atAddress, int64_t now)
 {
 	int64_t _now = RR->node->now();
 	Packet outp(_peer->_id.address(), RR->identity.address(), Packet::VERB_QOS_MEASUREMENT);
 	char qosData[ZT_QOS_MAX_PACKET_SIZE];
 	int16_t len = generateQoSPacket(pathIdx, _now, qosData);
-	_overheadBytes += len;
+	// debug("sending QOS via link %s (len=%d)", pathToStr(_paths[pathIdx].p).c_str(), len);
 	if (len) {
 		outp.append(qosData, len);
 		if (atAddress) {
@@ -738,8 +811,8 @@ void Bond::sendQOS_MEASUREMENT(void* tPtr, int pathIdx, int64_t localSocket, con
 		}
 		_paths[pathIdx].packetsReceivedSinceLastQoS = 0;
 		_paths[pathIdx].lastQoSMeasurement = now;
+		_overheadBytes += outp.size();
 	}
-	// debug("send QOS via link %s (len=%d)", pathToStr(_paths[pathIdx].p).c_str(), len);
 }
 
 void Bond::processBackgroundBondTasks(void* tPtr, int64_t now)
@@ -761,9 +834,9 @@ void Bond::processBackgroundBondTasks(void* tPtr, int64_t now)
 	for (unsigned int i = 0; i < ZT_MAX_PEER_NETWORK_PATHS; ++i) {
 		if (_paths[i].p && _paths[i].allowed()) {
 			if (_isLeaf) {
-				if ((_monitorInterval > 0) && (((now - _paths[i].p->_lastIn) >= _monitorInterval) /*|| ((now - _paths[i].p->_lastOut) >= _monitorInterval)*/)) {
+				if ((_monitorInterval > 0) && (((now - _paths[i].p->_lastIn) >= (_paths[i].alive ? _monitorInterval : _failoverInterval)))) {
 					if ((_peer->remoteVersionProtocol() >= 5) && (! ((_peer->remoteVersionMajor() == 1) && (_peer->remoteVersionMinor() == 1) && (_peer->remoteVersionRevision() == 0)))) {
-						Packet outp(_peer->address(), RR->identity.address(), Packet::VERB_ECHO); // ECHO (this is our bond's heartbeat)
+						Packet outp(_peer->address(), RR->identity.address(), Packet::VERB_ECHO);	// ECHO (this is our bond's heartbeat)
 						outp.armor(_peer->key(), true, _peer->aesKeysIfSupported());
 						RR->node->expectReplyTo(outp.packetId());
 						RR->node->putPacket(tPtr, _paths[i].p->localSocket(), _paths[i].p->address(), outp.data(), outp.size());
@@ -776,6 +849,12 @@ void Bond::processBackgroundBondTasks(void* tPtr, int64_t now)
 				if (_paths[i].needsToSendQoS(now, _qosSendInterval)) {
 					sendQOS_MEASUREMENT(tPtr, i, _paths[i].p->localSocket(), _paths[i].p->address(), now);
 				}
+				// ACK
+				/*
+				if (_paths[i].needsToSendAck(now, _ackSendInterval)) {
+					sendACK(tPtr, i, _paths[i].p->localSocket(), _paths[i].p->address(), now);
+				}
+				*/
 			}
 		}
 	}
@@ -815,6 +894,24 @@ void Bond::curateBond(int64_t now, bool rebuildBond)
 		if (! _paths[i].p) {
 			continue;
 		}
+
+		/**
+		 * Remove expired or invalid links from bond
+		 */
+		SharedPtr<Link> link = getLink(_paths[i].p);
+		if (! link) {
+			log("link is no longer valid, removing from bond");
+			_paths[i] = NominatedPath();
+			_paths[i].p = SharedPtr<Path>();
+			continue;
+		}
+		if ((now - _paths[i].p->_lastIn) > (ZT_PEER_EXPIRED_PATH_TRIAL_PERIOD)) {
+			log("link (%s) has expired or is invalid, removing from bond", pathToStr(_paths[i].p).c_str());
+			_paths[i] = NominatedPath();
+			_paths[i].p = SharedPtr<Path>();
+			continue;
+		}
+
 		tmpNumTotalLinks++;
 		if (_paths[i].eligible) {
 			tmpNumAliveLinks++;
@@ -875,42 +972,18 @@ void Bond::curateBond(int64_t now, bool rebuildBond)
 	}
 
 	/**
-	 * Determine health status to report to user
+	 * Trigger status report if number of links change
 	 */
 	_numAliveLinks = tmpNumAliveLinks;
 	_numTotalLinks = tmpNumTotalLinks;
-	bool tmpHealthStatus = true;
-
-	if (_policy == ZT_BOND_POLICY_BROADCAST) {
-		if (_numAliveLinks < 1) {
-			// Considered healthy if we're able to send frames at all
-			tmpHealthStatus = false;
-		}
-	}
-	if ((_policy == ZT_BOND_POLICY_BALANCE_RR) || (_policy == ZT_BOND_POLICY_BALANCE_XOR) || (_policy == ZT_BOND_POLICY_BALANCE_AWARE || (_policy == ZT_BOND_POLICY_ACTIVE_BACKUP))) {
-		if (_numAliveLinks < _numTotalLinks) {
-			tmpHealthStatus = false;
-		}
-	}
-	if (tmpHealthStatus != _isHealthy) {
-		std::string healthStatusStr;
-		if (tmpHealthStatus == true) {
-			healthStatusStr = "HEALTHY";
-		}
-		else {
-			healthStatusStr = "DEGRADED";
-		}
-		log("bond is %s (%d/%d links)", healthStatusStr.c_str(), _numAliveLinks, _numTotalLinks);
+	if ((_numAliveLinks != tmpNumAliveLinks) || (_numTotalLinks != tmpNumTotalLinks)) {
 		dumpInfo(now, true);
 	}
 
-	_isHealthy = tmpHealthStatus;
-
 	/**
 	 * Curate the set of paths that are part of the bond proper. Select a set of paths
 	 * per logical link according to eligibility and user-specified constraints.
 	 */
-
 	if ((_policy == ZT_BOND_POLICY_BALANCE_RR) || (_policy == ZT_BOND_POLICY_BALANCE_XOR) || (_policy == ZT_BOND_POLICY_BALANCE_AWARE)) {
 		if (! _numBondedPaths) {
 			rebuildBond = true;
@@ -923,7 +996,9 @@ void Bond::curateBond(int64_t now, bool rebuildBond)
 			for (int i = 0; i < ZT_MAX_PEER_NETWORK_PATHS; ++i) {
 				if (_paths[i].p) {
 					SharedPtr<Link> link = RR->bc->getLinkBySocket(_policyAlias, _paths[i].p->localSocket());
-					linkMap[link].push_back(i);
+					if (link) {
+						linkMap[link].push_back(i);
+					}
 				}
 			}
 			// Re-form bond from link<->path map
@@ -1008,15 +1083,19 @@ void Bond::estimatePathQuality(int64_t now)
 	uint32_t totUserSpecifiedLinkSpeed = 0;
 	if (_numBondedPaths) {	 // Compute relative user-specified speeds of links
 		for (unsigned int i = 0; i < _numBondedPaths; ++i) {
-			SharedPtr<Link> link = RR->bc->getLinkBySocket(_policyAlias, _paths[i].p->localSocket());
 			if (_paths[i].p && _paths[i].allowed()) {
-				totUserSpecifiedLinkSpeed += link->speed();
+				SharedPtr<Link> link = RR->bc->getLinkBySocket(_policyAlias, _paths[i].p->localSocket());
+				if (link) {
+					totUserSpecifiedLinkSpeed += link->speed();
+				}
 			}
 		}
 		for (unsigned int i = 0; i < _numBondedPaths; ++i) {
-			SharedPtr<Link> link = RR->bc->getLinkBySocket(_policyAlias, _paths[i].p->localSocket());
 			if (_paths[i].p && _paths[i].allowed()) {
-				link->setRelativeSpeed((uint8_t)round(((float)link->speed() / (float)totUserSpecifiedLinkSpeed) * 255));
+				SharedPtr<Link> link = RR->bc->getLinkBySocket(_policyAlias, _paths[i].p->localSocket());
+				if (link) {
+					link->setRelativeSpeed((uint8_t)round(((float)link->speed() / (float)totUserSpecifiedLinkSpeed) * 255));
+				}
 			}
 		}
 	}
@@ -1044,7 +1123,7 @@ void Bond::estimatePathQuality(int64_t now)
 		// Compute/Smooth average of real-world observations
 		_paths[i].latencyMean = _paths[i].latencySamples.mean();
 		_paths[i].latencyVariance = _paths[i].latencySamples.stddev();
-		_paths[i].packetErrorRatio = 1.0 - (_paths[i].packetValiditySamples.count() ? _paths[i].packetValiditySamples.mean() : 1.0);
+		//_paths[i].packetErrorRatio = 1.0 - (_paths[i].packetValiditySamples.count() ? _paths[i].packetValiditySamples.mean() : 1.0);
 
 		if (userHasSpecifiedLinkSpeeds()) {
 			// Use user-reported metrics
@@ -1054,20 +1133,52 @@ void Bond::estimatePathQuality(int64_t now)
 				_paths[i].throughputVariance = 0;
 			}
 		}
+
 		// Drain unacknowledged QoS records
+		int qosRecordTimeout = (_qosSendInterval * 3);
 		std::map<uint64_t, uint64_t>::iterator it = _paths[i].qosStatsOut.begin();
-		uint64_t currentLostRecords = 0;
+		int numDroppedQosOutRecords = 0;
 		while (it != _paths[i].qosStatsOut.end()) {
-			int qosRecordTimeout = 5000;   //_paths[i].p->monitorInterval() * ZT_BOND_QOS_ACK_INTERVAL_MULTIPLIER * 8;
 			if ((now - it->second) >= qosRecordTimeout) {
-				// Packet was lost
 				it = _paths[i].qosStatsOut.erase(it);
-				++currentLostRecords;
+				++numDroppedQosOutRecords;
 			}
 			else {
 				++it;
 			}
 		}
+		if (numDroppedQosOutRecords) {
+			log("Dropped %d QOS out-records", numDroppedQosOutRecords);
+		}
+
+		/*
+		for (unsigned int i = 0; i < ZT_MAX_PEER_NETWORK_PATHS; ++i) {
+			if (! _paths[i].p) {
+				continue;
+			}
+			// if ((now - _paths[i].lastAckReceived) > ackSendInterval) {
+			//	debug("been a while since ACK");
+			//	if (_paths[i].unackedBytes > 0) {
+			//		_paths[i].unackedBytes / _paths[i].bytesSen
+			//	}
+			// }
+		}
+		*/
+
+		it = _paths[i].qosStatsIn.begin();
+		int numDroppedQosInRecords = 0;
+		while (it != _paths[i].qosStatsIn.end()) {
+			if ((now - it->second) >= qosRecordTimeout) {
+				it = _paths[i].qosStatsIn.erase(it);
+				++numDroppedQosInRecords;
+			}
+			else {
+				++it;
+			}
+		}
+		if (numDroppedQosInRecords) {
+			log("Dropped %d QOS in-records", numDroppedQosInRecords);
+		}
 
 		quality[i] = 0;
 		totQuality = 0;
@@ -1197,6 +1308,7 @@ void Bond::dequeueNextActiveBackupPath(uint64_t now)
 
 bool Bond::abForciblyRotateLink()
 {
+	Mutex::Lock _l(_paths_m);
 	if (_policy == ZT_BOND_POLICY_ACTIVE_BACKUP) {
 		int prevPathIdx = _abPathIdx;
 		dequeueNextActiveBackupPath(RR->node->now());
@@ -1209,9 +1321,14 @@ bool Bond::abForciblyRotateLink()
 void Bond::processActiveBackupTasks(void* tPtr, int64_t now)
 {
 	int prevActiveBackupPathIdx = _abPathIdx;
-	int nonPreferredPathIdx;
+	int nonPreferredPathIdx = ZT_MAX_PEER_NETWORK_PATHS;
 	bool bFoundPrimaryLink = false;
 
+	if (_abPathIdx != ZT_MAX_PEER_NETWORK_PATHS && ! _paths[_abPathIdx].p) {
+		_abPathIdx = ZT_MAX_PEER_NETWORK_PATHS;
+		log("main active-backup path has been removed");
+	}
+
 	/**
 	 * Generate periodic status report
 	 */
@@ -1227,7 +1344,6 @@ void Bond::processActiveBackupTasks(void* tPtr, int64_t now)
 			log("failover queue is empty, bond is no longer fault-tolerant");
 		}
 	}
-
 	/**
 	 * Select initial "active" active-backup link
 	 */
@@ -1241,7 +1357,6 @@ void Bond::processActiveBackupTasks(void* tPtr, int64_t now)
 		 * simply find the next eligible path.
 		 */
 		if (! userHasSpecifiedLinks()) {
-			debug("no user-specified links");
 			for (int i = 0; i < ZT_MAX_PEER_NETWORK_PATHS; ++i) {
 				if (_paths[i].p && _paths[i].eligible) {
 					SharedPtr<Link> link = RR->bc->getLinkBySocket(_policyAlias, _paths[i].p->localSocket());
@@ -1253,6 +1368,7 @@ void Bond::processActiveBackupTasks(void* tPtr, int64_t now)
 				}
 			}
 		}
+
 		/**
 		 * [Manual mode]
 		 * The user has specified links or failover rules that the bonding policy should adhere to.
@@ -1264,34 +1380,38 @@ void Bond::processActiveBackupTasks(void* tPtr, int64_t now)
 						continue;
 					}
 					SharedPtr<Link> link = RR->bc->getLinkBySocket(_policyAlias, _paths[i].p->localSocket());
-					if (_paths[i].eligible && link->primary()) {
-						if (! _paths[i].preferred()) {
-							// Found path on primary link, take note in case we don't find a preferred path
-							nonPreferredPathIdx = i;
-							bFoundPrimaryLink = true;
-						}
-						if (_paths[i].preferred()) {
-							_abPathIdx = i;
-							bFoundPrimaryLink = true;
-							SharedPtr<Link> link = RR->bc->getLinkBySocket(_policyAlias, _paths[_abPathIdx].p->localSocket());
-							if (link) {
-								log("found preferred primary link %s", pathToStr(_paths[_abPathIdx].p).c_str());
+					if (link) {
+						if (_paths[i].eligible && link->primary()) {
+							if (! _paths[i].preferred()) {
+								// Found path on primary link, take note in case we don't find a preferred path
+								nonPreferredPathIdx = i;
+								bFoundPrimaryLink = true;
+							}
+							if (_paths[i].preferred()) {
+								_abPathIdx = i;
+								bFoundPrimaryLink = true;
+								if (_paths[_abPathIdx].p) {
+									SharedPtr<Link> abLink = RR->bc->getLinkBySocket(_policyAlias, _paths[_abPathIdx].p->localSocket());
+									if (abLink) {
+										log("found preferred primary link %s", pathToStr(_paths[_abPathIdx].p).c_str());
+									}
+									break;	 // Found preferred path on primary link
+								}
 							}
-							break;	 // Found preferred path on primary link
 						}
 					}
 				}
-				if (bFoundPrimaryLink && nonPreferredPathIdx) {
+				if (bFoundPrimaryLink && (nonPreferredPathIdx != ZT_MAX_PEER_NETWORK_PATHS)) {
 					log("found non-preferred primary link");
 					_abPathIdx = nonPreferredPathIdx;
 				}
 				if (_abPathIdx == ZT_MAX_PEER_NETWORK_PATHS) {
-					log("user-designated primary link is not yet ready");
+					log("user-designated primary link is not available");
 					// TODO: Should wait for some time (failover interval?) and then switch to spare link
 				}
 			}
+
 			else if (! userHasSpecifiedPrimaryLink()) {
-				log("user did not specify a primary link, select first available link");
 				for (int i = 0; i < ZT_MAX_PEER_NETWORK_PATHS; ++i) {
 					if (_paths[i].p && _paths[i].eligible) {
 						_abPathIdx = i;
@@ -1299,28 +1419,36 @@ void Bond::processActiveBackupTasks(void* tPtr, int64_t now)
 					}
 				}
 				if (_abPathIdx != ZT_MAX_PEER_NETWORK_PATHS) {
-					SharedPtr<Link> link = RR->bc->getLinkBySocket(_policyAlias, _paths[_abPathIdx].p->localSocket());
-					if (link) {
-						log("select non-primary link %s", pathToStr(_paths[_abPathIdx].p).c_str());
+					if (_paths[_abPathIdx].p) {
+						SharedPtr<Link> link = RR->bc->getLinkBySocket(_policyAlias, _paths[_abPathIdx].p->localSocket());
+						if (link) {
+							log("select non-primary link %s", pathToStr(_paths[_abPathIdx].p).c_str());
+						}
 					}
 				}
 			}
 		}
 	}
 
-	// Short-circuit if we don't have an active link yet
-	if (_abPathIdx == ZT_MAX_PEER_NETWORK_PATHS) {
+	// Short-circuit if we don't have an active link yet. Everything below is optimization from the base case
+	if (_abPathIdx < 0 || _abPathIdx == ZT_MAX_PEER_NETWORK_PATHS || (! _paths[_abPathIdx].p)) {
 		return;
 	}
 
 	// Remove ineligible paths from the failover link queue
 	for (std::deque<int>::iterator it(_abFailoverQueue.begin()); it != _abFailoverQueue.end();) {
+		if (! _paths[(*it)].p) {
+			log("link is no longer valid, removing from failover queue (%zu links remain in queue)", _abFailoverQueue.size());
+			it = _abFailoverQueue.erase(it);
+			continue;
+		}
 		if (_paths[(*it)].p && ! _paths[(*it)].eligible) {
 			SharedPtr<Link> link = RR->bc->getLinkBySocket(_policyAlias, _paths[(*it)].p->localSocket());
 			it = _abFailoverQueue.erase(it);
 			if (link) {
-				log("link %s is ineligible, removing from failover queue (%zu links in queue)", pathToStr(_paths[_abPathIdx].p).c_str(), _abFailoverQueue.size());
+				log("link %s is ineligible, removing from failover queue (%zu links remain in queue)", pathToStr(_paths[_abPathIdx].p).c_str(), _abFailoverQueue.size());
 			}
+			continue;
 		}
 		else {
 			++it;
@@ -1345,7 +1473,9 @@ void Bond::processActiveBackupTasks(void* tPtr, int64_t now)
 				continue;
 			}
 			SharedPtr<Link> link = RR->bc->getLinkBySocket(_policyAlias, _paths[i].p->localSocket());
-
+			if (! link) {
+				continue;
+			}
 			int failoverScoreHandicap = _paths[i].failoverScore;
 			if (_paths[i].preferred()) {
 				failoverScoreHandicap += ZT_BOND_FAILOVER_HANDICAP_PREFERRED;
@@ -1375,17 +1505,19 @@ void Bond::processActiveBackupTasks(void* tPtr, int64_t now)
 					}
 				}
 			}
-			if (_paths[i].p.ptr() != _paths[_abPathIdx].p.ptr()) {
-				bool bFoundPathInQueue = false;
-				for (std::deque<int>::iterator it(_abFailoverQueue.begin()); it != _abFailoverQueue.end(); ++it) {
-					if (_paths[i].p.ptr() == _paths[(*it)].p.ptr()) {
-						bFoundPathInQueue = true;
+			if (_paths[i].p) {
+				if (_paths[i].p.ptr() != _paths[_abPathIdx].p.ptr()) {
+					bool bFoundPathInQueue = false;
+					for (std::deque<int>::iterator it(_abFailoverQueue.begin()); it != _abFailoverQueue.end(); ++it) {
+						if (_paths[(*it)].p && (_paths[i].p.ptr() == _paths[(*it)].p.ptr())) {
+							bFoundPathInQueue = true;
+						}
+					}
+					if (! bFoundPathInQueue) {
+						_abFailoverQueue.push_front(i);
+						log("add link %s to failover queue (%zu links in queue)", pathToStr(_paths[i].p).c_str(), _abFailoverQueue.size());
+						addPathToBond(0, i);
 					}
-				}
-				if (! bFoundPathInQueue) {
-					_abFailoverQueue.push_front(i);
-					log("add link %s to failover queue (%zu links in queue)", pathToStr(_paths[i].p).c_str(), _abFailoverQueue.size());
-					addPathToBond(0, i);
 				}
 			}
 		}
@@ -1406,7 +1538,11 @@ void Bond::processActiveBackupTasks(void* tPtr, int64_t now)
 			if (! _paths[i].eligible) {
 				failoverScoreHandicap = -10000;
 			}
-			if (getLink(_paths[i].p)->primary() && _abLinkSelectMethod != ZT_BOND_RESELECTION_POLICY_OPTIMIZE) {
+			SharedPtr<Link> link = getLink(_paths[i].p);
+			if (! link) {
+				continue;
+			}
+			if (link->primary() && _abLinkSelectMethod != ZT_BOND_RESELECTION_POLICY_OPTIMIZE) {
 				// If using "optimize" primary re-select mode, ignore user link designations
 				failoverScoreHandicap = ZT_BOND_FAILOVER_HANDICAP_PRIMARY;
 			}
@@ -1458,7 +1594,7 @@ void Bond::processActiveBackupTasks(void* tPtr, int64_t now)
 	/**
 	 * Fulfill primary re-select obligations
 	 */
-	if (_paths[_abPathIdx].p && ! _paths[_abPathIdx].eligible) {   // Implicit ZT_BOND_RESELECTION_POLICY_FAILURE
+	if (! _paths[_abPathIdx].eligible) {   // Implicit ZT_BOND_RESELECTION_POLICY_FAILURE
 		log("link %s has failed, select link from failover queue (%zu links in queue)", pathToStr(_paths[_abPathIdx].p).c_str(), _abFailoverQueue.size());
 		if (! _abFailoverQueue.empty()) {
 			dequeueNextActiveBackupPath(now);
@@ -1474,16 +1610,29 @@ void Bond::processActiveBackupTasks(void* tPtr, int64_t now)
 	if (prevActiveBackupPathIdx != _abPathIdx) {
 		_lastActiveBackupPathChange = now;
 	}
+	if (_abFailoverQueue.empty()) {
+		return;	  // No sense in continuing since there are no links to switch to
+	}
+
 	if (_abLinkSelectMethod == ZT_BOND_RESELECTION_POLICY_ALWAYS) {
-		if (_paths[_abPathIdx].p && ! getLink(_paths[_abPathIdx].p)->primary() && getLink(_paths[_abFailoverQueue.front()].p)->primary()) {
+		SharedPtr<Link> abLink = getLink(_paths[_abPathIdx].p);
+		if (! _paths[_abFailoverQueue.front()].p) {
+			log("invalid link. not switching");
+			return;
+		}
+
+		SharedPtr<Link> abFailoverLink = getLink(_paths[_abFailoverQueue.front()].p);
+		if (abLink && ! abLink->primary() && _paths[_abFailoverQueue.front()].p && abFailoverLink && abFailoverLink->primary()) {
 			dequeueNextActiveBackupPath(now);
 			log("switch back to available primary link %s (select mode: always)", pathToStr(_paths[_abPathIdx].p).c_str());
 		}
 	}
 	if (_abLinkSelectMethod == ZT_BOND_RESELECTION_POLICY_BETTER) {
-		if (_paths[_abPathIdx].p && ! getLink(_paths[_abPathIdx].p)->primary()) {
+		SharedPtr<Link> abLink = getLink(_paths[_abPathIdx].p);
+		if (abLink && ! abLink->primary()) {
 			// Active backup has switched to "better" primary link according to re-select policy.
-			if (getLink(_paths[_abFailoverQueue.front()].p)->primary() && (_paths[_abFailoverQueue.front()].failoverScore > _paths[_abPathIdx].failoverScore)) {
+			SharedPtr<Link> abFailoverLink = getLink(_paths[_abFailoverQueue.front()].p);
+			if (_paths[_abFailoverQueue.front()].p && abFailoverLink && abFailoverLink->primary() && (_paths[_abFailoverQueue.front()].failoverScore > _paths[_abPathIdx].failoverScore)) {
 				dequeueNextActiveBackupPath(now);
 				log("switch back to user-defined primary link %s (select mode: better)", pathToStr(_paths[_abPathIdx].p).c_str());
 			}
@@ -1510,11 +1659,7 @@ void Bond::processActiveBackupTasks(void* tPtr, int64_t now)
 					if ((failoverScoreDifference > 0) && (failoverScoreDifference > thresholdQuantity)) {
 						SharedPtr<Path> oldPath = _paths[_abPathIdx].p;
 						dequeueNextActiveBackupPath(now);
-						log("switch from %s (score: %d) to better link %s (score: %d) (select mode: optimize)",
-							pathToStr(oldPath).c_str(),
-							prevFScore,
-							pathToStr(_paths[_abPathIdx].p).c_str(),
-							newFScore);
+						log("switch from %s (score: %d) to better link %s (score: %d) (select mode: optimize)", pathToStr(oldPath).c_str(), prevFScore, pathToStr(_paths[_abPathIdx].p).c_str(), newFScore);
 					}
 				}
 			}
@@ -1530,6 +1675,7 @@ void Bond::initTimers()
 	_lastPathNegotiationCheck = 0;
 	_lastPathNegotiationReceived = 0;
 	_lastQoSRateCheck = 0;
+	_lastAckRateCheck = 0;
 	_lastQualityEstimation = 0;
 	_lastBondStatusLog = 0;
 	_lastSummaryDump = 0;
@@ -1562,10 +1708,6 @@ void Bond::setBondParameters(int policy, SharedPtr<Bond> templateBond, bool useT
 	_localUtility = 0;
 	_negotiatedPathIdx = 0;
 
-	// QOS Verb (and related checks)
-
-	_qosCutoffCount = 0;
-
 	// User preferences which may override the default bonding algorithm's behavior
 
 	_userHasSpecifiedPrimaryLink = false;
@@ -1574,7 +1716,6 @@ void Bond::setBondParameters(int policy, SharedPtr<Bond> templateBond, bool useT
 
 	// Bond status
 
-	_isHealthy = false;
 	_numAliveLinks = 0;
 	_numTotalLinks = 0;
 	_numBondedPaths = 0;
@@ -1650,7 +1791,7 @@ void Bond::setBondParameters(int policy, SharedPtr<Bond> templateBond, bool useT
 		memcpy(_qw, templateBond->_qw, ZT_QOS_WEIGHT_SIZE * sizeof(float));
 	}
 
-	if (!_isLeaf) {
+	if (! _isLeaf) {
 		_policy = ZT_BOND_POLICY_ACTIVE_BACKUP;
 	}
 
@@ -1659,7 +1800,9 @@ void Bond::setBondParameters(int policy, SharedPtr<Bond> templateBond, bool useT
 	_monitorInterval = _failoverInterval / ZT_BOND_ECHOS_PER_FAILOVER_INTERVAL;
 	_qualityEstimationInterval = _failoverInterval * 2;
 	_qosSendInterval = _failoverInterval * 2;
+	_ackSendInterval = _failoverInterval * 2;
 	_qosCutoffCount = 0;
+	_ackCutoffCount = 0;
 	_defaultPathRefractoryPeriod = 8000;
 }
 
@@ -1678,17 +1821,24 @@ void Bond::setUserQualityWeights(float weights[], int len)
 
 SharedPtr<Link> Bond::getLink(const SharedPtr<Path>& path)
 {
-	return RR->bc->getLinkBySocket(_policyAlias, path->localSocket());
+	return ! path ? SharedPtr<Link>() : RR->bc->getLinkBySocket(_policyAlias, path->localSocket());
 }
 
 std::string Bond::pathToStr(const SharedPtr<Path>& path)
 {
 #ifdef ZT_TRACE
-	char pathStr[64] = { 0 };
-	char fullPathStr[384] = { 0 };
-	path->address().toString(pathStr);
-	snprintf(fullPathStr, 384, "%.16llx-%s/%s", (unsigned long long)(path->localSocket()), getLink(path)->ifname().c_str(), pathStr);
-	return std::string(fullPathStr);
+	if (path) {
+		char pathStr[64] = { 0 };
+		char fullPathStr[384] = { 0 };
+		path->address().toString(pathStr);
+		SharedPtr<Link> link = getLink(path);
+		if (link) {
+			std::string ifnameStr = std::string(link->ifname());
+			snprintf(fullPathStr, 384, "%.16llx-%s/%s", (unsigned long long)(path->localSocket()), ifnameStr.c_str(), pathStr);
+			return std::string(fullPathStr);
+		}
+	}
+	return "";
 #else
 	return "";
 #endif
@@ -1700,11 +1850,11 @@ void Bond::dumpPathStatus(int64_t now, int pathIdx)
 	std::string aliveOrDead = _paths[pathIdx].alive ? std::string("alive") : std::string("dead");
 	std::string eligibleOrNot = _paths[pathIdx].eligible ? std::string("eligible") : std::string("ineligible");
 	std::string bondedOrNot = _paths[pathIdx].bonded ? std::string("bonded") : std::string("unbonded");
-	log("path[%2d] --- %5s (in %7d, out: %7d), %10s, %8s, flows=%-6d lat=%-8.3f pdv=%-7.3f err=%-6.4f loss=%-6.4f alloc=%-3d --- (%s)",
+	log("path[%2u] --- %5s (in %7lld, out: %7lld), %10s, %8s, flows=%-6u lat=%-8.3f pdv=%-7.3f err=%-6.4f loss=%-6.4f alloc=%-3u --- (%s)",
 		pathIdx,
 		aliveOrDead.c_str(),
-		_paths[pathIdx].p->age(now),
-		(now - _paths[pathIdx].p->_lastOut),
+		static_cast<long long int>(_paths[pathIdx].p->age(now)),
+		static_cast<long long int>(_paths[pathIdx].p->_lastOut == 0 ? 0 : now - _paths[pathIdx].p->_lastOut),
 		eligibleOrNot.c_str(),
 		bondedOrNot.c_str(),
 		_paths[pathIdx].assignedFlowCount,
@@ -1727,7 +1877,7 @@ void Bond::dumpInfo(int64_t now, bool force)
 	_lastSummaryDump = now;
 	float overhead = (_overheadBytes / (timeSinceLastDump / 1000.0f) / 1000.0f);
 	_overheadBytes = 0;
-	log("bond: bp=%d, fi=%d, mi=%d, ud=%d, dd=%d, flows=%lu, leaf=%d, overhead=%f KB/s",
+	log("bond: bp=%d, fi=%d, mi=%d, ud=%d, dd=%d, flows=%lu, leaf=%d, overhead=%f KB/s, links=(%d/%d)",
 		_policy,
 		_failoverInterval,
 		_monitorInterval,
@@ -1735,7 +1885,9 @@ void Bond::dumpInfo(int64_t now, bool force)
 		_downDelay,
 		(unsigned long)_flows.size(),
 		_isLeaf,
-		overhead);
+		overhead,
+		_numAliveLinks,
+		_numTotalLinks);
 	for (int i = 0; i < ZT_MAX_PEER_NETWORK_PATHS; ++i) {
 		if (_paths[i].p) {
 			dumpPathStatus(now, i);

+ 68 - 13
node/Bond.hpp

@@ -436,7 +436,14 @@ class Bond {
 	 * @param peer Remote peer that this bond services
 	 * @return A pointer to the newly created Bond
 	 */
-	static SharedPtr<Bond> createTransportTriggeredBond(const RuntimeEnvironment* renv, const SharedPtr<Peer>& peer);
+	static SharedPtr<Bond> createBond(const RuntimeEnvironment* renv, const SharedPtr<Peer>& peer);
+
+	/**
+	 * Remove a bond from the bond controller.
+	 *
+	 * @param peerId Remote peer that this bond services
+	 */
+	static void destroyBond(uint64_t peerId);
 
 	/**
 	 * Periodically perform maintenance tasks for the bonding layer.
@@ -451,9 +458,10 @@ class Bond {
 	 *
 	 * @param policyAlias Policy in use
 	 * @param localSocket Local source socket
+	 * @param createIfNeeded Whether a Link object is created if the name wasn't previously in the link map
 	 * @return Physical link definition
 	 */
-	static SharedPtr<Link> getLinkBySocket(const std::string& policyAlias, uint64_t localSocket);
+	static SharedPtr<Link> getLinkBySocket(const std::string& policyAlias, uint64_t localSocket, bool createIfNeeded);
 
 	/**
 	 * Gets a reference to a physical link definition given its human-readable system name.
@@ -630,6 +638,15 @@ class Bond {
 	 */
 	void receivedQoS(const SharedPtr<Path>& path, int64_t now, int count, uint64_t* rx_id, uint16_t* rx_ts);
 
+	/**
+	 * Process the contents of an inbound VERB_ACK to gather path quality observations.
+	 *
+	 * @param pathIdx Path over which packet was received
+	 * @param now Current time
+	 * @param ackedBytes Number of bytes ACKed by this VERB_ACK
+	 */
+	void receivedAck(int pathIdx, int64_t now, int32_t ackedBytes);
+
 	/**
 	 * Generate the contents of a VERB_QOS_MEASUREMENT packet.
 	 *
@@ -871,6 +888,26 @@ class Bond {
 	 */
 	void processBackgroundBondTasks(void* tPtr, int64_t now);
 
+	/**
+	 * Rate limit gate for VERB_ACK
+	 *
+	 * @param now Current time
+	 * @return Whether the incoming packet should be rate-gated
+	 */
+	inline bool rateGateACK(const int64_t now)
+	{
+		_ackCutoffCount++;
+		int numToDrain = _lastAckRateCheck ? (now - _lastAckRateCheck) / ZT_ACK_DRAINAGE_DIVISOR : _ackCutoffCount;
+		_lastAckRateCheck = now;
+		if (_ackCutoffCount > numToDrain) {
+			_ackCutoffCount -= numToDrain;
+		}
+		else {
+			_ackCutoffCount = 0;
+		}
+		return (_ackCutoffCount < ZT_ACK_CUTOFF_LIMIT);
+	}
+
 	/**
 	 * Rate limit gate for VERB_QOS_MEASUREMENT
 	 *
@@ -1020,14 +1057,6 @@ class Bond {
 		return _policy;
 	}
 
-	/**
-	 * @return the health status of the bond
-	 */
-	inline bool isHealthy()
-	{
-		return _isHealthy;
-	}
-
 	/**
 	 * @return the number of links comprising this bond which are considered alive
 	 */
@@ -1142,6 +1171,9 @@ class Bond {
 	 *
 	 */
 	void log(const char* fmt, ...)
+#ifdef __GNUC__
+		__attribute__((format(printf, 2, 3)))
+#endif
 	{
 #ifdef ZT_TRACE
 		time_t rawtime;
@@ -1171,6 +1203,9 @@ class Bond {
 	 *
 	 */
 	void debug(const char* fmt, ...)
+#ifdef __GNUC__
+		__attribute__((format(printf, 2, 3)))
+#endif
 	{
 #ifdef ZT_DEBUG
 		time_t rawtime;
@@ -1198,7 +1233,11 @@ class Bond {
   private:
 	struct NominatedPath {
 		NominatedPath()
-			: lastQoSMeasurement(0)
+			: lastAckSent(0)
+			, lastAckReceived(0)
+			, unackedBytes(0)
+			, packetsReceivedSinceLastAck(0)
+			, lastQoSMeasurement(0)
 			, lastThroughputEstimation(0)
 			, lastRefractoryUpdate(0)
 			, lastAliveToggle(0)
@@ -1289,6 +1328,15 @@ class Bond {
 			return ((packetsReceivedSinceLastQoS >= ZT_QOS_TABLE_SIZE) || ((now - lastQoSMeasurement) > qosSendInterval)) && packetsReceivedSinceLastQoS;
 		}
 
+		/**
+		 * @param now Current time
+		 * @return Whether an ACK (VERB_ACK) packet needs to be emitted at this time
+		 */
+		inline bool needsToSendAck(int64_t now, int ackSendInterval)
+		{
+			return ((now - lastAckSent) >= ackSendInterval || (packetsReceivedSinceLastAck == ZT_QOS_TABLE_SIZE)) && packetsReceivedSinceLastAck;
+		}
+
 		/**
 		 * Reset packet counters
 		 */
@@ -1300,6 +1348,7 @@ class Bond {
 
 		std::map<uint64_t, uint64_t> qosStatsOut;	// id:egress_time
 		std::map<uint64_t, uint64_t> qosStatsIn;	// id:now
+		std::map<uint64_t, uint64_t> ackStatsIn;	// id:now
 
 		RingBuffer<int, ZT_QOS_SHORTTERM_SAMPLE_WIN_SIZE> qosRecordSize;
 		RingBuffer<float, ZT_QOS_SHORTTERM_SAMPLE_WIN_SIZE> qosRecordLossSamples;
@@ -1308,6 +1357,11 @@ class Bond {
 		RingBuffer<float, ZT_QOS_SHORTTERM_SAMPLE_WIN_SIZE> throughputVarianceSamples;
 		RingBuffer<uint16_t, ZT_QOS_SHORTTERM_SAMPLE_WIN_SIZE> latencySamples;
 
+		uint64_t lastAckSent;
+		uint64_t lastAckReceived;
+		uint64_t unackedBytes;
+		uint64_t packetsReceivedSinceLastAck;
+
 		uint64_t lastQoSMeasurement;		 // Last time that a VERB_QOS_MEASUREMENT was sent out on this path.
 		uint64_t lastThroughputEstimation;	 // Last time that the path's throughput was estimated.
 		uint64_t lastRefractoryUpdate;		 // The last time that the refractory period was updated.
@@ -1344,7 +1398,7 @@ class Bond {
 		int packetsIn;
 		int packetsOut;
 
-		AtomicCounter __refCount;
+		// AtomicCounter __refCount;
 
 		SharedPtr<Path> p;
 		void set(uint64_t now, const SharedPtr<Path>& path)
@@ -1490,7 +1544,6 @@ class Bond {
 	/**
 	 * Link state reporting
 	 */
-	bool _isHealthy;
 	uint8_t _numAliveLinks;
 	uint8_t _numTotalLinks;
 
@@ -1508,7 +1561,9 @@ class Bond {
 	 * Rate-limiting
 	 */
 	uint16_t _qosCutoffCount;
+	uint16_t _ackCutoffCount;
 	uint64_t _lastQoSRateCheck;
+	uint64_t _lastAckRateCheck;
 	uint16_t _pathNegotiationCutoffCount;
 	uint64_t _lastPathNegotiationReceived;
 

+ 25 - 15
node/Constants.hpp

@@ -50,16 +50,17 @@
 #define __UNIX_LIKE__
 #endif
 #include <endian.h>
-
 #if (defined(__amd64) || defined(__amd64__) || defined(__x86_64) || defined(__x86_64__) || defined(__AMD64) || defined(__AMD64__) || defined(_M_X64) || defined(__aarch64__))
-#define OIDC_SUPPORTED 1
-#else
-#define OIDC_SUPPORTED 0
+#ifdef ZT_SSO_SUPPORTED
+#define ZT_SSO_ENABLED 1
+#endif
 #endif
 #endif
 
 #ifdef __APPLE__
-#define OIDC_SUPPORTED 1
+#ifdef ZT_SSO_SUPPORTED
+#define ZT_SSO_ENABLED 1
+#endif
 #define likely(x) __builtin_expect((x),1)
 #define unlikely(x) __builtin_expect((x),0)
 #include <TargetConditionals.h>
@@ -73,7 +74,9 @@
 #endif
 
 #if defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__)
-#define OIDC_SUPPORTED 0
+#ifdef ZT_SSO_SUPPORTED
+#define ZT_SSO_ENABLED 0
+#endif
 #ifndef __UNIX_LIKE__
 #define __UNIX_LIKE__
 #endif
@@ -89,7 +92,9 @@
 #endif
 
 #if defined(_WIN32) || defined(_WIN64)
-#define OIDC_SUPPORTED 1
+#ifdef ZT_SSO_SUPPORTED
+#define ZT_SSO_ENABLED 1
+#endif
 #ifndef __WINDOWS__
 #define __WINDOWS__
 #endif
@@ -368,9 +373,14 @@
 #define ZT_QOS_TABLE_SIZE ((ZT_QOS_MAX_PACKET_SIZE * 8) / (64 + 16))
 
 /**
- * Maximum number of outgoing packets we monitor for QoS information
+ * Maximum number of packets we monitor for ACK information at any given time
+ */
+#define ZT_ACK_MAX_PENDING_RECORDS (32 * 1024)
+
+/**
+ * Maximum number of packets we monitor for QoS information at any given time
  */
-#define ZT_QOS_MAX_OUTSTANDING_RECORDS (1024 * 16)
+#define ZT_QOS_MAX_PENDING_RECORDS (ZT_QOS_TABLE_SIZE * 3)
 
 /**
  * Interval used for rate-limiting the computation of path quality estimates.
@@ -525,14 +535,14 @@
 #define ZT_PUSH_DIRECT_PATHS_MAX_PER_SCOPE_AND_FAMILY 8
 
 /**
- * Time horizon for VERB_NETWORK_CREDENTIALS cutoff
+ * Rate limit for network credential pushes from peer.
  */
-#define ZT_PEER_CREDENTIALS_CUTOFF_TIME 60000
+#define ZT_PEER_CREDENTIALS_RATE_LIMIT 1000
 
 /**
- * Maximum number of VERB_NETWORK_CREDENTIALS within cutoff time
+ * Rate limit for responding to peer credential requests
  */
-#define ZT_PEER_CREDENTIALS_CUTOFF_LIMIT 15
+#define ZT_PEER_CREDENTIALS_REQUEST_RATE_LIMIT 1000
 
 /**
  * WHOIS rate limit (we allow these to be pretty fast)
@@ -571,13 +581,13 @@
  * Anything below this value gets into thrashing territory since we divide
  * this value by ZT_BOND_ECHOS_PER_FAILOVER_INTERVAL to send ECHOs often.
  */
-#define ZT_BOND_FAILOVER_MIN_INTERVAL 250
+#define ZT_BOND_FAILOVER_MIN_INTERVAL 500
 
 /**
  * How many times per failover interval that an ECHO is sent. This should be
  * at least 2. Anything more then 4 starts to increase overhead significantly.
  */
-#define ZT_BOND_ECHOS_PER_FAILOVER_INTERVAL 4
+#define ZT_BOND_ECHOS_PER_FAILOVER_INTERVAL 3
 
 /**
  * A defensive timer to prevent path quality metrics from being

+ 34 - 30
node/IncomingPacket.cpp

@@ -47,14 +47,13 @@ bool IncomingPacket::tryDecode(const RuntimeEnvironment *RR,void *tPtr,int32_t f
 	try {
 		// Check for trusted paths or unencrypted HELLOs (HELLO is the only packet sent in the clear)
 		const unsigned int c = cipher();
-		bool trusted = false;
 		if (c == ZT_PROTO_CIPHER_SUITE__NO_CRYPTO_TRUSTED_PATH) {
 			// If this is marked as a packet via a trusted path, check source address and path ID.
 			// Obviously if no trusted paths are configured this always returns false and such
 			// packets are dropped on the floor.
 			const uint64_t tpid = trustedPathId();
 			if (RR->topology->shouldInboundPathBeTrusted(_path->address(),tpid)) {
-				trusted = true;
+				_authenticated = true;
 			} else {
 				RR->t->incomingPacketMessageAuthenticationFailure(tPtr,_path,packetId(),sourceAddress,hops(),"path not trusted");
 				return true;
@@ -66,7 +65,7 @@ bool IncomingPacket::tryDecode(const RuntimeEnvironment *RR,void *tPtr,int32_t f
 
 		const SharedPtr<Peer> peer(RR->topology->getPeer(tPtr,sourceAddress));
 		if (peer) {
-			if (!trusted) {
+			if (!_authenticated) {
 				if (!dearmor(peer->key(), peer->aesKeys())) {
 					RR->t->incomingPacketMessageAuthenticationFailure(tPtr,_path,packetId(),sourceAddress,hops(),"invalid MAC");
 					peer->recordIncomingInvalidPacket(_path);
@@ -79,6 +78,7 @@ bool IncomingPacket::tryDecode(const RuntimeEnvironment *RR,void *tPtr,int32_t f
 				return true;
 			}
 
+			_authenticated = true;
 			const Packet::Verb v = verb();
 
 			bool r = true;
@@ -88,6 +88,7 @@ bool IncomingPacket::tryDecode(const RuntimeEnvironment *RR,void *tPtr,int32_t f
 					peer->received(tPtr,_path,hops(),packetId(),payloadLength(),v,0,Packet::VERB_NOP,false,0,ZT_QOS_NO_FLOW);
 					break;
 				case Packet::VERB_HELLO:                      r = _doHELLO(RR,tPtr,true); break;
+				case Packet::VERB_ACK            :            r = _doACK(RR,tPtr,peer); break;
 				case Packet::VERB_QOS_MEASUREMENT:            r = _doQOS_MEASUREMENT(RR,tPtr,peer); break;
 				case Packet::VERB_ERROR:                      r = _doERROR(RR,tPtr,peer); break;
 				case Packet::VERB_OK:                         r = _doOK(RR,tPtr,peer); break;
@@ -169,7 +170,7 @@ bool IncomingPacket::_doERROR(const RuntimeEnvironment *RR,void *tPtr,const Shar
 			const SharedPtr<Network> network(RR->node->network(networkId));
 			const int64_t now = RR->node->now();
 			if ((network)&&(network->config().com))
-				network->pushCredentialsNow(tPtr,peer->address(),now);
+				network->peerRequestedCredentials(tPtr,peer->address(),now);
 		}	break;
 
 		case Packet::ERROR_NETWORK_ACCESS_DENIED_: {
@@ -250,28 +251,47 @@ bool IncomingPacket::_doERROR(const RuntimeEnvironment *RR,void *tPtr,const Shar
 	return true;
 }
 
-bool IncomingPacket::_doQOS_MEASUREMENT(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer)
+bool IncomingPacket::_doACK(const RuntimeEnvironment* RR, void* tPtr, const SharedPtr<Peer>& peer)
 {
+	/*
 	SharedPtr<Bond> bond = peer->bond();
-	if (!bond || !bond->rateGateQoS(RR->node->now(), _path)) {
+	if (! bond || ! bond->rateGateACK(RR->node->now())) {
+		return true;
+	}
+	int32_t ackedBytes;
+	if (payloadLength() != sizeof(ackedBytes)) {
+		return true;   // ignore
+	}
+	memcpy(&ackedBytes, payload(), sizeof(ackedBytes));
+	if (bond) {
+		bond->receivedAck(_path, RR->node->now(), Utils::ntoh(ackedBytes));
+	}
+	*/
+	return true;
+}
+
+bool IncomingPacket::_doQOS_MEASUREMENT(const RuntimeEnvironment* RR, void* tPtr, const SharedPtr<Peer>& peer)
+{
+	SharedPtr<Bond> bond = peer->bond();
+	if (! bond || ! bond->rateGateQoS(RR->node->now(), _path)) {
 		return true;
 	}
 	if (payloadLength() > ZT_QOS_MAX_PACKET_SIZE || payloadLength() < ZT_QOS_MIN_PACKET_SIZE) {
-		return true; // ignore
+		return true;   // ignore
 	}
 	const int64_t now = RR->node->now();
 	uint64_t rx_id[ZT_QOS_TABLE_SIZE];
 	uint16_t rx_ts[ZT_QOS_TABLE_SIZE];
-	char *begin = (char *)payload();
-	char *ptr = begin;
+	char* begin = (char*)payload();
+	char* ptr = begin;
 	int count = 0;
 	unsigned int len = payloadLength();
 	// Read packet IDs and latency compensation intervals for each packet tracked by this QoS packet
 	while (ptr < (begin + len) && (count < ZT_QOS_TABLE_SIZE)) {
 		memcpy((void*)&rx_id[count], ptr, sizeof(uint64_t));
-		ptr+=sizeof(uint64_t);
+		ptr += sizeof(uint64_t);
 		memcpy((void*)&rx_ts[count], ptr, sizeof(uint16_t));
-		ptr+=sizeof(uint16_t);
+		ptr += sizeof(uint16_t);
 		count++;
 	}
 	if (bond) {
@@ -860,7 +880,7 @@ bool IncomingPacket::_doEXT_FRAME(const RuntimeEnvironment *RR,void *tPtr,const
 bool IncomingPacket::_doECHO(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer)
 {
 	uint64_t now = RR->node->now();
-	if (!peer->rateGateEchoRequest(now)) {
+	if (!_path->rateGateEchoRequest(now)) {
 		return true;
 	}
 
@@ -1057,10 +1077,8 @@ bool IncomingPacket::_doNETWORK_CONFIG(const RuntimeEnvironment *RR,void *tPtr,c
 {
 	const SharedPtr<Network> network(RR->node->network(at<uint64_t>(ZT_PACKET_IDX_PAYLOAD)));
 	if (network) {
-		//fprintf(stderr, "IncomingPacket::_doNETWORK_CONFIG %.16llx\n", network->id());
 		const uint64_t configUpdateId = network->handleConfigChunk(tPtr,packetId(),source(),*this,ZT_PACKET_IDX_PAYLOAD);
 		if (configUpdateId) {
-			//fprintf(stderr, "Have config update ID: %llu\n", configUpdateId);
 			Packet outp(peer->address(), RR->identity.address(), Packet::VERB_OK);
 			outp.append((uint8_t)Packet::VERB_ECHO);
 			outp.append((uint64_t)packetId());
@@ -1069,9 +1087,7 @@ bool IncomingPacket::_doNETWORK_CONFIG(const RuntimeEnvironment *RR,void *tPtr,c
 			const int64_t now = RR->node->now();
 			outp.armor(peer->key(),true,peer->aesKeysIfSupported());
 			peer->recordOutgoingPacket(_path,outp.packetId(),outp.payloadLength(),outp.verb(),ZT_QOS_NO_FLOW,now);
-			if (!_path->send(RR,tPtr,outp.data(),outp.size(),RR->node->now())) {
-				//fprintf(stderr, "Error sending VERB_OK after NETWORK_CONFIG packet for %.16llx\n", network->id());
-			}
+			_path->send(RR,tPtr,outp.data(),outp.size(),RR->node->now());
 		}
 	}
 
@@ -1098,16 +1114,7 @@ bool IncomingPacket::_doMULTICAST_GATHER(const RuntimeEnvironment *RR,void *tPtr
 		} catch ( ... ) {} // discard invalid COMs
 	}
 
-	bool trustEstablished = false;
-	if (network) {
-		if (network->gate(tPtr,peer)) {
-			trustEstablished = true;
-		} else {
-			_sendErrorNeedCredentials(RR,tPtr,peer,nwid);
-			return false;
-		}
-	}
-
+	const bool trustEstablished = (network) ? network->gate(tPtr,peer) : false;
 	const int64_t now = RR->node->now();
 	if ((gatherLimit > 0)&&((trustEstablished)||(RR->topology->amUpstream())||(RR->node->localControllerHasAuthorized(now,nwid,peer->address())))) {
 		Packet outp(peer->address(),RR->identity.address(),Packet::VERB_OK);
@@ -1224,9 +1231,6 @@ bool IncomingPacket::_doMULTICAST_FRAME(const RuntimeEnvironment *RR,void *tPtr,
 		}
 
 		peer->received(tPtr,_path,hops(),packetId(),payloadLength(),Packet::VERB_MULTICAST_FRAME,0,Packet::VERB_NOP,true,nwid,ZT_QOS_NO_FLOW);
-	} else {
-		_sendErrorNeedCredentials(RR,tPtr,peer,nwid);
-		return false;
 	}
 
 	return true;

+ 8 - 2
node/IncomingPacket.hpp

@@ -51,7 +51,9 @@ class IncomingPacket : public Packet
 public:
 	IncomingPacket() :
 		Packet(),
-		_receiveTime(0)
+		_receiveTime(0),
+		_path(),
+		_authenticated(false)
 	{
 	}
 
@@ -67,7 +69,8 @@ public:
 	IncomingPacket(const void *data,unsigned int len,const SharedPtr<Path> &path,int64_t now) :
 		Packet(data,len),
 		_receiveTime(now),
-		_path(path)
+		_path(path),
+		_authenticated(false)
 	{
 	}
 
@@ -85,6 +88,7 @@ public:
 		copyFrom(data,len);
 		_receiveTime = now;
 		_path = path;
+		_authenticated = false;
 	}
 
 	/**
@@ -112,6 +116,7 @@ private:
 	// been authenticated, decrypted, decompressed, and classified.
 	bool _doERROR(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer);
 	bool _doHELLO(const RuntimeEnvironment *RR,void *tPtr,const bool alreadyAuthenticated);
+	bool _doACK(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer);
 	bool _doQOS_MEASUREMENT(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer);
 	bool _doOK(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer);
 	bool _doWHOIS(const RuntimeEnvironment *RR,void *tPtr,const SharedPtr<Peer> &peer);
@@ -134,6 +139,7 @@ private:
 
 	uint64_t _receiveTime;
 	SharedPtr<Path> _path;
+	bool _authenticated;
 };
 
 } // namespace ZeroTier

+ 2 - 1
node/Membership.cpp

@@ -115,7 +115,7 @@ Membership::AddCredentialResult Membership::addCredential(const RuntimeEnvironme
 		RR->t->credentialRejected(tPtr,com,"old");
 		return ADD_REJECTED;
 	}
-	if ((newts == oldts)&&(_com == com))
+	if (_com == com)
 		return ADD_ACCEPTED_REDUNDANT;
 
 	switch(com.verify(RR,tPtr)) {
@@ -123,6 +123,7 @@ Membership::AddCredentialResult Membership::addCredential(const RuntimeEnvironme
 			RR->t->credentialRejected(tPtr,com,"invalid");
 			return ADD_REJECTED;
 		case 0:
+			//printf("%.16llx %.10llx replacing COM %lld with %lld\n", com.networkId(), com.issuedTo().toInt(), _com.timestamp(), com.timestamp()); fflush(stdout);
 			_com = com;
 			return ADD_ACCEPTED_NEW;
 		case 1:

+ 4 - 10
node/Membership.hpp

@@ -64,13 +64,9 @@ public:
 	 */
 	void pushCredentials(const RuntimeEnvironment *RR,void *tPtr,const int64_t now,const Address &peerAddress,const NetworkConfig &nconf);
 
-	/**
-	 * @return True if we haven't pushed credentials in a long time (to cause proactive credential push)
-	 */
-	inline bool shouldPushCredentials(const int64_t now) const
-	{
-		return ((now - _lastPushedCredentials) > ZT_PEER_ACTIVITY_TIMEOUT);
-	}
+	inline int64_t lastPushedCredentials() { return _lastPushedCredentials; }
+	inline int64_t comTimestamp() { return _com.timestamp(); }
+	inline int64_t comRevocationThreshold() { return _comRevocationThreshold; }
 
 	/**
 	 * Check whether we should push MULTICAST_LIKEs to this peer, and update last sent time if true
@@ -96,9 +92,7 @@ public:
 	 */
 	inline bool isAllowedOnNetwork(const NetworkConfig &thisNodeNetworkConfig, const Identity &otherNodeIdentity) const
 	{
-		if (thisNodeNetworkConfig.isPublic()) return true;
-		if (_com.timestamp() <= _comRevocationThreshold) return false;
-		return thisNodeNetworkConfig.com.agreesWith(_com, otherNodeIdentity);
+		return thisNodeNetworkConfig.isPublic() || (((_com.timestamp() > _comRevocationThreshold) && (thisNodeNetworkConfig.com.agreesWith(_com, otherNodeIdentity))));
 	}
 
 	inline bool recentlyAssociated(const int64_t now) const

+ 8 - 0
node/Network.cpp

@@ -1223,10 +1223,16 @@ void Network::requestConfiguration(void *tPtr)
 bool Network::gate(void *tPtr,const SharedPtr<Peer> &peer)
 {
 	const int64_t now = RR->node->now();
+	//int64_t comTimestamp = 0;
+	//int64_t comRevocationThreshold = 0;
 	Mutex::Lock _l(_lock);
 	try {
 		if (_config) {
 			Membership *m = _memberships.get(peer->address());
+			//if (m) {
+			//	comTimestamp = m->comTimestamp();
+			//	comRevocationThreshold = m->comRevocationThreshold();
+			//}
 			if ( (_config.isPublic()) || ((m)&&(m->isAllowedOnNetwork(_config, peer->identity()))) ) {
 				if (!m)
 					m = &(_membership(peer->address()));
@@ -1237,6 +1243,8 @@ bool Network::gate(void *tPtr,const SharedPtr<Peer> &peer)
 			}
 		}
 	} catch ( ... ) {}
+	//printf("%.16llx %.10llx not allowed, COM ts %lld revocation %lld\n", _id, peer->address().toInt(), comTimestamp, comRevocationThreshold); fflush(stdout);
+
 	return false;
 }
 

+ 8 - 4
node/Network.hpp

@@ -372,10 +372,13 @@ public:
 	 * @param to Destination peer address
 	 * @param now Current time
 	 */
-	inline void pushCredentialsNow(void *tPtr,const Address &to,const int64_t now)
+	inline void peerRequestedCredentials(void *tPtr,const Address &to,const int64_t now)
 	{
 		Mutex::Lock _l(_lock);
-		_membership(to).pushCredentials(RR,tPtr,now,to,_config);
+		Membership &m = _membership(to);
+		const int64_t lastPushed = m.lastPushedCredentials();
+		if ((lastPushed < _lastConfigUpdate)||((now - lastPushed) > ZT_PEER_CREDENTIALS_REQUEST_RATE_LIMIT))
+			m.pushCredentials(RR,tPtr,now,to,_config);
 	}
 
 	/**
@@ -389,7 +392,8 @@ public:
 	{
 		Mutex::Lock _l(_lock);
 		Membership &m = _membership(to);
-		if (m.shouldPushCredentials(now))
+		const int64_t lastPushed = m.lastPushedCredentials();
+		if ((lastPushed < _lastConfigUpdate)||((now - lastPushed) > ZT_PEER_ACTIVITY_TIMEOUT))
 			m.pushCredentials(RR,tPtr,now,to,_config);
 	}
 
@@ -439,7 +443,7 @@ private:
 	Hashtable< MAC,Address > _remoteBridgeRoutes; // remote addresses where given MACs are reachable (for tracking devices behind remote bridges)
 
 	NetworkConfig _config;
-	uint64_t _lastConfigUpdate;
+	int64_t _lastConfigUpdate;
 
 	struct _IncomingConfigChunk
 	{

+ 9 - 10
node/NetworkConfig.hpp

@@ -41,20 +41,19 @@
 #include "Trace.hpp"
 
 /**
- * Default maximum time delta for COMs, tags, and capabilities
- *
- * The current value is two hours, providing ample time for a controller to
- * experience fail-over, etc.
+ * Default time delta for COMs, tags, and capabilities
  */
-#define ZT_NETWORKCONFIG_DEFAULT_CREDENTIAL_TIME_MAX_MAX_DELTA 7200000ULL
+#define ZT_NETWORKCONFIG_DEFAULT_CREDENTIAL_TIME_DFL_MAX_DELTA ((int64_t)(1000 * 60 * 30))
 
 /**
- * Default minimum credential TTL and maxDelta for COM timestamps
- *
- * This is just slightly over three minutes and provides three retries for
- * all currently online members to refresh.
+ * Maximum time delta for COMs, tags, and capabilities
+ */
+#define ZT_NETWORKCONFIG_DEFAULT_CREDENTIAL_TIME_MAX_MAX_DELTA ((int64_t)(1000 * 60 * 60 * 2))
+
+/**
+ * Minimum credential TTL and maxDelta for COM timestamps
  */
-#define ZT_NETWORKCONFIG_DEFAULT_CREDENTIAL_TIME_MIN_MAX_DELTA 185000ULL
+#define ZT_NETWORKCONFIG_DEFAULT_CREDENTIAL_TIME_MIN_MAX_DELTA ((int64_t)(1000 * 60 * 5))
 
 /**
  * Flag: enable broadcast

+ 1 - 2
node/Node.cpp

@@ -509,7 +509,6 @@ ZT_PeerList *Node::peers() const
 		if (pi->second->bond()) {
 			p->isBonded = pi->second->bond();
 			p->bondingPolicy = pi->second->bond()->policy();
-			p->isHealthy = pi->second->bond()->isHealthy();
 			p->numAliveLinks = pi->second->bond()->getNumAliveLinks();
 			p->numTotalLinks = pi->second->bond()->getNumTotalLinks();
 		}
@@ -741,7 +740,7 @@ void Node::ncSendError(uint64_t nwid,uint64_t requestPacketId,const Address &des
 				n->setAccessDenied(nullptr);
 				break;
 			case NetworkController::NC_ERROR_AUTHENTICATION_REQUIRED: {
-				fprintf(stderr, "\n\nGot auth required\n\n");
+				//fprintf(stderr, "\n\nGot auth required\n\n");
 
 				break;
 			} 

+ 17 - 0
node/Path.hpp

@@ -83,6 +83,7 @@ public:
 		_lastOut(0),
 		_lastIn(0),
 		_lastTrustEstablishedPacketReceived(0),
+		_lastEchoRequestReceived(0),
 		_localSocket(-1),
 		_latency(0xffff),
 		_addr(),
@@ -93,6 +94,7 @@ public:
 		_lastOut(0),
 		_lastIn(0),
 		_lastTrustEstablishedPacketReceived(0),
+		_lastEchoRequestReceived(0),
 		_localSocket(localSocket),
 		_latency(0xffff),
 		_addr(addr),
@@ -266,6 +268,18 @@ public:
 	 */
 	inline int64_t lastTrustEstablishedPacketReceived() const { return _lastTrustEstablishedPacketReceived; }
 
+	/**
+	 * Rate limit gate for inbound ECHO requests
+	 */
+	inline bool rateGateEchoRequest(const int64_t now)
+	{
+		if ((now - _lastEchoRequestReceived) >= (ZT_PEER_GENERAL_RATE_LIMIT / 6)) {
+			_lastEchoRequestReceived = now;
+			return true;
+		}
+		return false;
+	}
+
 	void *_bondingMetricPtr;
 
 private:
@@ -273,6 +287,9 @@ private:
 	volatile int64_t _lastOut;
 	volatile int64_t _lastIn;
 	volatile int64_t _lastTrustEstablishedPacketReceived;
+
+	int64_t _lastEchoRequestReceived;
+
 	int64_t _localSocket;
 	volatile unsigned int _latency;
 	InetAddress _addr;

+ 4 - 4
node/Peer.cpp

@@ -34,7 +34,6 @@ Peer::Peer(const RuntimeEnvironment *renv,const Identity &myIdentity,const Ident
 	_lastTriedMemorizedPath(0),
 	_lastDirectPathPushSent(0),
 	_lastDirectPathPushReceive(0),
-	_lastEchoRequestReceived(0),
 	_lastCredentialRequestSent(0),
 	_lastWhoisRequestReceived(0),
 	_lastCredentialsReceived(0),
@@ -48,7 +47,6 @@ Peer::Peer(const RuntimeEnvironment *renv,const Identity &myIdentity,const Ident
 	_vRevision(0),
 	_id(peerIdentity),
 	_directPathPushCutoffCount(0),
-	_credentialsCutoffCount(0),
 	_echoRequestCutoffCount(0),
 	_localMultipathSupported(false),
 	_lastComputedAggregateMeanLatency(0)
@@ -224,6 +222,8 @@ void Peer::received(
 		if (sinceLastPush >= ((hops == 0) ? ZT_DIRECT_PATH_PUSH_INTERVAL_HAVEPATH : ZT_DIRECT_PATH_PUSH_INTERVAL)) {
 			_lastDirectPathPushSent = now;
 			std::vector<InetAddress> pathsToPush(RR->node->directPaths());
+			std::vector<InetAddress> ma = RR->sa->whoami();
+			pathsToPush.insert(pathsToPush.end(), ma.begin(), ma.end());
 			if (!pathsToPush.empty()) {
 				std::vector<InetAddress>::const_iterator p(pathsToPush.begin());
 				while (p != pathsToPush.end()) {
@@ -453,7 +453,7 @@ void Peer::sendHELLO(void *tPtr,const int64_t localSocket,const InetAddress &atA
 	if (atAddress) {
 		outp.armor(_key,false,nullptr); // false == don't encrypt full payload, but add MAC
 		RR->node->expectReplyTo(outp.packetId());
-		RR->node->putPacket(tPtr,localSocket,atAddress,outp.data(),outp.size());
+		RR->node->putPacket(tPtr,-1,atAddress,outp.data(),outp.size());
 	} else {
 		RR->node->expectReplyTo(outp.packetId());
 		RR->sw->send(tPtr,outp,false); // false == don't encrypt full payload, but add MAC
@@ -502,7 +502,7 @@ void Peer::performMultipathStateCheck(void *tPtr, int64_t now)
 	_localMultipathSupported = ((numAlivePaths >= 1) && (RR->bc->inUse()) && (ZT_PROTO_VERSION > 9));
 	if (_localMultipathSupported && !_bond) {
 		if (RR->bc) {
-			_bond = RR->bc->createTransportTriggeredBond(RR, this);
+			_bond = RR->bc->createBond(RR, this);
 			/**
 			 * Allow new bond to retroactively learn all paths known to this peer
 			 */

+ 9 - 20
node/Peer.hpp

@@ -53,7 +53,10 @@ private:
 	Peer() {} // disabled to prevent bugs -- should not be constructed uninitialized
 
 public:
-	~Peer() { Utils::burn(_key,sizeof(_key)); }
+	~Peer() {
+		Utils::burn(_key,sizeof(_key));
+		RR->bc->destroyBond(_id.address().toInt());
+	}
 
 	/**
 	 * Construct a new peer
@@ -387,11 +390,11 @@ public:
 	 */
 	inline bool rateGateCredentialsReceived(const int64_t now)
 	{
-		if ((now - _lastCredentialsReceived) <= ZT_PEER_CREDENTIALS_CUTOFF_TIME)
-			++_credentialsCutoffCount;
-		else _credentialsCutoffCount = 0;
-		_lastCredentialsReceived = now;
-		return (_credentialsCutoffCount < ZT_PEER_CREDENTIALS_CUTOFF_LIMIT);
+		if ((now - _lastCredentialsReceived) >= ZT_PEER_CREDENTIALS_RATE_LIMIT) {
+			_lastCredentialsReceived = now;
+			return true;
+		}
+		return false;
 	}
 
 	/**
@@ -418,18 +421,6 @@ public:
 		return false;
 	}
 
-	/**
-	 * Rate limit gate for inbound ECHO requests
-	 */
-	inline bool rateGateEchoRequest(const int64_t now)
-	{
-		if ((now - _lastEchoRequestReceived) >= ZT_PEER_GENERAL_RATE_LIMIT) {
-			_lastEchoRequestReceived = now;
-			return true;
-		}
-		return false;
-	}
-
 	/**
 	 * Serialize a peer for storage in local cache
 	 *
@@ -546,7 +537,6 @@ private:
 	int64_t _lastTriedMemorizedPath;
 	int64_t _lastDirectPathPushSent;
 	int64_t _lastDirectPathPushReceive;
-	int64_t _lastEchoRequestReceived;
 	int64_t _lastCredentialRequestSent;
 	int64_t _lastWhoisRequestReceived;
 	int64_t _lastCredentialsReceived;
@@ -573,7 +563,6 @@ private:
 	Identity _id;
 
 	unsigned int _directPathPushCutoffCount;
-	unsigned int _credentialsCutoffCount;
 	unsigned int _echoRequestCutoffCount;
 
 	AtomicCounter __refCount;

+ 2 - 2
node/Revocation.hpp

@@ -67,7 +67,7 @@ public:
 	 * @param tgt Target node whose credential(s) are being revoked
 	 * @param ct Credential type being revoked
 	 */
-	Revocation(const uint32_t i,const uint64_t nwid,const uint32_t cid,const uint64_t thr,const uint64_t fl,const Address &tgt,const Credential::Type ct) :
+	Revocation(const uint32_t i,const uint64_t nwid,const uint32_t cid,const int64_t thr,const uint64_t fl,const Address &tgt,const Credential::Type ct) :
 		_id(i),
 		_credentialId(cid),
 		_networkId(nwid),
@@ -155,7 +155,7 @@ public:
 		_networkId = b.template at<uint64_t>(p); p += 8;
 		p += 4; // 4 bytes, currently unused
 		_credentialId = b.template at<uint32_t>(p); p += 4;
-		_threshold = b.template at<uint64_t>(p); p += 8;
+		_threshold = (int64_t)b.template at<uint64_t>(p); p += 8;
 		_flags = b.template at<uint64_t>(p); p += 8;
 		_target.setTo(b.field(p,ZT_ADDRESS_LENGTH),ZT_ADDRESS_LENGTH); p += ZT_ADDRESS_LENGTH;
 		_signedBy.setTo(b.field(p,ZT_ADDRESS_LENGTH),ZT_ADDRESS_LENGTH); p += ZT_ADDRESS_LENGTH;

+ 1 - 1
node/Salsa20.hpp

@@ -15,7 +15,7 @@
 #include "Constants.hpp"
 #include "Utils.hpp"
 
-#if (!defined(ZT_SALSA20_SSE)) && (defined(__SSE2__) || (defined(__WINDOWS__) && !defined(__MINGW32__)))
+#if (!defined(ZT_SALSA20_SSE)) && (defined(__SSE2__) || (defined(__WINDOWS__) && !defined(__MINGW32__) && !defined(_M_ARM64)))
 #define ZT_SALSA20_SSE 1
 #endif
 

+ 15 - 0
node/SelfAwareness.cpp

@@ -99,6 +99,21 @@ void SelfAwareness::iam(void *tPtr,const Address &reporter,const int64_t receive
 	}
 }
 
+std::vector<InetAddress> SelfAwareness::whoami()
+{
+	std::vector<InetAddress> surfaceAddresses;
+	Mutex::Lock _l(_phy_m);
+	Hashtable< PhySurfaceKey,PhySurfaceEntry >::Iterator i(_phy);
+	PhySurfaceKey *k = (PhySurfaceKey *)0;
+	PhySurfaceEntry *e = (PhySurfaceEntry *)0;
+	while (i.next(k,e)) {
+		if (std::find(surfaceAddresses.begin(), surfaceAddresses.end(), e->mySurface) == surfaceAddresses.end()) {
+			surfaceAddresses.push_back(e->mySurface);
+		}
+	}
+	return surfaceAddresses;
+}
+
 void SelfAwareness::clean(int64_t now)
 {
 	Mutex::Lock _l(_phy_m);

+ 7 - 0
node/SelfAwareness.hpp

@@ -44,6 +44,13 @@ public:
 	 */
 	void iam(void *tPtr,const Address &reporter,const int64_t receivedOnLocalSocket,const InetAddress &reporterPhysicalAddress,const InetAddress &myPhysicalAddress,bool trusted,int64_t now);
 
+	/**
+	 * Return all known external surface addresses reported by peers
+	 *
+	 * @return A vector of InetAddress(es)
+	 */
+	std::vector<InetAddress> whoami();
+
 	/**
 	 * Clean up database periodically
 	 *

+ 9 - 31
one.cpp

@@ -88,7 +88,7 @@
 
 #include "service/OneService.hpp"
 
-#include "ext/json/json.hpp"
+#include <nlohmann/json.hpp>
 
 #ifdef __APPLE__
 #include <SystemConfiguration/SystemConfiguration.h>
@@ -523,31 +523,23 @@ static int cli(int argc,char **argv)
 					printf("%s" ZT_EOL_S,OSUtils::jsonDump(j).c_str());
 				} else {
 					bool bFoundBond = false;
-					printf("    <peer>                        <bondtype>    <status>    <links>" ZT_EOL_S);
+					printf("    <peer>                        <bondtype>     <links>" ZT_EOL_S);
 					if (j.is_array()) {
 						for(unsigned long k=0;k<j.size();++k) {
 							nlohmann::json &p = j[k];
 							bool isBonded = p["isBonded"];
 							if (isBonded) {
 								int8_t bondingPolicy = p["bondingPolicy"];
-								bool isHealthy = p["isHealthy"];
 								int8_t numAliveLinks = p["numAliveLinks"];
 								int8_t numTotalLinks = p["numTotalLinks"];
 								bFoundBond = true;
-								std::string healthStr;
-								if (isHealthy) {
-									healthStr = "HEALTHY";
-								} else {
-									healthStr = "DEGRADED";
-								}
 								std::string policyStr = "none";
 								if (bondingPolicy >= ZT_BOND_POLICY_NONE && bondingPolicy <= ZT_BOND_POLICY_BALANCE_AWARE) {
 									policyStr = Bond::getPolicyStrByCode(bondingPolicy);
 								}
-								printf("%10s  %32s    %8s        %d/%d" ZT_EOL_S,
+								printf("%10s  %32s         %d/%d" ZT_EOL_S,
 									OSUtils::jsonString(p ["address"],"-").c_str(),
 									policyStr.c_str(),
-									healthStr.c_str(),
 									numAliveLinks,
 									numTotalLinks);
 							}
@@ -617,12 +609,6 @@ static int cli(int argc,char **argv)
 					if (json) {
 						printf("%s" ZT_EOL_S,OSUtils::jsonDump(j).c_str());
 					} else {
-						std::string healthStr;
-						if (OSUtils::jsonInt(j["isHealthy"],0)) {
-							healthStr = "Healthy";
-						} else {
-							healthStr = "Degraded";
-						}
 						int numAliveLinks = OSUtils::jsonInt(j["numAliveLinks"],0);
 						int numTotalLinks = OSUtils::jsonInt(j["numTotalLinks"],0);
 						printf("Peer               : %s\n", arg1.c_str());
@@ -630,7 +616,6 @@ static int cli(int argc,char **argv)
 						//if (bondingPolicy == ZT_BOND_POLICY_ACTIVE_BACKUP) {
 						printf("Link Select Method : %d\n", (int)OSUtils::jsonInt(j["linkSelectMethod"],0));
 						//}
-						printf("Status             : %s\n", healthStr.c_str());
 						printf("Links              : %d/%d\n", numAliveLinks, numTotalLinks);
 						printf("Failover Interval  : %d (ms)\n", (int)OSUtils::jsonInt(j["failoverInterval"],0));
 						printf("Up Delay           : %d (ms)\n", (int)OSUtils::jsonInt(j["upDelay"],0));
@@ -705,33 +690,23 @@ static int cli(int argc,char **argv)
 				printf("%s" ZT_EOL_S,OSUtils::jsonDump(j).c_str());
 			} else {
 				bool bFoundBond = false;
-				printf("    <peer>                        <bondtype>    <status>    <links>" ZT_EOL_S);
+				printf("    <peer>                        <bondtype>     <links>" ZT_EOL_S);
 				if (j.is_array()) {
 					for(unsigned long k=0;k<j.size();++k) {
 						nlohmann::json &p = j[k];
 						bool isBonded = p["isBonded"];
 						if (isBonded) {
 							int8_t bondingPolicy = p["bondingPolicy"];
-							bool isHealthy = p["isHealthy"];
 							int8_t numAliveLinks = p["numAliveLinks"];
 							int8_t numTotalLinks = p["numTotalLinks"];
-
 							bFoundBond = true;
-							std::string healthStr;
-							if (isHealthy) {
-								healthStr = "Healthy";
-							} else {
-								healthStr = "Degraded";
-							}
 							std::string policyStr = "none";
 							if (bondingPolicy >= ZT_BOND_POLICY_NONE && bondingPolicy <= ZT_BOND_POLICY_BALANCE_AWARE) {
 								policyStr = Bond::getPolicyStrByCode(bondingPolicy);
 							}
-
-							printf("%10s  %32s    %8s        %d/%d" ZT_EOL_S,
+							printf("%10s  %32s         %d/%d" ZT_EOL_S,
 								OSUtils::jsonString(p["address"],"-").c_str(),
 								policyStr.c_str(),
-								healthStr.c_str(),
 								numAliveLinks,
 								numTotalLinks);
 						}
@@ -800,7 +775,10 @@ static int cli(int argc,char **argv)
 								if (status == "AUTHENTICATION_REQUIRED") {
 									printf("    AUTH EXPIRED, URL: %s" ZT_EOL_S, OSUtils::jsonString(n["authenticationURL"], "(null)").c_str());
 								} else if (status == "OK") {
-									printf("    AUTH OK, expires in: %lld seconds" ZT_EOL_S, ((int64_t)authenticationExpiryTime - OSUtils::now()) / 1000LL);
+									int64_t expiresIn = ((int64_t)authenticationExpiryTime - OSUtils::now()) / 1000LL;
+									if (expiresIn >= 0) {
+										printf("    AUTH OK, expires in: %lld seconds" ZT_EOL_S, expiresIn);
+									}
 								}
 							}
 						}

+ 4 - 4
osdep/Binder.hpp

@@ -40,7 +40,7 @@
 #endif
 #endif
 
-#if (defined(__unix__) || defined(__APPLE__)) && !defined(__LINUX__)
+#if (defined(__unix__) || defined(__APPLE__)) && !defined(__LINUX__) && !defined(ZT_SDK)
 #include <net/if.h>
 #include <netinet6/in6_var.h>
 #include <sys/ioctl.h>
@@ -311,7 +311,7 @@ class Binder {
 			if (! gotViaProc) {
 				struct ifaddrs* ifatbl = (struct ifaddrs*)0;
 				struct ifaddrs* ifa;
-#if (defined(__unix__) || defined(__APPLE__)) && !defined(__LINUX__)
+#if (defined(__unix__) || defined(__APPLE__)) && !defined(__LINUX__) && !defined(ZT_SDK)
 				// set up an IPv6 socket so we can check the state of interfaces via SIOCGIFAFLAG_IN6
 				int infoSock = socket(AF_INET6, SOCK_DGRAM, 0);
 #endif
@@ -320,7 +320,7 @@ class Binder {
 					while (ifa) {
 						if ((ifa->ifa_name) && (ifa->ifa_addr)) {
 							InetAddress ip = *(ifa->ifa_addr);
-#if (defined(__unix__) || defined(__APPLE__)) && !defined(__LINUX__)
+#if (defined(__unix__) || defined(__APPLE__)) && !defined(__LINUX__) && !defined(ZT_SDK)
 							// Check if the address is an IPv6 Temporary Address, macOS/BSD version
 							if (ifa->ifa_addr->sa_family == AF_INET6) {
 								struct sockaddr_in6* sa6 = (struct sockaddr_in6*)ifa->ifa_addr;
@@ -368,7 +368,7 @@ class Binder {
 				else {
 					interfacesEnumerated = false;
 				}
-#if (defined(__unix__) || defined(__APPLE__)) && !defined(__LINUX__)
+#if (defined(__unix__) || defined(__APPLE__)) && !defined(__LINUX__) && !defined(ZT_SDK)
 				close(infoSock);
 #endif
 			}

+ 1 - 1
osdep/OSUtils.hpp

@@ -43,7 +43,7 @@
 #endif
 
 #ifndef OMIT_JSON_SUPPORT
-#include "../ext/json/json.hpp"
+#include <nlohmann/json.hpp>
 #endif
 
 namespace ZeroTier {

+ 30 - 10
osdep/Phy.hpp

@@ -229,23 +229,33 @@ public:
 	 * @param s Socket object
 	 * @return Underlying OS-type (usually int or long) file descriptor associated with object
 	 */
-	static inline ZT_PHY_SOCKFD_TYPE getDescriptor(PhySocket *s) throw() { return reinterpret_cast<PhySocketImpl *>(s)->sock; }
+	static inline ZT_PHY_SOCKFD_TYPE getDescriptor(PhySocket* s) throw()
+	{
+		return reinterpret_cast<PhySocketImpl*>(s)->sock;
+	}
 
 	/**
 	 * @param s Socket object
 	 * @return Pointer to user object
 	 */
-	static inline void** getuptr(PhySocket *s) throw() { return &(reinterpret_cast<PhySocketImpl *>(s)->uptr); }
+	static inline void** getuptr(PhySocket* s) throw()
+	{
+		return &(reinterpret_cast<PhySocketImpl*>(s)->uptr);
+	}
 
 	/**
 	 * @param s Socket object
 	 * @param nameBuf Buffer to store name of interface which this Socket object is bound to
 	 * @param buflen Length of buffer to copy name into
 	 */
-	static inline void getIfName(PhySocket *s, char *nameBuf, int buflen)
+	static inline void getIfName(PhySocket* s, char* nameBuf, int buflen)
 	{
+		PhySocketImpl& sws = *(reinterpret_cast<PhySocketImpl*>(s));
+		if (sws.type == ZT_PHY_SOCKET_CLOSED) {
+			return;
+		}
 		if (s) {
-			memcpy(nameBuf, reinterpret_cast<PhySocketImpl *>(s)->ifname, buflen);
+			memcpy(nameBuf, reinterpret_cast<PhySocketImpl*>(s)->ifname, buflen);
 		}
 	}
 
@@ -254,10 +264,14 @@ public:
 	 * @param ifname Buffer containing name of interface that this Socket object is bound to
 	 * @param len Length of name of interface
 	 */
-	static inline void setIfName(PhySocket *s, char *ifname, int len)
+	static inline void setIfName(PhySocket* s, char* ifname, int len)
 	{
+		PhySocketImpl& sws = *(reinterpret_cast<PhySocketImpl*>(s));
+		if (sws.type == ZT_PHY_SOCKET_CLOSED) {
+			return;
+		}
 		if (s) {
-			memcpy(&(reinterpret_cast<PhySocketImpl *>(s)->ifname), ifname, len);
+			memcpy(&(reinterpret_cast<PhySocketImpl*>(s)->ifname), ifname, len);
 		}
 	}
 
@@ -270,21 +284,27 @@ public:
 	inline void whack()
 	{
 #if defined(_WIN32) || defined(_WIN64)
-		::send(_whackSendSocket,(const char *)this,1,0);
+		::send(_whackSendSocket, (const char*)this, 1, 0);
 #else
-		(void)(::write(_whackSendSocket,(PhySocket *)this,1));
+		(void)(::write(_whackSendSocket, (PhySocket*)this, 1));
 #endif
 	}
 
 	/**
 	 * @return Number of open sockets
 	 */
-	inline unsigned long count() const throw() { return _socks.size(); }
+	inline unsigned long count() const throw()
+	{
+		return _socks.size();
+	}
 
 	/**
 	 * @return Maximum number of sockets allowed
 	 */
-	inline unsigned long maxCount() const throw() { return ZT_PHY_MAX_SOCKETS; }
+	inline unsigned long maxCount() const throw()
+	{
+		return ZT_PHY_MAX_SOCKETS;
+	}
 
 	/**
 	 * Wrap a raw file descriptor in a PhySocket structure

+ 4 - 0
pkg/README.md

@@ -0,0 +1,4 @@
+Third-party packaging
+=====
+
+Builds packages for various embedded devices and appliances and platforms

+ 10 - 0
pkg/asustor/Dockerfile

@@ -0,0 +1,10 @@
+# vim: ft=dockerfile
+
+FROM ubuntu:20.04
+
+RUN apt-get update -qq && apt-get install python2.7 -y
+
+COPY apkg-tools.py /apkg-tools.py
+
+COPY entrypoint.sh /entrypoint.sh
+ENTRYPOINT ["/entrypoint.sh"]

+ 13 - 0
pkg/asustor/build.sh

@@ -0,0 +1,13 @@
+#!/bin/bash
+
+build_packages()
+{
+  sudo docker run -v $(pwd):/zto ztasustor
+}
+
+build_container()
+{
+  sudo docker build -t ztasustor . --load
+}
+
+"$@"

+ 29 - 0
pkg/asustor/entrypoint.sh

@@ -0,0 +1,29 @@
+#!/bin/bash
+
+ZTO_VER=$(git describe --abbrev=0 --tags)
+ZTO_COMMIT=$(git rev-parse HEAD)
+ZTO_DESC=$(jq -r '.desc' ../config.json)
+
+# Clean up any pre-existing packages
+find pkg/asustor -type f -name "*.apk" -exec rm -rvf {} \;
+# Copy current license
+cp ../../LICENSE.txt zerotier/control/license.txt
+# Configure package data
+tmp="config-tmp.json"
+jq --arg a "$ZTO_VER" '.general.version = $a' pkg/asustor/zerotier/CONTROL/config.json > $tmp && mv $tmp pkg/asustor/zerotier/CONTROL/config.json
+echo $ZTO_DESC > pkg/asustor/zerotier/CONTROL/description.txt
+
+# Copy binaries into pkg directory
+cp -vf output/static/zerotier-one.${ZTO_VER}.alpine-aarch64 pkg/asustor/zerotier/bin/zerotier-one.aarch64
+cp -vf output/static/zerotier-one.${ZTO_VER}.alpine-i386 pkg/asustor/zerotier/bin/zerotier-one.i386
+cp -vf output/static/zerotier-one.${ZTO_VER}.alpine-x86_64 pkg/asustor/zerotier/bin/zerotier-one.x86-64
+
+# Package
+python pkg/asustor/apkg-tools.py create pkg/asustor/zerotier
+rm -rf output/asustor
+mkdir -p output/asustor
+mv ./*.apk output/asustor
+
+# Show output product
+cat pkg/asustor/zerotier/CONTROL/config.json
+tree output/asustor

+ 15 - 0
pkg/asustor/zerotier/CONTROL/config.json

@@ -0,0 +1,15 @@
+{
+  "general": {
+    "package": "zerotier",
+    "name": "ZeroTier",
+    "version": "1.8.6",
+    "depends": [],
+    "conflicts": [],
+    "developer": "ZeroTier, Inc.",
+    "maintainer": "ZeroTier, Inc.",
+    "email": "[email protected]",
+    "website": "http://www.zerotier.com/",
+    "architecture": "any",
+    "firmware": "2.1.0"
+  }
+}

+ 1 - 0
pkg/asustor/zerotier/CONTROL/description.txt

@@ -0,0 +1 @@
+Securely connect any device, anywhere.

BIN
pkg/asustor/zerotier/CONTROL/icon.png


+ 149 - 0
pkg/asustor/zerotier/CONTROL/license.txt

@@ -0,0 +1,149 @@
+-----------------------------------------------------------------------------
+
+Business Source License 1.1
+
+License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
+"Business Source License" is a trademark of MariaDB Corporation Ab.
+
+-----------------------------------------------------------------------------
+
+Parameters
+
+Licensor:             ZeroTier, Inc.
+Licensed Work:        ZeroTier Network Virtualization Engine 1.4.4
+                      The Licensed Work is (c)2019 ZeroTier, Inc.
+Additional Use Grant: You may make use of the Licensed Work, provided you
+                      do not use it in any of the following ways:
+
+                      * Sell hosted ZeroTier services as a "SaaS" Product
+
+                      (1) Operate or sell access to ZeroTier root servers,
+                      network controllers, or authorization key or certificate
+                      generation components of the Licensed Work as a
+                      for-profit service, regardless of whether the use of
+                      these components is sold alone or is bundled with other
+                      services. Note that this does not apply to the use of
+                      ZeroTier behind the scenes to operate a service not
+                      related to ZeroTier network administration.
+
+                      * Create Non-Open-Source Commercial Derviative Works
+
+                      (2) Link or directly include the Licensed Work in a
+                      commercial or for-profit application or other product
+                      not distributed under an Open Source Initiative (OSI)
+                      compliant license. See: https://opensource.org/licenses
+
+                      (3) Remove the name, logo, copyright, or other branding
+                      material from the Licensed Work to create a "rebranded"
+                      or "white labeled" version to distribute as part of
+                      any commercial or for-profit product or service.
+
+                      * Certain Government Uses
+
+                      (4) Use or deploy the Licensed Work in a government
+                      setting in support of any active government function
+                      or operation with the exception of the following:
+                      physical or mental health care, family and social
+                      services, social welfare, senior care, child care, and
+                      the care of persons with disabilities.
+
+Change Date:          2025-01-01
+
+Change License:       Apache License version 2.0 as published by the Apache
+                      Software Foundation
+											https://www.apache.org/licenses/
+
+Alternative Licensing
+
+If you would like to use the Licensed Work in any way that conflicts with
+the stipulations of the Additional Use Grant, contact ZeroTier, Inc. to
+obtain an alternative commercial license.
+
+Visit us on the web at: https://www.zerotier.com/
+
+Notice
+
+The Business Source License (this document, or the "License") is not an Open
+Source license. However, the Licensed Work will eventually be made available
+under an Open Source License, as stated in this License.
+
+For more information on the use of the Business Source License for ZeroTier
+products, please visit our pricing page which contains license details and
+and license FAQ: https://zerotier.com/pricing
+
+For more information on the use of the Business Source License generally,
+please visit the Adopting and Developing Business Source License FAQ at
+https://mariadb.com/bsl-faq-adopting.
+
+-----------------------------------------------------------------------------
+
+Business Source License 1.1
+
+Terms
+
+The Licensor hereby grants you the right to copy, modify, create derivative
+works, redistribute, and make non-production use of the Licensed Work. The
+Licensor may make an Additional Use Grant, above, permitting limited
+production use.
+
+Effective on the Change Date, or the fourth anniversary of the first publicly
+available distribution of a specific version of the Licensed Work under this
+License, whichever comes first, the Licensor hereby grants you rights under
+the terms of the Change License, and the rights granted in the paragraph
+above terminate.
+
+If your use of the Licensed Work does not comply with the requirements
+currently in effect as described in this License, you must purchase a
+commercial license from the Licensor, its affiliated entities, or authorized
+resellers, or you must refrain from using the Licensed Work.
+
+All copies of the original and modified Licensed Work, and derivative works
+of the Licensed Work, are subject to this License. This License applies
+separately for each version of the Licensed Work and the Change Date may vary
+for each version of the Licensed Work released by Licensor.
+
+You must conspicuously display this License on each original or modified copy
+of the Licensed Work. If you receive the Licensed Work in original or
+modified form from a third party, the terms and conditions set forth in this
+License apply to your use of that work.
+
+Any use of the Licensed Work in violation of this License will automatically
+terminate your rights under this License for the current and all other
+versions of the Licensed Work.
+
+This License does not grant you any right in any trademark or logo of
+Licensor or its affiliates (provided that you may use a trademark or logo of
+Licensor as expressly required by this License).
+
+TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
+AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
+EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
+TITLE.
+
+-----------------------------------------------------------------------------
+
+MariaDB hereby grants you permission to use this License’s text to license
+your works, and to refer to it using the trademark "Business Source License",
+as long as you comply with the Covenants of Licensor below.
+
+Covenants of Licensor
+
+In consideration of the right to use this License’s text and the "Business
+Source License" name and trademark, Licensor covenants to MariaDB, and to all
+other recipients of the licensed work to be provided by Licensor:
+
+1. To specify as the Change License the GPL Version 2.0 or any later version,
+   or a license that is compatible with GPL Version 2.0 or a later version,
+   where "compatible" means that software provided under the Change License can
+   be included in a program with software provided under GPL Version 2.0 or a
+   later version. Licensor may specify additional Change Licenses without
+   limitation.
+
+2. To either: (a) specify an additional grant of rights to use that does not
+   impose any additional restriction on the right granted in this License, as
+   the Additional Use Grant; or (b) insert the text "None".
+
+3. To specify a Change Date.
+
+4. Not to modify this License in any other way.

+ 24 - 0
pkg/asustor/zerotier/CONTROL/post-install.sh

@@ -0,0 +1,24 @@
+#!/bin/sh
+
+APKG_PKG_DIR=/usr/local/AppCentral/zerotier
+
+case "$APKG_PKG_STATUS" in
+
+	install)
+		modprobe tun
+		mkdir -p /usr/local/bin
+		mv ${APKG_PKG_DIR}/bin/zerotier-one.${AS_NAS_ARCH} ${APKG_PKG_DIR}/bin/zerotier-one
+		ln -s ${APKG_PKG_DIR}/bin/zerotier-one /usr/local/bin/zerotier-cli
+		ln -s ${APKG_PKG_DIR}/bin/zerotier-one /usr/local/bin/zerotier-idtool
+		ln -s $APKG_PKG_DIR/data /var/lib/zerotier-one
+		;;
+	upgrade)
+		# post upgrade script here (restore data)
+		# cp -af $APKG_TEMP_DIR/* $APKG_PKG_DIR/etc/.
+		;;
+	*)
+		;;
+
+esac
+
+exit 0

+ 3 - 0
pkg/asustor/zerotier/CONTROL/post-uninstall.sh

@@ -0,0 +1,3 @@
+#!/bin/sh
+
+rm -rf /var/lib/zerotier-one/

+ 0 - 0
pkg/asustor/zerotier/CONTROL/pre-install.sh


+ 0 - 0
pkg/asustor/zerotier/CONTROL/pre-uninstall.sh


+ 26 - 0
pkg/asustor/zerotier/CONTROL/start-stop.sh

@@ -0,0 +1,26 @@
+#!/bin/sh
+
+. /etc/script/lib/command.sh
+
+APKG_PKG_DIR=/usr/local/AppCentral/zerotier
+
+case $1 in
+
+	start)
+		modprobe tun
+		# start script here
+		$APKG_PKG_DIR/bin/zerotier-one $APKG_PKG_DIR/data -d
+		;;
+
+	stop)
+		# stop script here
+		pkill zerotier
+		;;
+
+	*)
+		echo "usage: $0 {start|stop}"
+		exit 1
+		;;
+esac
+
+exit 0

+ 1 - 0
pkg/asustor/zerotier/apkg-version

@@ -0,0 +1 @@
+2.0

+ 58 - 0
pkg/asustor/zerotier/www/index.html

@@ -0,0 +1,58 @@
+<div class="header">
+  <h2 id="zerotier">ZeroTier</h2>
+</div>
+
+<p>Welcome! ZeroTier is a peer-to-peer encrypted virtual networking solution that enables you to create Local Area Networks with static IP assignments for all of your devices. Access your NAS from anywhere in the world with a single IP and without the need of cloud services backhauling your traffic. To use the CLI:</p>
+<h2 id="using-the-cli-via-ssh-">Using the CLI via SSH:</h2>
+<ol>
+<li><p>Using your (admin) account, enable SSH:</p>
+<ul>
+<li><strong><code>Services</code></strong> -&gt; <strong><code>Terminal</code></strong> -&gt; <strong><code>Enable SSH</code></strong> (set to port <code>22</code>)</li>
+</ul>
+</li>
+<li><p>From a computer, open a terminal and SSH into your NAS device:</p>
+<ul>
+<li><strong><code>ssh admin@your_nas_device_lan_ip</code></strong></li>
+</ul>
+</li>
+<li><p>Create account and network ID at <a href="https://my.zerotier.com">my.zerotier.com</a></p>
+<ul>
+<li><em>Note: This account is merely to administer your network. Your traffic is not handled by ZeroTier except in the case where a direct connection cannot be established. This is a courtesy service we offer for free that you can disable if you&#39;d like. In any case, your traffic is <a href="https://www.zerotier.com/manual/#2_1_3">fully encrypted</a> end-to-end.</em></li>
+</ul>
+</li>
+<li><p>Join your device to the network:</p>
+<ul>
+<li><strong><code>zerotier-cli join your_network_id</code></strong></li>
+</ul>
+</li>
+<li><p>Use <a href="https://my.zerotier.com">my.zerotier.com</a> to authorize your NAS device to join your network.</p>
+</li>
+</ol>
+<h2 id="help-and-support-in-order-of-relevance-">Help and support (in order of relevance)</h2>
+<ul>
+<li>Github Repository: <a href="https://github.com/zerotier/ZeroTierNAS/issues">https://github.com/zerotier/ZeroTierNAS</a></li>
+<li>Forums: <a href="https://discuss.zerotier.com">https://discuss.zerotier.com</a></li>
+<li>Contact: <a href="mailto:[email protected]">[email protected]</a></li>
+</ul>
+
+<div class="footer">
+  <h2 id="zerotier"><h2>
+</div>
+
+<style>
+.header {
+  padding: 1px;
+  text-align: center;
+  background: #F2B464;
+  color: black;
+  font-size: 30px;
+}
+
+.footer {
+  padding: 1px;
+  text-align: center;
+  background: #F2B464;
+  color: black;
+  font-size: 30px;
+}
+</style>

+ 6 - 0
pkg/config.json

@@ -0,0 +1,6 @@
+{
+	"version": "1.8.7",
+	"rev": "1",
+	"desc": "Securely connect any device, anywhere.",
+	"email": "[email protected]"
+}

+ 8 - 0
pkg/qnap/Dockerfile

@@ -0,0 +1,8 @@
+# vim: ft=dockerfile
+
+FROM ubuntu:20.04
+
+# COPY zerotier/qbuild /qbuild
+
+COPY entrypoint.sh /entrypoint.sh
+ENTRYPOINT ["/entrypoint.sh"]

+ 13 - 0
pkg/qnap/build.sh

@@ -0,0 +1,13 @@
+#!/bin/bash
+
+build_packages()
+{
+  sudo docker run -v $(pwd):/zto ztqnap
+}
+
+build_container()
+{
+  sudo docker build -t ztqnap . --load
+}
+
+"$@"

+ 76 - 0
pkg/qnap/entrypoint.sh

@@ -0,0 +1,76 @@
+#!/bin/bash
+
+ZTO_VER=$(git describe --abbrev=0 --tags)
+ZTO_COMMIT=$(git rev-parse HEAD)
+ZTO_DESC=$(jq -r '.desc' ../config.json)
+
+mkdir -p pkg/qnap/zerotier/arm_64
+mkdir -p pkg/qnap/zerotier/arm_x09
+mkdir -p pkg/qnap/zerotier/arm_x10
+mkdir -p pkg/qnap/zerotier/arm_x12
+mkdir -p pkg/qnap/zerotier/arm_x19
+mkdir -p pkg/qnap/zerotier/arm_x31
+mkdir -p pkg/qnap/zerotier/arm_x41
+mkdir -p pkg/qnap/zerotier/x86_64
+mkdir -p pkg/qnap/zerotier/x86
+mkdir -p pkg/qnap/zerotier/x86_ce53xx
+
+cat > pkg/qnap/zerotier/qpkg.cfg <<- EOM
+# Update package config
+# Name of the packaged application.
+QPKG_NAME="zerotier"
+# Name of the display application.
+QPKG_DISPLAY_NAME="ZeroTier"
+# Version of the packaged application.
+QPKG_VER="$ZTO_VER"
+# Author or maintainer of the package
+QPKG_AUTHOR="ZeroTier, Inc."
+# License for the packaged application
+QPKG_LICENSE="BUSL-1.1"
+# One-line description of the packaged application
+QPKG_SUMMARY="$ZTO_DESC"
+
+# Preferred number in start/stop sequence.
+QPKG_RC_NUM="101"
+# Init-script used to control the start and stop of the installed application.
+QPKG_SERVICE_PROGRAM="zerotier.sh"
+
+# Specifies any packages required for the current package to operate.
+QPKG_REQUIRE="QVPN Service"
+# Specifies what packages cannot be installed if the current package
+# is to operate properly.
+#QPKG_CONFLICT="Python, OPT/sed"
+# Name of configuration file (multiple definitions are allowed).
+#QPKG_CONFIG="myApp.conf"
+#QPKG_CONFIG="/etc/config/myApp.conf"
+# Port number used by service program.
+QPKG_SERVICE_PORT="9993"
+
+# Minimum QTS version requirement
+QTS_MINI_VERSION="4.1.0"
+# Maximum QTS version requirement
+QTS_MAX_VERSION="5.0.0"
+
+# Location of icons for the packaged application.
+QDK_DATA_DIR_ICONS="icons"
+EOM
+
+# Copy binaries into pkg directory
+# See: https://github.com/qnap-dev/QDK
+cp -vf output/static/zerotier-one.${ZTO_VER}.alpine-aarch64 pkg/qnap/zerotier/arm_64/zerotier-one
+#cp -vf output/static/zerotier-one.${ZTO_VER}.alpine-armv5tejl pkg/qnap/zerotier/arm-x09/zerotier-one
+#cp -vf output/static/zerotier-one.${ZTO_VER}.alpine-armv5tel pkg/qnap/zerotier/arm-x19/zerotier-one
+cp -vf output/static/zerotier-one.${ZTO_VER}.alpine-armhf pkg/qnap/zerotier/arm-x31/zerotier-one
+cp -vf output/static/zerotier-one.${ZTO_VER}.alpine-armhf pkg/qnap/zerotier/arm-x41/zerotier-one
+cp -vf output/static/zerotier-one.${ZTO_VER}.alpine-i386 pkg/qnap/zerotier/x86/zerotier-one
+cp -vf output/static/zerotier-one.${ZTO_VER}.alpine-x86_64 pkg/qnap/zerotier/x86_64/zerotier-one
+cp -vf output/static/zerotier-one.${ZTO_VER}.alpine-i386 pkg/qnap/zerotier/x86_ce53xx/zerotier-one
+
+rm -rf output/qnap/*
+
+pushd pkg/qnap/zerotier
+./qbuild #--build-arch arm-x31
+cp -f build/* ../../../output/qnap
+
+# Show output product
+cd popd

+ 3 - 0
pkg/qnap/qdk.conf

@@ -0,0 +1,3 @@
+QDK_VERSION=2.3.11
+QDK_PATH_P=`pwd | awk 'BEGIN { FS = "QDK" } ; { print $1 }'`
+QDK_PATH="${QDK_PATH_P}/QDK"

+ 7 - 0
pkg/qnap/zerotier/Makefile

@@ -0,0 +1,7 @@
+.PHONY: all
+
+all:
+	@$(CC) -o qpkg_encrypt qpkg_encrypt.c
+
+clean:
+	rm -rf /bin/qpkg_encrypt

+ 0 - 0
pkg/qnap/zerotier/arm_64/.gitkeep


+ 0 - 0
pkg/qnap/zerotier/arm_x09/.gitkeep


+ 0 - 0
pkg/qnap/zerotier/arm_x10/.gitkeep


+ 0 - 0
pkg/qnap/zerotier/arm_x12/.gitkeep


+ 0 - 0
pkg/qnap/zerotier/arm_x19/.gitkeep


+ 0 - 0
pkg/qnap/zerotier/arm_x31/.gitkeep


+ 0 - 0
pkg/qnap/zerotier/arm_x41/.gitkeep


+ 0 - 0
pkg/qnap/zerotier/config/.gitkeep


+ 0 - 0
pkg/qnap/zerotier/icons/.gitkeep


BIN
pkg/qnap/zerotier/icons/ZeroTier.gif


BIN
pkg/qnap/zerotier/icons/ZeroTier_80.gif


BIN
pkg/qnap/zerotier/icons/ZeroTier_gray.gif


+ 158 - 0
pkg/qnap/zerotier/package_routines

@@ -0,0 +1,158 @@
+######################################################################
+# List of available definitions (it's not necessary to uncomment them)
+######################################################################
+###### Command definitions #####
+#CMD_AWK="/bin/awk"
+#CMD_CAT="/bin/cat"
+#CMD_CHMOD="/bin/chmod"
+#CMD_CHOWN="/bin/chown"
+#CMD_CP="/bin/cp"
+#CMD_CUT="/bin/cut"
+#CMD_DATE="/bin/date"
+#CMD_ECHO="/bin/echo"
+#CMD_EXPR="/usr/bin/expr"
+#CMD_FIND="/usr/bin/find"
+#CMD_GETCFG="/sbin/getcfg"
+#CMD_GREP="/bin/grep"
+#CMD_GZIP="/bin/gzip"
+#CMD_HOSTNAME="/bin/hostname"
+#CMD_LN="/bin/ln"
+#CMD_LOG_TOOL="/sbin/log_tool"
+#CMD_MD5SUM="/bin/md5sum"
+#CMD_MKDIR="/bin/mkdir"
+#CMD_MV="/bin/mv"
+#CMD_RM="/bin/rm"
+#CMD_RMDIR="/bin/rmdir"
+#CMD_SED="/bin/sed"
+#CMD_SETCFG="/sbin/setcfg"
+#CMD_SLEEP="/bin/sleep"
+#CMD_SORT="/usr/bin/sort"
+#CMD_SYNC="/bin/sync"
+#CMD_TAR="/bin/tar"
+#CMD_TOUCH="/bin/touch"
+#CMD_WGET="/usr/bin/wget"
+#CMD_WLOG="/sbin/write_log"
+#CMD_XARGS="/usr/bin/xargs"
+#CMD_7Z="/usr/local/sbin/7z"
+#
+###### System definitions #####
+#SYS_EXTRACT_DIR="$(pwd)"
+#SYS_CONFIG_DIR="/etc/config"
+#SYS_INIT_DIR="/etc/init.d"
+#SYS_STARTUP_DIR="/etc/rcS.d"
+#SYS_SHUTDOWN_DIR="/etc/rcK.d"
+#SYS_RSS_IMG_DIR="/home/httpd/RSS/images"
+#SYS_QPKG_DATA_FILE_GZIP="./data.tar.gz"
+#SYS_QPKG_DATA_FILE_BZIP2="./data.tar.bz2"
+#SYS_QPKG_DATA_FILE_7ZIP="./data.tar.7z"
+#SYS_QPKG_DATA_CONFIG_FILE="./conf.tar.gz"
+#SYS_QPKG_DATA_MD5SUM_FILE="./md5sum"
+#SYS_QPKG_DATA_PACKAGES_FILE="./Packages.gz"
+#SYS_QPKG_CONFIG_FILE="$SYS_CONFIG_DIR/qpkg.conf"
+#SYS_QPKG_CONF_FIELD_QPKGFILE="QPKG_File"
+#SYS_QPKG_CONF_FIELD_NAME="Name"
+#SYS_QPKG_CONF_FIELD_VERSION="Version"
+#SYS_QPKG_CONF_FIELD_ENABLE="Enable"
+#SYS_QPKG_CONF_FIELD_DATE="Date"
+#SYS_QPKG_CONF_FIELD_SHELL="Shell"
+#SYS_QPKG_CONF_FIELD_INSTALL_PATH="Install_Path"
+#SYS_QPKG_CONF_FIELD_CONFIG_PATH="Config_Path"
+#SYS_QPKG_CONF_FIELD_WEBUI="WebUI"
+#SYS_QPKG_CONF_FIELD_WEBPORT="Web_Port"
+#SYS_QPKG_CONF_FIELD_SERVICEPORT="Service_Port"
+#SYS_QPKG_CONF_FIELD_SERVICE_PIDFILE="Pid_File"
+#SYS_QPKG_CONF_FIELD_AUTHOR="Author"
+#SYS_QPKG_CONF_FIELD_RC_NUMBER="RC_Number"
+## The following variables are assigned values at run-time.
+#SYS_HOSTNAME=$($CMD_HOSTNAME)
+## Data file name (one of SYS_QPKG_DATA_FILE_GZIP, SYS_QPKG_DATA_FILE_BZIP2,
+## or SYS_QPKG_DATA_FILE_7ZIP)
+#SYS_QPKG_DATA_FILE=
+## Base location.
+#SYS_QPKG_BASE=""
+## Base location of QPKG installed packages.
+#SYS_QPKG_INSTALL_PATH=""
+## Location of installed software.
+#SYS_QPKG_DIR=""
+## If the QPKG should be enabled or disabled after the installation/upgrade.
+#SYS_QPKG_SERVICE_ENABLED=""
+## Architecture of the device the QPKG is installed on.
+#SYS_CPU_ARCH=""
+## Name and location of system shares
+#SYS_PUBLIC_SHARE=""
+#SYS_PUBLIC_PATH=""
+#SYS_DOWNLOAD_SHARE=""
+#SYS_DOWNLOAD_PATH=""
+#SYS_MULTIMEDIA_SHARE=""
+#SYS_MULTIMEDIA_PATH=""
+#SYS_RECORDINGS_SHARE=""
+#SYS_RECORDINGS_PATH=""
+#SYS_USB_SHARE=""
+#SYS_USB_PATH=""
+#SYS_WEB_SHARE=""
+#SYS_WEB_PATH=""
+## Path to ipkg or opkg package tool if installed.
+#CMD_PKG_TOOL=
+#
+
+######################################################################
+# All package specific functions shall call 'err_log MSG' if an error
+# is detected that shall terminate the installation.
+######################################################################
+
+######################################################################
+# Define any package specific operations that shall be performed when
+# the package is removed.
+######################################################################
+
+#PKG_PRE_REMOVE="{
+#}"
+
+PKG_MAIN_REMOVE="{
+	rm -rf /usr/sbin/zerotier-cli
+	# all identity files are stored in the Install_Path and will be removed automatically
+}"
+
+#PKG_POST_REMOVE="{
+#}"
+
+######################################################################
+# Define any package specific initialization that shall be performed
+# before the package is installed.
+######################################################################
+
+pkg_init()
+{
+	modprobe tun
+}
+
+
+######################################################################
+# Define any package specific requirement checks that shall be
+# performed before the package is installed.
+######################################################################
+
+#pkg_check_requirement()
+#{
+#}
+
+######################################################################
+# Define any package specific operations that shall be performed when
+# the package is installed.
+######################################################################
+
+#pkg_pre_install()
+#{
+#	log "pkg_pre_install"
+#}
+
+#pkg_install()
+#{
+#	log "pkg_install"
+#}
+
+pkg_post_install()
+{
+	log $SYS_QPKG_INSTALL_PATH
+	modprobe tun
+}

+ 99 - 0
pkg/qnap/zerotier/qpkg.cfg

@@ -0,0 +1,99 @@
+# Update package config
+# Name of the packaged application.
+QPKG_NAME="zerotier"
+# Name of the display application.
+QPKG_DISPLAY_NAME="ZeroTier"
+# Version of the packaged application.
+QPKG_VER="1.8.4"
+# Author or maintainer of the package
+QPKG_AUTHOR="ZeroTier, Inc."
+# License for the packaged application
+QPKG_LICENSE="BUSL-1.1"
+# One-line description of the packaged application
+QPKG_SUMMARY="Securely connect any device, anywhere."
+
+# Preferred number in start/stop sequence.
+QPKG_RC_NUM="101"
+# Init-script used to control the start and stop of the installed application.
+QPKG_SERVICE_PROGRAM="zerotier.sh"
+
+# Specifies any packages required for the current package to operate.
+QPKG_REQUIRE="QVPN Service"
+# Specifies what packages cannot be installed if the current package
+# is to operate properly.
+#QPKG_CONFLICT="Python, OPT/sed"
+# Name of configuration file (multiple definitions are allowed).
+#QPKG_CONFIG="myApp.conf"
+#QPKG_CONFIG="/etc/config/myApp.conf"
+# Port number used by service program.
+QPKG_SERVICE_PORT="9993"
+# Location of file with running service's PID
+#QPKG_SERVICE_PIDFILE="/var/lib/zerotier-one/zerotier-one.pid"
+# Relative path to web interface
+#QPKG_WEBUI=""
+# Port number for the web interface.
+#QPKG_WEB_PORT=""
+# Port number for the SSL web interface.
+#QPKG_WEB_SSL_PORT=""
+
+# Use QTS HTTP Proxy and set Proxy_Path in the qpkg.conf.
+# When the QPKG has its own HTTP service port, and want clients to connect via QTS HTTP port (default 8080).
+# Usually use this option when the QPKG need to connect via myQNAPcloud service.
+#QPKG_USE_PROXY="1"
+#QPKG_PROXY_PATH="/qpkg_name"
+
+#Desktop Application (since 4.1)
+# Set value to 1 means to open the QPKG's Web UI inside QTS desktop instead of new window.
+#QPKG_DESKTOP_APP="1"
+# Desktop Application Window default inner width (since 4.1) (not over 1178)
+#QPKG_DESKTOP_APP_WIN_WIDTH=""
+# Desktop Application Window default inner width (since 4.1) (not over 600)
+#QPKG_DESKTOP_APP_WIN_HEIGHT=""
+
+# Minimum QTS version requirement
+QTS_MINI_VERSION="4.1.0"
+# Maximum QTS version requirement
+QTS_MAX_VERSION="5.0.0"
+
+# Select volume
+# 1: support installation
+# 2: support migration
+# 3 (1+2): support both installation and migration
+#QPKG_VOLUME_SELECT="0"
+
+# Set timeout for QPKG enable and QPKG disable (since 4.1.0)
+# Format in seconds (enable, disable)
+#QPKG_TIMEOUT="10,30"
+
+# Visible setting for the QPKG that has web UI, show this QPKG on the Main menu of
+# 1(default): administrators, 2: all NAS users.
+#QPKG_VISIBLE="2"
+
+# Location of the chroot environment (only TS-x09)
+#QPKG_ROOTFS=""
+# Init-script used to controls the start and stop of the
+# installed application (only TS-x09)
+#QPKG_SERVICE_PROGRAM_CHROOT=""
+
+# Location of icons for the packaged application.
+QDK_DATA_DIR_ICONS="icons"
+# Location of files specific to arm-x09 packages.
+#QDK_DATA_DIR_X09="arm-x09"
+# Location of files specific to arm-x19 packages.
+#QDK_DATA_DIR_X19="arm-x19"
+# Location of files specific to arm-x31 packages.
+#QDK_DATA_DIR_X31="arm-x31"
+# Location of files specific to arm-x41 packages.
+#QDK_DATA_DIR_X41="arm_al"
+# Location of files specific to x86 packages.
+#QDK_DATA_DIR_X86="x86"
+# Location of files specific to x86 (64-bit) packages.
+#QDK_DATA_DIR_X86_64="x86_64"
+# Location of files common to all architectures.
+#QDK_DATA_DIR_SHARED="shared"
+# Location of configuration files.
+#QDK_DATA_DIR_CONFIG="config"
+# Name of local data package.
+#QDK_DATA_FILE=""
+# Name of extra package (multiple definitions are allowed).
+#QDK_EXTRA_FILE=""

+ 0 - 0
pkg/qnap/zerotier/shared/.gitkeep


BIN
pkg/qnap/zerotier/shared/.qpkg_icon.gif


BIN
pkg/qnap/zerotier/shared/.qpkg_icon_80.gif


BIN
pkg/qnap/zerotier/shared/.qpkg_icon_gray.gif


+ 34 - 0
pkg/qnap/zerotier/shared/zerotier.sh

@@ -0,0 +1,34 @@
+#!/bin/sh
+CONF=/etc/config/qpkg.conf
+QPKG_NAME="zerotier"
+QPKG_ROOT=`/sbin/getcfg $QPKG_NAME Install_Path -f ${CONF}`
+APACHE_ROOT=/share/`/sbin/getcfg SHARE_DEF defWeb -d Qweb -f /etc/config/def_share.info`
+case "$1" in
+  start)
+    modprobe tun
+    ln -s $QPKG_ROOT/zerotier-one /usr/sbin/zerotier-cli
+    ln -s $QPKG_ROOT/zerotier-one /usr/bin/zerotier-cli
+    ln -s $QPKG_ROOT /var/lib/zerotier-one
+    ENABLED=$(/sbin/getcfg $QPKG_NAME Enable -u -d FALSE -f $CONF)
+    if [ "$ENABLED" != "TRUE" ]; then
+        echo "$QPKG_NAME is disabled."
+        exit 1
+    fi
+    $QPKG_ROOT/zerotier-one $QPKG_ROOT -d
+    ;;
+
+  stop)
+    killall zerotier-one
+    ;;
+
+  restart)
+    $0 stop
+    $0 start
+    ;;
+
+  *)
+    echo "Usage: $0 {start|stop|restart}"
+    exit 1
+esac
+
+exit 0

Some files were not shown because too many files changed in this diff