[
  {
    "path": ".clang-format",
    "content": "BasedOnStyle: Google\n\n# alignment\nAlignAfterOpenBracket: AlwaysBreak\nAlignConsecutiveAssignments: 'false'\nAlignConsecutiveDeclarations: 'false'\nAlignEscapedNewlinesLeft: 'true'\nAlignOperands: 'false'\nAlignTrailingComments: 'true'\nColumnLimit: 120\nPointerAlignment: Left\nQualifierAlignment: Custom\nQualifierOrder: ['inline', 'static', 'constexpr', 'const', 'type']\nReferenceAlignment: Left\n\n# bracing\nBreakBeforeBraces: Custom\nBraceWrapping:\n  AfterCaseLabel: true\n  AfterClass: true\n  AfterControlStatement: true\n  AfterEnum: true\n  AfterFunction: true\n  AfterNamespace: true\n  AfterObjCDeclaration: true\n  AfterStruct: true\n  AfterUnion: true\n  AfterExternBlock: true\n  BeforeCatch: true\n  BeforeElse: true\n  SplitEmptyFunction: false\n  SplitEmptyRecord: false\n  SplitEmptyNamespace: false\n\n# breaking\nAlwaysBreakAfterDefinitionReturnType: None\nAlwaysBreakAfterReturnType: None\nAlwaysBreakTemplateDeclarations: 'true'\nBreakBeforeBinaryOperators: NonAssignment\nBreakBeforeTernaryOperators: 'true'\nBreakConstructorInitializers: BeforeColon\n\n# indentation\nAccessModifierOffset: -2\nConstructorInitializerIndentWidth: 4\nContinuationIndentWidth: 4\nIndentWidth: 4\nNamespaceIndentation: All\n\n# shorties\nAllowShortBlocksOnASingleLine: 'false'\nAllowShortCaseLabelsOnASingleLine: 'false'\nAllowShortFunctionsOnASingleLine: All\nAllowShortIfStatementsOnASingleLine: 'false'\nAllowShortLoopsOnASingleLine: 'false'\n\n# spacing\nKeepEmptyLinesAtTheStartOfBlocks: 'false'\nPenaltyBreakString: '3'\nSpaceBeforeParens: ControlStatements\nSpacesInAngles: 'false'\nSpacesInContainerLiterals: 'false'\nSpacesInParentheses: 'false'\nSpacesInSquareBrackets: 'false'\nStandard: c++20\nUseTab: Never\n\n# wrapping\nPackConstructorInitializers: NextLine\nBinPackParameters: 'false'\nBinPackArguments: 'false'\n\n# Include block sorting in the following order:\n#   - Main header for source file (clang-format default prioritizes this first)\n#   - Relative path includes in quotation marks\n#   - Absolute path includes in angle brackets\n#   - External dependencies\n#   - System dependencies\nSortIncludes: CaseInsensitive\nIncludeBlocks: Regroup\nIncludeCategories:\n  - Regex: '\".+\\.h'\n    Priority: 2\n  - Regex: '^<llarp'\n    Priority: 3\n  - Regex: '<winsock2\\.h>'\n    Priority: 4\n  - Regex: '<windows\\.h>'\n    Priority: 5\n  - Regex: '^<.*\\.h(pp)?>$'\n    Priority: 6\n  - Regex: '(<)(.)+(>)'\n    Priority: 7\n"
  },
  {
    "path": ".clang-tidy",
    "content": "HeaderFilterRegex: 'llarp/.*'\nChecks: \n'readability-else-after-return,\nclang-analyzer-core-*,modernize-*,\n-modernize-use-trailing-return-type,\n-modernize-use-nodiscard,\nbugprone-*,\n-bugprone-easily-swappable-parameters'\n"
  },
  {
    "path": ".dir-locals.el",
    "content": "((c++-mode\n  (eval add-hook 'before-save-hook #'clang-format-buffer nil t))\n (c-mode\n  (eval add-hook 'before-save-hook #'clang-format-buffer nil t)))\n"
  },
  {
    "path": ".dockerignore",
    "content": "build/\n.vscode/\nlokinet\nlokinet.exe\n"
  },
  {
    "path": ".drone.jsonnet",
    "content": "local default_deps_base = std.set([\n  'g++',\n  'libcli11-dev',\n  'libcurl4-openssl-dev',\n  'libevent-dev',\n  'libfmt-dev',\n  'libgnutls28-dev',\n  'libsodium-dev',\n  'libspdlog-dev',\n  'libsqlite3-dev',\n  'libssl-dev',\n  'libsystemd-dev',\n  'libunbound-dev',\n  'libzmq3-dev',\n  'libzstd-dev',\n  'make',\n  'nettle-dev',\n  'nlohmann-json3-dev',\n  'python3-dev',\n]);\nlocal default_deps(add=[], remove=[]) = std.setDiff(\n  std.setUnion(default_deps_base, if std.isArray(add) then std.set(add) else [add]),\n  std.set(if std.isArray(remove) then std.set(remove) else [remove])\n);\nlocal static_deps = std.set(['g++', 'python3-dev', 'automake', 'libtool']);\nlocal oxen_repo_default = ['liboxen-logging-dev', 'liboxenmq-dev', 'liboxenc-dev', 'liboxen-quic-dev'];\nlocal docker_base = 'registry.oxen.rocks/';\n\n\nlocal submodule_commands = [\n  'git fetch --tags',\n  'git submodule update --init --recursive --depth=1 --jobs=4',\n];\nlocal submodules = {\n  name: 'submodules',\n  image: 'drone/git',\n  commands: submodule_commands,\n};\n\n// cmake options for static deps mirror\nlocal ci_dep_mirror(want_mirror) = (if want_mirror then ' -DLOCAL_MIRROR=https://oxen.rocks/deps ' else '');\n\nlocal apt_get_quiet = 'apt-get -o=Dpkg::Use-Pty=0 -q';\n\nlocal kitware_repo(distro) = [\n  'eatmydata ' + apt_get_quiet + ' install -y curl ca-certificates',\n  'curl -sSL https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - >/usr/share/keyrings/kitware-archive-keyring.gpg',\n  'echo \"deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ ' + distro + ' main\" >/etc/apt/sources.list.d/kitware.list',\n  'eatmydata ' + apt_get_quiet + ' update',\n];\n\nlocal debian_backports(distro, pkgs) = [\n  'echo \"deb http://deb.debian.org/debian ' + distro + '-backports main\" >/etc/apt/sources.list.d/' + distro + '-backports.list',\n  'eatmydata ' + apt_get_quiet + ' update',\n  'eatmydata ' + apt_get_quiet + ' install -y ' + std.join(' ', std.map(function(x) x + '/' + distro + '-backports', pkgs)),\n];\n\n// Regular build on a debian-like system:\nlocal debian_pipeline(name,\n                      image,\n                      arch='amd64',\n                      deps=default_deps(),\n                      extra_setup=[],\n                      build_type='Release',\n                      lto=false,\n                      werror=true,\n                      cmake_extra='',\n                      local_mirror=true,\n                      extra_cmds=[],\n                      jobs=6,\n                      tests=false,  // FIXME TODO: temporary until test suite is fixed\n                      oxen_repo=oxen_repo_default,\n                      allow_fail=false) = {\n  kind: 'pipeline',\n  type: 'docker',\n  name: name,\n  platform: { arch: arch },\n  trigger: { branch: { exclude: ['debian/*', 'ubuntu/*'] } },\n  steps: [\n    submodules,\n    {\n      name: 'build',\n      image: image,\n      pull: 'always',\n      [if allow_fail then 'failure']: 'ignore',\n      environment: { SSH_KEY: { from_secret: 'SSH_KEY' } },\n      commands: [\n                  'echo \"Building on ${DRONE_STAGE_MACHINE}\"',\n                  'echo \"man-db man-db/auto-update boolean false\" | debconf-set-selections',\n                  apt_get_quiet + ' update',\n                  apt_get_quiet + ' install -y eatmydata',\n                ] + (\n                  if std.length(oxen_repo) > 0 then [\n                    'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y lsb-release',\n                    'cp contrib/deb.oxen.io.gpg /etc/apt/trusted.gpg.d',\n                    'echo deb http://deb.oxen.io $$(lsb_release -sc) main >/etc/apt/sources.list.d/oxen.list',\n                    'eatmydata ' + apt_get_quiet + ' update',\n                    apt_get_quiet + ' install -y ' + std.join(' ', oxen_repo),\n                  ] else []\n                ) + extra_setup\n                + [\n                  'eatmydata ' + apt_get_quiet + ' dist-upgrade -y',\n                  'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y gdb cmake git pkg-config ccache ' + std.join(' ', deps),\n                  'mkdir build',\n                  'cd build',\n                  'cmake .. -DWITH_SETCAP=OFF -DCMAKE_CXX_FLAGS=-fdiagnostics-color=always -DCMAKE_BUILD_TYPE=' + build_type + ' ' +\n                  '-DWARN_DEPRECATED=OFF ' +\n                  (if werror then '-DWARNINGS_AS_ERRORS=ON ' else '') +\n                  '-DWITH_LTO=' + (if lto then 'ON ' else 'OFF ') +\n                  '-DWITH_TESTS=' + (if tests then 'ON ' else 'OFF ') +\n                  cmake_extra +\n                  ci_dep_mirror(local_mirror),\n                  'VERBOSE=1 make -j' + jobs,\n                  'cd ..',\n                ]\n                + (if tests then ['./contrib/ci/drone-gdb.sh ./build/test/testAll --use-colour yes'] else [])\n                + extra_cmds,\n    },\n  ],\n};\nlocal apk_builder(name, image, extra_cmds=[], allow_fail=false, jobs=6) = {\n  kind: 'pipeline',\n  type: 'docker',\n  name: name,\n  platform: { arch: 'amd64' },\n  trigger: { branch: { exclude: ['debian/*', 'ubuntu/*'] } },\n  steps: [\n    submodules,\n    {\n      name: 'build',\n      image: image,\n      pull: 'always',\n      [if allow_fail then 'failure']: 'ignore',\n      environment: { SSH_KEY: { from_secret: 'SSH_KEY' }, ANDROID: 'android' },\n      commands: [\n        'VERBOSE=1 JOBS=' + jobs + ' NDK=/usr/lib/android-ndk ./contrib/android.sh',\n        'git clone https://github.com/oxen-io/lokinet-flutter-app lokinet-mobile',\n        'cp -av build-android/out/* lokinet-mobile/lokinet_lib/android/src/main/jniLibs/',\n        'cd lokinet-mobile',\n        'flutter build apk --debug',\n        'cd  ..',\n        'cp lokinet-mobile/build/app/outputs/apk/debug/app-debug.apk lokinet.apk',\n      ] + extra_cmds,\n    },\n  ],\n};\n// windows cross compile on debian\nlocal windows_cross_pipeline(name,\n                             image,\n                             gui_image=docker_base + 'nodejs-lts',\n                             arch='amd64',\n                             build_type='Release',\n                             lto=false,\n                             werror=false,\n                             cmake_extra='',\n                             local_mirror=true,\n                             extra_cmds=[],\n                             jobs=6,\n                             allow_fail=false) = {\n  kind: 'pipeline',\n  type: 'docker',\n  name: name,\n  platform: { arch: arch },\n  trigger: { branch: { exclude: ['debian/*', 'ubuntu/*'] } },\n  steps: [\n    submodules,\n    {\n      name: 'GUI',\n      image: gui_image,\n      pull: 'always',\n      [if allow_fail then 'failure']: 'ignore',\n      commands: [\n        'echo \"Building on ${DRONE_STAGE_MACHINE}\"',\n        'echo \"man-db man-db/auto-update boolean false\" | debconf-set-selections',\n        apt_get_quiet + ' update',\n        apt_get_quiet + ' install -y eatmydata',\n        'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y p7zip-full wine',\n        'cd gui',\n        'yarn install --frozen-lockfile',\n        'USE_SYSTEM_7ZA=true DISPLAY= WINEDEBUG=-all yarn win32',\n      ],\n    },\n    {\n      name: 'build',\n      image: image,\n      pull: 'always',\n      [if allow_fail then 'failure']: 'ignore',\n      environment: { SSH_KEY: { from_secret: 'SSH_KEY' }, WINDOWS_BUILD_NAME: 'x64' },\n      commands: [\n        'echo \"Building on ${DRONE_STAGE_MACHINE}\"',\n        'echo \"man-db man-db/auto-update boolean false\" | debconf-set-selections',\n        apt_get_quiet + ' update',\n        apt_get_quiet + ' install -y eatmydata',\n        'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y build-essential cmake git pkg-config ccache g++-mingw-w64-x86-64-posix nsis zip icoutils automake libtool librsvg2-bin bison',\n        'JOBS=' + jobs + ' VERBOSE=1 ./contrib/windows.sh -DSTRIP_SYMBOLS=ON -DGUI_EXE=$${DRONE_WORKSPACE}/gui/release/Lokinet-GUI_portable.exe' +\n        ci_dep_mirror(local_mirror),\n      ] + extra_cmds,\n    },\n  ],\n};\n\n// linux cross compile on debian\nlocal linux_cross_pipeline(name,\n                           cross_targets,\n                           arch='amd64',\n                           build_type='Release',\n                           cmake_extra='',\n                           local_mirror=true,\n                           extra_cmds=[],\n                           jobs=6,\n                           allow_fail=false) = {\n  kind: 'pipeline',\n  type: 'docker',\n  name: name,\n  platform: { arch: arch },\n  trigger: { branch: { exclude: ['debian/*', 'ubuntu/*'] } },\n  steps: [\n    submodules,\n    {\n      name: 'build',\n      image: docker_base + 'debian-stable-cross',\n      pull: 'always',\n      [if allow_fail then 'failure']: 'ignore',\n      environment: { SSH_KEY: { from_secret: 'SSH_KEY' }, CROSS_TARGETS: std.join(':', cross_targets) },\n      commands: [\n        'echo \"Building on ${DRONE_STAGE_MACHINE}\"',\n        'VERBOSE=1 JOBS=' + jobs + ' ./contrib/cross.sh ' + std.join(' ', cross_targets) +\n        ' -- ' + cmake_extra + ci_dep_mirror(local_mirror),\n      ],\n    },\n  ],\n};\n\n// Builds a snapshot .deb on a debian-like system by merging into the debian/* or ubuntu/* branch\nlocal deb_builder(image, distro, distro_branch, arch='amd64', oxen_repo=oxen_repo_default) = {\n  kind: 'pipeline',\n  type: 'docker',\n  name: 'DEB (' + distro + (if arch == 'amd64' then '' else '/' + arch) + ')',\n  platform: { arch: arch },\n  environment: { distro_branch: distro_branch, distro: distro },\n  steps: [\n    submodules,\n    {\n      name: 'build',\n      image: image,\n      pull: 'always',\n      failure: 'ignore',\n      environment: { SSH_KEY: { from_secret: 'SSH_KEY' } },\n      commands: [\n        'echo \"Building on ${DRONE_STAGE_MACHINE}\"',\n        'echo \"man-db man-db/auto-update boolean false\" | debconf-set-selections',\n      ] + (if oxen_repo then [\n             'cp contrib/deb.oxen.io.gpg /etc/apt/trusted.gpg.d',\n             'echo deb http://deb.oxen.io $${distro} main >/etc/apt/sources.list.d/oxen.list',\n           ] else []) + [\n        apt_get_quiet + ' update',\n        apt_get_quiet + ' install -y eatmydata',\n        'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y git devscripts equivs ccache git-buildpackage python3-dev',\n        |||\n          # Look for the debian branch in this repo first, try upstream if that fails.\n          if ! git checkout $${distro_branch}; then\n              git remote add --fetch upstream https://github.com/oxen-io/lokinet.git &&\n              git checkout $${distro_branch}\n          fi\n        |||,\n        // Tell the merge how to resolve conflicts in the source .drone.jsonnet (we don't\n        // care about it at all since *this* .drone.jsonnet is already loaded).\n        'git config merge.ours.driver true',\n        'echo .drone.jsonnet merge=ours >>.gitattributes',\n\n        'git merge ${DRONE_COMMIT}',\n        'export DEBEMAIL=\"${DRONE_COMMIT_AUTHOR_EMAIL}\" DEBFULLNAME=\"${DRONE_COMMIT_AUTHOR_NAME}\"',\n        'gbp dch -S -s \"HEAD^\" --spawn-editor=never -U low',\n        'eatmydata mk-build-deps --install --remove --tool \"' + apt_get_quiet + ' -o Debug::pkgProblemResolver=yes --no-install-recommends -y\"',\n        'export DEB_BUILD_OPTIONS=\"parallel=$$(nproc)\"',\n        //'grep -q lib debian/lokinet-bin.install || echo \"/usr/lib/lib*.so*\" >>debian/lokinet-bin.install',\n        'debuild -e CCACHE_DIR -b',\n        './contrib/ci/drone-debs-upload.sh ' + distro,\n      ],\n    },\n  ],\n};\n\nlocal clang(version) = debian_pipeline(\n  'Debian sid/clang-' + version,\n  docker_base + 'debian-sid-clang',\n  deps=default_deps(add='clang-' + version, remove='g++'),\n  cmake_extra='-DCMAKE_C_COMPILER=clang-' + version + ' -DCMAKE_CXX_COMPILER=clang++-' + version + ' '\n);\n\nlocal full_llvm(version) = debian_pipeline(\n  'Debian sid/llvm-' + version,\n  docker_base + 'debian-sid-clang',\n  deps=default_deps(add=['clang-' + version, ' lld-' + version, ' libc++-' + version + '-dev', 'libc++abi-' + version + '-dev', 'libngtcp2-crypto-gnutls-dev', 'libngtcp2-dev'],\n                    remove='g++'),\n  oxen_repo=[],\n  cmake_extra='-DCMAKE_C_COMPILER=clang-' + version +\n              ' -DCMAKE_CXX_COMPILER=clang++-' + version +\n              ' -DCMAKE_CXX_FLAGS=-stdlib=libc++ ' +\n              std.join(' ', [\n                '-DCMAKE_' + type + '_LINKER_FLAGS=-fuse-ld=lld-' + version\n                for type in ['EXE', 'MODULE', 'SHARED']\n              ]) +\n              ' -DOXEN_LOGGING_FORCE_SUBMODULES=ON'\n);\n\n// Macos build\nlocal mac_builder(name,\n                  build_type='Release',\n                  arch='arm64',\n                  werror=true,\n                  cmake_extra='',\n                  local_mirror=true,\n                  extra_cmds=[],\n                  jobs=6,\n                  codesign='-DCODESIGN=OFF',\n                  allow_fail=false) = {\n  kind: 'pipeline',\n  type: 'exec',\n  name: name,\n  platform: { os: 'darwin', arch: arch },\n  steps: [\n    { name: 'submodules', commands: submodule_commands },\n    {\n      name: 'build',\n      environment: { SSH_KEY: { from_secret: 'SSH_KEY' } },\n      commands: [\n        'echo \"Building on ${DRONE_STAGE_MACHINE}\"',\n        // If you don't do this then the C compiler doesn't have an include path containing\n        // basic system headers.  WTF apple:\n        'export SDKROOT=\"$(xcrun --sdk macosx --show-sdk-path)\"',\n        'ulimit -n 1024',  // because macos sets ulimit to 256 for some reason yeah idk\n        './contrib/mac-configure.sh ' + ci_dep_mirror(local_mirror) + '-DWARN_DEPRECATED=OFF ' + codesign,\n        'cd build-mac',\n        // We can't use the 'package' target here because making a .dmg requires an active logged in\n        // macos gui to invoke Finder to invoke the partitioning tool to create a partitioned (!)\n        // disk image.  Most likely the GUI is required because if you lose sight of how pretty the\n        // surface of macOS is you might see how ugly the insides are.\n        'ninja -j' + jobs + ' assemble_gui',\n        'cd ..',\n      ] + extra_cmds,\n    },\n  ],\n};\n\nlocal docs_pipeline(name, image, extra_cmds=[], allow_fail=false) = {\n  kind: 'pipeline',\n  type: 'docker',\n  name: name,\n  platform: { arch: 'amd64' },\n  trigger: { branch: { exclude: ['debian/*', 'ubuntu/*'] } },\n  steps: [\n    submodules,\n    {\n      name: 'build',\n      image: image,\n      pull: 'always',\n      [if allow_fail then 'failure']: 'ignore',\n      environment: { SSH_KEY: { from_secret: 'SSH_KEY' } },\n      commands: [\n        'cmake -S . -B build-docs',\n        'make -C build-docs doc',\n      ] + extra_cmds,\n    },\n  ],\n};\n\n\n[\n  {\n    name: 'lint check',\n    kind: 'pipeline',\n    type: 'docker',\n    steps: [{\n      name: 'build',\n      image: docker_base + 'lint',\n      pull: 'always',\n      commands: [\n        'echo \"Building on ${DRONE_STAGE_MACHINE}\"',\n        apt_get_quiet + ' update',\n        apt_get_quiet + ' install -y eatmydata',\n        'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y git clang-format-16 jsonnet',\n        './contrib/ci/drone-format-verify.sh',\n      ],\n    }],\n  },\n  // documentation builder\n  //docs_pipeline('Documentation',\n  //              docker_base + 'docbuilder',\n  //              extra_cmds=['UPLOAD_OS=docs ./contrib/ci/drone-static-upload.sh']),\n\n  // Debian sid\n  debian_pipeline('Debian sid', docker_base + 'debian-sid'),\n  debian_pipeline('Debian sid/debug', docker_base + 'debian-sid', build_type='Debug'),\n  debian_pipeline('Debian sid/debug [arm64]', docker_base + 'debian-sid', build_type='Debug', arch='arm64', jobs=4),\n\n  clang(17),\n  full_llvm(17),\n  clang(19),\n  full_llvm(19),\n\n  // Debian 14\n  debian_pipeline('Debian 14', docker_base + 'debian-forky'),\n  debian_pipeline('Debian 14 [i386]', docker_base + 'debian-forky/i386'),\n  debian_pipeline('Debian 14 [arm64]', docker_base + 'debian-forky', arch='arm64', jobs=4),\n  debian_pipeline('Debian 14 [armhf]', docker_base + 'debian-forky/arm32v7', arch='arm64', jobs=4),\n\n  // Debian 13\n  debian_pipeline('Debian 13', docker_base + 'debian-trixie'),\n  debian_pipeline('Debian 13 [arm64]', docker_base + 'debian-trixie', arch='arm64', jobs=4),\n\n  // Debian 12\n  debian_pipeline('Debian 12', docker_base + 'debian-bookworm'),\n  debian_pipeline('Debian 12 static/debug',\n                  docker_base + 'debian-bookworm',\n                  build_type='Debug',\n                  deps=static_deps,\n                  oxen_repo=[],\n                  cmake_extra='-DBUILD_STATIC_DEPS=ON -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON'),\n\n  // Static debian 12 armhf (upload to builds.lokinet.dev)\n  debian_pipeline('Debian 12 static [armhf]',\n                  docker_base + 'debian-bookworm/arm32v7',\n                  arch='arm64',\n                  deps=static_deps,\n                  oxen_repo=[],\n                  cmake_extra='-DBUILD_STATIC_DEPS=ON -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON ' +\n                              '-DCMAKE_CXX_FLAGS=\"-march=armv7-a+fp -Wno-psabi\" -DCMAKE_C_FLAGS=\"-march=armv7-a+fp\" ' +\n                              '-DNATIVE_BUILD=OFF -DWITH_SYSTEMD=OFF -DWITH_BOOTSTRAP=OFF',\n                  extra_cmds=[\n                    './contrib/ci/drone-check-static-libs.sh',\n                    'UPLOAD_OS=linux-armhf ./contrib/ci/drone-static-upload.sh',\n                  ],\n                  jobs=4),\n\n  // Ubuntu\n  debian_pipeline('Ubuntu latest', docker_base + 'ubuntu-rolling'),\n  debian_pipeline('Ubuntu 24.04', docker_base + 'ubuntu-noble'),\n  debian_pipeline('Ubuntu 22.04', docker_base + 'ubuntu-jammy'),\n\n  // Static ubuntu jammy amd64 build (upload to builds.lokinet.dev)\n  debian_pipeline('Ubuntu 22.04 static',\n                  docker_base + 'ubuntu-jammy',\n                  deps=static_deps,\n                  lto=true,\n                  tests=false,\n                  oxen_repo=[],\n                  cmake_extra='-DBUILD_STATIC_DEPS=ON -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON ' +\n                              '-DCMAKE_CXX_FLAGS=\"-march=x86-64 -mtune=haswell\" ' +\n                              '-DCMAKE_C_FLAGS=\"-march=x86-64 -mtune=haswell\" ' +\n                              '-DNATIVE_BUILD=OFF -DWITH_SYSTEMD=OFF -DWITH_BOOTSTRAP=OFF -DBUILD_LIBLOKINET=OFF',\n                  extra_cmds=[\n                    './contrib/ci/drone-check-static-libs.sh',\n                    './contrib/ci/drone-static-upload.sh',\n                  ]),\n\n\n  // cross compile targets\n  // Aug 11: these are exhibiting some dumb failures in libsodium and external deps, TOFIX later\n  //linux_cross_pipeline('Cross Compile (arm/arm64)', cross_targets=['arm-linux-gnueabihf', 'aarch64-linux-gnu']),\n  //linux_cross_pipeline('Cross Compile (ppc64le)', cross_targets=['powerpc64le-linux-gnu']),\n\n  // Not currently building successfully:\n  //linux_cross_pipeline('Cross Compile (mips)', cross_targets=['mips-linux-gnu', 'mipsel-linux-gnu']),\n\n  // android apk builder\n  // Aug 11: this is also failing in openssl, TOFIX later\n  //apk_builder('android apk', docker_base + 'flutter', extra_cmds=['UPLOAD_OS=android ./contrib/ci/drone-static-upload.sh']),\n\n  // Windows builds (x64)\n  /*\n  windows_cross_pipeline('Windows (x64)',\n                         docker_base + 'debian-win32-cross',\n                         extra_cmds=[\n                           './contrib/ci/drone-static-upload.sh',\n                         ]),\n  */\n\n  /*\n  // integration tests\n  debian_pipeline('Router Hive',\n                  docker_base + 'ubuntu-lts',\n                  deps=default_deps(add=['python3-dev', 'python3-pytest', 'python3-pybind11']),\n                  cmake_extra='-DWITH_HIVE=ON'),\n\n  // Deb builds:\n  deb_builder(docker_base + 'debian-sid-builder', 'sid', 'debian/sid'),\n  deb_builder(docker_base + 'debian-bullseye-builder', 'bullseye', 'debian/bullseye'),\n  deb_builder(docker_base + 'ubuntu-jammy-builder', 'jammy', 'ubuntu/jammy'),\n  deb_builder(docker_base + 'debian-sid-builder', 'sid', 'debian/sid', arch='arm64'),\n  */\n\n  // Macos builds:\n  /*\n  mac_builder('macOS (Release, arm64)', extra_cmds=[\n    './contrib/ci/drone-check-static-libs.sh',\n    './contrib/ci/drone-static-upload.sh',\n  ]),\n  mac_builder('macOS (Debug, arm64)', build_type='Debug'),\n  */\n]\n"
  },
  {
    "path": ".gitattributes",
    "content": "external/date/test export-ignore\nexternal/nlohmann/doc export-ignore\nexternal/nlohmann/test export-ignore\nexternal/nlohmann/benchmarks/data export-ignore\n*.signed binary\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "\n* RUN `make format && make lint -j8` BEFORE COMMITING ALWAYS.\n\n* no tabs\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n\n**Screenshots Or Logs**\nIf applicable, add screenshots or log files to help explain your problem.\n\n**Device and Operating system (please complete the following information):**\n - OS: [e.g. iOS, Windows, Android]\n-  Device: [ e.g. Mac, PC, IPhone]\n - Lokinet Version number or Git commit hash:\n"
  },
  {
    "path": ".github/workflows/clean_issues.yml",
    "content": "name: Close incomplete issues\non:\n  schedule:\n    - cron: \"30 1 * * *\"\n\njobs:\n  close-issues:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n    steps:\n      - uses: actions/stale@v4.1.1\n        with:\n          only-labels: incomplete\n          days-before-issue-stale: 14\n          days-before-issue-close: 7\n          stale-issue-label: \"stale\"\n          stale-issue-message: \"This issue is stale because it has been 'incomplete' for 14 days with no activity.\"\n          close-issue-message: \"This issue was closed because it has been inactive for 7 days since being marked as stale.\"\n          days-before-pr-stale: -1\n          days-before-pr-close: -1\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "*~\n*\\#*\n\n*.a\n*.o\n*.so\n\n/build*/\n**/__pycache__/**\n\nllarpd\n*.test\n*.bin\n\n*.ninja\ncmake_install.cmake\nCMakeFiles\nCMakeCache.txt\n.ninja_log\n.ninja_deps\n/.cache/\n/compile_commands.json\n\ncallgrind.*\n.gdb_history\n\n*.sig\n*.signed\n!/contrib/bootstrap/mainnet.signed\n!/contrib/bootstrap/testnet.signed\n*.key\n\nshadow.data\nshadow.config.xml\n*.log\n*.pdf\n*.xz\n\ntestnet_tmp\n\n\n*.pid\nvsproject/\n.vs\n\n*.ini\n\n\n.gradle/\n.idea\n.vscode\nbuild64/\nbuild2/\n/contrib/lokinet-bootstrap-winnt/cacert.pem\n/contrib/lokinet-bootstrap-winnt/data.enc\n/contrib/lokinet-bootstrap-winnt/out.bine\ndefault.profraw\n\n# ctags shit\nGTAGS\nGRTAGS\nGPATH\nversion.txt\n\nlokinet-bootstrap.exe\nregdbhelper.dll\n# xcode\nxcuserdata/\n\nscc.py\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"external/nlohmann\"]\n\tpath = external/nlohmann\n\turl = https://github.com/nlohmann/json.git\n[submodule \"test/Catch2\"]\n\tpath = test/Catch2\n\turl = https://github.com/catchorg/Catch2\n[submodule \"external/pybind11\"]\n\tpath = external/pybind11\n\turl = https://github.com/pybind/pybind11\n\tbranch = stable\n[submodule \"external/sqlite_orm\"]\n\tpath = external/sqlite_orm\n\turl = https://github.com/fnc12/sqlite_orm\n[submodule \"external/oxen-mq\"]\n\tpath = external/oxen-mq\n\turl = https://github.com/oxen-io/oxen-mq\n[submodule \"gui\"]\n\tpath = gui\n\turl = https://github.com/oxen-io/lokinet-gui.git\n[submodule \"external/CLI11\"]\n\tpath = external/CLI11\n\turl = https://github.com/CLIUtils/CLI11.git\n[submodule \"external/oxen-libquic\"]\n\tpath = external/oxen-libquic\n\turl = https://github.com/oxen-io/oxen-libquic.git\n"
  },
  {
    "path": ".swift-version",
    "content": "5.4.2\n"
  },
  {
    "path": "CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.13...3.24)  # 3.13 is buster's version\n\n# Cmake 3.24+ breaks extraction timestamps by default, hurray, but the option to not break\n# timestamps fails in cmake <3.24, extra hurray!\nif(CMAKE_VERSION VERSION_GREATER_EQUAL 3.24)\n    cmake_policy(SET CMP0135 OLD)\nendif()\n\nset(CMAKE_EXPORT_COMPILE_COMMANDS ON)\n\n# Has to be set before `project()`, and ignored on non-macos:\nset(CMAKE_OSX_DEPLOYMENT_TARGET 10.15 CACHE STRING \"macOS deployment target (Apple clang only)\")\n\nset(lokinet_full_def ON)\nif(IOS)\n  set(lokinet_full_def OFF)\nendif()\n\n# These two are not exclusive: daemon requires full platform, but the main lokinet library itself\n# can support both.\noption(LOKINET_FULL \"build with full client/relay support. Disabling this will only allow embedded lokinet usage\" ${lokinet_full_def})\noption(LOKINET_DAEMON \"build lokinet daemon and associated utils (requires LOKINET_FULL)\" ${LOKINET_FULL})\n\nif(LOKINET_DAEMON AND NOT LOKINET_FULL)\n  message(FATAL_ERROR \"Cannot use LOKINET_DAEMON without LOKINET_FULL platform support!\")\nendif()\n\noption(LOKINET_GRAPH_DEPENDENCIES \"Produce graphviz representation of cmake dependencies\" OFF)\n\noption(LOKINET_VERSION_SO \"Add the project version to the shared library filename, e.g. liblokinet1.2.3.so instead of liblokinet.so\" OFF)\n\nset(LANGS C CXX)\nif(APPLE AND LOKINET_DAEMON)\n  set(LANGS ${LANGS} OBJC Swift)\nendif()\n\nfind_program(CCACHE_PROGRAM ccache)\nif(CCACHE_PROGRAM)\n  foreach(lang ${LANGS})\n    if(NOT DEFINED CMAKE_${lang}_COMPILER_LAUNCHER AND NOT CMAKE_${lang}_COMPILER MATCHES \".*/ccache\")\n      message(STATUS \"Enabling ccache for ${lang}\")\n      set(CMAKE_${lang}_COMPILER_LAUNCHER ${CCACHE_PROGRAM} CACHE STRING \"\")\n    endif()\n  endforeach()\nendif()\n\n\nproject(lokinet\n    VERSION 0.10.0\n    DESCRIPTION \"lokinet - IP packet onion router\"\n    LANGUAGES ${LANGS})\n\nif(APPLE)\n    # Apple build number: must be incremented to submit a new build for the same lokinet version,\n    # should be reset to 0 when the lokinet version increments.\n    set(LOKINET_APPLE_BUILD 5)\nendif()\n\nlist(APPEND CMAKE_MODULE_PATH \"${CMAKE_CURRENT_LIST_DIR}/cmake\")\n\n# Core options\noption(LOKINET_AVX2 \"enable avx2 code\" OFF)\noption(LOKINET_NATIVE_BUILD \"optimise for host system and FPU\" ON)\noption(LOKINET_XSAN \"use sanitiser, if your system has it (requires -DCMAKE_BUILD_TYPE=Debug)\" OFF)\noption(LOKINET_JEMALLOC \"Link to jemalloc for memory allocations, if found (requires non-static build)\" ON)\noption(LOKINET_COVERAGE \"generate coverage data\" OFF)\noption(LOKINET_WARNINGS_AS_ERRORS \"treat all warnings as errors. turn off for development, on for release\" OFF)\noption(LOKINET_TESTS \"build unit tests\" OFF)\noption(LOKINET_HIVE \"build simulation stubs\" OFF)\noption(LOKINET_PACKAGE \"builds extra components for making an installer (with 'make package')\" OFF)\noption(LOKINET_PEERSTATS \"build with experimental peerstats db support\" OFF)\noption(LOKINET_STRIP \"strip off all debug symbols into an external archive for all executables built\" OFF)\noption(LOKINET_DEBUG_PATH_SEED \"enable support for the debug-mode [paths]:debug-path-seed option for reproducible, non-random paths\" OFF)\n\nset(LOKINET_BOOTSTRAP_FALLBACK_MAINNET \"${PROJECT_SOURCE_DIR}/contrib/bootstrap/mainnet.signed\" CACHE PATH \"Fallback bootstrap path (mainnet)\")\nset(LOKINET_BOOTSTRAP_FALLBACK_TESTNET \"${PROJECT_SOURCE_DIR}/contrib/bootstrap/testnet.signed\" CACHE PATH \"Fallback bootstrap path (testnet)\")\n\ninclude(cmake/enable_lto.cmake)\n\noption(CROSS_PLATFORM \"cross compiler platform\" \"Linux\")\noption(CROSS_PREFIX \"toolchain cross compiler prefix\" \"\")\n\noption(BUILD_SHARED_LIBS \"Build shared library\" OFF)\noption(BUILD_STATIC_DEPS \"Download, build, and statically link against core dependencies\" OFF)\noption(STATIC_LINK \"link statically against dependencies\" ${BUILD_STATIC_DEPS})\nif(BUILD_STATIC_DEPS AND NOT STATIC_LINK)\n  message(FATAL_ERROR \"Option BUILD_STATIC_DEPS requires STATIC_LINK to be enabled as well\")\nendif()\n\nif(BUILD_STATIC_DEPS AND BUILD_SHARED_LIBS)\n  message(FATAL_ERROR \"Incompatible options: BUILD_STATIC_DEPS cannot be used with BUILD_SHARED_LIBS\")\nendif()\n\nif(BUILD_STATIC_DEPS)\n  set(CMAKE_FIND_PACKAGE_PREFER_CONFIG TRUE)\n  include(cmake/StaticBuild.cmake)\nendif()\n\nif(NOT CMAKE_BUILD_TYPE)\n  set(CMAKE_BUILD_TYPE RelWithDebInfo)\nendif()\n\n# set(debug OFF)\n# if(CMAKE_BUILD_TYPE MATCHES \"[Dd][Ee][Bb][Uu][Gg]\")\n#   set(debug ON)\n#   add_definitions(-DLOKINET_DEBUG)\n# endif()\n\noption(LOKINET_WARN_DEPRECATED \"show deprecation warnings\" OFF)\n\ninclude(CheckCXXSourceCompiles)\ninclude(CheckLibraryExists)\n\nset(CMAKE_CXX_STANDARD 20)\nset(CMAKE_CXX_STANDARD_REQUIRED ON)\nset(CMAKE_CXX_EXTENSIONS OFF)\nset(CMAKE_C_STANDARD 11)\nset(CMAKE_C_STANDARD_REQUIRED ON)\n\ninclude(cmake/target_link_libraries_system.cmake)\ninclude(cmake/add_import_library.cmake)\ninclude(cmake/libatomic.cmake)\n\nif (STATIC_LINK)\n  set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_STATIC_LIBRARY_SUFFIX})\n  message(STATUS \"setting static library suffix search\")\nendif()\n\ninclude(cmake/gui-option.cmake)\n\ninclude(cmake/solaris.cmake)\ninclude(cmake/win32.cmake)\ninclude(cmake/macos.cmake)\n\n# No in-source building\ninclude(MacroEnsureOutOfSourceBuild)\nmacro_ensure_out_of_source_build(\"${PROJECT_NAME} requires an out-of-source build.  Create a build directory and run 'cmake ${CMAKE_SOURCE_DIR} [options]'.\")\n\n# Always build PIC\nset(CMAKE_POSITION_INDEPENDENT_CODE ON)\n\ninclude(cmake/unix.cmake)\n\nif(NOT WIN32)\n  if(IOS OR ANDROID)\n    set(NON_PC_TARGET ON)\n  else()\n    include(TargetArch)\n    target_architecture(COMPILE_ARCH)\n    if(COMPILE_ARCH MATCHES i386 OR COMPILE_ARCH MATCHES x86_64)\n      set(NON_PC_TARGET OFF)\n    else()\n      set(NON_PC_TARGET ON)\n    endif()\n  endif()\nendif()\n\nfind_package(PkgConfig REQUIRED)\n\nif(NOT TARGET sodium)\n  # Allow -DLOKINET_DOWNLOAD_SODIUM=FORCE to download without even checking for a local libsodium\n  option(LOKINET_DOWNLOAD_SODIUM \"Allow libsodium to be downloaded and built locally if not found on the system\" OFF)\n  if(NOT LOKINET_DOWNLOAD_SODIUM STREQUAL \"FORCE\" AND NOT BUILD_STATIC_DEPS)\n    pkg_check_modules(SODIUM libsodium>=1.0.18 IMPORTED_TARGET)\n  endif()\n\n  add_library(sodium INTERFACE)\n  if(SODIUM_FOUND AND NOT LOKINET_DOWNLOAD_SODIUM STREQUAL \"FORCE\" AND NOT BUILD_STATIC_DEPS)\n    target_link_libraries(sodium INTERFACE PkgConfig::SODIUM)\n  else()\n    if(NOT LOKINET_DOWNLOAD_SODIUM AND NOT BUILD_STATIC_DEPS)\n      message(FATAL_ERROR \"Could not find libsodium >= 1.0.18; either install it on your system or use -DLOKINET_DOWNLOAD_SODIUM=ON to download and build an internal copy\")\n    endif()\n    message(STATUS \"Sodium >= 1.0.18 not found, but LOKINET_DOWNLOAD_SODIUM specified, so downloading it\")\n    include(DownloadLibSodium)\n    target_link_libraries(sodium INTERFACE sodium_vendor)\n  endif()\n\n  # Need this target export so that loki-mq properly picks up sodium\n  export(TARGETS sodium NAMESPACE sodium:: FILE sodium-exports.cmake)\nendif()\n\nset(warning_flags -Wall -Wextra -Wno-unknown-pragmas -Wno-unused-function -Werror=vla)\n\nif(CMAKE_CXX_COMPILER_ID MATCHES \"Clang\")\n  list(APPEND warning_flags -Wno-unknown-warning-option)\nendif()\n\nif(LOKINET_WARNINGS_AS_ERRORS)\n  list(APPEND warning_flags -Werror -Wno-error=array-bounds)\nendif()\n\n# GCC 12's stringop-overflow warnings are really broken, with tons and tons of false positives all\n# over the place (not just in our code, but also in its own stdlibc++ code).  It's better in 13\n# w.r.t. stdlibc++, but our code still throws tons of warnings, so disable it for now.\nif (CMAKE_CXX_COMPILER_ID STREQUAL \"GNU\" AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 13)\n    list(APPEND warning_flags -Wno-stringop-overflow)\nendif()\nlist(APPEND warning_flags -Wno-deprecated-declarations)\n\nadd_library(lokinet-base-internal_warnings INTERFACE)\n\n# If we blindly add these directly as compile_options then they get passed to swiftc on Apple and\n# break, so we use a generate expression to set them only for C++/C/ObjC\ntarget_compile_options(lokinet-base-internal_warnings INTERFACE \"$<$<OR:$<COMPILE_LANGUAGE:CXX>,$<COMPILE_LANGUAGE:C>,$<COMPILE_LANGUAGE:OBJC>>:${warning_flags}>\")\n\nif(LOKINET_XSAN)\n  string(APPEND CMAKE_CXX_FLAGS_DEBUG \" -fsanitize=${LOKINET_XSAN} -fno-omit-frame-pointer -fno-sanitize-recover\")\n  foreach(type EXE MODULE SHARED STATIC)\n    string(APPEND CMAKE_${type}_LINKER_FLAGS_DEBUG \" -fsanitize=${LOKINET_XSAN} -fno-omit-frame-pointer -fno-sanitize-recover\")\n  endforeach()\n  message(STATUS \"Doing a ${LOKINET_XSAN} sanitizer build\")\nendif()\n\ninclude(cmake/coverage.cmake)\n\n# these vars are set by the cmake toolchain spec\nif (WOW64_CROSS_COMPILE OR WIN64_CROSS_COMPILE)\n  include(cmake/cross_compile.cmake)\nendif()\n\nif(NOT APPLE)\n  if(LOKINET_NATIVE_BUILD)\n    if(CMAKE_SYSTEM_PROCESSOR STREQUAL ppc64le)\n      add_compile_options(-mcpu=native -mtune=native)\n    else()\n      add_compile_options(-march=native -mtune=native)\n    endif()\n  elseif(NOT NON_PC_TARGET)\n    if (LOKINET_AVX2)\n      add_compile_options(-march=haswell -mtune=haswell -mfpmath=sse)\n    else()\n      # Public binary releases\n      add_compile_options(-march=nocona -mtune=haswell -mfpmath=sse)\n    endif()\n  endif()\nendif()\n\n# now have libevent2.21 w/ pthreads\nset(CMAKE_THREAD_PREFER_PTHREAD TRUE)\nset(THREADS_PREFER_PTHREAD_FLAG TRUE)\nfind_package(Threads REQUIRED)\n\nunset(GIT_VERSION)\nunset(GIT_VERSION_REAL)\n\nif(NOT GIT_VERSION)\n  exec_program(\"git\" ${CMAKE_CURRENT_SOURCE_DIR} ARGS \"rev-parse --short HEAD\" OUTPUT_VARIABLE GIT_VERSION_UNSTRIP)\n  string(STRIP \"${GIT_VERSION_UNSTRIP}\" GIT_VERSION)\nendif()\n\nstring(REGEX REPLACE \"^fatal.*$\" nogit GIT_VERSION_REAL \"${GIT_VERSION}\")\n\nfind_package(PkgConfig REQUIRED)\n\nif (NOT BUILD_STATIC_DEPS)\n  pkg_check_modules(UNBOUND libunbound REQUIRED IMPORTED_TARGET)\n  add_library(libunbound INTERFACE)\n  target_link_libraries(libunbound INTERFACE PkgConfig::UNBOUND)\n\n  pkg_check_modules(SD libsystemd IMPORTED_TARGET)\n  # Default WITH_SYSTEMD to true if we found it\n  option(WITH_SYSTEMD \"enable systemd integration for sd_notify\" ${SD_FOUND})\nendif()\n\nadd_subdirectory(external)\n\n# interface library for setting common includes, linkage and flags\nadd_library(lokinet-base INTERFACE)\ntarget_include_directories(lokinet-base INTERFACE . include)\ntarget_link_libraries(lokinet-base INTERFACE oxen::quic fmt::fmt nlohmann_json::nlohmann_json)\n\ntarget_compile_features(lokinet-base INTERFACE cxx_std_20)\n\nadd_library(lokinet-base-internal INTERFACE)\ntarget_include_directories(lokinet-base-internal INTERFACE include/lokinet)\nif(NOT LOKINET_FULL)\n  target_compile_definitions(lokinet-base-internal INTERFACE LOKINET_EMBEDDED_ONLY)\nendif()\n\ntarget_link_libraries(lokinet-base-internal INTERFACE lokinet-base-internal_warnings)\n\nif (TARGET lokinet_static_deps)\n  target_link_libraries(lokinet-base-internal INTERFACE lokinet_static_deps)\nendif()\n\nif(WITH_SYSTEMD AND (NOT ANDROID))\n  if(NOT SD_FOUND)\n    message(FATAL_ERROR \"libsystemd not found\")\n  endif()\n  target_link_libraries(lokinet-base INTERFACE PkgConfig::SD)\n  target_compile_definitions(lokinet-base INTERFACE WITH_SYSTEMD)\nendif()\n\nif(LOKINET_JEMALLOC AND NOT STATIC_LINK)\n  pkg_check_modules(JEMALLOC jemalloc IMPORTED_TARGET)\n  if(JEMALLOC_FOUND)\n    target_link_libraries(lokinet-base INTERFACE PkgConfig::JEMALLOC)\n  else()\n    message(STATUS \"jemalloc not found, not linking to jemalloc\")\n  endif()\nelse()\n  message(STATUS \"jemalloc support disabled\")\nendif()\n\n\nif(ANDROID)\n  target_link_libraries(lokinet-base INTERFACE log)\n  target_compile_definitions(lokinet-base INTERFACE ANDROID)\n  set(ANDROID_PLATFORM_SRC android/ifaddrs.c)\nendif()\n\nif(LOKINET_HIVE)\n  add_definitions(-DLOKINET_HIVE)\nendif()\n\nadd_subdirectory(llarp)\n\nif(LOKINET_DAEMON)\n  add_subdirectory(daemon)\nendif()\n\nif(LOKINET_HIVE)\n  add_subdirectory(pybind)\nendif()\nif(LOKINET_TESTS OR LOKINET_HIVE)\n  add_subdirectory(test)\nendif()\nif(ANDROID)\n  add_subdirectory(jni)\nendif()\n\nadd_subdirectory(docs)\n\ninclude(cmake/gui.cmake)\n\nif(APPLE AND LOKINET_FULL)\n  macos_target_setup()\nendif()\n\nadd_executable(liblokinet_jank_test EXCLUDE_FROM_ALL contrib/liblokinet_jank_test.cpp)\ntarget_link_libraries(liblokinet_jank_test PRIVATE liblokinet)\n\n# uninstall target\nif(NOT TARGET uninstall)\n  configure_file(\n    \"${CMAKE_CURRENT_SOURCE_DIR}/cmake/cmake_uninstall.cmake.in\"\n    \"${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake\"\n    IMMEDIATE @ONLY)\n\n  add_custom_target(uninstall\n    COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_BINARY_DIR}/cmake_uninstall.cmake)\nendif()\n\nif(LOKINET_PACKAGE AND NOT APPLE)\n  include(cmake/installer.cmake)\nendif()\n\nif(TARGET package)\n  add_dependencies(package assemble_gui)\nendif()\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "[Español](CONTRIBUTING_es.md)\n# Do\n\n* Act like a responsible adult.\n\n* RUN `./contrib/format.sh` BEFORE COMMITING ALWAYS.\n\n# Do NOT\n\n* Bring off topic or non technical issues to the issue tracker.\n\n* Pester contributors.\n\n# We WILL\n\n* Merge Spelling mistake corrections (I have lots of those because spelling is HARD)\n\n* Merge code based on its correctness, Including patches via email from (pseudo/fully) anonymous contributors.\n\n# We WILL NOT\n\n* Accept patches with tabs\n\n* Merge large and pointless or pedantic changes, (i.e. code formatting shift)\n\n## additional notes\n\ngithub's ui doesn't seem accept this file as a real code of conduct.\n"
  },
  {
    "path": "CONTRIBUTING_es.md",
    "content": "[Ingles](CONTRIBUTING.md)\n# Lo que debe\n\n* Actuar como un adulto responsable.\n\n* CORRER `make format` SIEMPRE ANTES DEL COMMIT.\n\n# Lo que NO debe\n\n* Plantear asuntos fuera de lugar o que no son tecnicos en el registro de problemas.\n\n* Ser un contribuyente molestoso.\n\n# Lo que nosotros HAREMOS\n\n* Integrar correcciones de Ortografia (Yo tengo un monton porque la ortografia es COMPLICADA)\n\n* Integrar codigo en base a que tan correcto es, Incluyendo parches recibidos por correo electronico de contribuyentes (pseudo/completamente) anonimos.\n\n# Lo que nosotros NO HAREMOS\n\n* Aceptar parches con tabulaciones\n\n* Integrar cambios grandes sin sentido o pedantes, (por ejemplo codigo con cambios de formato)\n\n## notas adicionales\n\ngithub no parece aceptar este archivo como un codigo de conducta real.\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>."
  },
  {
    "path": "cmake/CMakeGraphVizOptions.cmake",
    "content": "set(GRAPHVIZ_GRAPH_NAME \"graph.dot\" CACHE STRING \"\")\nset(GRAPHVIZ_GENERATE_PER_TARGET FALSE CACHE BOOL \"\")\nset(GRAPHVIZ_GENERATE_DEPENDERS FALSE CACHE BOOL \"\")\nset(GRAPHVIZ_OBJECT_LIBS OFF CACHE BOOL \"\")\n"
  },
  {
    "path": "cmake/DownloadLibSodium.cmake",
    "content": "set(LIBSODIUM_PREFIX ${CMAKE_BINARY_DIR}/libsodium)\nset(LIBSODIUM_URL https://github.com/jedisct1/libsodium/releases/download/1.0.18-RELEASE/libsodium-1.0.18.tar.gz https://download.libsodium.org/libsodium/releases/libsodium-1.0.18.tar.gz)\nset(LIBSODIUM_HASH SHA512=17e8638e46d8f6f7d024fe5559eccf2b8baf23e143fadd472a7d29d228b186d86686a5e6920385fe2020729119a5f12f989c3a782afbd05a8db4819bb18666ef)\n\nif(SODIUM_TARBALL_URL)\n    # make a build time override of the tarball url so we can fetch it if the original link goes away\n    set(LIBSODIUM_URL ${SODIUM_TARBALL_URL})\nendif()\n\n\nfile(MAKE_DIRECTORY ${LIBSODIUM_PREFIX}/include)\n\ninclude(ExternalProject)\ninclude(ProcessorCount)\nProcessorCount(PROCESSOR_COUNT)\nif(PROCESSOR_COUNT EQUAL 0)\n    set(PROCESSOR_COUNT 1)\nendif()\n\nset(sodium_cc ${CMAKE_C_COMPILER})\nif(CCACHE_PROGRAM)\n  set(sodium_cc \"${CCACHE_PROGRAM} ${sodium_cc}\")\nendif()\nset(SODIUM_CONFIGURE ./configure --prefix=${LIBSODIUM_PREFIX} --enable-static --disable-shared --with-pic --quiet CC=${sodium_cc})\nif (CMAKE_C_COMPILER_ARG1)\n  set(SODIUM_CONFIGURE ${SODIUM_CONFIGURE} CPPFLAGS=${CMAKE_C_COMPILER_ARG1})\nendif()\n\nif (CROSS_TARGET)\n    set(SODIUM_CONFIGURE ${SODIUM_CONFIGURE} --target=${CROSS_TARGET} --host=${CROSS_TARGET})\nendif()\n\n\nExternalProject_Add(libsodium_external\n    BUILD_IN_SOURCE ON\n    PREFIX ${LIBSODIUM_PREFIX}\n    URL ${LIBSODIUM_URL}\n    URL_HASH ${LIBSODIUM_HASH}\n    CONFIGURE_COMMAND ${SODIUM_CONFIGURE}\n    PATCH_COMMAND patch -p1 -d <SOURCE_DIR> < ${CMAKE_SOURCE_DIR}/win32-setup/libsodium-1.0.18-win32.patch\n    BUILD_COMMAND make -j${PROCESSOR_COUNT}\n    INSTALL_COMMAND ${MAKE}\n    BUILD_BYPRODUCTS ${LIBSODIUM_PREFIX}/lib/libsodium.a ${LIBSODIUM_PREFIX}/include\n    )\n\nadd_library(sodium_vendor STATIC IMPORTED GLOBAL)\nadd_dependencies(sodium_vendor libsodium_external)\nset_target_properties(sodium_vendor PROPERTIES\n    IMPORTED_LOCATION ${LIBSODIUM_PREFIX}/lib/libsodium.a\n    INTERFACE_INCLUDE_DIRECTORIES ${LIBSODIUM_PREFIX}/include\n    )\n"
  },
  {
    "path": "cmake/FindJemalloc.cmake",
    "content": "#\n# Find the JEMALLOC client includes and library\n# \n\n# This module defines\n# JEMALLOC_INCLUDE_DIR, where to find jemalloc.h\n# JEMALLOC_LIBRARIES, the libraries to link against\n# JEMALLOC_FOUND, if false, you cannot build anything that requires JEMALLOC\n\n# also defined, but not for general use are\n# JEMALLOC_LIBRARY, where to find the JEMALLOC library.\n\nset( JEMALLOC_FOUND 0 )\n\nif ( UNIX )\n  FIND_PATH( JEMALLOC_INCLUDE_DIR\n    NAMES\n      jemalloc/jemalloc.h\n    PATHS\n      /usr/include\n      /usr/include/jemalloc\n      /usr/local/include\n      /usr/local/include/jemalloc\n      $ENV{JEMALLOC_ROOT}\n      $ENV{JEMALLOC_ROOT}/include\n  DOC\n    \"Specify include-directories that might contain jemalloc.h here.\"\n  )\n  FIND_LIBRARY( JEMALLOC_LIBRARY \n    NAMES\n      jemalloc libjemalloc JEMALLOC\n    PATHS\n      /usr/lib\n      /usr/lib/jemalloc\n      /usr/local/lib\n      /usr/local/lib/jemalloc\n      /usr/local/jemalloc/lib\n      $ENV{JEMALLOC_ROOT}/lib\n      $ENV{JEMALLOC_ROOT}\n    DOC \"Specify library-locations that might contain the jemalloc library here.\"\n  )\n\n  if ( JEMALLOC_LIBRARY )\n    if ( JEMALLOC_INCLUDE_DIR )\n      set( JEMALLOC_FOUND 1 )\n      message( STATUS \"Found JEMALLOC library: ${JEMALLOC_LIBRARY}\")\n      message( STATUS \"Found JEMALLOC headers: ${JEMALLOC_INCLUDE_DIR}\")\n    else ( JEMALLOC_INCLUDE_DIR )\n      message(FATAL_ERROR \"Could not find jemalloc headers! Please install jemalloc libraries and headers\")\n    endif ( JEMALLOC_INCLUDE_DIR )\n  endif ( JEMALLOC_LIBRARY )\n  add_library(jemalloc SHARED IMPORTED)\n  set_target_properties(jemalloc PROPERTIES\n    INTERFACE_INCLUDE_DIRECTORIES \"${JEMALLOC_INCLUDE_DUR}\"\n    IMPORTED_LOCATION \"${JEMALLOC_LIBRARY}\")\n  mark_as_advanced( JEMALLOC_FOUND JEMALLOC_LIBRARY JEMALLOC_EXTRA_LIBRARIES JEMALLOC_INCLUDE_DIR )\nendif (UNIX)\n"
  },
  {
    "path": "cmake/GenVersion.cmake",
    "content": "# Copyright (c) 2014-2019, The Monero Project\n# Copyright (c)      2019, The Loki Project\n# \n# All rights reserved.\n# \n# Redistribution and use in source and binary forms, with or without modification, are\n# permitted provided that the following conditions are met:\n# \n# 1. Redistributions of source code must retain the above copyright notice, this list of\n#    conditions and the following disclaimer.\n# \n# 2. Redistributions in binary form must reproduce the above copyright notice, this list\n#    of conditions and the following disclaimer in the documentation and/or other\n#    materials provided with the distribution.\n# \n# 3. Neither the name of the copyright holder nor the names of its contributors may be\n#    used to endorse or promote products derived from this software without specific\n#    prior written permission.\n# \n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND ANY\n# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL\n# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\n# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\n# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF\n# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n# \n# Parts of this file are originally copyright (c) 2012-2013 The Cryptonote developers\n\n# Check what commit we're on\nexecute_process(COMMAND \"${GIT}\" rev-parse --short HEAD RESULT_VARIABLE RET OUTPUT_VARIABLE COMMIT OUTPUT_STRIP_TRAILING_WHITESPACE)\n\nif(RET)\n\t# Something went wrong, set the version tag to -unknown\n    message(WARNING \"Cannot determine current commit. Make sure that you are building either from a Git working tree or from a source archive.\")\n    set(VERSIONTAG \"unknown\")\nelse()\n\tmessage(STATUS \"You are currently on commit ${COMMIT}\")\n\n\t# Get all the tags\n\texecute_process(COMMAND \"${GIT}\" rev-list --tags --max-count=1 --abbrev-commit RESULT_VARIABLE RET OUTPUT_VARIABLE TAGGEDCOMMIT OUTPUT_STRIP_TRAILING_WHITESPACE)\n\n    if(NOT TAGGEDCOMMIT)\n        message(WARNING \"Cannot determine most recent tag. Make sure that you are building either from a Git working tree or from a source archive.\")\n        set(VERSIONTAG \"${COMMIT}\")\n    else()\n        # Check if we're building that tagged commit or a different one\n        if(COMMIT STREQUAL TAGGEDCOMMIT)\n            message(STATUS \"${COMMIT} is a tagged release; setting version tag to 'release'\")\n            set(VERSIONTAG \"release\")\n        else()\n            message(STATUS \"You are not building a tagged release; setting version tag to '${COMMIT}'\")\n            set(VERSIONTAG \"${COMMIT}\")\n        endif()\n    endif()\nendif()\n\nconfigure_file(\"${SRC}\" \"${DEST}\" @ONLY)\n"
  },
  {
    "path": "cmake/MacroEnsureOutOfSourceBuild.cmake",
    "content": "# - MACRO_ENSURE_OUT_OF_SOURCE_BUILD(<errorMessage>)\n# MACRO_ENSURE_OUT_OF_SOURCE_BUILD(<errorMessage>)\n\n# Copyright (c) 2006, Alexander Neundorf, <neundorf@kde.org>\n#\n# Redistribution and use is allowed according to the terms of the BSD license:\n# \n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions\n# are met:\n# \n# 1. Redistributions of source code must retain the copyright\n#    notice, this list of conditions and the following disclaimer.\n# 2. Redistributions in binary form must reproduce the copyright\n#    notice, this list of conditions and the following disclaimer in the\n#    documentation and/or other materials provided with the distribution.\n# 3. The name of the author may not be used to endorse or promote products \n#    derived from this software without specific prior written permission.\n# \n# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\n# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES\n# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.\n# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,\n# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\n# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\n# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF\n# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nmacro (MACRO_ENSURE_OUT_OF_SOURCE_BUILD _errorMessage)\n\n   string(COMPARE EQUAL \"${CMAKE_SOURCE_DIR}\" \"${CMAKE_BINARY_DIR}\" _insource)\n   if (_insource)\n     message(SEND_ERROR \"${_errorMessage}\")\n     message(FATAL_ERROR \"Remove the file CMakeCache.txt in ${CMAKE_SOURCE_DIR} first.\")\n   endif (_insource)\n\nendmacro (MACRO_ENSURE_OUT_OF_SOURCE_BUILD)\n"
  },
  {
    "path": "cmake/StaticBuild.cmake",
    "content": "# cmake bits to do a full static build, downloading and building all dependencies.\n\n# Most of these are CACHE STRINGs so that you can override them using -DWHATEVER during cmake\n# invocation to override.\n\ninclude_guard(GLOBAL)\n\ninclude(\"${CMAKE_CURRENT_LIST_DIR}/../external/oxen-libquic/cmake/StaticBuild.cmake\")\n\nset(LOCAL_MIRROR \"\" CACHE STRING \"local mirror path/URL for lib downloads\")\n\nset(EXPAT_VERSION 2.7.1 CACHE STRING \"expat version\")\nstring(REPLACE \".\" \"_\" EXPAT_TAG \"R_${EXPAT_VERSION}\")\nset(EXPAT_MIRROR ${LOCAL_MIRROR} https://github.com/libexpat/libexpat/releases/download/${EXPAT_TAG}\n    CACHE STRING \"expat download mirror(s)\")\nset(EXPAT_SOURCE expat-${EXPAT_VERSION}.tar.xz)\nset(EXPAT_HASH SHA512=4c9a6c1c1769d2c4404da083dd3013dbc73883da50e2b7353db2349a420e9b6d27cac7dbcb645991d6c7cdbf79bd88486fc1ac353084ce48e61081fb56e13d46\n    CACHE STRING \"expat source hash\")\n\nset(UNBOUND_VERSION 1.23.0 CACHE STRING \"unbound version\")\nset(UNBOUND_MIRROR ${LOCAL_MIRROR} https://nlnetlabs.nl/downloads/unbound CACHE STRING \"unbound download mirror(s)\")\nset(UNBOUND_SOURCE unbound-${UNBOUND_VERSION}.tar.gz)\nset(UNBOUND_HASH SHA512=9b5ca48f4f5189f168f76396f5895f39262a4333e589f8c64bb9298a55c6266f626a4a4399370c68edd9f6318215a401146bf9e16a101c54decf623668a398af\n    CACHE STRING \"unbound source hash\")\n\nset(SQLITE3_VERSION 3500200 CACHE STRING \"sqlite3 version\")\nset(SQLITE3_MIRROR ${LOCAL_MIRROR} https://www.sqlite.org/2025\n    CACHE STRING \"sqlite3 download mirror(s)\")\nset(SQLITE3_SOURCE sqlite-autoconf-${SQLITE3_VERSION}.tar.gz)\nset(SQLITE3_HASH SHA3_256=e4d2b4332988f479ec032ccff00963a9bbd24a3a0f0222b4e249653fa680b4c0\n  CACHE STRING \"sqlite3 source hash\")\n\nset(SODIUM_VERSION 1.0.20 CACHE STRING \"libsodium version\")\nset(SODIUM_MIRROR ${LOCAL_MIRROR}\n  https://download.libsodium.org/libsodium/releases\n  https://github.com/jedisct1/libsodium/releases/download/${SODIUM_VERSION}-RELEASE\n  CACHE STRING \"libsodium mirror(s)\")\nset(SODIUM_SOURCE libsodium-${SODIUM_VERSION}.tar.gz)\nset(SODIUM_HASH SHA512=7ea165f3c1b1609790e30a16348b9dfdc5731302da00c07c65e125c8ab115c75419a5631876973600f8a4b560ca2c8267001770b68f2eb3eebc9ba095d312702\n  CACHE STRING \"libsodium source hash\")\n\nset(ZMQ_VERSION 4.3.5 CACHE STRING \"libzmq version\")\nset(ZMQ_MIRROR ${LOCAL_MIRROR} https://github.com/zeromq/libzmq/releases/download/v${ZMQ_VERSION}\n    CACHE STRING \"libzmq mirror(s)\")\nset(ZMQ_SOURCE zeromq-${ZMQ_VERSION}.tar.gz)\nset(ZMQ_HASH SHA512=a71d48aa977ad8941c1609947d8db2679fc7a951e4cd0c3a1127ae026d883c11bd4203cf315de87f95f5031aec459a731aec34e5ce5b667b8d0559b157952541\n    CACHE STRING \"libzmq source hash\")\n\nset(ZLIB_VERSION 1.3.1 CACHE STRING \"zlib version\")\nset(ZLIB_MIRROR ${LOCAL_MIRROR} https://zlib.net\n    CACHE STRING \"zlib mirror(s)\")\nset(ZLIB_SOURCE zlib-${ZLIB_VERSION}.tar.xz)\nset(ZLIB_HASH SHA256=38ef96b8dfe510d42707d9c781877914792541133e1870841463bfa73f883e32\n  CACHE STRING \"zlib source hash\")\n\nset(ZSTD_VERSION 1.5.7 CACHE STRING \"zstd version\")\nset(ZSTD_MIRROR ${LOCAL_MIRROR} https://github.com/facebook/zstd/releases/download/v${ZSTD_VERSION}\n    CACHE STRING \"zstd mirror(s)\")\nset(ZSTD_SOURCE zstd-${ZSTD_VERSION}.tar.gz)\nset(ZSTD_HASH SHA256=eb33e51f49a15e023950cd7825ca74a4a2b43db8354825ac24fc1b7ee09e6fa3\n    CACHE STRING \"zstd source hash\")\n\ninclude(ExternalProject)\n\nset(DEPS_DESTDIR ${CMAKE_BINARY_DIR}/static-deps)\nset(DEPS_SOURCEDIR ${CMAKE_BINARY_DIR}/static-deps-sources)\n\ninclude_directories(BEFORE SYSTEM ${DEPS_DESTDIR}/include)\n\nfile(MAKE_DIRECTORY ${DEPS_DESTDIR}/include)\n\nset(deps_cc \"${CMAKE_C_COMPILER}\")\nset(deps_cxx \"${CMAKE_CXX_COMPILER}\")\nif(CMAKE_C_COMPILER_LAUNCHER)\n  set(deps_cc \"${CMAKE_C_COMPILER_LAUNCHER} ${deps_cc}\")\nendif()\nif(CMAKE_CXX_COMPILER_LAUNCHER)\n  set(deps_cxx \"${CMAKE_CXX_COMPILER_LAUNCHER} ${deps_cxx}\")\nendif()\n\n\nfunction(expand_urls output source_file)\n  set(expanded)\n  foreach(mirror ${ARGN})\n    list(APPEND expanded \"${mirror}/${source_file}\")\n  endforeach()\n  set(${output} \"${expanded}\" PARENT_SCOPE)\nendfunction()\n\n\nadd_library(lokinet_static_deps INTERFACE)\n\nfunction(add_static_target target ext_target libname)\n  add_library(${target} STATIC IMPORTED GLOBAL)\n  add_dependencies(${target} ${ext_target})\n  target_link_libraries(lokinet_static_deps INTERFACE ${target})\n  set_target_properties(${target} PROPERTIES\n    IMPORTED_LOCATION ${DEPS_DESTDIR}/lib/${libname}\n  )\n  if (ARGN)\n    target_link_libraries(${target} INTERFACE ${ARGN})\n  endif()\nendfunction()\n\n\nset(cross_host \"\")\nset(cross_rc \"\")\nif(CMAKE_CROSSCOMPILING)\n  set(cross_host \"--host=${ARCH_TRIPLET}\")\n  if (ARCH_TRIPLET MATCHES mingw AND CMAKE_RC_COMPILER)\n    set(cross_rc \"WINDRES=${CMAKE_RC_COMPILER}\")\n  endif()\nendif()\nif(ANDROID)\n  set(android_toolchain_suffix linux-android)\n  set(android_compiler_suffix linux-android23)\n  if(CMAKE_ANDROID_ARCH_ABI MATCHES x86_64)\n    set(android_machine x86_64)\n    set(cross_host \"--host=x86_64-linux-android\")\n    set(android_compiler_prefix x86_64)\n    set(android_compiler_suffix linux-android23)\n    set(android_toolchain_prefix x86_64)\n    set(android_toolchain_suffix linux-android)\n  elseif(CMAKE_ANDROID_ARCH_ABI MATCHES x86)\n    set(android_machine x86)\n    set(cross_host \"--host=i686-linux-android\")\n    set(android_compiler_prefix i686)\n    set(android_compiler_suffix linux-android23)\n    set(android_toolchain_prefix i686)\n    set(android_toolchain_suffix linux-android)\n  elseif(CMAKE_ANDROID_ARCH_ABI MATCHES armeabi-v7a)\n    set(android_machine arm)\n    set(cross_host \"--host=armv7a-linux-androideabi\")\n    set(android_compiler_prefix armv7a)\n    set(android_compiler_suffix linux-androideabi23)\n    set(android_toolchain_prefix arm)\n    set(android_toolchain_suffix linux-androideabi)\n  elseif(CMAKE_ANDROID_ARCH_ABI MATCHES arm64-v8a)\n    set(android_machine arm64)\n    set(cross_host \"--host=aarch64-linux-android\")\n    set(android_compiler_prefix aarch64)\n    set(android_compiler_suffix linux-android23)\n    set(android_toolchain_prefix aarch64)\n    set(android_toolchain_suffix linux-android)\n  else()\n    message(FATAL_ERROR \"unknown android arch: ${CMAKE_ANDROID_ARCH_ABI}\")\n  endif()\n  set(deps_cc \"${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/${android_compiler_prefix}-${android_compiler_suffix}-clang\")\n  set(deps_cxx \"${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/${android_compiler_prefix}-${android_compiler_suffix}-clang++\")\n  set(deps_ld \"${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/${android_compiler_prefix}-${android_toolchain_suffix}-ld\")\n  set(deps_ranlib \"${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/${android_toolchain_prefix}-${android_toolchain_suffix}-ranlib\")\n  set(deps_ar \"${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64/bin/${android_toolchain_prefix}-${android_toolchain_suffix}-ar\")\nendif()\n\nset(deps_CFLAGS \"-O2\")\nset(deps_CXXFLAGS \"-O2\")\n\nif(WITH_LTO)\n  set(deps_CFLAGS \"${deps_CFLAGS} -flto\")\nendif()\n\nif(APPLE)\n  set(deps_CFLAGS \"${deps_CFLAGS} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}\")\n  set(deps_CXXFLAGS \"${deps_CXXFLAGS} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}\")\nendif()\n\nif(_winver)\n  set(deps_CFLAGS \"${deps_CFLAGS} -D_WIN32_WINNT=${_winver}\")\n  set(deps_CXXFLAGS \"${deps_CXXFLAGS} -D_WIN32_WINNT=${_winver}\")\nendif()\n\n\nif(\"${CMAKE_GENERATOR}\" STREQUAL \"Unix Makefiles\")\n  set(_make $(MAKE))\nelse()\n  set(_make make)\nendif()\n\n\n# Builds a target; takes the target name (e.g. \"readline\") and builds it in an external project with\n# target name suffixed with `_external`.  Its upper-case value is used to get the download details\n# (from the variables set above).  The following options are supported and passed through to\n# ExternalProject_Add if specified.  If omitted, these defaults are used:\nset(build_def_DEPENDS \"\")\nset(build_def_PATCH_COMMAND \"\")\nset(build_def_CONFIGURE_COMMAND ./configure ${cross_host} --disable-shared --prefix=${DEPS_DESTDIR} --with-pic\n    \"CC=${deps_cc}\" \"CXX=${deps_cxx}\" \"CFLAGS=${deps_CFLAGS}\" \"CXXFLAGS=${deps_CXXFLAGS}\" ${cross_rc})\nset(build_def_BUILD_COMMAND ${_make})\nset(build_def_INSTALL_COMMAND ${_make} install)\nset(build_def_BUILD_BYPRODUCTS ${DEPS_DESTDIR}/lib/lib___TARGET___.a ${DEPS_DESTDIR}/include/___TARGET___.h)\n\nfunction(build_external target)\n  set(options DEPENDS PATCH_COMMAND CONFIGURE_COMMAND BUILD_COMMAND INSTALL_COMMAND BUILD_BYPRODUCTS)\n  cmake_parse_arguments(PARSE_ARGV 1 arg \"\" \"\" \"${options}\")\n  foreach(o ${options})\n    if(NOT DEFINED arg_${o})\n      set(arg_${o} ${build_def_${o}})\n    endif()\n  endforeach()\n  string(REPLACE ___TARGET___ ${target} arg_BUILD_BYPRODUCTS \"${arg_BUILD_BYPRODUCTS}\")\n\n  if(arg_CONFIGURE_COMMAND MATCHES \"^DEFAULT_CMAKE\")\n      string(REGEX REPLACE \"^DEFAULT_CMAKE(;?)\" \"CMAKE_ARGS;-DCMAKE_INSTALL_PREFIX=${DEPS_DESTDIR}\\\\1\" configure \"${arg_CONFIGURE_COMMAND}\")\n  else()\n    set(configure CONFIGURE_COMMAND ${arg_CONFIGURE_COMMAND})\n  endif()\n\n  string(TOUPPER \"${target}\" prefix)\n  expand_urls(urls ${${prefix}_SOURCE} ${${prefix}_MIRROR})\n  ExternalProject_Add(\"${target}_external\"\n    DEPENDS ${arg_DEPENDS}\n    BUILD_IN_SOURCE ON\n    PREFIX ${DEPS_SOURCEDIR}\n    URL ${urls}\n    URL_HASH ${${prefix}_HASH}\n    DOWNLOAD_NO_PROGRESS ON\n    PATCH_COMMAND ${arg_PATCH_COMMAND}\n    ${configure}\n    BUILD_COMMAND ${arg_BUILD_COMMAND}\n    INSTALL_COMMAND ${arg_INSTALL_COMMAND}\n    BUILD_BYPRODUCTS ${arg_BUILD_BYPRODUCTS}\n  )\nendfunction()\n\nif(NOT TARGET sodium)\n  build_external(sodium CONFIGURE_COMMAND ./configure ${cross_host} ${cross_rc} --prefix=${DEPS_DESTDIR} --disable-shared\n            --enable-static --with-pic \"CC=${deps_cc}\" \"CFLAGS=${deps_CFLAGS}\")\n  add_static_target(sodium sodium_external libsodium.a)\nendif()\n\n\nif(LOKINET_PEERSTATS)\n  build_external(sqlite3)\n  add_static_target(sqlite3 sqlite3_external libsqlite3.a)\nendif()\n\n\nif(NOT TARGET libzstd::static)\n  build_external(zstd\n      CONFIGURE_COMMAND DEFAULT_CMAKE\n        -DZSTD_BUILD_PROGRAMS=OFF -DZSTD_BUILD_TESTS=OFF -DZSTD_BUILD_STATIC=ON -DZSTD_BUILD_SHARED=OFF -DZSTD_BUILD_DICTBUILDER=OFF\n      SOURCE_SUBDIR build/cmake\n      BUILD_BYPRODUCTS\n        ${DEPS_DESTDIR}/lib/libzstd.a\n        ${DEPS_DESTDIR}/include/zstd.h\n  )\n  # Use the same libzstd::static target name as libsession-util so that we can use libsession's\n  # static zstd if we are being built as part of libsession:\n  add_static_target(libzstd::static zstd_external libzstd.a)\nendif()\n\n\nif(LOKINET_FULL)\n\n  build_external(zlib\n    CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env \"CC=${deps_cc}\" \"CFLAGS=${deps_CFLAGS} -fPIC\" ${cross_extra} ./configure --prefix=${DEPS_DESTDIR} --static\n    BUILD_BYPRODUCTS\n      ${DEPS_DESTDIR}/lib/libz.a\n      ${DEPS_DESTDIR}/include/zlib.h\n  )\n  add_static_target(zlib zlib_external libz.a)\n\n  build_external(expat\n    CONFIGURE_COMMAND ./configure ${cross_host} --prefix=${DEPS_DESTDIR} --enable-static\n    --disable-shared --with-pic --without-examples --without-tests --without-docbook --without-xmlwf\n    \"CC=${deps_cc}\" \"CFLAGS=${deps_CFLAGS}\"\n  )\n  add_static_target(expat expat_external libexpat.a)\n\n\n  if(WIN32)\n    set(unbound_patch\n      PATCH_COMMAND ${PROJECT_SOURCE_DIR}/contrib/apply-patches.sh\n          ${PROJECT_SOURCE_DIR}/contrib/patches/unbound-delete-crash-fix.patch)\n  endif()\n  build_external(unbound\n    DEPENDS nettle_external expat_external\n    ${unbound_patch}\n    CONFIGURE_COMMAND ./configure ${cross_host} ${cross_rc} --prefix=${DEPS_DESTDIR}\n    --with-libunbound-only --disable-shared --enable-static\n    --with-pic --$<IF:$<BOOL:${WITH_LTO}>,enable,disable>-flto\n    --with-nettle=${DEPS_DESTDIR} --with-libexpat=${DEPS_DESTDIR}\n    --without-ssl\n    \"CC=${deps_cc}\" \"CFLAGS=${deps_CFLAGS}\" \"LDFLAGS=${unbound_ldflags}\"\n  )\n  add_static_target(libunbound unbound_external libunbound.a)\n  if(NOT WIN32)\n    set_target_properties(libunbound PROPERTIES INTERFACE_LINK_LIBRARIES \"hogweed::hogweed;nettle::nettle\")\n  else()\n    set_target_properties(libunbound PROPERTIES INTERFACE_LINK_LIBRARIES \"hogweed::hogweed;nettle::nettle;ws2_32;crypt32;iphlpapi\")\n  endif()\n\n\n\n  if(ARCH_TRIPLET MATCHES mingw)\n    option(WITH_WEPOLL \"use wepoll zmq poller (crashy)\" OFF)\n    if(WITH_WEPOLL)\n      set(zmq_extra --with-poller=wepoll)\n    endif()\n  endif()\n\n\n  build_external(zmq\n    DEPENDS sodium_external\n    CONFIGURE_COMMAND ./configure ${cross_host} --prefix=${DEPS_DESTDIR} --enable-static --disable-shared\n      --disable-curve-keygen --enable-curve --disable-drafts --disable-libunwind --with-libsodium\n      --without-pgm --without-norm --without-vmci --without-docs --with-pic --disable-Werror --disable-libbsd ${zmq_extra}\n      \"CC=${deps_cc}\" \"CXX=${deps_cxx}\" \"CFLAGS=${deps_CFLAGS} -fstack-protector\" \"CXXFLAGS=${deps_CXXFLAGS} -fstack-protector\"\n      \"sodium_CFLAGS=-I${DEPS_DESTDIR}/include\" \"sodium_LIBS=-L${DEPS_DESTDIR}/lib -lsodium\"\n  )\n  add_static_target(libzmq zmq_external libzmq.a)\n\n\n  set(libzmq_link_libs \"sodium\")\n  if(CMAKE_CROSSCOMPILING AND ARCH_TRIPLET MATCHES mingw)\n    list(APPEND libzmq_link_libs iphlpapi)\n  endif()\n\n  set_target_properties(libzmq PROPERTIES\n    INTERFACE_LINK_LIBRARIES \"${libzmq_link_libs}\"\n    INTERFACE_COMPILE_DEFINITIONS \"ZMQ_STATIC\")\n\nendif(LOKINET_FULL)\n"
  },
  {
    "path": "cmake/TargetArch.cmake",
    "content": "# Based on the Qt 5 processor detection code, so should be very accurate\n# https://qt.gitorious.org/qt/qtbase/blobs/master/src/corelib/global/qprocessordetection.h\n# Currently handles arm (v5, v6, v7), x86 (32/64), ia64, and ppc (32/64)\n\n# Regarding POWER/PowerPC, just as is noted in the Qt source,\n# \"There are many more known variants/revisions that we do not handle/detect.\"\n\nset(archdetect_c_code \"\n#if defined(__arm__) || defined(__TARGET_ARCH_ARM)\n    #if defined(__ARM_ARCH_7__) \\\\\n        || defined(__ARM_ARCH_7A__) \\\\\n        || defined(__ARM_ARCH_7R__) \\\\\n        || defined(__ARM_ARCH_7M__) \\\\\n        || (defined(__TARGET_ARCH_ARM) && __TARGET_ARCH_ARM-0 >= 7)\n        #error cmake_ARCH armv7\n    #elif defined(__ARM_ARCH_6__) \\\\\n        || defined(__ARM_ARCH_6J__) \\\\\n        || defined(__ARM_ARCH_6T2__) \\\\\n        || defined(__ARM_ARCH_6Z__) \\\\\n        || defined(__ARM_ARCH_6K__) \\\\\n        || defined(__ARM_ARCH_6ZK__) \\\\\n        || defined(__ARM_ARCH_6M__) \\\\\n        || (defined(__TARGET_ARCH_ARM) && __TARGET_ARCH_ARM-0 >= 6)\n        #error cmake_ARCH armv6\n    #elif defined(__ARM_ARCH_5TEJ__) \\\\\n        || (defined(__TARGET_ARCH_ARM) && __TARGET_ARCH_ARM-0 >= 5)\n        #error cmake_ARCH armv5\n    #else\n        #error cmake_ARCH arm\n    #endif\n#elif defined(__i386) || defined(__i386__) || defined(_M_IX86)\n    #error cmake_ARCH i386\n#elif defined(__x86_64) || defined(__x86_64__) || defined(__amd64) || defined(_M_X64)\n    #error cmake_ARCH x86_64\n#elif defined(__ia64) || defined(__ia64__) || defined(_M_IA64)\n    #error cmake_ARCH ia64\n#elif defined(__ppc__) || defined(__ppc) || defined(__powerpc__) \\\\\n      || defined(_ARCH_COM) || defined(_ARCH_PWR) || defined(_ARCH_PPC)  \\\\\n      || defined(_M_MPPC) || defined(_M_PPC)\n    #if defined(__ppc64__) || defined(__powerpc64__) || defined(__64BIT__)\n        #error cmake_ARCH ppc64\n    #else\n        #error cmake_ARCH ppc\n    #endif\n#endif\n\n#error cmake_ARCH unknown\n\")\n\n# Set ppc_support to TRUE before including this file or ppc and ppc64\n# will be treated as invalid architectures since they are no longer supported by Apple\n\nfunction(target_architecture output_var)\n    if(APPLE AND CMAKE_OSX_ARCHITECTURES)\n        # On OS X we use CMAKE_OSX_ARCHITECTURES *if* it was set\n        # First let's normalize the order of the values\n\n        # Note that it's not possible to compile PowerPC applications if you are using\n        # the OS X SDK version 10.6 or later - you'll need 10.4/10.5 for that, so we\n        # disable it by default\n        # See this page for more information:\n        # http://stackoverflow.com/questions/5333490/how-can-we-restore-ppc-ppc64-as-well-as-full-10-4-10-5-sdk-support-to-xcode-4\n\n        # Architecture defaults to i386 or ppc on OS X 10.5 and earlier, depending on the CPU type detected at runtime.\n        # On OS X 10.6+ the default is x86_64 if the CPU supports it, i386 otherwise.\n\n        foreach(osx_arch ${CMAKE_OSX_ARCHITECTURES})\n            if(\"${osx_arch}\" STREQUAL \"ppc\" AND ppc_support)\n                set(osx_arch_ppc TRUE)\n            elseif(\"${osx_arch}\" STREQUAL \"i386\")\n                set(osx_arch_i386 TRUE)\n            elseif(\"${osx_arch}\" STREQUAL \"x86_64\")\n                set(osx_arch_x86_64 TRUE)\n            elseif(\"${osx_arch}\" STREQUAL \"ppc64\" AND ppc_support)\n                set(osx_arch_ppc64 TRUE)\n            else()\n                message(FATAL_ERROR \"Invalid OS X arch name: ${osx_arch}\")\n            endif()\n        endforeach()\n\n        # Now add all the architectures in our normalized order\n        if(osx_arch_ppc)\n            list(APPEND ARCH ppc)\n        endif()\n\n        if(osx_arch_i386)\n            list(APPEND ARCH i386)\n        endif()\n\n        if(osx_arch_x86_64)\n            list(APPEND ARCH x86_64)\n        endif()\n\n        if(osx_arch_ppc64)\n            list(APPEND ARCH ppc64)\n        endif()\n    else()\n        file(WRITE \"${CMAKE_BINARY_DIR}/arch.c\" \"${archdetect_c_code}\")\n\n        enable_language(C)\n\n        # Detect the architecture in a rather creative way...\n        # This compiles a small C program which is a series of ifdefs that selects a\n        # particular #error preprocessor directive whose message string contains the\n        # target architecture. The program will always fail to compile (both because\n        # file is not a valid C program, and obviously because of the presence of the\n        # #error preprocessor directives... but by exploiting the preprocessor in this\n        # way, we can detect the correct target architecture even when cross-compiling,\n        # since the program itself never needs to be run (only the compiler/preprocessor)\n        try_run(\n            run_result_unused\n            compile_result_unused\n            \"${CMAKE_BINARY_DIR}\"\n            \"${CMAKE_BINARY_DIR}/arch.c\"\n            COMPILE_OUTPUT_VARIABLE ARCH\n            CMAKE_FLAGS CMAKE_OSX_ARCHITECTURES=${CMAKE_OSX_ARCHITECTURES}\n        )\n\n        # Parse the architecture name from the compiler output\n        string(REGEX MATCH \"cmake_ARCH ([a-zA-Z0-9_]+)\" ARCH \"${ARCH}\")\n\n        # Get rid of the value marker leaving just the architecture name\n        string(REPLACE \"cmake_ARCH \" \"\" ARCH \"${ARCH}\")\n\n        # If we are compiling with an unknown architecture this variable should\n        # already be set to \"unknown\" but in the case that it's empty (i.e. due\n        # to a typo in the code), then set it to unknown\n        if (NOT ARCH)\n            set(ARCH unknown)\n        endif()\n    endif()\n\n    set(${output_var} \"${ARCH}\" PARENT_SCOPE)\nendfunction()\n"
  },
  {
    "path": "cmake/Version.cmake",
    "content": "# We do this via a custom command that re-invokes a cmake script because we need the DEPENDS on .git/index so that we will re-run it (to regenerate the commit tag in the version) whenever the current commit changes. If we used a configure_file directly here, it would only re-run when something else causes cmake to re-run.\n\nif(LOKINET_VERSIONTAG)\n  set(VERSIONTAG \"${LOKINET_VERSIONTAG}\")\n  configure_file(\"${CMAKE_CURRENT_SOURCE_DIR}/constants/version.cpp.in\" \"${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp\" @ONLY)\nelse()\n  set(VERSIONTAG \"${GIT_VERSION}\")\n  set(GIT_INDEX_FILE \"${PROJECT_SOURCE_DIR}/.git/index\")\n  find_package(Git)\n  if(EXISTS \"${GIT_INDEX_FILE}\" AND ( GIT_FOUND OR Git_FOUND) )\n      message(STATUS \"Found Git: ${GIT_EXECUTABLE}\")\n      set(genversion_args \"-DGIT=${GIT_EXECUTABLE}\")\n      foreach(v lokinet_VERSION lokinet_VERSION_MAJOR lokinet_VERSION_MINOR lokinet_VERSION_PATCH RELEASE_MOTTO)\n          list(APPEND genversion_args \"-D${v}=${${v}}\")\n      endforeach()\n\n      add_custom_command(\n          OUTPUT            \"${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp\"\n          COMMAND           \"${CMAKE_COMMAND}\"\n          ${genversion_args}\n          \"-D\" \"SRC=${CMAKE_CURRENT_SOURCE_DIR}/constants/version.cpp.in\"\n          \"-D\" \"DEST=${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp\"\n          \"-P\" \"${CMAKE_CURRENT_LIST_DIR}/GenVersion.cmake\"\n          DEPENDS           \"${CMAKE_CURRENT_SOURCE_DIR}/constants/version.cpp.in\"\n          \"${GIT_INDEX_FILE}\")\n  else()\n    configure_file(\"${CMAKE_CURRENT_SOURCE_DIR}/constants/version.cpp.in\" \"${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp\" @ONLY)\n  endif()\nendif()\n\n\nif(WIN32)\n  foreach(exe IN ITEMS lokinet lokinet-vpn lokinet-bootstrap)\n    set(lokinet_EXE_NAME \"${exe}.exe\")\n    configure_file(\"${CMAKE_CURRENT_SOURCE_DIR}/win32/version.rc.in\" \"${CMAKE_BINARY_DIR}/${exe}.rc\" @ONLY)\n    set_property(SOURCE \"${CMAKE_BINARY_DIR}/${exe}.rc\" PROPERTY GENERATED 1)\n  endforeach()\nendif()\n\nadd_custom_target(genversion_cpp DEPENDS \"${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp\")\nif(WIN32)\n  add_custom_target(genversion_rc DEPENDS \"${CMAKE_BINARY_DIR}/lokinet.rc\" \"${CMAKE_BINARY_DIR}/lokinet-vpn.rc\" \"${CMAKE_BINARY_DIR}/lokinet-bootstrap.rc\")\nelse()\n  add_custom_target(genversion_rc)\nendif()\nadd_custom_target(genversion DEPENDS genversion_cpp genversion_rc)\n"
  },
  {
    "path": "cmake/add_import_library.cmake",
    "content": "function(add_import_library libname)\n  add_library(libname SHARED IMPORTED)\n  if(NOT TARGET libname)\n    message(FATAL \"unable to find library ${libname}\")\n  endif()\nendfunction()\n"
  },
  {
    "path": "cmake/cmake_uninstall.cmake.in",
    "content": "if(NOT EXISTS \"@CMAKE_BINARY_DIR@/install_manifest.txt\")\n  message(FATAL_ERROR \"Cannot find install manifest: @CMAKE_BINARY_DIR@/install_manifest.txt\")\nendif()\n\nfile(READ \"@CMAKE_BINARY_DIR@/install_manifest.txt\" files)\nstring(REGEX REPLACE \"\\n\" \";\" files \"${files}\")\nforeach(file ${files})\n  message(STATUS \"Uninstalling $ENV{DESTDIR}${file}\")\n  if(IS_SYMLINK \"$ENV{DESTDIR}${file}\" OR EXISTS \"$ENV{DESTDIR}${file}\")\n    exec_program(\n      \"@CMAKE_COMMAND@\" ARGS \"-E remove \\\"$ENV{DESTDIR}${file}\\\"\"\n      OUTPUT_VARIABLE rm_out\n      RETURN_VALUE rm_retval\n      )\n    if(NOT \"${rm_retval}\" STREQUAL 0)\n      message(FATAL_ERROR \"Problem when removing $ENV{DESTDIR}${file}\")\n    endif()\n  else(IS_SYMLINK \"$ENV{DESTDIR}${file}\" OR EXISTS \"$ENV{DESTDIR}${file}\")\n    message(STATUS \"File $ENV{DESTDIR}${file} does not exist.\")\n  endif()\nendforeach()\n"
  },
  {
    "path": "cmake/coverage.cmake",
    "content": "if (LOKINET_COVERAGE)\n  if (CMAKE_CXX_COMPILER_ID MATCHES \"Clang\")\n    add_compile_options( -fprofile-instr-generate -fcoverage-mapping )\n    link_libraries( -fprofile-instr-generate )\n  else()\n    add_compile_options( --coverage -g0 )\n    link_libraries( --coverage )\n  endif()\nendif()\n"
  },
  {
    "path": "cmake/cross_compile.cmake",
    "content": "# dynamic linking does this all the time\nif (CMAKE_CXX_COMPILER_ID MATCHES \"Clang\")\n  option(NO_LIBGCC \"use libunwind+compiler-rt instead, must already be installed in mingw-w64 sysroot\" OFF)\n  add_compile_options(-Wno-unused-command-line-argument -Wno-c++11-narrowing)\n  add_compile_options($<$<COMPILE_LANGUAGE:C>:-Wno-bad-function-cast>)\n  if (NO_LIBGCC)\n    find_library(UNWIND_LIB unwind)\n    link_libraries(${UNWIND_LIB})\n    find_library(PSAPI_LIB psapi)\n    link_libraries(${PSAPI_LIB})\n  endif(NO_LIBGCC)\nelse()\n  # found it. this is GNU only\n  add_compile_options(-Wno-cast-function-type)\nendif()\n"
  },
  {
    "path": "cmake/enable_lto.cmake",
    "content": "# -flto\ninclude(CheckIPOSupported)\noption(WITH_LTO \"enable lto on compile time\" ON)\nif(WITH_LTO)\n  if(WIN32)\n    message(FATAL_ERROR \"LTO not supported on win32 targets, please set -DWITH_LTO=OFF\")\n  endif()\n  check_ipo_supported(RESULT IPO_ENABLED OUTPUT ipo_error)\n  if(IPO_ENABLED)\n    message(STATUS \"LTO enabled\")\n  else()\n    message(WARNING \"LTO not supported by compiler: ${ipo_error}\")\n  endif()\nelse()\n  message(STATUS \"LTO disabled\")\n  set(IPO_ENABLED OFF)\nendif()\n\nfunction(enable_lto)\n  if(IPO_ENABLED)\n    set_target_properties(${ARGN} PROPERTIES INTERPROCEDURAL_OPTIMIZATION ON)\n  endif()\nendfunction()\n"
  },
  {
    "path": "cmake/gui-option.cmake",
    "content": "set(default_build_gui OFF)\nif(LOKINET_DAEMON AND (APPLE OR WIN32))\n  set(default_build_gui ON)\nendif()\n\nif(WIN32)\n  set(GUI_EXE \"\" CACHE FILEPATH \"path to a pre-built Windows GUI .exe to use (implies -DLOKINET_GUI=OFF)\")\n  if(GUI_EXE)\n    set(default_build_gui OFF)\n  endif()\nendif()\n\noption(LOKINET_GUI \"build electron gui from 'gui' submodule source\" ${default_build_gui})\n\nif(LOKINET_GUI AND GUI_EXE)\n  message(FATAL_ERROR \"-DGUI_EXE=... and -DLOKINET_GUI=ON are mutually exclusive\")\nendif()\n"
  },
  {
    "path": "cmake/gui.cmake",
    "content": "\nif(WIN32 AND GUI_EXE)\n  message(STATUS \"using pre-built lokinet gui executable: ${GUI_EXE}\")\n  execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different \"${GUI_EXE}\" \"${PROJECT_BINARY_DIR}/gui/lokinet-gui.exe\")\nelseif(LOKINET_GUI)\n  message(STATUS \"Building lokinet-gui from source\")\n\n  set(default_gui_target pack)\n  if(APPLE)\n    set(default_gui_target macos:raw)\n  elseif(WIN32)\n    set(default_gui_target win32)\n  endif()\n\n  set(GUI_YARN_TARGET \"${default_gui_target}\" CACHE STRING \"yarn target for building the GUI\")\n  set(GUI_YARN_EXTRA_OPTS \"\" CACHE STRING \"extra options to pass into the yarn build command\")\n\n  # allow manually specifying yarn with -DYARN=\n  if(NOT YARN)\n    find_program(YARN NAMES yarnpkg yarn REQUIRED)\n  endif()\n  message(STATUS \"Building lokinet-gui with yarn ${YARN}, target ${GUI_YARN_TARGET}\")\n\n  if(NOT WIN32)\n    add_custom_target(lokinet-gui\n      COMMAND ${YARN} install --frozen-lockfile &&\n      ${YARN} ${GUI_YARN_EXTRA_OPTS} ${GUI_YARN_TARGET}\n      WORKING_DIRECTORY \"${PROJECT_SOURCE_DIR}/gui\")\n  endif()\n\n  if(APPLE)\n    add_custom_target(assemble_gui ALL\n      DEPENDS assemble lokinet-gui\n      COMMAND mkdir \"${lokinet_app}/Contents/Helpers\"\n      COMMAND cp -a \"${PROJECT_SOURCE_DIR}/gui/release/mac/Lokinet-GUI.app\" \"${lokinet_app}/Contents/Helpers/\"\n      COMMAND mkdir -p \"${lokinet_app}/Contents/Resources/en.lproj\"\n      COMMAND cp \"${PROJECT_SOURCE_DIR}/contrib/macos/InfoPlist.strings\" \"${lokinet_app}/Contents/Resources/en.lproj/\"\n      COMMAND cp \"${lokinet_app}/Contents/Resources/icon.icns\" \"${lokinet_app}/Contents/Helpers/Lokinet-GUI.app/Contents/Resources/icon.icns\"\n      COMMAND cp \"${PROJECT_SOURCE_DIR}/contrib/macos/InfoPlist.strings\" \"${lokinet_app}/Contents/Helpers/Lokinet-GUI.app/Contents/Resources/en.lproj/\"\n      COMMAND /usr/libexec/PlistBuddy\n      -c \"Delete :CFBundleDisplayName\"\n      -c \"Add :LSHasLocalizedDisplayName bool true\"\n      -c \"Add :CFBundleDevelopmentRegion string en\"\n      -c \"Set :CFBundleShortVersionString ${lokinet_VERSION}\"\n      -c \"Set :CFBundleVersion ${lokinet_VERSION}.${LOKINET_APPLE_BUILD}\"\n      \"${lokinet_app}/Contents/Helpers/Lokinet-GUI.app/Contents/Info.plist\"\n    )\n  elseif(WIN32)\n    file(MAKE_DIRECTORY \"${PROJECT_BINARY_DIR}/gui\")\n    add_custom_command(OUTPUT \"${PROJECT_BINARY_DIR}/gui/lokinet-gui.exe\"\n      COMMAND ${YARN} install --frozen-lockfile &&\n      USE_SYSTEM_7ZA=true DISPLAY= WINEDEBUG=-all WINEPREFIX=\"${PROJECT_BINARY_DIR}/wineprefix\" ${YARN} ${GUI_YARN_EXTRA_OPTS} ${GUI_YARN_TARGET}\n      COMMAND ${CMAKE_COMMAND} -E copy_if_different\n      \"${PROJECT_SOURCE_DIR}/gui/release/Lokinet-GUI_portable.exe\"\n      \"${PROJECT_BINARY_DIR}/gui/lokinet-gui.exe\"\n      WORKING_DIRECTORY \"${PROJECT_SOURCE_DIR}/gui\")\n    add_custom_target(assemble_gui ALL COMMAND \"true\" DEPENDS \"${PROJECT_BINARY_DIR}/gui/lokinet-gui.exe\")\n  else()\n    message(FATAL_ERROR \"Building/bundling the GUI from this repository is not supported on this platform\")\n  endif()\nelse()\n  message(STATUS \"not building gui\")\nendif()\n\nif(NOT TARGET assemble_gui)\n  add_custom_target(assemble_gui COMMAND \"true\")\nendif()\n"
  },
  {
    "path": "cmake/installer.cmake",
    "content": "set(CPACK_PACKAGE_VENDOR \"lokinet.org\")\nset(CPACK_PACKAGE_HOMEPAGE_URL \"https://lokinet.org/\")\nset(CPACK_RESOURCE_FILE_README \"${PROJECT_SOURCE_DIR}/contrib/readme-installer.txt\")\nset(CPACK_RESOURCE_FILE_LICENSE \"${PROJECT_SOURCE_DIR}/LICENSE\")\n\nif(WIN32)\n  include(cmake/win32_installer_deps.cmake)\n  install(FILES ${CMAKE_SOURCE_DIR}/contrib/configs/00-exit.ini DESTINATION share/conf.d COMPONENT exit_configs)\n  install(FILES ${CMAKE_SOURCE_DIR}/contrib/configs/00-keyfile.ini DESTINATION share/conf.d COMPONENT keyfile_configs)\n  install(FILES ${CMAKE_SOURCE_DIR}/contrib/configs/00-debug-log.ini DESTINATION share/conf.d COMPONENT debug_configs)\n  get_cmake_property(CPACK_COMPONENTS_ALL COMPONENTS)\n  list(REMOVE_ITEM CPACK_COMPONENTS_ALL \"Unspecified\" \"lokinet\" \"gui\" \"exit_configs\" \"keyfile_configs\" \"debug_configs\")\n  list(APPEND CPACK_COMPONENTS_ALL \"lokinet\" \"gui\" \"exit_configs\" \"keyfile_configs\" \"debug_configs\")\nelseif(APPLE)\n  set(CPACK_GENERATOR DragNDrop;ZIP)\n  get_cmake_property(CPACK_COMPONENTS_ALL COMPONENTS)\n  list(REMOVE_ITEM CPACK_COMPONENTS_ALL \"Unspecified\")\nendif()\n\ninclude(CPack)\n\nif(WIN32)\n  cpack_add_component(lokinet\n    DISPLAY_NAME \"lokinet\"\n    DESCRIPTION \"core required lokinet files\"\n    REQUIRED)\n\n  cpack_add_component(gui\n    DISPLAY_NAME \"lokinet gui\"\n    DESCRIPTION \"electron based control panel for lokinet\")\n\n  cpack_add_component(exit_configs\n    DISPLAY_NAME \"auto-enable exit\"\n    DESCRIPTION \"automatically enable usage of exit.loki as an exit node\\n\"\n    DISABLED)\n\n  cpack_add_component(keyfile_configs\n    DISPLAY_NAME \"persist address\"\n    DESCRIPTION \"persist .loki address across restarts of lokinet\\nnot recommended when enabling exit nodes\"\n    DISABLED)\n\n  cpack_add_component(debug_configs\n    DISPLAY_NAME \"debug logging\"\n    DESCRIPTION \"enable debug spew log level by default\"\n    DISABLED)\nendif()\n"
  },
  {
    "path": "cmake/libatomic.cmake",
    "content": "function(check_working_cxx_atomics64 varname)\n  set(OLD_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS})\n  if (EMBEDDED_CFG)\n    set(CMAKE_REQUIRED_FLAGS \"${CMAKE_REQUIRED_FLAGS} -m32 -march=i486\")\n  elseif(MSVC OR MSVC_VERSION)\n    set(CMAKE_REQUIRED_FLAGS \"${CMAKE_REQUIRED_FLAGS} -arch:IA32 -std:c++14\")\n  else()\n  # CMAKE_CXX_STANDARD does not propagate to cmake compile tests\n    set(CMAKE_REQUIRED_FLAGS \"${CMAKE_REQUIRED_FLAGS} -std=c++14\")\n  endif()\n  check_cxx_source_compiles(\"\n#include <atomic>\n#include <cstdint>\nstd::atomic<uint64_t> x (0);\nint main() {\n  uint64_t i = x.load(std::memory_order_relaxed);\n  return 0;\n}\n\" ${varname})\n  set(CMAKE_REQUIRED_FLAGS ${OLD_CMAKE_REQUIRED_FLAGS})\nendfunction()\n\nfunction(link_libatomic)\n  check_working_cxx_atomics64(HAVE_CXX_ATOMICS64_WITHOUT_LIB)\n\n  if(HAVE_CXX_ATOMICS64_WITHOUT_LIB)\n    message(STATUS \"Have working 64bit atomics\")\n    return()\n  endif()\n\n  if (NOT MSVC AND NOT MSVC_VERSION)\n    check_library_exists(atomic __atomic_load_8 \"\" HAVE_CXX_LIBATOMICS64)\n    if (HAVE_CXX_LIBATOMICS64)\n      message(STATUS \"Have 64bit atomics via library\")\n      list(APPEND CMAKE_REQUIRED_LIBRARIES \"atomic\")\n      check_working_cxx_atomics64(HAVE_CXX_ATOMICS64_WITH_LIB)\n      if (HAVE_CXX_ATOMICS64_WITH_LIB)\n        message(STATUS \"Can link with libatomic\")\n        link_libraries(-latomic)\n        return()\n      endif()\n    endif()\n  endif()\n  if (MSVC OR MSVC_VERSION)\n    message(FATAL_ERROR \"Host compiler must support 64-bit std::atomic! (What does MSVC do to inline atomics?)\")\n  else()\n    message(FATAL_ERROR \"Host compiler must support 64-bit std::atomic!\")\n  endif()\nendfunction()\n"
  },
  {
    "path": "cmake/macos.cmake",
    "content": "if((NOT APPLE) OR NOT LOKINET_DAEMON)\n  return()\nendif()\n\n\noption(MACOS_SYSTEM_EXTENSION\n  \"Build the network extension as a system extension rather than a plugin.  This must be ON for non-app store release builds, and must be OFF for dev builds and Mac App Store distribution builds\"\n  OFF)\noption(CODESIGN \"codesign the resulting app and extension\" ON)\nset(CODESIGN_ID \"\" CACHE STRING \"codesign the macos app using this key identity; if empty we'll try to guess\")\nset(default_profile_type \"dev\")\nif(MACOS_SYSTEM_EXTENSION)\n  set(default_profile_type \"release\")\nendif()\nset(CODESIGN_PROFILE \"${PROJECT_SOURCE_DIR}/contrib/macos/lokinet.${default_profile_type}.provisionprofile\" CACHE FILEPATH\n  \"Path to a .provisionprofile to use for the main app\")\nset(CODESIGN_EXT_PROFILE \"${PROJECT_SOURCE_DIR}/contrib/macos/lokinet-extension.${default_profile_type}.provisionprofile\" CACHE FILEPATH\n  \"Path to a .provisionprofile to use for the lokinet extension\")\n\nif(CODESIGN AND NOT CODESIGN_ID)\n  if(MACOS_SYSTEM_EXTENSION)\n    set(codesign_cert_pattern \"Developer ID Application\")\n  else()\n    set(codesign_cert_pattern \"Apple Development\")\n  endif()\n  execute_process(\n    COMMAND security find-identity -v -p codesigning\n    COMMAND sed -n \"s/^ *[0-9][0-9]*)  *\\\\([A-F0-9]\\\\{40\\\\}\\\\)  *\\\"\\\\(${codesign_cert_pattern}.*\\\\)\\\"\\$/\\\\1 \\\\2/p\"\n    RESULT_VARIABLE find_id_exit_code\n    OUTPUT_VARIABLE find_id_output)\n  if(NOT find_id_exit_code EQUAL 0)\n    message(FATAL_ERROR \"Finding signing identities with security find-identity failed; try specifying an id using -DCODESIGN_ID=...\")\n  endif()\n\n  string(REGEX MATCHALL \"(^|\\n)[0-9A-F]+\" find_id_sign_id \"${find_id_output}\")\n  if(NOT find_id_sign_id)\n    message(FATAL_ERROR \"Did not find any \\\"${codesign_cert_pattern}\\\" identity; try specifying an id using -DCODESIGN_ID=...\")\n  endif()\n  if (find_id_sign_id MATCHES \";\")\n    message(FATAL_ERROR \"Found multiple \\\"${codesign_cert_pattern}\\\" identities:\\n${find_id_output}\\nSpecify an identify using -DCODESIGN_ID=...\")\n  endif()\n  set(CODESIGN_ID \"${find_id_sign_id}\" CACHE STRING \"\" FORCE)\nendif()\n\nif(CODESIGN)\n  message(STATUS \"Codesigning using ${CODESIGN_ID}\")\n\n  if (NOT MACOS_NOTARIZE_USER AND NOT MACOS_NOTARIZE_PASS AND NOT MACOS_NOTARIZE_ASC AND EXISTS \"$ENV{HOME}/.notarization.cmake\")\n    message(STATUS \"Loading notarization info from ~/.notarization.cmake\")\n    include(\"$ENV{HOME}/.notarization.cmake\")\n  endif()\n\n  if (MACOS_NOTARIZE_USER AND MACOS_NOTARIZE_PASS AND MACOS_NOTARIZE_ASC)\n    message(STATUS \"Enabling notarization with account ${MACOS_NOTARIZE_ASC}/${MACOS_NOTARIZE_USER}\")\n  else()\n    message(WARNING \"You have not set one or more of MACOS_NOTARIZE_USER, MACOS_NOTARIZE_PASS, MACOS_NOTARIZE_ASC: notarization will fail; see contrib/macos/README.txt\")\n  endif()\n\nelse()\n  message(WARNING \"Codesigning disabled; the resulting build will not run on most macOS systems\")\nendif()\n\n\nforeach(prof IN ITEMS CODESIGN_PROFILE CODESIGN_EXT_PROFILE)\n  if(NOT ${prof})\n    message(WARNING \"Missing a ${prof} provisioning profile: Apple will most likely log an uninformative error message to the system log and then kill harmless kittens if you try to run the result\")\n  elseif(NOT EXISTS \"${${prof}}\")\n    message(FATAL_ERROR \"Provisioning profile ${${prof}} does not exist; fix your -D${prof} path\")\n  endif()\nendforeach()\nmessage(STATUS \"Using ${CODESIGN_PROFILE} app provisioning profile\")\nmessage(STATUS \"Using ${CODESIGN_EXT_PROFILE} extension provisioning profile\")\n\n\n\nset(lokinet_installer \"${PROJECT_BINARY_DIR}/Lokinet ${PROJECT_VERSION}\")\nif(NOT CODESIGN)\n  set(lokinet_installer \"${lokinet_installer}-UNSIGNED\")\nendif()\nset(lokinet_app \"${lokinet_installer}/Lokinet.app\")\n\n\nif(MACOS_SYSTEM_EXTENSION)\n  set(lokinet_ext_dir Contents/Library/SystemExtensions)\nelse()\n  set(lokinet_ext_dir Contents/PlugIns)\nendif()\n\nif(CODESIGN)\n  if(MACOS_SYSTEM_EXTENSION)\n    set(LOKINET_ENTITLEMENTS_TYPE sysext)\n    set(notarize_py_is_sysext True)\n  else()\n    set(LOKINET_ENTITLEMENTS_TYPE plugin)\n    set(notarize_py_is_sysext False)\n  endif()\n\n  configure_file(\n    \"${PROJECT_SOURCE_DIR}/contrib/macos/sign.sh.in\"\n    \"${PROJECT_BINARY_DIR}/sign.sh\"\n    @ONLY)\n\n  add_custom_target(\n    sign\n    DEPENDS \"${PROJECT_BINARY_DIR}/sign.sh\"\n    COMMAND \"${PROJECT_BINARY_DIR}/sign.sh\"\n    )\n\n  if(MACOS_NOTARIZE_USER AND MACOS_NOTARIZE_PASS AND MACOS_NOTARIZE_ASC)\n    configure_file(\n      \"${PROJECT_SOURCE_DIR}/contrib/macos/notarize.py.in\"\n      \"${PROJECT_BINARY_DIR}/notarize.py\"\n      @ONLY)\n    add_custom_target(\n      notarize\n      DEPENDS \"${PROJECT_BINARY_DIR}/notarize.py\" sign\n      COMMAND \"${PROJECT_BINARY_DIR}/notarize.py\"\n      )\n  else()\n    message(WARNING \"You have not set one or more of MACOS_NOTARIZE_USER, MACOS_NOTARIZE_PASS, MACOS_NOTARIZE_ASC: notarization disabled\")\n  endif()\nelse()\n  add_custom_target(sign COMMAND \"true\")\n  add_custom_target(notarize DEPENDS sign COMMAND \"true\")\nendif()\n\nset(mac_icon \"${PROJECT_BINARY_DIR}/lokinet.icns\")\nadd_custom_command(OUTPUT \"${mac_icon}\"\n  COMMAND ${PROJECT_SOURCE_DIR}/contrib/macos/mk-icns.sh ${PROJECT_SOURCE_DIR}/contrib/lokinet-mac.svg \"${mac_icon}\"\n  DEPENDS ${PROJECT_SOURCE_DIR}/contrib/lokinet-mac.svg ${PROJECT_SOURCE_DIR}/contrib/macos/mk-icns.sh)\nadd_custom_target(icon DEPENDS \"${mac_icon}\")\n\nif(LOKINET_PACKAGE)\n  add_executable(seticon \"${PROJECT_SOURCE_DIR}/contrib/macos/seticon.swift\")\n  add_custom_command(OUTPUT \"${lokinet_installer}.dmg\"\n    DEPENDS notarize seticon\n    COMMAND create-dmg\n      --volname \"Lokinet ${PROJECT_VERSION}\"\n      --volicon lokinet.icns\n      --background \"${PROJECT_SOURCE_DIR}/contrib/macos/installer.tiff\"\n      --text-size 16\n      --icon-size 128\n      --window-size 555 440\n      --icon Lokinet.app 151 196\n      --hide-extension Lokinet.app\n      --app-drop-link 403 196\n      --eula \"${PROJECT_SOURCE_DIR}/LICENSE\"\n      --no-internet-enable\n      \"${lokinet_installer}.dmg\"\n      \"${lokinet_installer}\"\n      COMMAND ./seticon lokinet.icns \"${lokinet_installer}.dmg\"\n  )\n  add_custom_target(dmg DEPENDS \"${lokinet_installer}.dmg\")\nendif()\n\n\n# Called later to set things up, after the main lokinet targets are set up\nfunction(macos_target_setup)\n\n  if(NOT LOKINET_DAEMON)\n    return()\n  endif()\n\n  if(MACOS_SYSTEM_EXTENSION)\n    target_compile_definitions(lokinet PRIVATE MACOS_SYSTEM_EXTENSION)\n  endif()\n\n  set_target_properties(lokinet\n    PROPERTIES\n    OUTPUT_NAME Lokinet\n    MACOSX_BUNDLE TRUE\n    MACOSX_BUNDLE_INFO_STRING \"Lokinet IP Packet Onion Router\"\n    MACOSX_BUNDLE_BUNDLE_NAME \"Lokinet\"\n    MACOSX_BUNDLE_BUNDLE_VERSION \"${lokinet_VERSION}\"\n    MACOSX_BUNDLE_LONG_VERSION_STRING \"${lokinet_VERSION}\"\n    MACOSX_BUNDLE_SHORT_VERSION_STRING \"${lokinet_VERSION_MAJOR}.${lokinet_VERSION_MINOR}\"\n    MACOSX_BUNDLE_GUI_IDENTIFIER \"org.lokinet\"\n    MACOSX_BUNDLE_INFO_PLIST \"${PROJECT_SOURCE_DIR}/contrib/macos/lokinet.Info.plist.in\"\n    MACOSX_BUNDLE_COPYRIGHT \"© 2022, The Oxen Project\"\n  )\n\n  add_custom_target(copy_bootstrap\n    DEPENDS lokinet-extension\n    COMMAND ${CMAKE_COMMAND} -E copy_if_different ${PROJECT_SOURCE_DIR}/contrib/bootstrap/mainnet.signed\n      $<TARGET_BUNDLE_DIR:lokinet-extension>/Contents/Resources/bootstrap.signed\n  )\n\n\n  add_dependencies(lokinet lokinet-extension icon)\n\n\n  if(CODESIGN_PROFILE)\n    add_custom_target(copy_prov_prof\n      DEPENDS lokinet\n      COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CODESIGN_PROFILE}\n        $<TARGET_BUNDLE_DIR:lokinet>/Contents/embedded.provisionprofile\n    )\n  else()\n    add_custom_target(copy_prov_prof COMMAND true)\n  endif()\n\n  add_custom_target(assemble ALL\n    DEPENDS lokinet lokinet-extension icon copy_prov_prof copy_bootstrap\n    COMMAND rm -rf \"${lokinet_app}\"\n    COMMAND mkdir -p \"${lokinet_installer}\"\n    COMMAND cp -a $<TARGET_BUNDLE_DIR:lokinet> \"${lokinet_app}\"\n    COMMAND mkdir -p \"${lokinet_app}/${lokinet_ext_dir}\"\n    COMMAND cp -a $<TARGET_BUNDLE_DIR:lokinet-extension> \"${lokinet_app}/${lokinet_ext_dir}/\"\n    COMMAND mkdir -p \"${lokinet_app}/Contents/Resources\"\n    COMMAND cp -a \"${mac_icon}\" \"${lokinet_app}/Contents/Resources/icon.icns\"\n  )\n\n  if(LOKINET_GUI)\n    add_dependencies(sign assemble_gui)\n  else()\n    add_dependencies(sign assemble)\n  endif()\nendfunction()\n"
  },
  {
    "path": "cmake/ngtcp2_lib.cmake",
    "content": "# ngtcp2's top-level CMakeLists.txt loads a bunch of crap we don't want (examples, a conflicting\n# 'check' target, etc.); instead we directly include it's lib subdirectory to build just the\n# library, but we have to set up a couple things to make that work:\nfunction(add_ngtcp2_lib)\n  file(STRINGS ngtcp2/CMakeLists.txt ngtcp2_project_line REGEX \"^project\\\\(ngtcp2 \")\n  if(NOT ngtcp2_project_line MATCHES \"^project\\\\(ngtcp2 VERSION ([0-9]+)\\\\.([0-9]+)\\\\.([0-9]+)\\\\)$\")\n    message(FATAL_ERROR \"Unable to extract ngtcp2 version from ngtcp2/CMakeLists.txt (found '${ngtcp2_project_line}')\")\n  endif()\n\n  set(PACKAGE_VERSION \"${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}\")\n  include(ngtcp2/cmake/Version.cmake)\n  HexVersion(PACKAGE_VERSION_NUM ${CMAKE_MATCH_1} ${CMAKE_MATCH_2} ${CMAKE_MATCH_3})\n  configure_file(\"ngtcp2/lib/includes/ngtcp2/version.h.in\" \"ngtcp2/lib/includes/ngtcp2/version.h\" @ONLY)\n\n  set(BUILD_SHARED_LIBS OFF)\n\n  # Checks for header files.\n  include(CheckIncludeFile)\n  check_include_file(\"arpa/inet.h\"   HAVE_ARPA_INET_H)\n  check_include_file(\"netinet/in.h\"  HAVE_NETINET_IN_H)\n  check_include_file(\"stddef.h\"      HAVE_STDDEF_H)\n  check_include_file(\"stdint.h\"      HAVE_STDINT_H)\n  check_include_file(\"stdlib.h\"      HAVE_STDLIB_H)\n  check_include_file(\"string.h\"      HAVE_STRING_H)\n  check_include_file(\"unistd.h\"      HAVE_UNISTD_H)\n  check_include_file(\"sys/endian.h\"  HAVE_SYS_ENDIAN_H)\n  check_include_file(\"endian.h\"      HAVE_ENDIAN_H)\n  check_include_file(\"byteswap.h\"    HAVE_BYTESWAP_H)\n\n  include(CheckTypeSize)\n  check_type_size(\"ssize_t\" SIZEOF_SSIZE_T)\n  if(SIZEOF_SSIZE_T STREQUAL \"\")\n    set(ssize_t ptrdiff_t)\n  endif()\n\n  include(CheckSymbolExists)\n  if(HAVE_ENDIAN_H)\n    check_symbol_exists(be64toh \"endian.h\" HAVE_BE64TOH)\n  endif()\n  if(NOT HAVE_BE64TO AND HAVE_SYS_ENDIAN_H)\n    check_symbol_exists(be64toh \"sys/endian.h\" HAVE_BE64TOH)\n  endif()\n\n  check_symbol_exists(bswap_64 \"byteswap.h\" HAVE_BSWAP_64)\n\n  configure_file(ngtcp2/cmakeconfig.h.in ngtcp2/config.h)\n  include_directories(\"${CMAKE_CURRENT_BINARY_DIR}/ngtcp2\") # for config.h\n  set(ENABLE_STATIC_LIB ON FORCE BOOL)\n  set(ENABLE_SHARED_LIB OFF FORCE BOOL)\n  add_subdirectory(ngtcp2/lib EXCLUDE_FROM_ALL)\n\n  target_compile_definitions(ngtcp2_static PRIVATE -DHAVE_CONFIG_H -D_GNU_SOURCE)\nendfunction()\n"
  },
  {
    "path": "cmake/solaris.cmake",
    "content": "if(${CMAKE_SYSTEM_NAME} MATCHES \"SunOS\")\n  set(SOLARIS ON)\n  set(CMAKE_CXX_STANDARD_LIBRARIES \"${CMAKE_CXX_STANDARD_LIBRARIES} -lsocket -lnsl\")\n  add_definitions(-D_POSIX_PTHREAD_SEMANTICS)\nendif()\n"
  },
  {
    "path": "cmake/target_link_libraries_system.cmake",
    "content": "# This adds a dependency as a \"system\" dep - e.g -isystem\nfunction(target_link_libraries_system target)\n  set(libs ${ARGN})\n  foreach(lib ${libs})\n    get_target_property(lib_include_dirs ${lib} INTERFACE_INCLUDE_DIRECTORIES)\n    target_include_directories(${target} SYSTEM PUBLIC ${lib_include_dirs})\n    target_link_libraries(${target} PUBLIC ${lib})\n  endforeach(lib)\nendfunction()\n"
  },
  {
    "path": "cmake/unix.cmake",
    "content": "if(NOT ANDROID)\n  if(NOT UNIX)\n    return()\n  endif()\nendif()\n\ninclude(CheckCXXSourceCompiles)\ninclude(CheckLibraryExists)\n\nadd_definitions(-DUNIX)\nadd_definitions(-DPOSIX)\n\nif(EMBEDDED_CFG OR ${CMAKE_SYSTEM_NAME} MATCHES \"Linux\")\n  link_libatomic()\nendif()\n\nif (${CMAKE_SYSTEM_NAME} MATCHES \"OpenBSD\")\n  add_definitions(-D_BSD_SOURCE)\n  add_definitions(-D_GNU_SOURCE)\n  add_definitions(-D_XOPEN_SOURCE=700)\nendif()\n"
  },
  {
    "path": "cmake/win32.cmake",
    "content": "if(NOT WIN32)\n  return()\nendif()\nif (NOT STATIC_LINK)\n  message(FATAL_ERROR \"windows requires static builds (thanks balmer)\")\nendif()\n\nenable_language(RC)\n\noption(WITH_WINDOWS_32 \"build 32 bit windows\" OFF)\n\n# unlike unix where you get a *single* compiler ID string in .comment\n# GNU ld sees fit to merge *all* the .ident sections in object files\n# to .r[o]data section one after the other!\nadd_compile_options(-fno-ident -Wa,-mbig-obj)\n\nfunction(expand_urls output source_file)\n  set(expanded)\n  foreach(mirror ${ARGN})\n    list(APPEND expanded \"${mirror}/${source_file}\")\n  endforeach()\n  set(${output} \"${expanded}\" PARENT_SCOPE)\nendfunction()\n\nfunction(add_static_target target ext_target libname)\n  add_library(${target} STATIC IMPORTED GLOBAL)\n  add_dependencies(${target} ${ext_target})\n  set_target_properties(${target} PROPERTIES\n    IMPORTED_LOCATION ${DEPS_DESTDIR}/lib/${libname}\n  )\nendfunction()\n\nif(EMBEDDED_CFG)\n  link_libatomic()\nendif()\n\nset(WINTUN_VERSION 0.14.1 CACHE STRING \"wintun version\")\nset(WINTUN_MIRROR ${LOCAL_MIRROR} https://www.wintun.net/builds\n  CACHE STRING \"wintun mirror(s)\")\nset(WINTUN_SOURCE wintun-${WINTUN_VERSION}.zip)\nset(WINTUN_HASH SHA256=07c256185d6ee3652e09fa55c0b673e2624b565e02c4b9091c79ca7d2f24ef51\n  CACHE STRING \"wintun source hash\")\n\nset(WINDIVERT_VERSION 2.2.2-A CACHE STRING \"windivert version\")\nset(WINDIVERT_MIRROR ${LOCAL_MIRROR} https://reqrypt.org/download\n  CACHE STRING \"windivert mirror(s)\")\nset(WINDIVERT_SOURCE WinDivert-${WINDIVERT_VERSION}.zip)\nset(WINDIVERT_HASH SHA512=92eb2ef98ced175d44de1cdb7c52f2ebc534b6a997926baeb83bfe94cba9287b438f796aff11f6163918bcdbc25bcd4e3383715f139f690d207ce219f846a345\n  CACHE STRING \"windivert source hash\")\n\nexpand_urls(WINTUN_URL ${WINTUN_SOURCE} ${WINTUN_MIRROR})\nexpand_urls(WINDIVERT_URL ${WINDIVERT_SOURCE} ${WINDIVERT_MIRROR})\n\nmessage(STATUS \"Downloading wintun from ${WINTUN_URL}\")\nfile(DOWNLOAD ${WINTUN_URL} ${CMAKE_BINARY_DIR}/wintun.zip EXPECTED_HASH ${WINTUN_HASH})\nmessage(STATUS \"Downloading windivert from ${WINDIVERT_URL}\")\nfile(DOWNLOAD ${WINDIVERT_URL} ${CMAKE_BINARY_DIR}/windivert.zip EXPECTED_HASH ${WINDIVERT_HASH})\n\nexecute_process(COMMAND ${CMAKE_COMMAND} -E tar x ${CMAKE_BINARY_DIR}/wintun.zip\n  WORKING_DIRECTORY ${CMAKE_BINARY_DIR})\n\nexecute_process(COMMAND ${CMAKE_COMMAND} -E tar x ${CMAKE_BINARY_DIR}/windivert.zip\n  WORKING_DIRECTORY ${CMAKE_BINARY_DIR})\n"
  },
  {
    "path": "cmake/win32_installer_deps.cmake",
    "content": "install(DIRECTORY ${CMAKE_BINARY_DIR}/gui DESTINATION share COMPONENT gui)\n\nif(WITH_WINDOWS_32)\n  install(FILES ${CMAKE_BINARY_DIR}/wintun/bin/x86/wintun.dll DESTINATION bin COMPONENT lokinet)\n  install(FILES ${CMAKE_BINARY_DIR}/WinDivert-${WINDIVERT_VERSION}/x86/WinDivert.sys DESTINATION lib COMPONENT lokinet)\n  install(FILES ${CMAKE_BINARY_DIR}/WinDivert-${WINDIVERT_VERSION}/x86/WinDivert.dll DESTINATION bin COMPONENT lokinet)\nelse()\n  install(FILES ${CMAKE_BINARY_DIR}/wintun/bin/amd64/wintun.dll DESTINATION bin COMPONENT lokinet)\n  install(FILES ${CMAKE_BINARY_DIR}/WinDivert-${WINDIVERT_VERSION}/x64/WinDivert64.sys DESTINATION lib COMPONENT lokinet)\n  install(FILES ${CMAKE_BINARY_DIR}/WinDivert-${WINDIVERT_VERSION}/x64/WinDivert.dll DESTINATION bin COMPONENT lokinet)\nendif()\n\nset(BOOTSTRAP_FILE \"${PROJECT_SOURCE_DIR}/contrib/bootstrap/mainnet.signed\")\ninstall(FILES ${BOOTSTRAP_FILE} DESTINATION share COMPONENT lokinet RENAME bootstrap.signed)\n\nset(win_ico \"${PROJECT_BINARY_DIR}/lokinet.ico\")\nadd_custom_command(OUTPUT \"${win_ico}\"\n  COMMAND ${PROJECT_SOURCE_DIR}/contrib/make-ico.sh ${PROJECT_SOURCE_DIR}/contrib/lokinet.svg \"${win_ico}\"\n  DEPENDS ${PROJECT_SOURCE_DIR}/contrib/lokinet.svg ${PROJECT_SOURCE_DIR}/contrib/make-ico.sh)\nadd_custom_target(icon ALL DEPENDS \"${win_ico}\")\n\nset(CPACK_PACKAGE_INSTALL_DIRECTORY \"Lokinet\")\nset(CPACK_NSIS_MUI_ICON \"${PROJECT_BINARY_DIR}/lokinet.ico\")\nset(CPACK_NSIS_DEFINES \"RequestExecutionLevel admin\")\nset(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON)\n\nfunction(read_nsis_file filename outvar)\n  file(STRINGS \"${filename}\" _outvar)\n  list(TRANSFORM _outvar REPLACE \"\\\\\\\\\" \"\\\\\\\\\\\\\\\\\")\n  list(JOIN _outvar \"\\\\n\" out)\n  set(${outvar} ${out} PARENT_SCOPE)\nendfunction()\n\nread_nsis_file(\"${CMAKE_SOURCE_DIR}/win32-setup/extra_preinstall.nsis\" _extra_preinstall)\nread_nsis_file(\"${CMAKE_SOURCE_DIR}/win32-setup/extra_install.nsis\" _extra_install)\nread_nsis_file(\"${CMAKE_SOURCE_DIR}/win32-setup/extra_uninstall.nsis\" _extra_uninstall)\nread_nsis_file(\"${CMAKE_SOURCE_DIR}/win32-setup/extra_create_icons.nsis\" _extra_create_icons)\nread_nsis_file(\"${CMAKE_SOURCE_DIR}/win32-setup/extra_delete_icons.nsis\" _extra_delete_icons)\n\nset(CPACK_NSIS_EXTRA_PREINSTALL_COMMANDS \"${_extra_preinstall}\")\nset(CPACK_NSIS_EXTRA_INSTALL_COMMANDS \"${_extra_install}\")\nset(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS \"${_extra_uninstall}\")\nset(CPACK_NSIS_CREATE_ICONS_EXTRA \"${_extra_create_icons}\")\nset(CPACK_NSIS_DELETE_ICONS_EXTRA \"${_extra_delete_icons}\")\n\nset(CPACK_NSIS_COMPRESSOR \"/SOLID lzma\")\n"
  },
  {
    "path": "contrib/NetworkManager/dnsmasq/README.md",
    "content": "Place in `/etc/NetworkManager/dnsmasq.d/lokinet.conf`.\n\nTo make use of this, first install dnsmasq.\n\nThen enable NetworkManager dnsmasq backend by editing `/etc/NetworkManager/NetworkManager.conf` to something like:\n```\n[main]\ndns=dnsmasq\n```\nIf NetworkManager is currently running, restart it for changes to take effect:\n```\nsudo systemctl restart NetworkManager\n```\n"
  },
  {
    "path": "contrib/NetworkManager/dnsmasq/lokinet.conf",
    "content": "server=/loki/snode/127.3.2.1\n"
  },
  {
    "path": "contrib/android-configure.sh",
    "content": "#!/bin/bash\nset -e\n\ndefault_abis=\"armeabi-v7a arm64-v8a x86_64\"\nbuild_abis=${ABIS:-$default_abis}\n\ntest x$NDK = x && test -e /usr/lib/android-ndk && export NDK=/usr/lib/android-ndk\ntest x$NDK = x && exit 1\n\necho \"building abis: $build_abis\"\n\nroot=$(readlink -f \"$1\")\nshift\nbuild=$(readlink -f \"$1\")\nshift\nmkdir -p $build\ncd $build\n\nfor abi in $build_abis; do\n    mkdir -p build-$abi\n    cd build-$abi\n    cmake \\\n        -S \"$root\" -B . \\\n        -G 'Unix Makefiles' \\\n        -DANDROID=ON \\\n        -DANDROID_ABI=$abi \\\n        -DANDROID_ARM_MODE=arm \\\n        -DANDROID_PLATFORM=android-23 \\\n        -DANDROID_API=23 \\\n        -DANDROID_STL=c++_static \\\n        -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \\\n        -DBUILD_STATIC_DEPS=ON \\\n        -DLOKINET_PACKAGE=ON \\\n        -DBUILD_SHARED_LIBS=OFF \\\n        -DBUILD_TESTING=OFF \\\n        -DLOKINET_TESTS=OFF \\\n        -DLOKINET_BOOTSTRAP=OFF \\\n        -DLOKINET_NATIVE_BUILD=OFF \\\n        -DSTATIC_LINK=ON \\\n        -DWITH_SYSTEMD=OFF \\\n        -DFORCE_OXENMQ_SUBMODULE=ON \\\n        -DFORCE_OXENC_SUBMODULE=ON \\\n        -DFORCE_FMT_SUBMODULE=ON \\\n        -DFORCE_SPDLOG_SUBMODULE=ON \\\n        -DFORCE_NLOHMANN_SUBMODULE=ON \\\n        -DSUBMODULE_CHECK=OFF \\\n        -DWITH_LTO=OFF \\\n        -DCMAKE_BUILD_TYPE=Release \\\n        \"$@\"\n    cd -\ndone\nrm -f $build/Makefile\necho \"# generated makefile\" >> $build/Makefile\necho \"all: $build_abis\" >> $build/Makefile\nfor abi in $build_abis; do\n    echo -ne \"$abi:\\n\\t\" >> $build/Makefile\n    echo -ne '$(MAKE) -C ' >> $build/Makefile\n    echo \"build-$abi lokinet-android\" >> $build/Makefile\n    echo -ne \"\\tmkdir -p out/$abi && cp build-$abi/jni/liblokinet-android.so out/$abi/liblokinet-android.so\\n\\n\" >> $build/Makefile\n    echo -ne \"clean-$abi:\\n\\t\" >> $build/Makefile\n    echo -ne '$(MAKE) -C ' >> $build/Makefile\n    echo \"build-$abi clean\" >> $build/Makefile\ndone\n\necho -ne \"clean:\" >> $build/Makefile\nfor targ in $build_abis ; do echo -ne \" clean-$targ\" >> $build/Makefile ; done\necho \"\" >> $build/Makefile\n"
  },
  {
    "path": "contrib/android.sh",
    "content": "#!/bin/bash\nset -e\nset +x\n\nroot=\"$(readlink -f $(dirname $0)/../)\"\ncd \"$root\"\n./contrib/android-configure.sh . build-android \"$@\"\nmake -C build-android -j ${JOBS:-$(nproc)}\n"
  },
  {
    "path": "contrib/apparmor/usr.bin.lokinet",
    "content": "# Last Modified: Fri 05 Feb 2021 08:13:58 PM UTC\n#include <tunables/global>\n\nprofile lokinet /usr/bin/lokinet {\n  #include <abstractions/base>\n  #include <abstractions/nameservice>\n\n  capability net_admin,\n  capability net_bind_service,\n\n  network inet dgram,\n  network inet6 dgram,\n  network netlink raw,\n\n  /etc/loki/lokinet.ini r,\n  /dev/net/tun rw,\n  /usr/bin/lokinet mr,\n\n  owner /{var/,}lib/lokinet/ rw,\n  owner /{var/,}lib/lokinet/** rwk,\n  owner ${HOME}/.lokinet/ rw,\n  owner ${HOME}/.lokinet/** rwk,\n  owner @{PROC}/@{pid}/task/@{pid}/comm rw,\n  owner /tmp/lokinet.*/{**,} rw,\n\n  #include if exists <local/usr.bin.lokinet>\n}\n"
  },
  {
    "path": "contrib/apply-patches.sh",
    "content": "#!/usr/bin/env bash\nfor f in \"$@\" ; do\n    patch -p1 -i \"$f\"\ndone\n"
  },
  {
    "path": "contrib/bencode-dump.py",
    "content": "#!/usr/bin/python3\n\nimport sys\nimport pprint\n\nif len(sys.argv) == 1 or (len(sys.argv) == 2 and sys.argv[1] == '-'):\n    f = sys.stdin.buffer\nelif len(sys.argv) != 2 or sys.argv[1].startswith('-'):\n    print(\"Usage: {} FILE -- dumps a bencoded file\".format(sys.argv[0]), file=sys.stderr)\n    sys.exit(1)\nelse:\n    f = open(sys.argv[1], 'rb')\n\n\ninitial = f.peek(2)\nis_hex = False\nif initial.startswith(b'64') or initial.startswith(b'6c'):\n    print(\"Input looks like hex bencoded data; parsing as hex input\", file=sys.stderr)\n    is_hex = True\n\nclass HexPrinter():\n    def __init__(self, data):\n        self.data = data\n\n    def __repr__(self):\n        return \"hex({} bytes):'{}'\".format(len(self.data), self.data.hex())\n\n\ndef next_byte():\n    if is_hex:\n        pair = f.read(2)\n        assert pair is not None and len(pair) == 2\n        b = int(pair, 16).to_bytes(1, 'big')\n    else:\n        b = f.read(1)\n    assert b is not None and len(b) == 1\n    return b\n\n\ndef parse_int():\n    s = b''\n    x = next_byte()\n    while x in b\"0123456789-\":\n        s += x\n        x = next_byte()\n    assert x == b'e' and len(s) > 0, \"Invalid integer encoding\"\n    return int(s)\n\n\ndef parse_string(s):\n    x = next_byte()\n    while x in b\"0123456789\":\n        s += x\n        x = next_byte()\n    assert x == b':', \"Invalid string encoding\"\n    s = int(s)\n    if is_hex:\n        data = bytes.fromhex(f.read(2*s).decode('ascii'))\n    else:\n        data = f.read(s)\n    assert len(data) == s, \"Truncated string data\"\n    # If the string is ascii then convert to string:\n    if all(0x20 <= b <= 0x7e for b in data):\n        return data.decode()\n    # Otherwise display as hex:\n    return HexPrinter(data)\n\n\ndef parse_dict():\n    d = {}\n    last_key = None\n    while True:\n        t = next_byte()\n        if t == b'e':\n            return d\n        assert t in b\"0123456789\", \"Invalid dict: dict keys must be strings\"\n        key = parse_string(t)\n        raw_key = key.data if isinstance(key, HexPrinter) else key.encode()\n        if last_key is not None and raw_key <= last_key:\n            print(\"Warning: found out-of-order dict keys ({} after {})\".format(raw_key, last_key), file=sys.stderr)\n        last_key = raw_key\n        t = next_byte()\n        d[key] = parse_thing(t)\n\n\ndef parse_list():\n    l = []\n    while True:\n        t = next_byte()\n        if t == b'e':\n            return l\n        l.append(parse_thing(t))\n\n\ndef parse_thing(t):\n    if t == b'd':\n        return parse_dict()\n    if t == b'l':\n        return parse_list()\n    if t == b'i':\n        return parse_int()\n    if t in b\"0123456789\":\n        return parse_string(t)\n    assert False, \"Parsing error: encountered invalid type '{}'\".format(t)\n\n\npprint.PrettyPrinter(\n        indent=2\n        ).pprint(parse_thing(next_byte()))\n"
  },
  {
    "path": "contrib/bootstrap/make-bootstrap-list.sh",
    "content": "#!/usr/bin/env bash\necho -n 'l'\nfor arg in $@ ; do cat \"$arg\" ; done\necho -n 'e'\n"
  },
  {
    "path": "contrib/bootstrap/readme.txt",
    "content": "usage:\n\n./make-bootstrap-list.sh $(find $HOME/.lokinet/netdb | grep \\\\.signed$) > bootstrap.signed\n"
  },
  {
    "path": "contrib/ci/docker/readme.md",
    "content": "## drone-ci docker jizz\n\nTo rebuild all ci images and push them to the oxen registry server do:\n\n    $ docker login registry.oxen.rocks\n    $ ./rebuild-docker-images.py\n\nIf you aren't part of the Oxen team, you'll likely need to set up your own registry and change\nregistry.oxen.rocks to your own domain name in order to do anything useful with this.\n"
  },
  {
    "path": "contrib/ci/docker/rebuild-docker-images.py",
    "content": "#!/usr/bin/env python3\n\nimport subprocess\nimport tempfile\nimport optparse\nimport sys\nfrom concurrent.futures import ThreadPoolExecutor\nimport threading\n\nparser = optparse.OptionParser()\nparser.add_option(\"--no-cache\", action=\"store_true\",\n                  help=\"Run `docker build` with the `--no-cache` option to ignore existing images\")\nparser.add_option(\"--parallel\", \"-j\", type=\"int\", default=1,\n                  help=\"Run up to this many builds in parallel\")\nparser.add_option(\"--distro\", type=\"string\", default=\"\",\n                  help=\"Build only this distro; should be DISTRO-CODE or DISTRO-CODE/ARCH, \"\n                       \"e.g. debian-sid/amd64\")\n(options, args) = parser.parse_args()\n\nregistry_base = 'registry.oxen.rocks/lokinet-ci-'\n\ndistros = [*(('debian', x) for x in ('sid', 'stable', 'testing', 'bullseye', 'buster')),\n           *(('ubuntu', x) for x in ('rolling', 'lts', 'impish', 'hirsute', 'focal', 'bionic'))]\n\nif options.distro:\n    d = options.distro.split('-')\n    if len(d) != 2 or d[0] not in ('debian', 'ubuntu') or not d[1]:\n        print(\"Bad --distro value '{}'\".format(options.distro), file=sys.stderr)\n        sys.exit(1)\n    distros = [(d[0], d[1].split('/')[0])]\n\n\nmanifests = {}  # \"image:latest\": [\"image/amd64\", \"image/arm64v8\", ...]\nmanifestlock = threading.Lock()\n\n\ndef arches(distro):\n    if options.distro and '/' in options.distro:\n        arch = options.distro.split('/')\n        if arch not in ('amd64', 'i386', 'arm64v8', 'arm32v7'):\n            print(\"Bad --distro value '{}'\".format(options.distro), file=sys.stderr)\n            sys.exit(1)\n        return [arch]\n\n    a = ['amd64', 'arm64v8', 'arm32v7']\n    if distro[0] == 'debian' or distro == ('ubuntu', 'bionic'):\n        a.append('i386')  # i386 builds don't work on later ubuntu\n    return a\n\n\nhacks = {\n    registry_base + 'ubuntu-bionic-builder': \"\"\"g++-8 \\\n            && mkdir -p /usr/lib/x86_64-linux-gnu/pgm-5.2/include\"\"\",\n}\n\n\nfailure = False\n\nlineno = 0\nlinelock = threading.Lock()\n\n\ndef print_line(myline, value):\n    linelock.acquire()\n    global lineno\n    if sys.__stdout__.isatty():\n        jump = lineno - myline\n        print(\"\\033[{jump}A\\r\\033[K{value}\\033[{jump}B\\r\".format(jump=jump, value=value), end='')\n        sys.stdout.flush()\n    else:\n        print(value)\n    linelock.release()\n\n\ndef run_or_report(*args, myline):\n    try:\n        subprocess.run(\n            args, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding='utf8')\n    except subprocess.CalledProcessError as e:\n        with tempfile.NamedTemporaryFile(suffix=\".log\", delete=False) as log:\n            log.write(\"Error running {}: {}\\n\\nOutput:\\n\\n\".format(' '.join(args), e).encode())\n            log.write(e.output.encode())\n            global failure\n            failure = True\n            print_line(myline, \"\\033[31;1mError! See {} for details\".format(log.name))\n            raise e\n\n\ndef build_tag(tag_base, arch, contents):\n    if failure:\n        raise ChildProcessError()\n\n    linelock.acquire()\n    global lineno\n    myline = lineno\n    lineno += 1\n    print()\n    linelock.release()\n\n    with tempfile.NamedTemporaryFile() as dockerfile:\n        dockerfile.write(contents.encode())\n        dockerfile.flush()\n\n        tag = '{}/{}'.format(tag_base, arch)\n        print_line(myline, \"\\033[33;1mRebuilding     \\033[35;1m{}\\033[0m\".format(tag))\n        run_or_report('docker', 'build', '--pull', '-f', dockerfile.name, '-t', tag,\n                      *(('--no-cache',) if options.no_cache else ()), '.', myline=myline)\n        print_line(myline, \"\\033[33;1mPushing        \\033[35;1m{}\\033[0m\".format(tag))\n        run_or_report('docker', 'push', tag, myline=myline)\n        print_line(myline, \"\\033[32;1mFinished build \\033[35;1m{}\\033[0m\".format(tag))\n\n        latest = tag_base + ':latest'\n        global manifests\n        manifestlock.acquire()\n        if latest in manifests:\n            manifests[latest].append(tag)\n        else:\n            manifests[latest] = [tag]\n        manifestlock.release()\n\n\ndef base_distro_build(distro, arch):\n    tag = '{r}{distro[0]}-{distro[1]}-base'.format(r=registry_base, distro=distro)\n    codename = 'latest' if distro == ('ubuntu', 'lts') else distro[1]\n    build_tag(tag, arch, \"\"\"\nFROM {}/{}:{}\nRUN /bin/bash -c 'echo \"man-db man-db/auto-update boolean false\" | debconf-set-selections'\nRUN apt-get -o=Dpkg::Use-Pty=0 -q update \\\n    && apt-get -o=Dpkg::Use-Pty=0 -q dist-upgrade -y \\\n    && apt-get -o=Dpkg::Use-Pty=0 -q autoremove -y \\\n        {hacks}\n\"\"\".format(arch, distro[0], codename, hacks=hacks.get(tag, '')))\n\n\ndef distro_build(distro, arch):\n    prefix = '{r}{distro[0]}-{distro[1]}'.format(r=registry_base, distro=distro)\n    fmtargs = dict(arch=arch, distro=distro, prefix=prefix)\n\n    # (distro)-(codename)-base: Base image from upstream: we sync the repos, but do nothing else.\n    if (distro, arch) != (('debian', 'stable'), 'amd64'):  # debian-stable-base/amd64 already built\n        base_distro_build(distro, arch)\n\n    # (distro)-(codename)-builder: Deb builder image used for building debs; we add the basic tools\n    # we use to build debs, not including things that should come from the dependencies in the\n    # debian/control file.\n    build_tag(prefix + '-builder', arch, \"\"\"\nFROM {prefix}-base/{arch}\nRUN apt-get -o=Dpkg::Use-Pty=0 -q update \\\n    && apt-get -o=Dpkg::Use-Pty=0 -q dist-upgrade -y \\\n    && apt-get -o=Dpkg::Use-Pty=0 --no-install-recommends -q install -y \\\n        ccache \\\n        devscripts \\\n        equivs \\\n        g++ \\\n        git \\\n        git-buildpackage \\\n        openssh-client \\\n        {hacks}\n\"\"\".format(**fmtargs, hacks=hacks.get(prefix + '-builder', '')))\n\n    # (distro)-(codename): Basic image we use for most builds.  This takes the -builder and adds\n    # most dependencies found in our packages.\n    build_tag(prefix, arch, \"\"\"\nFROM {prefix}-builder/{arch}\nRUN apt-get -o=Dpkg::Use-Pty=0 -q update \\\n    && apt-get -o=Dpkg::Use-Pty=0 -q dist-upgrade -y \\\n    && apt-get -o=Dpkg::Use-Pty=0 --no-install-recommends -q install -y \\\n        automake \\\n        ccache \\\n        cmake \\\n        eatmydata \\\n        g++ \\\n        gdb \\\n        git \\\n        libboost-program-options-dev \\\n        libboost-serialization-dev \\\n        libboost-thread-dev \\\n        libcurl4-openssl-dev \\\n        libevent-dev \\\n        libgtest-dev \\\n        libhidapi-dev \\\n        libjemalloc-dev \\\n        libminiupnpc-dev \\\n        libreadline-dev \\\n        libsodium-dev \\\n        libsqlite3-dev \\\n        libssl-dev \\\n        libsystemd-dev \\\n        libtool \\\n        libunbound-dev \\\n        libunwind8-dev \\\n        libusb-1.0.0-dev \\\n        libuv1-dev \\\n        libzmq3-dev \\\n        lsb-release \\\n        make \\\n        nettle-dev \\\n        ninja-build \\\n        openssh-client \\\n        patch \\\n        pkg-config \\\n        pybind11-dev \\\n        python3-dev \\\n        python3-pip \\\n        python3-pybind11 \\\n        python3-pytest \\\n        python3-setuptools \\\n        qttools5-dev \\\n        {hacks}\n\"\"\".format(**fmtargs, hacks=hacks.get(prefix, '')))\n\n\n# Android and flutter builds on top of debian-stable-base and adds a ton of android crap; we\n# schedule this job as soon as the debian-sid-base/amd64 build finishes, because they easily take\n# the longest and are by far the biggest images.\ndef android_builds():\n    build_tag(registry_base + 'android', 'amd64', \"\"\"\nFROM {r}debian-stable-base\nRUN /bin/bash -c 'sed -i \"s/main/main contrib/g\" /etc/apt/sources.list'\nRUN apt-get -o=Dpkg::Use-Pty=0 -q update \\\n    && apt-get -o=Dpkg::Use-Pty=0 -q dist-upgrade -y \\\n    && apt-get -o=Dpkg::Use-Pty=0 -q install --no-install-recommends -y \\\n        android-sdk \\\n        automake \\\n        ccache \\\n        cmake \\\n        curl \\\n        git \\\n        google-android-ndk-installer \\\n        libtool \\\n        make \\\n        openssh-client \\\n        patch \\\n        pkg-config \\\n        wget \\\n        xz-utils \\\n        zip \\\n    && git clone https://github.com/Shadowstyler/android-sdk-licenses.git /tmp/android-sdk-licenses \\\n    && cp -a /tmp/android-sdk-licenses/*-license /usr/lib/android-sdk/licenses \\\n    && rm -rf /tmp/android-sdk-licenses\n\"\"\".format(r=registry_base))\n\n    build_tag(registry_base + 'flutter', 'amd64', \"\"\"\nFROM {r}android\nRUN cd /opt \\\n    && curl https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_2.2.2-stable.tar.xz \\\n        | tar xJv \\\n    && ln -s /opt/flutter/bin/flutter /usr/local/bin/ \\\n    && flutter precache\n\"\"\".format(r=registry_base))\n\n\n# lint is a tiny build (on top of debian-stable-base) with just formatting checking tools\ndef lint_build():\n    build_tag(registry_base + 'lint', 'amd64', \"\"\"\nFROM {r}debian-stable-base\nRUN apt-get -o=Dpkg::Use-Pty=0 -q install --no-install-recommends -y \\\n    clang-format-11 \\\n    eatmydata \\\n    git \\\n    jsonnet\n\"\"\".format(r=registry_base))\n\n\ndef nodejs_build():\n    build_tag(registry_base + 'nodejs', 'amd64', \"\"\"\nFROM node:14.16.1\nRUN /bin/bash -c 'echo \"man-db man-db/auto-update boolean false\" | debconf-set-selections'\nRUN apt-get -o=Dpkg::Use-Pty=0 -q update \\\n    && apt-get -o=Dpkg::Use-Pty=0 -q dist-upgrade -y \\\n    && apt-get -o=Dpkg::Use-Pty=0 -q install --no-install-recommends -y \\\n        ccache \\\n        cmake \\\n        eatmydata \\\n        g++ \\\n        gdb \\\n        git \\\n        make \\\n        ninja-build \\\n        openssh-client \\\n        patch \\\n        pkg-config \\\n        wine\n\"\"\")\n\n\n# Start debian-stable-base/amd64 on its own, because other builds depend on it and we want to get\n# those (especially android/flutter) fired off as soon as possible (because it's slow and huge).\nif ('debian', 'stable') in distros:\n    base_distro_build(['debian', 'stable'], 'amd64')\n\nexecutor = ThreadPoolExecutor(max_workers=max(options.parallel, 1))\n\nif options.distro:\n    jobs = []\nelse:\n    jobs = [executor.submit(b) for b in (android_builds, lint_build, nodejs_build)]\n\nfor d in distros:\n    for a in arches(d):\n        jobs.append(executor.submit(distro_build, d, a))\nwhile len(jobs):\n    j = jobs.pop(0)\n    try:\n        j.result()\n    except (ChildProcessError, subprocess.CalledProcessError):\n        for k in jobs:\n            k.cancel()\n\n\nif failure:\n    print(\"Error(s) occured, aborting!\", file=sys.stderr)\n    sys.exit(1)\n\n\nprint(\"\\n\\n\\033[32;1mAll builds finished successfully; pushing manifests...\\033[0m\\n\")\n\n\ndef push_manifest(latest, tags):\n    if failure:\n        raise ChildProcessError()\n\n    linelock.acquire()\n    global lineno\n    myline = lineno\n    lineno += 1\n    print()\n    linelock.release()\n\n    subprocess.run(['docker', 'manifest', 'rm', latest], stderr=subprocess.DEVNULL, check=False)\n    print_line(myline, \"\\033[33;1mCreating manifest \\033[35;1m{}\\033[0m\".format(latest))\n    run_or_report('docker', 'manifest', 'create', latest, *tags, myline=myline)\n    print_line(myline, \"\\033[33;1mPushing manifest  \\033[35;1m{}\\033[0m\".format(latest))\n    run_or_report('docker', 'manifest', 'push', latest, myline=myline)\n    print_line(myline, \"\\033[32;1mFinished manifest \\033[35;1m{}\\033[0m\".format(latest))\n\n\nfor latest, tags in manifests.items():\n    jobs.append(executor.submit(push_manifest, latest, tags))\n\nwhile len(jobs):\n    j = jobs.pop(0)\n    try:\n        j.result()\n    except (ChildProcessError, subprocess.CalledProcessError):\n        for k in jobs:\n            k.cancel()\n\n\nprint(\"\\n\\n\\033[32;1mAll done!\\n\")\n"
  },
  {
    "path": "contrib/ci/drone-check-static-libs.sh",
    "content": "#!/usr/bin/env bash\n\n# Script used with Drone CI to check that a statically build lokinet only links against the expected\n# base system libraries.  Expects to be run with pwd of the project directory with a build in\n# `build` or $1 (if given).\n\nset -o errexit\n\nbuild=${1:-build}\n\nbad=\nif [ \"$DRONE_STAGE_OS\" == \"darwin\" ]; then\n    if otool -L ${build}/llarp/apple/org.lokinet.network-extension.systemextension/Contents/MacOS/org.lokinet.network-extension | \\\n        grep -Ev '^llarp/apple:|^\\t(/usr/lib/lib(System\\.|c\\+\\+|objc))|/System/Library/Frameworks/(CoreFoundation|NetworkExtension|Foundation|Network)\\.framework'; then\n        bad=1\n    fi\nelif [ \"$DRONE_STAGE_OS\" == \"linux\" ]; then\n    if ldd ${build}/daemon/lokinet | grep -Ev '(linux-vdso|ld-linux-(x86-64|armhf|aarch64)|lib(pthread|dl|rt|stdc\\+\\+|gcc_s|c|m))\\.so'; then\n        bad=1\n    fi\nelse\n    echo -e \"\\n\\n\\n\\n\\e[31;1mDon't know how to check linked libs on $DRONE_STAGE_OS\\e[0m\\n\\n\\n\"\n    exit 1\nfi\n\nif [ -n \"$bad\" ]; then\n    echo -e \"\\n\\n\\n\\n\\e[31;1mlokinet links to unexpected libraries\\e[0m\\n\\n\\n\"\n    exit 1\nfi\n\necho -e \"\\n\\n\\n\\n\\e[32;1mNo unexpected linked libraries found\\e[0m\\n\\n\\n\"\n"
  },
  {
    "path": "contrib/ci/drone-debs-upload.sh",
    "content": "#!/bin/bash\n\n# Script used with Drone CI to upload debs from the deb building pipelines (because specifying all\n# this in .drone.jsonnet is too painful).  This is expected to run from the base project dir after\n# having build with debuild (which will leave the debs in ..).\n\nset -o errexit\n\ndistro=\"$1\"\n\nif [ -z \"$distro\" ]; then\n    echo \"Bad usage: need distro name as first argument\"\n    exit 1\nfi\n\nif [ -z \"$SSH_KEY\" ]; then\n    echo -e \"\\n\\n\\n\\e[31;1mUnable to upload debs: SSH_KEY not set\\e[0m\"\n    # Just warn but don't fail, so that this doesn't trigger a build failure for untrusted builds\n    exit 0\nfi\n\necho \"$SSH_KEY\" >~/ssh_key\n\nset -o xtrace  # Don't start tracing until *after* we write the ssh key\n\nchmod 600 ~/ssh_key\n\nupload_to=\"oxen.rocks/debs/${DRONE_REPO// /_}@${DRONE_BRANCH// /_}/$(date --date=@$DRONE_BUILD_CREATED +%Y%m%dT%H%M%SZ)-${DRONE_COMMIT:0:9}/$distro/$DRONE_STAGE_ARCH\"\n\n# sftp doesn't have any equivalent to mkdir -p, so we have to split the above up into a chain of\n# -mkdir a/, -mkdir a/b/, -mkdir a/b/c/, ... commands.  The leading `-` allows the command to fail\n# without error.\nupload_dirs=(${upload_to//\\// })\nmkdirs=\ndir_tmp=\"\"\nfor p in \"${upload_dirs[@]}\"; do\n    dir_tmp=\"$dir_tmp$p/\"\n    mkdirs=\"$mkdirs\n-mkdir $dir_tmp\"\ndone\n\nsftp -i ~/ssh_key -b - -o StrictHostKeyChecking=off drone@oxen.rocks <<SFTP\n$mkdirs\nput ../*.*deb $upload_to\nSFTP\n\nset +o xtrace\n\necho -e \"\\n\\n\\n\\n\\e[32;1mUploaded debs to https://${upload_to}/\\e[0m\\n\\n\\n\"\n\n"
  },
  {
    "path": "contrib/ci/drone-format-verify.sh",
    "content": "#!/usr/bin/env bash\ntest \"x$IGNORE\" != \"x\" && exit 0\n\n. $(dirname $0)/../format-version.sh\n\nrepo=$(readlink -e $(dirname $0)/../../)\n$CLANG_FORMAT -i $(find $repo/jni $repo/daemon $repo/llarp $repo/include $repo/pybind | grep -E '\\.[hc](pp)?$')\njsonnetfmt -i $repo/.drone.jsonnet\ngit --no-pager diff --exit-code --color || (echo -ne '\\n\\n\\e[31;1mLint check failed; please run ./contrib/format.sh\\e[0m\\n\\n' ; exit 1)\n"
  },
  {
    "path": "contrib/ci/drone-gdb.sh",
    "content": "#!/usr/bin/env bash\nrm -f crash.out.txt exit.out.txt\ngdb -q -x $(readlink -e $(dirname $0))/gdb-filter.py --args $@\ntest -e crash.out.txt && cat crash.out.txt\nexit $(cat exit.out.txt)\n"
  },
  {
    "path": "contrib/ci/drone-run-router-hive.sh",
    "content": "#!/usr/bin/env bash\nexport PYTHONPATH=pybind\nrm -f crash.out.txt exit.out.txt\ngdb -q -x $(readlink -e $(dirname $0))/gdb-filter.py --args /usr/bin/python3 -m pytest ../test/\ntest -e crash.out.txt && cat crash.out.txt\nexit $(cat exit.out.txt)\n"
  },
  {
    "path": "contrib/ci/drone-static-upload.sh",
    "content": "#!/usr/bin/env bash\n\n# Script used with Drone CI to upload build artifacts (because specifying all this in\n# .drone.jsonnet is too painful).\n\n\n\nset -o errexit\n\nif [ -z \"$SSH_KEY\" ]; then\n    echo -e \"\\n\\n\\n\\e[31;1mUnable to upload artifact: SSH_KEY not set\\e[0m\"\n    # Just warn but don't fail, so that this doesn't trigger a build failure for untrusted builds\n    exit 0\nfi\n\necho \"$SSH_KEY\" >ssh_key\n\nset -o xtrace  # Don't start tracing until *after* we write the ssh key\n\nchmod 600 ssh_key\n\nos=\"$UPLOAD_OS\"\nif [ -z \"$os\" ]; then\n    if [ \"$DRONE_STAGE_OS\" == \"darwin\" ]; then\n        os=\"macos-$DRONE_STAGE_ARCH\"\n    elif [ -n \"$WINDOWS_BUILD_NAME\" ]; then\n        os=\"windows-$WINDOWS_BUILD_NAME\"\n    else\n        os=\"$DRONE_STAGE_OS-$DRONE_STAGE_ARCH\"\n    fi\nfi\n\nif [ -n \"$DRONE_TAG\" ]; then\n    # For a tag build use something like `lokinet-linux-amd64-v1.2.3`\n    base=\"lokinet-$os-$DRONE_TAG\"\nelse\n    # Otherwise build a length name from the datetime and commit hash, such as:\n    # lokinet-linux-amd64-20200522T212342Z-04d7dcc54\n    base=\"lokinet-$os-$(date --date=@$DRONE_BUILD_CREATED +%Y%m%dT%H%M%SZ)-${DRONE_COMMIT:0:9}\"\nfi\n\nmkdir -v \"$base\"\nif [ -e build/win32 ]; then\n    # save debug symbols\n    cp -av build/win32/daemon/debug-symbols.tar.xz \"$base-debug-symbols.tar.xz\"\n    # save installer\n    cp -av build/win32/*.exe \"$base\"\n    # zipit up yo\n    archive=\"$base.zip\"\n    zip -r \"$archive\" \"$base\"\nelif [ -e lokinet.apk ] ; then\n    # android af ngl\n    archive=\"$base.apk\"\n    cp -av lokinet.apk \"$archive\"\nelif [ -e build-docs ]; then\n    archive=\"$base.tar.xz\"\n    cp -av build-docs/docs/mkdocs.yml build-docs/docs/markdown \"$base\"\n    tar cJvf \"$archive\" \"$base\"\nelif [ -e build-mac ]; then\n    archive=\"$base.tar.xz\"\n    mv build-mac/Lokinet*/ \"$base\"\n    tar cJvf \"$archive\" \"$base\"\nelse\n    cp -av build/daemon/lokinet{,-cntrl} \"$base\"\n    cp -av contrib/bootstrap/mainnet.signed \"$base/bootstrap.signed\"\n    # tar dat shiz up yo\n    archive=\"$base.tar.xz\"\n    tar cJvf \"$archive\" \"$base\"\nfi\n\nupload_to=\"oxen.rocks/${DRONE_REPO// /_}/${DRONE_BRANCH// /_}\"\n\n# sftp doesn't have any equivalent to mkdir -p, so we have to split the above up into a chain of\n# -mkdir a/, -mkdir a/b/, -mkdir a/b/c/, ... commands.  The leading `-` allows the command to fail\n# without error.\nupload_dirs=(${upload_to//\\// })\nput_debug=\nmkdirs=\ndir_tmp=\"\"\nfor p in \"${upload_dirs[@]}\"; do\n    dir_tmp=\"$dir_tmp$p/\"\n    mkdirs=\"$mkdirs\n-mkdir $dir_tmp\"\ndone\nif [ -e \"$base-debug-symbols.tar.xz\" ] ; then\n    put_debug=\"put $base-debug-symbols.tar.xz $upload_to\"\nfi\nsftp -i ssh_key -b - -o StrictHostKeyChecking=off drone@oxen.rocks <<SFTP\n$mkdirs\nput $archive $upload_to\n$put_debug\nSFTP\n\nset +o xtrace\n\necho -e \"\\n\\n\\n\\n\\e[32;1mUploaded to https://${upload_to}/${archive}\\e[0m\\n\\n\\n\"\n"
  },
  {
    "path": "contrib/ci/gdb-filter.py",
    "content": "def exit_handler (event):\n    \"\"\"\n    write exit code of the program running in gdb to a file called exit.out.txt\n    \"\"\"\n    code = 1\n    if hasattr(event, \"exit_code\"):\n        code = event.exit_code\n    with open(\"exit.out.txt\", 'w') as f:\n        f.write(\"{}\".format(code))\n\ndef gdb_execmany(*cmds):\n    \"\"\"\n    run multiple gdb commands\n    \"\"\"\n    for cmd in cmds:\n        gdb.execute(cmd)\n\ndef crash_handler (event):\n    \"\"\"\n    handle a crash from the program running in gdb\n    \"\"\"\n    if isinstance(event, gdb.SignalEvent):\n        log_file_name = \"crash.out.txt\"\n        # poop out log file for stack trace of all threads\n        gdb_execmany(\"set logging file {}\".format(log_file_name), \"set logging on\", \"set logging redirect on\", \"thread apply all bt full\")\n        # quit gdb\n        gdb.execute(\"q\")\n\n# set up event handlers to catch shit\ngdb.events.stop.connect(crash_handler)\ngdb.events.exited.connect(exit_handler)\n\n# run settings setup\ngdb_execmany(\"set confirm off\", \"set pagination off\", \"set print thread-events off\")\n# run program and exit\ngdb_execmany(\"r\", \"q\")\n"
  },
  {
    "path": "contrib/cross/android.toolchain.cmake",
    "content": "\nset(CMAKE_SYSTEM_NAME Android)\nset(CMAKE_SYSTEM_VERSION ${ANDROID_API}) # API level\nset(CMAKE_ANDROID_ARCH_ABI ${ANDROID_ARCH_ABI})\nset(CMAKE_ANDROID_NDK ${ANDROID_NDK})\nset(CMAKE_ANDROID_STL_TYPE gnustl_static)"
  },
  {
    "path": "contrib/cross/cross.toolchain.cmake",
    "content": "set(CMAKE_SYSTEM_NAME ${CROSS_PLATFORM})\nset(TOOLCHAIN_PREFIX ${CROSS_PREFIX})\nset(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX})\n\nset(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\nset(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\nset(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\n\nset(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc${TOOLCHAIN_SUFFIX})\nset(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++${TOOLCHAIN_SUFFIX})\nset(ARCH_TRIPLET ${TOOLCHAIN_PREFIX})\n"
  },
  {
    "path": "contrib/cross/mingw32.cmake",
    "content": "set(CMAKE_SYSTEM_NAME Windows)\nset(TOOLCHAIN_PREFIX i686-w64-mingw32)\nset(WOW64_CROSS_COMPILE ON)\nset(CROSS_TARGET i686-w64-mingw32)\n\nset(TOOLCHAIN_PATHS\n  /usr/${TOOLCHAIN_PREFIX}\n  /usr/local/opt/mingw-w64/toolchain-i686\n  /usr/local/opt/mingw-w64/toolchain-i686/i686-w64-mingw32\n  /opt/mingw32\n  /home/$ENV{USER}/mingw32\n  /home/$ENV{USER}/mingw32/${TOOLCHAIN_PREFIX}\n)\n\ninclude(\"${CMAKE_CURRENT_LIST_DIR}/mingw_core.cmake\")\n"
  },
  {
    "path": "contrib/cross/mingw64.cmake",
    "content": "set(CMAKE_SYSTEM_NAME Windows)\nset(TOOLCHAIN_PREFIX x86_64-w64-mingw32)\nset(WIN64_CROSS_COMPILE ON)\nset(CROSS_TARGET x86_64-w64-mingw32)\n\nset(TOOLCHAIN_PATHS\n /usr/${TOOLCHAIN_PREFIX}\n /usr/local/opt/mingw-w64/toolchain-x86_64\n /usr/local/opt/mingw-w64/toolchain-x86_64/x86_64-w64-mingw32\n /opt/mingw64\n /home/$ENV{USER}/mingw32\n /home/$ENV{USER}/mingw64\n /home/$ENV{USER}/mingw64/${TOOLCHAIN_PREFIX}\n /home/$ENV{USER}/mingw32/${TOOLCHAIN_PREFIX})\n\ninclude(\"${CMAKE_CURRENT_LIST_DIR}/mingw_core.cmake\")\n"
  },
  {
    "path": "contrib/cross/mingw_core.cmake",
    "content": "set(CMAKE_SYSTEM_VERSION 6.0)\n\n# the minimum windows version, set to 6 rn because supporting older windows is hell\nset(_winver 0x0600)\nadd_definitions(-D_WIN32_WINNT=${_winver})\n\n# target environment on the build host system\n# second one is for non-root installs\nset(CMAKE_FIND_ROOT_PATH ${TOOLCHAIN_PATHS})\n\nadd_definitions(\"-DWINNT_CROSS_COMPILE\")\n\n# modify default behavior of FIND_XXX() commands\nset(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)\nset(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)\nset(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)\nset(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)\n\n# cross compilers to use\nif($ENV{COMPILER} MATCHES \"clang\")\n    set(USING_CLANG ON)\n    set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-clang)\n    set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-clang++)\nelse()\n    set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc${TOOLCHAIN_SUFFIX})\n    set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++${TOOLCHAIN_SUFFIX})\nendif()\n\nset(CMAKE_RC_COMPILER ${TOOLCHAIN_PREFIX}-windres)\nset(ARCH_TRIPLET ${CROSS_TARGET})\n"
  },
  {
    "path": "contrib/cross.sh",
    "content": "#!/bin/bash\n#\n# helper script for me for when i cross compile\n# t. jeff\n#\nset -e\n\ndie() {\n    echo $@\n    exit 1\n}\n\nplatform=${PLATFORM:-Linux}\nroot=\"$(readlink -e $(dirname $0)/../)\"\ncd $root\nmkdir -p build-cross\n\ntargets=()\ncmake_extra=()\n\nwhile [ \"$#\" -gt 0 ]; do\n    if [ \"$1\" = \"--\" ]; then\n        shift\n        cmake_extra=(\"$@\")\n        break\n    fi\n    targets+=(\"$1\")\n    shift\ndone\ntest ${#targets[@]} = 0 && die no targets provided\n\narchs=\"${targets[@]}\"\necho \"all: $archs\" > build-cross/Makefile\nfor arch in $archs ; do\n    mkdir -p $root/build-cross/build-$arch\n    cd $root/build-cross/build-$arch\n    cmake \\\n        -G 'Unix Makefiles' \\\n        -DCROSS_PLATFORM=$platform \\\n        -DCROSS_PREFIX=$arch \\\n        -DCMAKE_EXE_LINKER_FLAGS=-fstack-protector \\\n        -DCMAKE_CXX_FLAGS=-fdiagnostics-color=always \\\n        -DCMAKE_TOOLCHAIN_FILE=$root/contrib/cross/cross.toolchain.cmake \\\n        -DBUILD_STATIC_DEPS=ON \\\n        -DSTATIC_LINK=ON \\\n        -DBUILD_SHARED_LIBS=OFF \\\n        -DBUILD_TESTING=OFF \\\n        -DBUILD_LIBLOKINET=OFF \\\n        -DLOKINET_TESTS=OFF \\\n        -DLOKINET_NATIVE_BUILD=OFF \\\n        -DSTATIC_LINK=ON \\\n        -DWITH_SYSTEMD=OFF \\\n        -DFORCE_OXENMQ_SUBMODULE=ON \\\n        -DSUBMODULE_CHECK=OFF \\\n        -DWITH_LTO=OFF \\\n        -DLOKINET_BOOTSTRAP=OFF \\\n        -DCMAKE_BUILD_TYPE=RelWithDeb \\\n        \"${cmake_extra[@]}\" \\\n        $root\n    cd $root/build-cross\n    echo -ne \"$arch:\\n\\t\\$(MAKE) -C  build-$arch\\n\" >> $root/build-cross/Makefile\n\ndone\ncd $root\nmake -j${JOBS:-$(nproc)} -C build-cross\n"
  },
  {
    "path": "contrib/format-version.sh",
    "content": "\nCLANG_FORMAT_DESIRED_VERSION=19\n\nCLANG_FORMAT=$(command -v clang-format-$CLANG_FORMAT_DESIRED_VERSION 2>/dev/null)\nif [ $? -ne 0 ]; then\n    CLANG_FORMAT=$(command -v clang-format-mp-$CLANG_FORMAT_DESIRED_VERSION 2>/dev/null)\nfi\nif [ $? -ne 0 ]; then\n    CLANG_FORMAT=$(command -v clang-format 2>/dev/null)\n    if [ $? -ne 0 ]; then\n        echo \"Please install clang-format version $CLANG_FORMAT_DESIRED_VERSION and re-run this script.\"\n        exit 1\n    fi\n    version=$(clang-format --version)\n    if [[ ! $version == *\"clang-format version $CLANG_FORMAT_DESIRED_VERSION\"* ]]; then\n        echo \"Please install clang-format version $CLANG_FORMAT_DESIRED_VERSION and re-run this script.\"\n        exit 1\n    fi\nfi\n"
  },
  {
    "path": "contrib/format.sh",
    "content": "#!/usr/bin/env bash\n\n# set -x\nset -e\n\n. $(dirname $0)/format-version.sh\n\ncd \"$(dirname $0)/../\"\n\nsources=($(find jni daemon llarp include pybind | grep -E '\\.([hc](pp)?|m(m)?)$' | grep -v '#'))\n\nincl_pat='^(#include +)\"(llarp|libntrup|oxen|oxenc|oxenmq|quic|CLI|cpr|nlohmann|ghc|fmt|spdlog|uvw?)([/.][^\"]*)\"'\n\nif [ \"$1\" = \"verify\" ] ; then\n    if [ $($CLANG_FORMAT --output-replacements-xml \"${sources[@]}\" | grep '</replacement>' | wc -l) -ne 0 ] ; then\n        exit 2\n    fi\n\n    if grep --color -E \"$incl_pat\" \"${sources[@]}\"; then\n        exit 5\n    fi\nelse\n    $CLANG_FORMAT -i \"${sources[@]}\" &> /dev/null\n\n    perl -pi -e \"s{$incl_pat}\"'{$1<$2$3>}' \"${sources[@]}\" &> /dev/null\nfi\n\n# Some includes just shouldn't exist anywhere, but need to be fixed manually:\nif grep --color -E '^#include ([<\"]external/|<bits/|<.*/impl)' \"${sources[@]}\"; then\n    echo \"Format failed: bad includes detected that can't be auto-corrected\"\n    exit 5\nfi\n\nswift_format=$(command -v swiftformat 2>/dev/null)\nif [ $? -eq 0 ]; then\n    if [ \"$1\" = \"verify\" ] ; then\n        for f in $(find daemon | grep -E '\\.swift$' | grep -v '#') ; do\n            if [ $($swift_format --quiet --dryrun < \"$f\" | diff \"$f\" - | wc -l) -ne 0 ] ; then\n                exit 3\n            fi\n        done\n    else\n        $swift_format --quiet $(find daemon | grep -E '\\.swift$' | grep -v '#')\n    fi\n\nfi\n\njsonnet_format=$(command -v jsonnetfmt 2>/dev/null)\nif [ $? -eq 0 ]; then\n    if [ \"$1\" = \"verify\" ]; then\n        if ! $jsonnet_format --test .drone.jsonnet; then\n            exit 4\n        fi\n    else\n        $jsonnet_format --in-place .drone.jsonnet\n    fi\nfi\n"
  },
  {
    "path": "contrib/git-hook-pre-push.sh",
    "content": "#!/bin/bash\n#\n# pre-push hook for git\n# this script is probably overkill for most contributors\n#\n# \"i use this to prevent foot cannons caused by commiting broken code\"\n#\n# ~ jeff (lokinet author and crazy person)\n#\n#\n# to use this as a git hook do this in the root of the repo:\n#\n# cp contrib/git-hook-pre-push.sh .git/hooks/pre-push\n#\n\n\nset -e\n\ncd \"$(dirname $0)/../..\"\necho \"check format...\"\n./contrib/format.sh verify\necho \"format is gucci af fam\"\n\necho \"remove old test build directory...\"\nrm -rf build-git-hook\nmkdir build-git-hook\necho \"configuring test build jizz...\"\ncmake -S . -B build-git-hook -DWITH_LTO=OFF -DLOKINET_HIVE=ON -G Ninja\necho \"ensure this shit compiles...\"\nninja -C build-git-hook all\necho \"ensure unit tests aren't fucked...\"\nninja -C build-git-hook check\n\necho \"we gud UmU\"\necho \"\"\n"
  },
  {
    "path": "contrib/hex-to-base32z.py",
    "content": "#!/usr/bin/python3\n\nimport sys\n\nbase32z_dict = 'ybndrfg8ejkmcpqxot1uwisza345h769'\nbase32z_map = {base32z_dict[i]: i for i in range(len(base32z_dict))}\n\ndef lokinet_snode_addr(pk_hex):\n    \"\"\"Returns the lokinet snode address from a hex ed25519 pubkey\"\"\"\n    assert(len(pk_hex) == 64)\n    bits = 0\n    val = 0\n    result = ''\n    for x in pk_hex:\n        bits += 4\n        val = (val << 4) + int(x, 16)\n        if bits >= 5:\n            bits -= 5\n            v = val >> bits\n            val &= (1 << bits) - 1\n            result += base32z_dict[v]\n    result += base32z_dict[val << (5 - bits)]\n    return result + \".snode\"\n\n\ndef hex_from_snode(b32z):\n    \"\"\"undoes what the above does; b32z should have '.snode' already stripped off\"\"\"\n    assert(len(b32z) == 52)\n    val = 0\n    bits = 0\n    for x in b32z:\n        val = (val << 5) | base32z_map[x]  # Arbitrary precision integers FTW\n\n    # `val` is now a 260 bit value (52 * 5 bits per char); but we only use the first bit of the last\n    # value (which is why lokinet addresses always end with y or o)\n    assert(b32z[-1] in 'yo')\n    val >>= 4\n\n    return \"{:64x}\".format(val)\n\n\nreverse = False\nif len(sys.argv) >= 2 and sys.argv[1] == '-r':\n    reverse = True\n    del sys.argv[1]\n\nif len(sys.argv) < 2 or (\n        any(len(x) not in (52, 58) for x in sys.argv[1:])\n        if reverse else\n        any(len(x) != 64 for x in sys.argv[1:])\n        ):\n    print(\"Usage: {} PUBKEY [PUBKEY ...] -- converts ed25519 pubkeys to .snode addresses\".format(sys.argv[0]))\n    print(\"Usage: {} -r SNODE [SNODE ...] -- converts snode addresses to ed25519 pubkeys\".format(sys.argv[0]))\n    sys.exit(1)\n\nif reverse:\n    for key in sys.argv[1:]:\n        print(\"{}.snode -> {}\".format(key[0:52], hex_from_snode(key[0:52])))\nelse:\n    for key in sys.argv[1:]:\n        print(\"{} -> {}\".format(key, lokinet_snode_addr(key)))\n"
  },
  {
    "path": "contrib/keygen.py",
    "content": "#!/usr/bin/env python3\n#\n# .loki secret key generator script\n# makes keyfile contents\n#\n# usage: python3 keygen.py out.private\n#        python3 keygen.py > /some/where/over/the/rainbow\n#\nfrom nacl.bindings import crypto_sign_keypair\nimport sys\n\nout = sys.stdout\n\nclose_out = lambda : None\nargs = sys.argv[1:]\n\nif args and args[0] != '-':\n  out = open(args[0], 'wb')\n  close_out = out.close\n\npk, sk = crypto_sign_keypair()\nout.write(b'64:')\nout.write(sk)\nout.flush()\nclose_out()\n\n"
  },
  {
    "path": "contrib/liblokinet/CMakeLists.txt",
    "content": "\ncmake_minimum_required(VERSION 3.10)\n\nproject(udptest LANGUAGES CXX)\n\nset(CMAKE_CXX_STANDARD 17)\nadd_executable(udptest udptest.cpp)\ninclude_directories(../../include)\ntarget_link_libraries(udptest PUBLIC lokinet)\n\n"
  },
  {
    "path": "contrib/liblokinet/readme.md",
    "content": "# liblokinet examples\n\nbuilding:\n\n    $ mkdir -p build\n    $ cd build\n    $ cp /path/to/liblokinet.so .\n    $ cmake .. -DCMAKE_EXE_LINKER_FLAGS='-L.'\n    $ make\n\nrunning:\n\n    $ ./udptest /path/to/bootstrap.signed\n"
  },
  {
    "path": "contrib/liblokinet/udptest.cpp",
    "content": "#include <lokinet.h>\n\n#include <signal.h>\n\n#include <memory>\n#include <stdexcept>\n#include <iostream>\n#include <fstream>\n#include <sstream>\n#include <string>\n#include <vector>\n#include <cstring>\n#include <algorithm>\n\nbool _run{true};\n\nusing Lokinet_ptr = std::shared_ptr<lokinet_context>;\n\n[[nodiscard]] auto\nMakeLokinet(const std::vector<char>& bootstrap)\n{\n  auto ctx = std::shared_ptr<lokinet_context>(lokinet_context_new(), lokinet_context_free);\n  if (auto err = lokinet_add_bootstrap_rc(bootstrap.data(), bootstrap.size(), ctx.get()))\n    throw std::runtime_error{strerror(err)};\n  if (lokinet_context_start(ctx.get()))\n    throw std::runtime_error{\"could not start context\"};\n  return ctx;\n}\n\nvoid\nWaitForReady(const Lokinet_ptr& ctx)\n{\n  while (_run and lokinet_wait_for_ready(1000, ctx.get()))\n  {\n    std::cout << \"waiting for context...\" << std::endl;\n  }\n}\n\nclass Flow\n{\n  lokinet_udp_flowinfo const _info;\n  lokinet_context* const _ctx;\n\n public:\n  explicit Flow(const lokinet_udp_flowinfo* info, lokinet_context* ctx) : _info{*info}, _ctx{ctx}\n  {}\n\n  lokinet_context*\n  Context() const\n  {\n    return _ctx;\n  }\n\n  std::string\n  String() const\n  {\n    std::stringstream ss;\n    ss << std::string{_info.remote_host} << \":\" << std::to_string(_info.remote_port)\n       << \" on socket \" << _info.socket_id;\n    return ss.str();\n  }\n};\n\nstruct ConnectJob\n{\n  lokinet_udp_flowinfo remote;\n  lokinet_context* ctx;\n};\n\nvoid\nCreateOutboundFlow(void* user, void** flowdata, int* timeout)\n{\n  auto* job = static_cast<ConnectJob*>(user);\n  Flow* flow = new Flow{&job->remote, job->ctx};\n  *flowdata = flow;\n  *timeout = 30;\n  std::cout << \"made outbound flow: \" << flow->String() << std::endl;\n  ;\n}\n\nint\nProcessNewInboundFlow(void* user, const lokinet_udp_flowinfo* remote, void** flowdata, int* timeout)\n{\n  auto* ctx = static_cast<lokinet_context*>(user);\n  Flow* flow = new Flow{remote, ctx};\n  std::cout << \"new udp flow: \" << flow->String() << std::endl;\n  *flowdata = flow;\n  *timeout = 30;\n\n  return 0;\n}\n\nvoid\nDeleteFlow(const lokinet_udp_flowinfo* remote, void* flowdata)\n{\n  auto* flow = static_cast<Flow*>(flowdata);\n  std::cout << \"udp flow from \" << flow->String() << \" timed out\" << std::endl;\n  delete flow;\n}\n\nvoid\nHandleUDPPacket(const lokinet_udp_flowinfo* remote, const char* pkt, size_t len, void* flowdata)\n{\n  auto* flow = static_cast<Flow*>(flowdata);\n  std::cout << \"we got \" << len << \" bytes of udp from \" << flow->String() << std::endl;\n}\n\nvoid\nBounceUDPPacket(const lokinet_udp_flowinfo* remote, const char* pkt, size_t len, void* flowdata)\n{\n  auto* flow = static_cast<Flow*>(flowdata);\n  std::cout << \"bounce \" << len << \" bytes of udp from \" << flow->String() << std::endl;\n  if (auto err = lokinet_udp_flow_send(remote, pkt, len, flow->Context()))\n  {\n    std::cout << \"bounce failed: \" << strerror(err) << std::endl;\n  }\n}\n\nLokinet_ptr sender, recip;\n\nvoid\nsignal_handler(int)\n{\n  _run = false;\n}\n\nint\nmain(int argc, char* argv[])\n{\n  if (argc == 1)\n  {\n    std::cout << \"usage: \" << argv[0] << \" bootstrap.signed\" << std::endl;\n    return 1;\n  }\n\n  /*\n  signal(SIGINT, signal_handler);\n  signal(SIGTERM, signal_handler);\n  */\n\n  std::vector<char> bootstrap;\n\n  // load bootstrap.signed\n  {\n    std::ifstream inf{argv[1], std::ifstream::ate | std::ifstream::binary};\n    size_t len = inf.tellg();\n    inf.seekg(0);\n    bootstrap.resize(len);\n    inf.read(bootstrap.data(), bootstrap.size());\n  }\n\n  if (auto* loglevel = getenv(\"LOKINET_LOG\"))\n    lokinet_log_level(loglevel);\n  else\n    lokinet_log_level(\"none\");\n\n  std::cout << \"starting up\" << std::endl;\n\n  recip = MakeLokinet(bootstrap);\n  WaitForReady(recip);\n\n  lokinet_udp_bind_result recipBindResult{};\n\n  const auto port = 10000;\n\n  if (auto err = lokinet_udp_bind(\n          port,\n          ProcessNewInboundFlow,\n          BounceUDPPacket,\n          DeleteFlow,\n          recip.get(),\n          &recipBindResult,\n          recip.get()))\n  {\n    std::cout << \"failed to bind recip udp socket \" << strerror(err) << std::endl;\n    return 0;\n  }\n\n  std::cout << \"bound recip udp\" << std::endl;\n\n  sender = MakeLokinet(bootstrap);\n  WaitForReady(sender);\n\n  std::string recipaddr{lokinet_address(recip.get())};\n\n  std::cout << \"recip ready at \" << recipaddr << std::endl;\n\n  lokinet_udp_bind_result senderBindResult{};\n\n  if (auto err = lokinet_udp_bind(\n          port,\n          ProcessNewInboundFlow,\n          HandleUDPPacket,\n          DeleteFlow,\n          sender.get(),\n          &senderBindResult,\n          sender.get()))\n  {\n    std::cout << \"failed to bind sender udp socket \" << strerror(err) << std::endl;\n    return 0;\n  }\n\n  ConnectJob connect{};\n  connect.remote.socket_id = senderBindResult.socket_id;\n  connect.remote.remote_port = port;\n  std::copy_n(recipaddr.c_str(), recipaddr.size(), connect.remote.remote_host);\n  connect.ctx = sender.get();\n\n  std::cout << \"bound sender udp\" << std::endl;\n\n  do\n  {\n    std::cout << \"try establish to \" << connect.remote.remote_host << std::endl;\n    if (auto err =\n            lokinet_udp_establish(CreateOutboundFlow, &connect, &connect.remote, sender.get()))\n    {\n      std::cout << \"failed to establish to recip: \" << strerror(err) << std::endl;\n      usleep(100000);\n    }\n    else\n      break;\n  } while (true);\n  std::cout << \"sender established\" << std::endl;\n\n  const std::string buf{\"liblokinet\"};\n\n  const std::string senderAddr{lokinet_address(sender.get())};\n\n  do\n  {\n    std::cout << senderAddr << \" send to remote: \" << buf << std::endl;\n    if (auto err = lokinet_udp_flow_send(&connect.remote, buf.data(), buf.size(), sender.get()))\n    {\n      std::cout << \"send failed: \" << strerror(err) << std::endl;\n    }\n    usleep(100000);\n  } while (_run);\n  return 0;\n}\n"
  },
  {
    "path": "contrib/liblokinet_jank_test.cpp",
    "content": "#include <lokinet.hpp>\n\n#include <exception>\n#include <filesystem>\n#include <future>\n#include <iostream>\n#include <thread>\n\nusing namespace std::literals;\n\nint main(int argc, char** argv)\n{\n    if (argc <= 1)\n    {\n        std::cerr << \"USAGE: \" << argv[0] << \" {WHATEVER.loki | WHATEVER.snode}\\n\";\n        return 1;\n    }\n\n    std::string target{argv[1]};\n\n    lokinet::Lokinet loki{std::filesystem::path{\"lokinet.ini\"}};\n\n    std::promise<void> prom;\n    loki.on_connected([&] {\n        std::cout << \"\\n\\x1b[32;1mLokinet connected!\\x1b[0m\\n\\n\\x1b[33;1mINITIATING SESSION TO \" << target\n                  << \"\\x1b[0m\\n\\n\"\n                  << std::flush;\n        loki.establish_udp(\n            target,\n            12345,\n            [](auto udp_info) {\n                std::cout << \"\\n\\x1b[32;1mUDP bound to port \" << udp_info.local_port << \"\\x1b[0m\\n\\n\" << std::flush;\n            },\n            [&prom](std::string_view fail_msg) {\n                try\n                {\n                    throw std::runtime_error{std::string{fail_msg}};\n                }\n                catch (...)\n                {\n                    prom.set_exception(std::current_exception());\n                }\n            });\n    });\n    try\n    {\n        prom.get_future().get();\n    }\n    catch (const std::exception& e)\n    {\n        std::cerr << \"\\n\\n\\x1b[31;1mError establishing session to \" << target << \": \" << e.what() << \"\\x1b[0m\\n\\n\";\n        return 1;\n    }\n\n    /*\n    loki.map_tcp_remote_port(std::string{argv[1]}, 12345,\n        [&](auto tunnel_info) {\n          std::cout << \"\\n\\nTCP bound to port \" << tunnel_info.local_port << \"\\n\\n\";\n        },\n        [&](auto error_str) {\n          std::cerr << \"\\nTCP Tunnel map error: \" << error_str << \"\\n\";\n        });\n    */\n    std::cout << \"\\nPRESS ENTER TO EXIT\\n\";\n    std::string ignored;\n    std::getline(std::cin, ignored);\n    std::cout << \"\\nEXITING\\n\";\n}\n"
  },
  {
    "path": "contrib/lokinet-resolvconf",
    "content": "#!/bin/bash\n\n# Script to invoke resolvconf (if installed) to add/remove lokinet into/from the resolvconf DNS\n# server list.  This script does not add if any of these are true:\n#\n# - /sbin/resolvconf does not exist\n# - the systemd-resolved service is active\n# - a `no-resolvconf=1` item is present in the [dns] section of lokinet.ini\n#\n# It always attempts to remove if resolvconf is installed (so that commenting out while running,\n# then stopping still removes the added entry).\n#\n# Usage: lokinet-resolvconf {add|remove} /etc/loki/lokinet.ini\n\nset -e\n\naction=\"$1\"\nconf=\"$2\"\n\nif [[ ! (\"$action\" == \"add\" || \"$action\" == \"remove\") || ! -f \"$conf\" ]]; then\n    echo \"Usage: $0 {add|remove} /path/to/lokinet.ini\" >&2\n    exit 1\nfi\n\nif ! [ -x /sbin/resolvconf ]; then\n    exit 0\nfi\n\nif [ -x /bin/systemctl ] && /bin/systemctl --quiet is-active systemd-resolved.service; then\n    exit 0\nfi\n\nif [ \"$action\" == \"add\" ]; then\n    if ! [ -x /sbin/resolvconf ]; then exit 0; fi\n\n    lokinet_ns=$(perl -e '\n    $ns = \"127.3.2.1\"; # default if none found in .ini\n    while (<>) {\n        if ((/^\\[dns\\]/ ... /^\\[/)) {\n            if (/^bind\\s*=\\s*([\\d.]+)(?::53)?\\s*$/) {\n                $ns=$1;\n            } elsif (/^no-resolvconf\\s*=\\s*1\\s*/) {\n                exit;\n            }\n        }\n    }\n    print $ns' \"$conf\")\n\n    if [ -n \"$lokinet_ns\" ]; then\n        echo \"nameserver $lokinet_ns\" | /sbin/resolvconf -a lo.000lokinet\n    fi\nelse\n    /sbin/resolvconf -d lo.000lokinet\nfi\n"
  },
  {
    "path": "contrib/mac-configure.sh",
    "content": "#!/bin/bash\n\nset -e\nset -x\n\nif ! [ -f LICENSE ] || ! [ -d llarp ]; then\n    echo \"You need to run this as ./contrib/mac.sh from the top-level lokinet project directory\" >&2\n    exit 1\nfi\n\nmkdir -p build-mac\ncd build-mac\ncmake \\\n      -G Ninja \\\n      -DBUILD_STATIC_DEPS=ON \\\n      -DLOKINET_TESTS=OFF \\\n      -DLOKINET_BOOTSTRAP=OFF \\\n      -DLOKINET_NATIVE_BUILD=OFF \\\n      -DWITH_LTO=ON \\\n      -DCMAKE_BUILD_TYPE=Release \\\n      -DMACOS_SYSTEM_EXTENSION=ON \\\n      -DCODESIGN=ON \\\n      -DLOKINET_PACKAGE=ON \\\n      \"$@\" \\\n      ..\n\necho \"cmake build configured in build-mac\"\n"
  },
  {
    "path": "contrib/mac.sh",
    "content": "#!/bin/bash\n#\n# Build the shit on mac\n#\n# You will generally need to add: -DCODESIGN_APP=... to make this work, and (unless you are a\n# lokinet team member) will need to pay Apple money for your own team ID and arse around with\n# provisioning profiles.  See macos/README.txt.\n#\n\nset -e\nset -x\n\nif ! [ -f LICENSE ] || ! [ -d llarp ]; then\n    echo \"You need to run this as ./contrib/mac.sh from the top-level lokinet project directory\" >&2\n    exit 1\nfi\n\n./contrib/mac-configure.sh \"$@\"\n\ncd build-mac\nrm -rf Lokinet\\ *\nninja -j${JOBS:-1} dmg\ncd ..\n\necho -e \"Build complete, your app is here:\\n\"\nls -lad $(pwd)/build-mac/Lokinet\\ *\necho \"\"\n"
  },
  {
    "path": "contrib/macos/lokinet-extension.Info.plist.in",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n\t<dict>\n\t\t<key>CFBundleDevelopmentRegion</key>\n\t\t<string>en</string>\n\n\t\t<key>CFBundleDisplayName</key>\n\t\t<string>Lokinet Network Extension</string>\n\n\t\t<key>CFBundleExecutable</key>\n\t\t<string>org.lokinet.network-extension</string>\n\n\t\t<key>CFBundleIdentifier</key>\n\t\t<string>org.lokinet.network-extension</string>\n\n\t\t<key>CFBundleInfoDictionaryVersion</key>\n\t\t<string>6.0</string>\n\n\t\t<key>CFBundlePackageType</key>\n\t\t<string>SYSX</string>\n\t\t\n\t\t<key>CFBundleName</key>\n\t\t<string>org.lokinet.network-extension</string>\n\n\t\t<key>CFBundleVersion</key>\n\t\t<string>@lokinet_VERSION@.@LOKINET_APPLE_BUILD@</string>\n\n\t\t<key>CFBundleShortVersionString</key>\n\t\t<string>@lokinet_VERSION@</string>\n\n\t\t<key>CFBundleSupportedPlatforms</key>\n\t\t<array>\n\t\t\t<string>MacOSX</string>\n\t\t</array>\n\n\t\t<key>ITSAppUsesNonExemptEncryption</key>\n\t\t<false/>\n\n\t\t<key>LSMinimumSystemVersion</key>\n\t\t<string>10.15</string>\n\n        <key>NSHumanReadableCopyright</key>\n        <string>Copyright © 2022 The Oxen Project, licensed under GPLv3-or-later</string>\n\n\t\t<key>NSSystemExtensionUsageDescription</key>\n\t\t<string>Provides Lokinet Network connectivity.</string>\n\n\t\t<key>NetworkExtension</key>\n\t\t<dict>\n\t\t\t<key>NEMachServiceName</key>\n\t\t\t<string>SUQ8J2PCT7.org.lokinet.network-extension</string>\n\n\t\t\t<key>NEProviderClasses</key>\n\t\t\t<dict>\n\t\t\t\t<key>com.apple.networkextension.packet-tunnel</key>\n\t\t\t\t<string>LLARPPacketTunnel</string>\n\n\t\t\t\t<key>com.apple.networkextension.dns-proxy</key>\n\t\t\t\t<string>LLARPDNSProxy</string>\n\t\t\t</dict>\n\t\t</dict>\n\t</dict>\n</plist>\n"
  },
  {
    "path": "contrib/macos/lokinet-extension.plugin.entitlements.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n\t<dict>\n\t\t<key>com.apple.application-identifier</key>\n\t\t<string>SUQ8J2PCT7.org.lokinet.network-extension</string>\n\n\t\t<key>com.apple.developer.networking.networkextension</key>\n\t\t<array>\n\t\t\t<string>packet-tunnel-provider</string>\n\t\t\t<string>dns-proxy</string>\n\t\t</array>\n\n\t\t<key>com.apple.developer.team-identifier</key>\n\t\t<string>SUQ8J2PCT7</string>\n\n\t\t<key>com.apple.security.app-sandbox</key>\n\t\t<true/>\n\n\t\t<key>com.apple.security.network.client</key>\n\t\t<true/>\n\n\t\t<key>com.apple.security.network.server</key>\n\t\t<true/>\n\n\t</dict>\n</plist>\n"
  },
  {
    "path": "contrib/macos/lokinet-extension.sysext.entitlements.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n\t<dict>\n\t\t<key>com.apple.application-identifier</key>\n\t\t<string>SUQ8J2PCT7.org.lokinet.network-extension</string>\n\n\t\t<key>com.apple.developer.networking.networkextension</key>\n\t\t<array>\n\t\t\t<string>packet-tunnel-provider-systemextension</string>\n\t\t\t<string>dns-proxy-systemextension</string>\n\t\t</array>\n\n\t\t<key>com.apple.developer.team-identifier</key>\n\t\t<string>SUQ8J2PCT7</string>\n\n\t\t<key>com.apple.security.app-sandbox</key>\n\t\t<true/>\n\n        <key>com.apple.security.application-groups</key>\n        <array>\n            <string>SUQ8J2PCT7.org.lokinet</string>\n        </array>\n\n\t\t<key>com.apple.security.network.client</key>\n\t\t<true/>\n\n\t\t<key>com.apple.security.network.server</key>\n\t\t<true/>\n\n\t</dict>\n</plist>\n"
  },
  {
    "path": "contrib/macos/lokinet-newsyslog.conf",
    "content": "/var/log/lokinet.log   644  5  5M  $D0   J"
  },
  {
    "path": "contrib/macos/lokinet.Info.plist.in",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n\t<dict>\n\t\t<key>CFBundleDevelopmentRegion</key>\n\t\t<string>en</string>\n\n\t\t<key>CFBundleExecutable</key>\n\t\t<string>Lokinet</string>\n\n\t\t<key>CFBundleIdentifier</key>\n\t\t<string>org.lokinet</string>\n\n\t\t<key>CFBundleInfoDictionaryVersion</key>\n\t\t<string>6.0</string>\n\n\t\t<key>CFBundleName</key>\n\t\t<string>Lokinet</string>\n\n\t\t<key>CFBundleIconFile</key>\n\t\t<string>icon.icns</string>\n\n\t\t<key>CFBundlePackageType</key>\n\t\t<string>APPL</string>\n\n\t\t<key>CFBundleShortVersionString</key>\n\t\t<string>@lokinet_VERSION@</string>\n\n\t\t<key>CFBundleVersion</key>\n\t\t<string>@lokinet_VERSION@.@LOKINET_APPLE_BUILD@</string>\n\n\t\t<key>LSMinimumSystemVersion</key>\n\t\t<string>10.15</string>\n\n\t\t<key>NSHumanReadableCopyright</key>\n\t\t<string>Copyright © 2022 The Oxen Project, licensed under GPLv3-or-later</string>\n\n\t\t<key>LSUIElement</key>\n\t\t<true/>\n\n\t\t<key>LSHasLocalizedDisplayName</key>\n\t\t<true/>\n\n\t  </dict>\n</plist>\n"
  },
  {
    "path": "contrib/macos/lokinet.plugin.entitlements.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n\t<dict>\n\t\t<key>com.apple.application-identifier</key>\n\t\t<string>SUQ8J2PCT7.org.lokinet</string>\n\n\t\t<key>com.apple.developer.networking.networkextension</key>\n\t\t<array>\n\t\t\t<string>packet-tunnel-provider</string>\n\t\t\t<string>dns-proxy</string>\n\t\t\t<string>dns-settings</string>\n\t\t</array>\n\n\t\t<key>com.apple.developer.team-identifier</key>\n\t\t<string>SUQ8J2PCT7</string>\n\n\t\t<key>com.apple.security.app-sandbox</key>\n\t\t<true/>\n\n\t\t<key>com.apple.security.network.client</key>\n\t\t<true/>\n\n\t\t<key>com.apple.security.network.server</key>\n\t\t<true/>\n\n\t</dict>\n</plist>\n"
  },
  {
    "path": "contrib/macos/lokinet.sysext.entitlements.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n\t<dict>\n\t\t<key>com.apple.application-identifier</key>\n\t\t<string>SUQ8J2PCT7.org.lokinet</string>\n\n\t\t<key>com.apple.developer.networking.networkextension</key>\n\t\t<array>\n\t\t\t<string>packet-tunnel-provider-systemextension</string>\n\t\t\t<string>dns-proxy-systemextension</string>\n\t\t\t<string>dns-settings</string>\n\t\t</array>\n\n\t\t<key>com.apple.developer.team-identifier</key>\n\t\t<string>SUQ8J2PCT7</string>\n\n        <key>com.apple.developer.system-extension.install</key>\n        <true/>\n\n\t\t<key>com.apple.security.app-sandbox</key>\n\t\t<true/>\n\n        <key>com.apple.security.application-groups</key>\n        <array>\n            <string>SUQ8J2PCT7.org.lokinet</string>\n        </array>\n\n\t\t<key>com.apple.security.network.client</key>\n\t\t<true/>\n\n\t\t<key>com.apple.security.network.server</key>\n\t\t<true/>\n\n\t</dict>\n</plist>\n"
  },
  {
    "path": "contrib/macos/mk-icns.sh",
    "content": "#!/bin/bash\n\n# Invoked from cmake as mk-icns.sh /path/to/icon.svg /path/to/output.icns\nsvg=\"$1\"\nout=\"$2\"\noutdir=\"${out/%.icns/.iconset}\"\n\nset -e\n\n# Apple's PNG encoding/decoding is buggy and likes to inject yellow lines, particularly for the\n# smaller images.  This is apparently a known issue since macOS 11 that apple just doesn't give a\n# shit about fixing (https://en.wikipedia.org/wiki/Apple_Icon_Image_format#Known_issues).\n#\n# So moral of the story: we have to arse around and edit the png to put a tranparent pixel in the\n# bottom-left corner but that pixel *must* be different from the preceeding color, otherwise Apple's\n# garbage breaks exposing the dumpster fire that lies beneath and drops the blue channel from the\n# last pixel (or run of pixels, if they are the same color (ignoring transparency).  So, just to be\n# consistent, we make *all* 4 corners transparent yellow, because it seems unlikely for our logo to\n# have full-on yellow in the corner, and the color itself is irrelevant because it is fully\n# transparent.\n#\n# Why is there so much broken, buggy crap in the macOS core???\n\nno_r_kelly() {\n    size=$1\n    last=$((size - 1))\n    for x in 0 $last; do\n        for y in 0 $last; do\n            echo -n \"color $x,$y point \"\n        done\n    done\n}\n\n\n\nmkdir -p \"${outdir}\"\nfor size in 32 64 128 256 512 1024; do\n    # Yay Apple thanks for this utter trash OS.\n    last=$((size - 1))\n    convert -background none -resize \"${size}x${size}\" \"$svg\" \\\n        -fill '#ff00' -draw \"$(no_r_kelly $size)\" \\\n        -strip \"png32:${outdir}/icon_${size}x${size}.png\"\ndone\n\n\n# Outputs the imagemagick -draw command to color the corner-adjacent pixels as half-transparent\n# white.  We use this for the 16x16 (the others pick up corner transparency from the svg).\nsemitransparent_off_corners() {\n    size=$1\n    for x in 1 $((size - 2)); do\n        for y in 0 $((size - 1)); do\n            echo -n \"color $x,$y point \"\n        done\n    done\n    for x in 0 $((size -1)); do\n        for y in 1 $((size - 2)); do\n            echo -n \"color $x,$y point \"\n        done\n    done\n}\n\n# For 16x16 we crop the image to 5/8 of its regular size before resizing which effectively zooms in\n# on it a bit because if we resize the full icon it ends up a fuzzy mess, while the crop and resize\n# lets us retain some detail of the logo.  (We don't do this for the 16x16@2x because that is really\n# 32x32 where it retains enough detail).\nconvert -background none -resize 512x512 \"$svg\" -gravity Center -extent 320x320 -resize 16x16 \\\n    -fill '#ff00' -draw \"$(no_r_kelly 16)\" \\\n    -fill '#fff8' -draw \"$(semitransparent_off_corners 16)\" \\\n    -strip \"png32:$outdir/icon_16x16.png\"\n\n# Create all the \"@2x\" versions which are just the double-size versions\nrm -f \"${outdir}/icon_*@2x.png\"\nmv \"${outdir}/icon_1024x1024.png\" \"${outdir}/icon_512x512@2x.png\"\nfor size in 16 32 128 256; do\n    double=$((size * 2))\n    ln -f \"${outdir}/icon_${double}x${double}.png\" \"${outdir}/icon_${size}x${size}@2x.png\"\ndone\n\niconutil -c icns \"${outdir}\"\n"
  },
  {
    "path": "contrib/macos/notarize.py.in",
    "content": "#!/usr/bin/env python3\n\nimport sys\nimport plistlib\nimport subprocess\nimport time\nimport os\nimport os.path\n\ndef bold_red(x):\n    return \"\\x1b[31;1m\" + x + \"\\x1b[0m\"\n\nif not @notarize_py_is_sysext@:\n    print(bold_red(\"\\nUnable to notarize: this lokinet is not built as a system extension\\n\"), file=sys.stderr)\n    sys.exit(1)\n\nif not all((\"@MACOS_NOTARIZE_USER@\", \"@MACOS_NOTARIZE_PASS@\", \"@MACOS_NOTARIZE_ASC@\")):\n    print(bold_red(\"\\nUnable to notarize: one or more required notarization variable not set; see contrib/macos/README.txt\\n\") +\n            \"  Called with -DMACOS_NOTARIZE_USER=@MACOS_NOTARIZE_USER@\\n\"\n            \"              -DMACOS_NOTARIZE_PASS=@MACOS_NOTARIZE_PASS@\\n\"\n            \"              -DMACOS_NOTARIZE_ASC=@MACOS_NOTARIZE_ASC@\\n\",\n            file=sys.stderr)\n    sys.exit(1)\n\nos.chdir(\"@PROJECT_BINARY_DIR@\")\napp = \"@lokinet_app@\"\nzipfile = f\"Lokinet.app.notarize.zip\"\nprint(f\"Creating {zipfile} from {app}\")\nif os.path.exists(zipfile):\n    os.remove(zipfile)\nsubprocess.run(['ditto', '-v', '-c', '-k', '--sequesterRsrc', '--keepParent', app, zipfile])\n\nuserpass = ('--username', \"@MACOS_NOTARIZE_USER@\", '--password', \"@MACOS_NOTARIZE_PASS@\")\nprint(\"Submitting {} for notarization; this may take a minute...\".format(zipfile))\n\nstarted = time.time()\ncommand = [\n    'xcrun', 'altool',\n    '--notarize-app',\n    '--primary-bundle-id', 'org.lokinet.@PROJECT_VERSION@',\n    *userpass,\n    '--asc-provider', \"@MACOS_NOTARIZE_ASC@\",\n    '--file', zipfile,\n    '--output-format', 'xml'\n    ]\nprint(command)\nresult = subprocess.run(command, stdout=subprocess.PIPE)\n\ndata = plistlib.loads(result.stdout)\nif 'success-message' not in data or 'notarization-upload' not in data or 'RequestUUID' not in data['notarization-upload']:\n    print(\"Something failed, leaving you with this nice XML to figure out:\\n{}\".format(data))\n    sys.exit(1)\n\nuuid = data['notarization-upload']['RequestUUID']\nelapsed = time.time() - started\nmins, secs = int(elapsed // 60), elapsed % 60\nprint(\"Notarization submitted with request uuid = {} in {:d}m{:05.2f}s\".format(uuid, mins, secs))\nprint(data['success-message'])\n\nprint(\"Begin polling for notarization result\")\nstarted_waiting = time.time()\ndone = False\nsuccess = False\nwhile not done:\n    time.sleep(5)\n    result = subprocess.run([\n        'xcrun', 'altool',\n        '--notarization-info', uuid,\n        *userpass,\n        '--output-format', 'xml'\n        ], stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n    if result.returncode == 1 and b'Gateway Timeout' in result.stderr:\n        status = \"Apple's servers are trash (aka Gateway Timeout)\"\n    else:\n        result.check_returncode()\n        data = plistlib.loads(result.stdout)\n        if 'notarization-info' not in data or 'Status' not in data['notarization-info']:\n            status = 'Request failed'\n        else:\n            status = data['notarization-info']['Status Message'] if 'Status Message' in data['notarization-info'] else ''\n            st = data['notarization-info']['Status']\n            if st == 'success':\n                success = True\n                done = True\n            elif st == 'invalid':\n                done = True\n            elif st == 'in progress' and len(status) == 0:\n                status = 'Notarization in progress'\n\n            if done and 'LogFileURL' in data['notarization-info']:\n                status += '\\n\\nlog file: {}'.format(data['notarization-info']['LogFileURL'])\n\n    elapsed = time.time() - started_waiting\n    mins, secs = int(elapsed // 60), int(elapsed % 60)\n\n    print(\"\\033[1K\\r(+{:d}m{:02d}s) {}: {}\".format(mins, secs, st, status), end='', flush=True)\n\nprint(\"\\n\")\nif not success:\n    sys.exit(42)\n\nif os.path.exists(zipfile):\n    os.remove(zipfile)\n\nprint(\"Stapling {}...\".format(app), end='')\nresult = subprocess.run(['xcrun', 'stapler', 'staple', app])\n\nresult.check_returncode()\n\nwith open(\"macos-notarized.stamp\", 'w'):\n    pass\n\nprint(\" success.\\n\")\n"
  },
  {
    "path": "contrib/macos/seticon.swift",
    "content": "import Foundation\nimport AppKit\n\n// Apple deprecated their command line tools to set images on things and replaced them with a\n// barely-documented swift function.  Yay!\n\n// Usage: ./seticon /path/to/my.icns /path/to/some.dmg\n\nlet args = CommandLine.arguments\n\nif args.count != 3 {\n    print(\"Error: usage: ./seticon /path/to/my.icns /path/to/some.dmg\")\n    exit(1)\n}\n\nvar icns = args[1]\nvar dmg = args[2]\n\nvar img = NSImage(byReferencingFile: icns)!\n\nif NSWorkspace.shared.setIcon(img, forFile: dmg) {\n    print(\"Set \\(dmg) icon to \\(icns) [\\(img.size)]\")\n} else {\n    print(\"Setting icon failed, don't know why\")\n    exit(2)\n}\n"
  },
  {
    "path": "contrib/macos/sign.sh.in",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nif [ \"@CODESIGN@\" != \"ON\" ]; then\n  echo \"Cannot codesign: this build was not configured with codesigning\" >&2\n  exit 1\nfi\n\nsignit() {\n    target=\"$1\"\n    entitlements=\"$2\"\n    echo -e \"\\n\\e[33;1mSigning ${target/*\\/Lokinet.app/Lokinet.app}...\\e[0m\" >&2\n    codesign \\\n        --verbose=4 \\\n        --force \\\n        -s \"@CODESIGN_ID@\" \\\n        --entitlements \"$entitlements\" \\\n        --strict \\\n        --timestamp \\\n        --options=runtime \\\n        \"$target\"\n}\n\ngui_entitlements=\"@PROJECT_SOURCE_DIR@/gui/node_modules/app-builder-lib/templates/entitlements.mac.plist\"\next_entitlements=\"@PROJECT_SOURCE_DIR@/contrib/macos/lokinet-extension.@LOKINET_ENTITLEMENTS_TYPE@.entitlements.plist\"\napp_entitlements=\"@PROJECT_SOURCE_DIR@/contrib/macos/lokinet.@LOKINET_ENTITLEMENTS_TYPE@.entitlements.plist\"\n\nSIGN_TARGET=\"@PROJECT_BINARY_DIR@/Lokinet @PROJECT_VERSION@/Lokinet.app\"\n\nfor ext in systemextension appex; do\n    netext=\"$SIGN_TARGET/@lokinet_ext_dir@/org.lokinet.network-extension.$ext\"\n    if [ -e \"$netext\" ]; then\n        signit \"$netext\" \"$ext_entitlements\"\n    fi\ndone\n\nif [ \"@LOKINET_GUI@\" == \"ON\" ]; then\n    gui_app=\"$SIGN_TARGET\"/Contents/Helpers/Lokinet-GUI.app\n    gui_sign_targets=()\n    for bundle in \\\n        \"$gui_app\"/Contents/Frameworks/*.framework \\\n        \"$gui_app\"/Contents/Frameworks/*.app\n    do\n\n        if [ -d \"$bundle/Libraries\" ]; then\n            gui_sign_targets+=(\"$bundle\"/Libraries/*.dylib)\n        fi\n        if [ -d \"$bundle/Helpers\" ]; then\n            gui_sign_targets+=(\"$bundle\"/Helpers/*)\n        fi\n        if [ -d \"$bundle/Resources\" ]; then\n            for f in \"$bundle/Resources\"/*; do\n                if [[ -f \"$f\" && -x \"$f\" && \"$(file -b \"$f\")\" == Mach-O* ]]; then\n                    gui_sign_targets+=(\"$f\")\n                fi\n            done\n        fi\n\n        gui_sign_targets+=(\"$bundle\")\n    done\n\n    gui_sign_targets+=(\"$gui_app\")\n\n    for target in \"${gui_sign_targets[@]}\"; do\n        signit \"$target\" \"$gui_entitlements\"\n    done\n\n    signit \"$SIGN_TARGET\"/Contents/MacOS/Lokinet \"$app_entitlements\"\nfi\n\nsignit \"$SIGN_TARGET\" \"$app_entitlements\"\n\ntouch \"@PROJECT_BINARY_DIR@\"/macos-signed.stamp\n"
  },
  {
    "path": "contrib/make-ico.sh",
    "content": "#!/bin/bash\n\n# Invoked from cmake as make-ico.sh /path/to/icon.svg /path/to/output.ico\nsvg=\"$1\"\nout=\"$2\"\noutdir=\"$out.d\"\n\nset -e\n\nsizes=(16 24 32 40 48 64 96 192 256)\nouts=\"\"\n\nmkdir -p \"${outdir}\"\nfor size in \"${sizes[@]}\"; do\n    outf=\"${outdir}/${size}x${size}.png\"\n    if [ $size -lt 32 ]; then\n        # For 16x16 and 24x24 we crop the image to 2/3 of its regular size make it all white\n        # (instead of transparent) to zoom in on it a bit because if we resize the full icon to the\n        # target size it ends up a fuzzy mess, while the crop and resize lets us retain some detail\n        # of the logo.\n        rsvg-convert -b white \\\n            --page-height $size --page-width $size \\\n            -w $(($size*3/2)) -h $(($size*3/2)) --left \" -$(($size/4))\" --top \" -$(($size/4))\" \\\n            \"$svg\" >\"$outf\"\n    else\n        rsvg-convert -b transparent -w $size -h $size \"$svg\" >\"$outf\"\n    fi\n    outs=\"-r $outf $outs\"\ndone\n\nicotool -c -b 32 -o \"$out\" $outs\n"
  },
  {
    "path": "contrib/omq-rpc.py",
    "content": "#!/usr/bin/env python3\n\nimport nacl.bindings as sodium\nfrom nacl.public import PrivateKey\nfrom nacl.signing import SigningKey, VerifyKey\nimport nacl.encoding\nimport requests\nimport zmq\nimport zmq.utils.z85\nimport sys\nimport re\nimport time\nimport random\nimport shutil\n\n\ncontext = zmq.Context()\nsocket = context.socket(zmq.DEALER)\nsocket.setsockopt(zmq.CONNECT_TIMEOUT, 5000)\nsocket.setsockopt(zmq.HANDSHAKE_IVL, 5000)\n#socket.setsockopt(zmq.IMMEDIATE, 1)\n\nif len(sys.argv) > 1 and any(sys.argv[1].startswith(x) for x in (\"ipc://\", \"tcp://\", \"curve://\")):\n    remote = sys.argv[1]\n    del sys.argv[1]\nelse:\n    remote = \"ipc://./rpc.sock\"\n\ncurve_pubkey = b''\nmy_privkey, my_pubkey = b'', b''\n\n# If given a curve://whatever/pubkey argument then transform it into 'tcp://whatever' and put the\n# 'pubkey' back into argv to be handled below.\nif remote.startswith(\"curve://\"):\n    pos = remote.rfind('/')\n    pkhex = remote[pos+1:]\n    remote = \"tcp://\" + remote[8:pos]\n    if len(pkhex) != 64 or not all(x in \"0123456789abcdefABCDEF\" for x in pkhex):\n        print(\"curve:// addresses must be in the form curve://HOST:PORT/REMOTE_PUBKEY_HEX\", file=sys.stderr)\n        sys.exit(1)\n    sys.argv[1:0] = [pkhex]\n\nif len(sys.argv) > 1 and len(sys.argv[1]) == 64 and all(x in \"0123456789abcdefABCDEF\" for x in sys.argv[1]):\n    curve_pubkey = bytes.fromhex(sys.argv[1])\n    del sys.argv[1]\n    socket.curve_serverkey = curve_pubkey\n    if len(sys.argv) > 1 and len(sys.argv[1]) == 64 and all(x in \"0123456789abcdefABCDEF\" for x in sys.argv[1]):\n        my_privkey = bytes.fromhex(sys.argv[1])\n        del sys.argv[1]\n        my_pubkey = zmq.utils.z85.decode(zmq.curve_public(zmq.utils.z85.encode(my_privkey)))\n    else:\n        my_privkey = PrivateKey.generate()\n        my_pubkey = my_privkey.public_key.encode()\n        my_privkey = my_privkey.encode()\n\n        print(\"No curve client privkey given; generated a random one (pubkey: {}, privkey: {})\".format(\n            my_pubkey.hex(), my_privkey.hex()), file=sys.stderr)\n    socket.curve_secretkey = my_privkey\n    socket.curve_publickey = my_pubkey\n\nif not 2 <= len(sys.argv) <= 3 or any(x in y for x in (\"--help\", \"-h\") for y in sys.argv[1:]):\n    print(\"Usage: {} [ipc:///path/to/sock|tcp://1.2.3.4:5678] [SERVER_CURVE_PUBKEY [LOCAL_CURVE_PRIVKEY]] COMMAND ['JSON']\".format(\n        sys.argv[0]), file=sys.stderr)\n    sys.exit(1)\n\nbeginning_of_time = time.clock_gettime(time.CLOCK_MONOTONIC)\n\nprint(\"Connecting to {}\".format(remote), file=sys.stderr)\nsocket.connect(remote)\nto_send = [sys.argv[1].encode(), b'tagxyz123']\nto_send += (x.encode() for x in sys.argv[2:])\nprint(\"Sending {}\".format(to_send[0]), file=sys.stderr)\nsocket.send_multipart(to_send)\nif socket.poll(timeout=5000):\n    m = socket.recv_multipart()\n    recv_time = time.clock_gettime(time.CLOCK_MONOTONIC)\n    if len(m) < 3 or m[0:2] != [b'REPLY', b'tagxyz123']:\n        print(\"Received unexpected {}-part reply:\".format(len(m)), file=sys.stderr)\n        for x in m:\n            print(\"- {}\".format(x))\n    else: # m[2] is numeric value, m[3] is data part, and will become m[2] <- changed\n        print(\"Received reply in {:.6f}s:\".format(recv_time - beginning_of_time), file=sys.stderr)\n        if len(m) < 3:\n            print(\"(empty reply data)\", file=sys.stderr)\n        else:\n            for x in m[2:]:\n                print(\"{} bytes data part:\".format(len(x)), file=sys.stderr)\n                if any(x.startswith(y) for y in (b'd', b'l', b'i')) and x.endswith(b'e'):\n                    sys.stdout.buffer.write(x)\n                else:\n                    print(x.decode(), end=\"\\n\\n\")\n\nelse:\n    print(\"Request timed out\", file=sys.stderr)\n    socket.close(linger=0)\n    sys.exit(1)\n\n# sample usage:\n# ./omq-rpc.py ipc://$HOME/.oxen/testnet/oxend.sock 'llarp.get_service_nodes' | jq\n"
  },
  {
    "path": "contrib/patches/libzmq-mingw-unistd.patch",
    "content": "diff --git a/tests/testutil.hpp b/tests/testutil.hpp\nindex c6f5e4de78..09b9fa77e5 100644\n--- a/tests/testutil.hpp\n+++ b/tests/testutil.hpp\n@@ -41,6 +41,9 @@\n //  For AF_INET and IPPROTO_TCP\n #if defined _WIN32\n #include \"../src/windows.hpp\"\n+#if defined(__MINGW32__)\n+#include <unistd.h>\n+#endif\n #else\n #include <arpa/inet.h>\n #include <unistd.h>\n"
  },
  {
    "path": "contrib/patches/libzmq-mingw-wepoll.patch",
    "content": "diff --git a/external/wepoll/wepoll.c b/external/wepoll/wepoll.c\n--- a/external/wepoll/wepoll.c\n+++ b/external/wepoll/wepoll.c\n@@ -140,9 +140,9 @@\n #pragma warning(push, 1)\n #endif\n \n-#include <WS2tcpip.h>\n-#include <WinSock2.h>\n-#include <Windows.h>\n+#include <ws2tcpip.h>\n+#include <winsock2.h>\n+#include <windows.h>\n \n #ifndef __GNUC__\n #pragma warning(pop)\n"
  },
  {
    "path": "contrib/patches/unbound-delete-crash-fix.patch",
    "content": "commit 56d816014d5e8a7eb055169c7e13a303dad5e50f\nAuthor: Jason Rhinelander <jason@imaginary.ca>\nDate:   Mon Oct 31 22:07:03 2022 -0300\n\n    Set tube->ev_listen to NULL to prevent double unregister\n    \n    On windows when using threaded mode (i.e. `ub_ctx_async(ctx, 1)`)\n    tube_remove_bg_listen gets called twice: once when the thread does its\n    own cleanup, then again in `tube_delete()`.  Because `ev_listen` doesn't\n    get cleared, however, we end we calling ub_winsock_unregister_wsaevent\n    with a freed pointer.\n    \n    This doesn't always manifest because, apparently, for various compilers\n    and settings that memory *might* be overwritten in which case the\n    additional check for ev->magic will prevent anything actually happening,\n    but in my case under mingw32 that doesn't happen and we end up\n    eventually crashing.\n    \n    This fixes the crash by properly NULLing the pointer so that the second\n    ub_winsock_unregister_wsaevent(...) becomes a no-op.\n\ndiff --git a/util/tube.c b/util/tube.c\nindex 43455fee..a92dfa77 100644\n--- a/util/tube.c\n+++ b/util/tube.c\n@@ -570,6 +570,7 @@ void tube_remove_bg_listen(struct tube* tube)\n {\n \tverbose(VERB_ALGO, \"tube remove_bg_listen\");\n \tub_winsock_unregister_wsaevent(tube->ev_listen);\n+\ttube->ev_listen = NULL;\n }\n \n void tube_remove_bg_write(struct tube* tube)\n"
  },
  {
    "path": "contrib/py/.gitignore",
    "content": "*.egg-info\nv\n__pycache__\ndist"
  },
  {
    "path": "contrib/py/admin/.gitignore",
    "content": "v/"
  },
  {
    "path": "contrib/py/admin/lokinetmon",
    "content": "#!/usr/bin/env python3\n\nimport curses\nimport json\nimport sys\nimport time\nimport platform\nimport os\nimport re\nfrom argparse import ArgumentParser as AP\n\nis_windows = lambda : platform.system().lower() == 'windows'\nis_linux = lambda : platform.system().lower() == 'linux'\n\n\ntry:\n    import zmq\nexcept ImportError:\n    print(\"zmq module not found\")\n    print()\n    if is_linux():\n        print(\"for debian-based linux do:\")\n        print(\"\\tsudo apt install python3-zmq\")\n        print(\"for other linuxs do:\")\n        print(\"\\tpip3 install --user zmq\")\n    else:\n        print(\"install it with:\")\n        print(\"\\tpip3 install --user zmq\")\n    sys.quit()\n\ngeo = None\n\ntry:\n    import GeoIP\nexcept ImportError:\n    print(\"geoip module not found\")\n    print()\n    if is_linux():\n        print(\"for debian-based linux do:\")\n        print(\"\\tsudo apt install python3-geoip\")\n        print(\"for other linuxs do:\")\n        print(\"\\tpip3 install --user geoip\")\n        print(\"for other linuxs you are responsible for obtaining your owen geoip databases, glhf\")\n        time.sleep(1)\n    else:\n        print(\"install it with:\")\n        print(\"\\tpip3 install --user geoip\")\n        print()\n        print(\"press enter to continue without geoip\")\n        sys.stdin.read(1)\nelse:\n    try:\n        geoip_env_var = 'GEOIP_DB_FILE'\n        if is_windows():\n            geoip_default_db = '.\\\\GeoIP.dat'\n        else:\n            geoip_default_db = \"/usr/share/GeoIP/GeoIP.dat\"\n        geoip_db_file = geoip_env_var in os.environ and os.environ[geoip_env_var] or geoip_default_db\n        if not os.path.exists(geoip_db_file):\n            print(\"no geoip database found at {}\".format(geoip_db_file))\n            print(\"you can override the path to it using the {} environmental variable\".format(geoip_env_var))\n            sys.quit()\n        geo = GeoIP.open(geoip_db_file, GeoIP.GEOIP_STANDARD)\n    except Exception as ex:\n        print('failed to load geoip database: {}'.format(ex))\n        time.sleep(1)\n\nnow = lambda : int(time.time()) * 1000\n\n\ndef ip_to_flag(ip):\n    \"\"\"\n    convert an ip to a flag emoji\n    \"\"\"\n    # bail if no geoip available\n    if not geo:\n        return ''\n    # trim off excess ipv6 jizz\n    ip = ip.replace(\"::ffff:\", \"\")\n    # get the country code\n    cc = geo.country_code_by_addr(ip)\n    # Unicode flag sequences are just country codes transposed into the REGIONAL\n    # INDICATOR SYMBOL LETTER A ... Z range (U+1F1E6 ... U+1F1FF):\n    flag = ''.join(chr(0x1f1e6 + ord(i) - ord('A')) for i in cc)\n    return '({}) {}'.format(cc, flag)\n\n\nclass Monitor:\n\n    _sample_size = 12\n    filter = lambda x : True\n\n    def __init__(self, url, introsetMode=False):\n        self.txrate = 0\n        self.rxrate = 0\n        self.data = dict()\n        self.win = curses.initscr()\n        curses.start_color()\n        curses.init_pair(1, curses.COLOR_RED, curses.COLOR_BLACK)\n        self._rpc_context = zmq.Context()\n        self._rpc_socket = self._rpc_context.socket(zmq.DEALER)\n        self._rpc_socket.setsockopt(zmq.CONNECT_TIMEOUT, 5000)\n        self._rpc_socket.setsockopt(zmq.HANDSHAKE_IVL, 5000)\n        self._rpc_socket.connect(url)\n        self._speed_samples = [(0,0,0,0)] * self._sample_size\n        self._run = True\n        self._introsetMode = introsetMode\n\n    def rpc(self, method):\n        self._rpc_socket.send_multipart([method.encode(), b'lokinetmon'+method.encode()])\n        if not self._rpc_socket.poll(timeout=50):\n            return\n        reply = self._rpc_socket.recv_multipart()\n        if len(reply) >= 3 and reply[0:2] == [b'REPLY', b'lokinetmon'+method.encode()]:\n            return reply[2].decode()\n\n    def _close(self):\n        self._rpc_socket.close(linger=0)\n        self._run = False\n        curses.endwin()\n\n    def update_data(self):\n        \"\"\"update data from lokinet\"\"\"\n        try:\n            data = json.loads(self.rpc(\"llarp.status\"))\n            self.data = data['result']\n        except:\n            self.data = None\n        return self.data is not None and self._run\n\n    def _render_path(self, y_pos, path, name):\n        \"\"\"render a path at current position\"\"\"\n        self.win.move(y_pos, 1)\n        self.win.addstr(\"({}) \".format(name))\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        y_pos += 1\n        self.win.addstr(\"[tx:\\t{}]\\t[rx:\\t{}]\".format(\n            self.speed_of(path['txRateCurrent']), self.speed_of(path['rxRateCurrent'])))\n        self.win.move(y_pos, 1)\n        y_pos += 1\n        self.win.addstr(\"me -> \")\n        for hop in path[\"hops\"]:\n            hopstr = hop['router'][:4]\n            if 'ip' in hop:\n                hopstr += ' {}'.format(ip_to_flag(hop['ip']))\n            self.win.addstr(\" {} ->\".format(hopstr))\n\n        self.win.addstr(\" [{} ms latency]\".format(path[\"intro\"][\"latency\"]))\n        self.win.addstr(\" [expires: {}]\".format(self.time_to(path[\"expiresAt\"])))\n        if path[\"expiresSoon\"]:\n            self.win.addstr(\"(expiring)\")\n        elif path[\"expired\"]:\n            self.win.addstr(\"(expired)\")\n        return y_pos\n\n    @staticmethod\n    def time_to(timestamp):\n        \"\"\" return time until timestamp formatted\"\"\"\n        if timestamp:\n            unit = 'seconds'\n            val = (timestamp - now()) / 1000.0\n            if abs(val) > 60.0:\n                val /= 60.0\n                unit = 'minutes'\n            if val < 0:\n                return \"{:.2f} {} ago\".format(0-val, unit)\n            else:\n                return \"in {:.2f} {}\".format(val, unit)\n        else:\n            return 'never'\n\n    @staticmethod\n    def speed_of(rate):\n        \"\"\"turn int speed into string formatted\"\"\"\n        units = [\"b\", \"Kb\", \"Mb\", \"Gb\"]\n        idx = 0\n        rate *= 8\n        while rate > 1000 and idx < len(units):\n            rate /= 1000.0\n            idx += 1\n        return \"{} {}ps\".format(\"%.2f\" % rate, units[idx])\n\n    def get_all_paths(self):\n        \"\"\" yield all paths in current data \"\"\"\n        for key in self.data['services']:\n            status = self.data['services'][key]\n            for path in (status['paths'] or []):\n                yield path\n            for sess in (status['remoteSessions'] or []):\n                for path in sess['paths']:\n                    yield path\n            for sess in (status['snodeSessions'] or []):\n                for path in sess['paths']:\n                    yield path\n\n    def display_service(self, y_pos, name, status):\n        \"\"\"display a service at current position\"\"\"\n        self.win.move(y_pos, 1)\n        self.win.addstr(\"service [{}]\".format(name))\n        build = status[\"buildStats\"]\n        ratio = build[\"success\"] / (build[\"attempts\"] or 1)\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        self.win.addstr(\"build success: {} %\".format(int(100 * ratio)))\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        paths = status[\"paths\"]\n        self.win.addstr(\"paths: {}\".format(len(paths)))\n        for path in paths:\n            if self.filter('localhost.loki'):\n                y_pos = self._render_path(y_pos, path, \"localhost.loki\")\n        for session in (status[\"remoteSessions\"] or []):\n            for path in session[\"paths\"]:\n                if self.filter(session[\"remoteIdentity\"]):\n                    y_pos = self._render_path(\n                        y_pos, path, \"[active] {}\".format(session[\"currentConvoTag\"])\n                    )\n        for session in (status[\"snodeSessions\"] or []):\n            for path in session[\"paths\"]:\n                if self.filter(session[\"endpoint\"]):\n                    y_pos = self._render_path(y_pos, path, \"[snode]\")\n        return y_pos\n\n    def display_links(self, y_pos, data):\n        \"\"\" display links section \"\"\"\n        self.txrate = 0\n        self.rxrate = 0\n        for link in data[\"outbound\"]:\n            y_pos += 1\n            self.win.move(y_pos, 1)\n            self.win.addstr(\"outbound sessions:\")\n            y_pos = self.display_link(y_pos, link)\n        for link in data[\"inbound\"]:\n            y_pos += 1\n            self.win.move(y_pos, 1)\n            self.win.addstr(\"inbound sessions:\")\n            y_pos = self.display_link(y_pos, link)\n        y_pos += 2\n        self.win.move(y_pos, 1)\n        self.win.addstr(\n            \"throughput:\\t\\t[{}\\ttx]\\t[{}\\trx]\".format(\n                self.speed_of(self.txrate), self.speed_of(self.rxrate)\n            )\n        )\n        bloat_tx, bloat_rx = self.calculate_bloat(self.data['links']['outbound'])\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        self.win.addstr(\"goodput:\\t\\t[{}\\ttx]\\t[{}\\trx]\".format(\n            self.speed_of(self.txrate-bloat_tx), self.speed_of(self.rxrate-bloat_rx)))\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        self.win.addstr(\"overhead:\\t\\t[{}\\ttx]\\t[{}\\trx]\".format(\n            self.speed_of(bloat_tx), self.speed_of(bloat_rx)))\n        self._speed_samples.append((self.txrate, self.rxrate, bloat_tx, bloat_rx))\n        while len(self._speed_samples) > self._sample_size:\n            self._speed_samples.pop(0)\n        return self.display_speedgraph(y_pos + 2)\n\n    @staticmethod\n    def _scale(_x, _n):\n        while _n > 0:\n            _x /= 2\n            _n -= 1\n        return int(_x)\n\n\n    @staticmethod\n    def _makebar(samp, badsamp, maxsamp):\n        barstr = \"#\" * (samp - badsamp)\n        pad = \" \" * (maxsamp - samp)\n        return pad, barstr, '#' * badsamp\n\n    def display_speedgraph(self, y_pos, maxsz=40):\n        \"\"\" display global speed graph \"\"\"\n        txmax, rxmax = 1024, 1024\n        for _tx, _rx, b_tx, b_rx in self._speed_samples:\n            if _tx > txmax:\n                txmax = _tx\n            if _rx > rxmax:\n                rxmax = _rx\n\n        rxscale = 0\n        while rxmax > maxsz:\n            rxscale += 1\n            rxmax /= 2\n\n        txscale = 0\n        while txmax > maxsz:\n            txscale += 1\n            txmax /= 2\n\n        txlabelpad = int(txmax / 2)\n        rxlabelpad = int(rxmax / 2)\n        if txlabelpad <= 0:\n            txlabelpad = 1\n        if rxlabelpad <= 0:\n            rxlabelpad = 1\n        txlabelpad_str = \" \" * txlabelpad\n        rxlabelpad_str = \" \" * rxlabelpad\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        for val in [txlabelpad_str, 'tx', txlabelpad_str, rxlabelpad_str, 'rx', rxlabelpad_str]:\n            self.win.addstr(val)\n        for _tx, _rx, b_tx, b_rx in self._speed_samples:\n            y_pos += 1\n            self.win.move(y_pos, 1)\n            txpad, txbar, btxbar = self._makebar(self._scale(_tx, txscale), self._scale(b_tx, txscale), int(txmax))\n            rxpad, rxbar, brxbar = self._makebar(self._scale(_rx, rxscale), self._scale(b_rx, rxscale), int(rxmax))\n            self.win.addstr(txpad)\n            self.win.addstr(btxbar, curses.color_pair(1))\n            self.win.addstr(txbar)\n            self.win.addstr('|')\n            self.win.addstr(rxbar)\n            self.win.addstr(brxbar, curses.color_pair(1))\n            self.win.addstr(rxpad)\n        return y_pos + 2\n\n    def calculate_bloat(self, links):\n        \"\"\"\n        calculate bandwith overhead\n        \"\"\"\n        paths = self.get_all_paths()\n        lltx = 0\n        llrx = 0\n        _tx = 0\n        _rx = 0\n        for link in links:\n            sessions = link[\"sessions\"][\"established\"]\n            for sess in sessions:\n                lltx += sess['tx']\n                llrx += sess['rx']\n        for path in paths:\n            _tx += path['txRateCurrent']\n            _rx += path['rxRateCurrent']\n        lltx -= _tx\n        llrx -= _rx\n        if lltx < 0:\n            lltx = 0\n        if llrx < 0:\n            llrx = 0\n        return lltx, llrx\n\n    def display_link(self, y_pos, link):\n        \"\"\" display links \"\"\"\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        sessions = link[\"sessions\"][\"established\"] or []\n        for sess in sessions:\n            y_pos = self.display_link_session(y_pos, sess)\n        return y_pos\n\n    def display_link_session(self, y_pos, sess):\n        \"\"\" display link sessions \"\"\"\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        self.txrate += sess[\"txRateCurrent\"]\n        self.rxrate += sess[\"rxRateCurrent\"]\n        addr = sess['remoteAddr']\n        if geo:\n            ip = addr.split(':')[0]\n            addr += '\\t{}'.format(ip_to_flag(ip))\n        self.win.addstr(\n            \"{}\\t[{}\\ttx]\\t[{}\\trx]\".format(\n                addr, self.speed_of(sess[\"txRateCurrent\"]), self.speed_of(sess[\"rxRateCurrent\"])\n            )\n        )\n        if (sess['txMsgQueueSize'] or 0) > 1:\n            self.win.addstr(\" [out window: {}]\".format(sess['txMsgQueueSize']))\n            if (sess['rxMsgQueueSize'] or 0) > 1:\n                self.win.addstr(\" [in window: {}]\".format(sess['rxMsgQueueSize']))\n        def display(acks, label, num='acks', dem='packets'):\n            if acks[dem] > 0:\n                self.win.addstr(\" [{}: {}]\".format(label, round(float(acks[num]) / float(acks[dem]), 2)))\n        if ('recvMACKs' in sess) and ('sendMACKs' in sess):\n            display(sess['sendMACKs'], 'out MACK density')\n            display(sess['recvMACKs'], 'in MACK density')\n        dats = {'recvAcks': 'in acks',\n                'sendAcks': 'out acks',\n                'recvRTX': 'in RTX',\n                'sendRTX': 'out RTX'}\n        for key in dats:\n            val = dats[key]\n            if (key in sess) and (sess[key] > 0):\n                self.win.addstr(\" [{}: {}]\".format(val, sess[key]))\n        return y_pos\n\n    def display_dht(self, y_pos, data):\n        \"\"\" display dht window \"\"\"\n        y_pos += 2\n        self.win.move(y_pos, 1)\n        self.win.addstr(\"DHT:\")\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        self.win.addstr(\"introset lookups\")\n        y_pos = self.display_bucket(y_pos, data[\"pendingIntrosetLookups\"])\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        self.win.addstr(\"router lookups\")\n        return self.display_bucket(y_pos, data[\"pendingRouterLookups\"])\n\n    def display_bucket(self, y_pos, data):\n        \"\"\" display dht bucket \"\"\"\n        txs = data[\"tx\"]\n        self.win.addstr(\" ({} lookups)\".format(len(txs)))\n        for transaction in txs:\n            y_pos += 1\n            self.win.move(y_pos, 1)\n            self.win.addstr(\"search for {}\".format(transaction[\"tx\"][\"target\"]))\n        return y_pos\n\n\n\n    def display_introsets(self, y_pos, service):\n        \"\"\"\n        display introsets on a service\n        \"\"\"\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        if self.filter(\"localhost.loki\"):\n            self.win.addstr(\"localhost.loki\")\n            y_pos = self._display_our_introset(y_pos, service)\n            y_pos += 1\n        remotes = service['remoteSessions'] or []\n        for session in remotes:\n            if self.filter(session['remoteIdentity']):\n                y_pos = self._display_session_introset(y_pos, session)\n\n    def _display_intro(self, y_pos, intro, label, paths):\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        path = 'path' in intro and intro['path'][:4] or '????'\n        self.win.addstr('{}: ({}|{}) [expires: {}] [{} paths]'.format(label, intro['router'][:8], path, self.time_to(intro['expiresAt']), self.count_endpoints_in_path(paths, intro['router'])))\n        return y_pos\n\n    @staticmethod\n    def count_endpoints_in_path(paths, endpoint):\n        num = 0\n        for path in paths:\n            if path['hops'][-1]['router'] == endpoint and path['ready']:\n                num += 1\n        return num\n\n    @staticmethod\n    def count_ready_paths(paths):\n        num = 0\n        for path in paths:\n            if path['ready']:\n                num += 1\n        return num\n\n\n\n    @staticmethod\n    def make_bar(timestamp, scale=1, char='#'):\n        if timestamp > 0:\n            return int((abs(timestamp - now()) / 1000) / scale) * char\n        return ''\n\n    def _display_our_introset(self, y_pos, context):\n        for intro in context['introset']['intros'] or []:\n            y_pos = self._display_intro(y_pos, intro, \"introset intro\", context['paths'])\n        for path in context['paths']:\n            y_pos = self._display_intro(y_pos, path['intro'], \"inbound path [created {}]\".format(self.time_to(path['buildStarted'])), context['paths'])\n        return y_pos\n\n\n    def _display_session_introset(self, y_pos, context):\n        #print(context.keys())\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        readyState = context['readyToSend'] and '️✅' or '❌'\n        self.win.addstr('{} ({}) [{}]'.format(context['remoteIdentity'], context['currentConvoTag'], readyState))\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        self.win.addstr('created: {}'.format(self.time_to(context['sessionCreatedAt'])))\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        self.win.addstr('last good send: {}'.format(self.time_to(context['lastGoodSend'])))\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        self.win.addstr(self.make_bar(context['lastGoodSend']))\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        self.win.addstr('last recv: {}'.format(self.time_to(context['lastRecv'])))\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        self.win.addstr(self.make_bar(context['lastRecv']))\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        self.win.addstr('last introset update: {}'.format(self.time_to(context['lastIntrosetUpdate'])))\n        y_pos += 1\n        self.win.move(y_pos, 1)\n        self.win.addstr(self.make_bar(context['lastIntrosetUpdate'], 30))\n        y_pos += 2\n\n        self.win.move(y_pos, 1)\n        self.win.addstr('last shift: {}'.format(self.time_to(context['lastShift'])))\n\n        paths = context['paths'] or []\n\n        y_pos = self._display_intro(y_pos + 1, context['nextIntro'], 'next intro', paths) + 1\n        y_pos = self._display_intro(y_pos, context['remoteIntro'], 'current intro', paths) + 1\n        for intro in context['currentRemoteIntroset']['intros'] or []:\n            y_pos = self._display_intro(y_pos, intro, \"introset intro\", paths)\n        y_pos += 1\n        return y_pos\n\n    def display_data(self):\n        \"\"\" draw main window \"\"\"\n        if self.data is not None:\n            if self.version:\n                self.win.addstr(1, 1, self.version)\n            services = self.data[\"services\"] or {}\n            y_pos = 3\n            try:\n                if not self._introsetMode:\n                    y_pos = self.display_links(y_pos, self.data[\"links\"])\n                    for key in services:\n                        y_pos = self.display_service(y_pos, key, services[key])\n                    y_pos = self.display_dht(y_pos, self.data[\"dht\"])\n                else:\n                    for key in services:\n                        y_pos = self.display_introsets(y_pos, services[key])\n            except Exception as ex:\n                print('{}'.format(ex))\n        else:\n            self.win.move(1, 1)\n            self.win.addstr(\"lokinet offline\")\n\n    def run(self):\n        \"\"\" run mainloop \"\"\"\n        try:\n            self.version = json.loads(self.rpc(\"llarp.version\"))['result']['version']\n        except:\n            self.version = None\n\n        while self._run:\n            if self.update_data():\n                self.win.box()\n                self.display_data()\n            elif self._run:\n                self.win.addstr(1, 1, \"offline\")\n            else:\n                self._close()\n                return\n            self.win.refresh()\n            try:\n                time.sleep(1)\n            except:\n                self._close()\n                return\n            self.win.clear()\n\ndef main():\n    \"\"\" main function \"\"\"\n\n    ap = AP()\n\n    ap.add_argument(\"--introset\", action='store_const', const=True, default=False, help=\"run in introset inspection mode\")\n    ap.add_argument(\"--url\", default='tcp://127.0.0.1:1190', type=str, help='url to lokinet rpc')\n    ap.add_argument('--filter', default='.+', type=str, help=\"regex to filter entries\")\n    ap.add_argument('--invert-filter', const=True, default=False, action='store_const', help='invert regex filter matching')\n\n    args = ap.parse_args()\n\n    mon = Monitor(\n        args.url,\n        args.introset\n    )\n    mon.filter = lambda x : re.match(args.filter, x) is not None\n    if args.invert_filter:\n        old_filter = mon.filter\n        mon.filter = lambda x : not old_filter(x)\n    mon.run()\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "contrib/py/admin/requirements.txt",
    "content": "requests"
  },
  {
    "path": "contrib/py/ffi-example/lokinet.py",
    "content": "#!/usr/bin/env python3\n\n\nfrom ctypes import *\nimport signal\nimport time\nimport threading\nimport os\n\nlib_file = os.path.join(os.path.realpath('.'), 'liblokinet.so')\n\nclass LokiNET(threading.Thread):\n\n    lib = None\n    ctx = None\n\n    def load(self, lib, conf):\n        self.lib = CDLL(lib)\n        self.lib.llarp_ensure_config(conf)\n        self.ctx = self.lib.llarp_main_init(conf)\n        return self.ctx != 0\n\n    def inform_fail(self):\n        \"\"\"\n        inform lokinet crashed\n        \"\"\"\n\n    def inform_end(self):\n        \"\"\"\n        inform lokinet ended clean\n        \"\"\"\n\n\n    def signal(self, sig):\n        if self.ctx and self.lib:\n            self.lib.llarp_main_signal(self.ctx, int(sig))\n\n    def run(self):\n        code = self.lib.llarp_main_run(self.ctx)\n        print(\"llarp_main_run exited with status {}\".format(code))\n        if code:\n            self.inform_fail()\n        else:\n            self.inform_end()\n            \n    def close(self):\n        if self.lib and self.ctx:\n            self.lib.llarp_main_free(self.ctx)\n\ndef main():\n    loki = LokiNET()\n    if loki.load(lib_file, b'daemon.ini'):\n        if loki.configure():\n            loki.start()\n        else:\n            print(\"failed to configure lokinet context\")\n        try:\n            while True:\n                time.sleep(1)\n        except KeyboardInterrupt:\n            llarp.signal(signal.SIGINT)\n        finally:\n            loki.close()\n            return\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "contrib/py/keygen/.gitignore",
    "content": "*.private"
  },
  {
    "path": "contrib/py/keygen/keygen.py",
    "content": "#!/usr/bin/env python3\n\"\"\"\nkeygen tool for lokinet\n\"\"\"\n\nfrom argparse import ArgumentParser as AP\nfrom base64 import b32encode\n\nfrom nacl.signing import SigningKey\n\ndef base32z(data):\n    \"\"\" base32 z encode \"\"\"\n    return b32encode(data).translate(\n        bytes.maketrans(\n            b'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567',\n            b'ybndrfg8ejkmcpqxot1uwisza345h769')).decode().rstrip('=')\n\n\ndef main():\n    \"\"\"\n    main function for keygen\n    \"\"\"\n    argparser = AP()\n    argparser.add_argument('--keyfile', type=str, required=True, help='place to put generated keys')\n    args = argparser.parse_args()\n    secret = SigningKey.generate()\n    with open(args.keyfile, 'wb') as wfile:\n        wfile.write(b'd1:s64:')\n        wfile.write(secret.encode())\n        wfile.write(secret.verify_key.encode())\n        wfile.write(b'e')\n    print(\"{}.loki\".format(base32z(secret.verify_key.encode())))\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "contrib/py/keygen/readme.md",
    "content": "# lokinet key generator\n\nrequires:\n\n* python3.7 or higher\n* pynacl\n\nusage:\n\n```bash\n./keygen.py --keyfile somekeyfile.private\n```\n\nthis will overwrite the keyfile with new keys\n"
  },
  {
    "path": "contrib/py/lnproxy/lnproxy/__main__.py",
    "content": "#!/usr/bin/env python3\n\nfrom http.server import ThreadingHTTPServer as Server\nfrom http.server import BaseHTTPRequestHandler as BaseHandler\n\n\nbootstrapFromURL = True\ntry:\n    import requests\nexcept ImportError:\n    bootstrapFromURL = False\n\nimport ctypes\nfrom ctypes.util import find_library\n\nimport selectors\nimport socket\n\nimport os\n\nclass ResultStruct(ctypes.Structure):\n    _pack_ = 1\n    _fields_ = [\n        (\"err\", ctypes.c_int),\n        (\"local_address\", ctypes.c_char * 256),\n        (\"local_port\", ctypes.c_int),\n        (\"stream_id\", ctypes.c_int)\n    ]\n\n    def __repr__(self):\n        return \"<Result err={} addr={} port={} id={}>\".format(self.err, self.local_address, self.local_port, self.stream_id)\n\n\nclass LNContext(ctypes.Structure):\n    pass\n\nclass Context:\n    \"\"\"\n    wrapper around liblokinet\n    \"\"\"\n\n    def __init__(self, debug=False):\n        self._ln = ctypes.CDLL(find_library(\"lokinet\"))\n        self._c = ctypes.CDLL(find_library(\"c\"))\n        self._ln.lokinet_context_new.restype = ctypes.POINTER(LNContext)\n        self._ln.lokinet_address.restype = ctypes.c_char_p\n        self._ln.lokinet_address.argtypes = (ctypes.POINTER(LNContext), )\n        self._ln.lokinet_outbound_stream.restype = ctypes.POINTER(ResultStruct)\n        self._ln.lokinet_outbound_stream.argtypes = (ctypes.POINTER(ResultStruct), ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(LNContext))\n        self._ctx = self._ln.lokinet_context_new()\n        self._addrmap = dict()\n        self._debug = debug\n        lvl = 'none'\n        if self._debug:\n            lvl = 'debug'\n        self._ln.lokinet_log_level(ctypes.create_string_buffer(lvl.encode('ascii')))\n\n\n    def free(self, ptr):\n        self._c.free(ptr)\n\n    def add_bootstrap(self, data):\n        ptr = ctypes.create_string_buffer(data)\n        ptrlen = ctypes.c_size_t(len(data))\n        return self.ln_call(\"lokinet_add_bootstrap_rc\", ptr, ptrlen)\n\n    def wait_for_ready(self, ms):\n        return self.ln_call(\"lokinet_wait_for_ready\", ms) == 0\n\n    def ready(self):\n        return self.ln_call(\"lokinet_status\") == 0\n\n    def addr(self):\n        return self._ln.lokinet_address(self._ctx).decode('ascii')\n\n    def expose(self, port):\n        return self.ln_call('lokinet_inbound_stream', port)\n\n    def ln_call(self, funcname, *args):\n        args += (self._ctx,)\n        if self._debug:\n            print(\"call {}{}\".format(funcname, args))\n        return self._ln[funcname](*args)\n\n    def expose(self, port):\n        port = int(port)\n        print(\"exposing loopback port: {}\".format(port))\n        return self.ln_call(\"lokinet_inbound_stream\", port)\n\n    def start(self):\n        return self.ln_call(\"lokinet_context_start\")\n\n    def stop(self):\n        self.ln_call(\"lokinet_context_stop\")\n\n    def hasAddr(self, addr):\n        return addr in self._addrmap\n\n    def putAddr(self, addr, val):\n        self._addrmap[addr] = val\n\n    def getAddr(self, addr):\n        if addr in self._addrmap:\n            return self._addrmap[addr]\n\n    def delAddr(self, addr):\n        if addr in self._addrmap:\n            del self._addrmap[addr]\n\n    def __del__(self):\n        self.stop()\n        self._ln_call(\"lokinet_context_free\")\n\n    def set_netid(self, netid):\n        self._ln.lokinet_set_netid(ctypes.create_string_buffer(netid.encode('ascii')))\n\nclass Stream:\n\n    def __init__(self, ctx):\n        self._ctx = ctx\n        self._id = None\n\n    def connect(self, remote):\n        result = ResultStruct()\n        self._ctx.ln_call(\"lokinet_outbound_stream\", ctypes.cast(ctypes.addressof(result), ctypes.POINTER(ResultStruct)), ctypes.create_string_buffer(remote.encode()), ctypes.c_char_p(0))\n\n        if result.err:\n            print(result.err)\n            return\n        addr = result.local_address.decode('ascii')\n        port = result.local_port\n        self._id = result.stream_id\n        print(\"connection to {} made via {}:{} via {}\".format(remote, addr, port, self._id))\n        return addr, port\n\n\n    def close(self):\n        if self._id is not None:\n            self._ctx.ln_call(\"lokinet_close_stream\", self._id)\n\ndef read_and_forward_or_close(readfd, writefd):\n    read = 0\n    while True:\n        data = os.read(readfd, 1024)\n        read += len(data)\n        if data and len(data) > 0:\n            writefd.write(data)\n            writefd.flush()\n            return True\n        else:\n            return read > 0\n\nctx = None\n\n\nclass Handler(BaseHandler):\n\n    def do_CONNECT(self):\n        self.connect(self.path)\n\n    def connect(self, host):\n        global ctx\n        if not ctx.ready():\n            self.send_error(501)\n            return\n\n        if not ctx.hasAddr(host):\n            stream = Stream(ctx)\n            result = None\n            try:\n                result = stream.connect(host)\n            except:\n                pass\n            if not result:\n                self.send_error(503)\n                return\n            ctx.putAddr(host, result)\n\n        sock = socket.socket()\n        try:\n            sock.connect(ctx.getAddr(host))\n        except:\n            ctx.delAddr(host)\n            self.send_error(504)\n            return\n\n        self.send_response_only(200)\n        self.end_headers()\n        sel = selectors.DefaultSelector()\n        sock.setblocking(False)\n        sockfd = sock.makefile('rwb')\n        sel.register(self.rfile.fileno(), selectors.EVENT_READ, lambda x : read_and_forward_or_close(x, sockfd))\n        sel.register(sock.fileno(), selectors.EVENT_READ, lambda x : read_and_forward_or_close(x, self.wfile))\n        while True:\n            events = sel.select(1)\n            if not events:\n                continue\n            for key, mask in events:\n                if not key.data(key.fileobj):\n                    sel.unregister(self.rfile)\n                    sel.unregister(sock)\n                    sel.close()\n                    return\n\nimport os\nimport sys\nfrom argparse import ArgumentParser as AP\n\nap = AP()\nap.add_argument(\"--ip\", type=str, help=\"ip to bind to\", default=\"127.0.0.1\")\nap.add_argument(\"--port\", type=int, help=\"port to bind to\", default=3000)\nap.add_argument(\"--expose\", type=int, help=\"expose a port to loopback\")\nap.add_argument(\"--bootstrap\", type=str, help=\"bootstrap file\", default=\"bootstrap.signed\")\nap.add_argument(\"--netid\", type=str, help=\"override network id\")\nap.add_argument(\"--debug\", action=\"store_const\", const=True, default=False, help=\"enable verose logging\")\nif bootstrapFromURL:\n    ap.add_argument(\"--bootstrap-url\", type=str, help=\"bootstrap from remote url\", default=\"https://seed.lokinet.org/lokinet.signed\")\n\nargs = ap.parse_args()\naddr = (args.ip, args.port)\nserver = Server(addr, Handler)\n\nctx = Context(args.debug)\n\nif args.netid is not None:\n    print(\"overriding netid: {}\".format(args.netid))\n    ctx.set_netid(args.netid)\n\nif os.path.exists(args.bootstrap):\n    with open(args.bootstrap, 'rb') as f:\n        if ctx.add_bootstrap(f.read()) == 0:\n            print(\"loaded {}\".format(args.bootstrap))\n\nif args.bootstrap_url is not None:\n    print('getting bootstrap info from {}'.format(args.bootstrap_url))\n    resp = requests.get(args.bootstrap_url)\n    if resp.status_code == 200 and ctx.add_bootstrap(resp.content) == 0:\n        pass\n    else:\n        print(\"failed\")\n\nprint(\"starting up...\")\nif ctx.start() != 0:\n    print(\"failed to start\")\n    ctx.stop()\n    sys.exit(-1)\n\nid = None\n\ntry:\n    while not ctx.wait_for_ready(500):\n        print(\"waiting for lokinet...\")\n    lokiaddr = ctx.addr()\n    print(\"we are {}\".format(lokiaddr))\n    if args.expose:\n        id = ctx.expose(args.expose)\n        print(\"exposed {}:{}\".format(lokiaddr, args.expose))\n    print(\"serving on {}:{}\".format(*addr))\n    server.serve_forever()\nfinally:\n    if id is not None:\n        ctx.ln_call(\"lokinet_close_stream\", id)\n    ctx.stop()\n"
  },
  {
    "path": "contrib/py/lnproxy/readme.md",
    "content": "# LN Proxy\n\nembedded lokinet that provides an http tunnel proxy (using http connect)\n\nif `python3-requests` is installed then we can bootstrap from url using `--bootstrap-url` flag\n\nusage:\n\n    $ python3 -m lnproxy [--ip ip | --port port]\n"
  },
  {
    "path": "contrib/py/quic_tester.py",
    "content": "#!/usr/bin/env python3\n\nimport nacl.bindings as sodium\nfrom nacl.public import PrivateKey\nfrom nacl.signing import SigningKey, VerifyKey\nimport nacl.encoding\nimport requests\nimport zmq\nimport zmq.utils.z85\nimport sys\nimport os\nimport re\nimport time\nimport random\nimport shutil\n\nimport json\n\ncontext = zmq.Context()\nsocket = context.socket(zmq.DEALER)\nsocket.setsockopt(zmq.CONNECT_TIMEOUT, 5000)\nsocket.setsockopt(zmq.HANDSHAKE_IVL, 5000)\n#socket.setsockopt(zmq.IMMEDIATE, 1)\n\nif len(sys.argv) > 1 and any(sys.argv[1].startswith(x) for x in (\"ipc://\", \"tcp://\")):\n    remote = sys.argv[1]\n    del sys.argv[1]\nelse:\n    remote = \"ipc://./loki.sock\"\n\ncurve_pubkey = b''\nmy_privkey, my_pubkey = b'', b''\nif len(sys.argv) > 1 and len(sys.argv[1]) == 64 and all(x in \"0123456789abcdefABCDEF\" for x in sys.argv[1]):\n    curve_pubkey = bytes.fromhex(sys.argv[1])\n    del sys.argv[1]\n    socket.curve_serverkey = curve_pubkey\n    if len(sys.argv) > 1 and len(sys.argv[1]) == 64 and all(x in \"0123456789abcdefABCDEF\" for x in sys.argv[1]):\n        my_privkey = bytes.fromhex(sys.argv[1])\n        del sys.argv[1]\n        my_pubkey = zmq.utils.z85.decode(zmq.curve_public(zmq.utils.z85.encode(my_privkey)))\n    else:\n        my_privkey = PrivateKey.generate()\n        my_pubkey = my_privkey.public_key.encode()\n        my_privkey = my_privkey.encode()\n\n        print(\"No curve client privkey given; generated a random one (pubkey: {}, privkey: {})\".format(\n            my_pubkey.hex(), my_privkey.hex()), file=sys.stderr)\n    socket.curve_secretkey = my_privkey\n    socket.curve_publickey = my_pubkey\n\nif not 2 <= len(sys.argv) or any(x in y for x in (\"--help\", \"-h\") for y in sys.argv[1:]):\n    print(\"Usage: {} [ipc:///path/to/sock|tcp://1.2.3.4:5678] [connect|listen] host port\".format(\n        sys.argv[0]), file=sys.stderr)\n    sys.exit(1)\n\naction = sys.argv[1].lower()\nhost = sys.argv[2]\nport = int(sys.argv[3])\nrequest_path = len(sys.argv) >= 5 and sys.argv[4] or '/'\n\nbeginning_of_time = time.clock_gettime(time.CLOCK_MONOTONIC)\n\n#print(\"Connecting to {}\".format(remote), file=sys.stderr)\nsocket.connect(remote)\n\n\ndef rpc(method, args, timeout=15000):\n    to_send = [method.encode(), b'tagxyz123']\n    to_send += (x.encode() for x in [json.dumps(args)])\n    #print(\"Sending {}\".format(to_send[0]), file=sys.stderr)\n    socket.send_multipart(to_send)\n    if socket.poll(timeout=timeout):\n        m = socket.recv_multipart()\n        recv_time = time.clock_gettime(time.CLOCK_MONOTONIC)\n        if len(m) < 3 or m[0:2] != [b'REPLY', b'tagxyz123']:\n            pass\n        else:\n            return json.loads(m[2].decode())\n\n\nargs = {\"host\":host, \"port\":port}\n\ndef success_or_die(response):\n    if response:\n        if 'error' in response and response['error']:\n            print(\"error: {}\".format(response['error']))\n            socket.close(linger=0)\n            sys.exit(1)\n    if response and 'result' in response:\n        return response[\"result\"]\n    else:\n        print(\"no response\")\n        socket.close(linger=0)\n        sys.exit(1)\n\nif action == \"connect\":\n    result = success_or_die(rpc(\"llarp.quic_connect\", args))\n    print(result)\n    cmd = \"curl -vv http://{}{} -o /dev/null\".format(result[\"addr\"], request_path)\n    print(\"{}\".format(cmd))\n    os.system(cmd)\nif action == \"listen\":\n    result = success_or_die(rpc(\"llarp.quic_listener\", args))\n    print(\"ID={} addr={}\".format(result[\"id\"], result[\"addr\"]))\n"
  },
  {
    "path": "contrib/readme-installer.txt",
    "content": "Lokinet is the reference implementation of LLARP (low latency anonymous routing protocol), a layer 3 onion routing protocol.\n\nThis installer provides the needed control panel to get up an running on Lokinet.\n\nYou can view additional documentation and information on Lokinet at https://lokinet.org\n"
  },
  {
    "path": "contrib/systemd-resolved/README.md",
    "content": "Lokinet now talks to systemd directly via sdbus to set up DNS, but in order for this to work the\nuser running lokinet (assumed `_lokinet` in these example files) needs permission to set dns servers\nand domains.\n\nTo set up the permissions:\n\n- If lokinet is running as some user other than `_lokinet` the change the `_lokinet` username inside\n  `lokinet.rules` and `lokinet.pkla`.\n\n- If on a Debian or Debian-derived distribution (such as Ubuntu) using polkit 105,\n  copy `lokinet.pkla` to `/var/lib/polkit-1/localauthority/10-vendor.d/lokinet.pkla` (for a distro\n  install) or `/etc/polkit-1/localauthority.conf.d/` (for a local install).\n\n- Copy `lokinet.rules` to `/usr/share/polkit-1/rules.d/` (distro install) or `/etc/polkit-1/rules.d`\n  (local install).\n\nMake use of it by switching to systemd-resolved:\n```\nsudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf\nsudo systemctl enable --now systemd-resolved\n```\n"
  },
  {
    "path": "contrib/systemd-resolved/lokinet.pkla",
    "content": "[Allow lokinet to set DNS settings]\nIdentity=unix-user:_lokinet\nAction=org.freedesktop.resolve1.set-dns-servers;org.freedesktop.resolve1.set-domains\nResultAny=yes\n"
  },
  {
    "path": "contrib/systemd-resolved/lokinet.rules",
    "content": "/* Allow lokinet to set DNS settings */\npolkit.addRule(function(action, subject) {\n    if ((action.id == \"org.freedesktop.resolve1.set-dns-servers\" ||\n         action.id == \"org.freedesktop.resolve1.set-domains\") &&\n        subject.user == \"_lokinet\") {\n        return polkit.Result.YES;\n    }\n});\n\n"
  },
  {
    "path": "contrib/tarball.sh",
    "content": "#!/usr/bin/env bash\n#\n# create signed release tarball with submodules bundled\n# usage: ./contrib/tarball.sh [keyid]\n#\nrepo=$(readlink -e $(dirname $0)/..)\nbranch=$(test -e $repo/.git/ && git rev-parse --abbrev-ref HEAD)\nout=\"lokinet-$(git describe --exact-match --tags $(git log -n1 --pretty='%h') 2> /dev/null || ( echo -n $branch- && git rev-parse --short HEAD)).tar.xz\"\ngit-archive-all -C $repo --force-submodules $out && rm -f $out.sig && (gpg -u ${1:-jeff@lokinet.io} --sign --detach $out &> /dev/null && gpg --verify $out.sig)\n"
  },
  {
    "path": "contrib/windows-configure.sh",
    "content": "#!/bin/bash\nset -e\nset -x\n\n# Usage: windows-configure.sh [rootdir [builddir]] -DWHATEVER=BLAH ...\n\nif [ $# -ge 1 ] && [[ \"$1\" != -* ]]; then\n    root=\"$1\"\n    shift\nelse\n    root=\"$(dirname $0)\"/..\nfi\nroot=\"$(readlink -f \"$root\")\"\n\nif [ $# -ge 1 ] && [[ \"$1\" != -* ]]; then\n    build=\"$(readlink -f \"$1\")\"\n    shift\nelse\n    build=\"$root/build/win32\"\n    echo \"Setting up build in $build\"\nfi\n\nmkdir -p \"$build\"\ncmake \\\n    -S \"$root\" -B \"$build\" \\\n    -G 'Unix Makefiles' \\\n    -DCMAKE_EXE_LINKER_FLAGS=-fstack-protector \\\n    -DCMAKE_CXX_FLAGS=-fdiagnostics-color=always \\\n    -DCMAKE_TOOLCHAIN_FILE=\"$root/contrib/cross/mingw64.cmake\" \\\n    -DCMAKE_BUILD_TYPE=Release \\\n    -DBUILD_STATIC_DEPS=ON \\\n    -DLOKINET_PACKAGE=ON \\\n    -DBUILD_SHARED_LIBS=OFF \\\n    -DBUILD_TESTING=OFF \\\n    -DLOKINET_TESTS=OFF \\\n    -DLOKINET_BOOTSTRAP=OFF \\\n    -DLOKINET_NATIVE_BUILD=OFF \\\n    -DSTATIC_LINK=ON \\\n    -DWITH_SYSTEMD=OFF \\\n    -DFORCE_OXENMQ_SUBMODULE=ON \\\n    -DFORCE_OXENC_SUBMODULE=ON \\\n    -DFORCE_FMT_SUBMODULE=ON \\\n    -DFORCE_SPDLOG_SUBMODULE=ON \\\n    -DFORCE_NLOHMANN_SUBMODULE=ON \\\n    -DWITH_LTO=OFF \\\n    \"$@\"\n"
  },
  {
    "path": "contrib/windows.sh",
    "content": "#!/bin/bash\n#\n# helper script for me for when i cross compile for windows\n# t. jeff\n#\n\nset -e\nset +x\nroot=\"$(readlink -f $(dirname $0)/../)\"\nmkdir -p $root/build/win32\n$root/contrib/windows-configure.sh $root $root/build/win32 \"$@\"\nmake package -j${JOBS:-$(nproc)} -C $root/build/win32\n\n"
  },
  {
    "path": "daemon/CMakeLists.txt",
    "content": "set(exetargets lokinet)\n\nadd_library(lokinet_daemon STATIC utils.cpp)\ntarget_link_libraries(lokinet_daemon PUBLIC liblokinet)\n\nif(APPLE)\n  add_executable(lokinet lokinet.swift)\n  target_compile_options(lokinet BEFORE PRIVATE -target x86_64-apple-macos${CMAKE_OSX_DEPLOYMENT_TARGET})\nelse()\n  add_executable(lokinet lokinet.cpp)\nendif()\n\nadd_executable(lokinet-cntrl lokinet-cntrl.cpp)\nenable_lto(lokinet lokinet-cntrl)\nlist(APPEND exetargets lokinet-cntrl)\n\n\nset(should_install ON)\nset(SETCAP)\n\nif(CMAKE_SYSTEM_NAME MATCHES \"Linux\")\n  option(WITH_SETCAP \"use setcap when installing\" ON)\n  if(WITH_SETCAP)\n    find_program(SETCAP NAMES setcap HINTS /sbin /usr/sbin)\n    if(SETCAP)\n      message(STATUS \"Found setcap binary: ${SETCAP}\")\n    else()\n      message(WARNING \"cannot find setcap binary you will not be able use the install targets unless you use -DWITH_SETCAP=OFF\")\n      set(should_install OFF)\n    endif()\n  endif()\nendif()\n\n# cmake interface library for bunch of cmake hacks to fix final link order\nadd_library(hax_and_shims_for_cmake INTERFACE)\nif(WIN32)\n  target_link_libraries(hax_and_shims_for_cmake INTERFACE uvw oxenmq::oxenmq -lws2_32 -lshlwapi -ldbghelp -luser32 -liphlpapi -lpsapi -luserenv)\nendif()\n\nforeach(exe ${exetargets})\n  if(WIN32)\n    target_sources(${exe} PRIVATE ${CMAKE_BINARY_DIR}/${exe}.rc)\n    target_link_libraries(${exe} PRIVATE -static-libstdc++ -static-libgcc --static -Wl,--pic-executable,-e,mainCRTStartup,--subsystem,console:5.00)\n  elseif(CMAKE_SYSTEM_NAME MATCHES \"FreeBSD\")\n    target_link_directories(${exe} PRIVATE /usr/local/lib)\n  endif()\n  target_link_libraries(${exe} PRIVATE lokinet_daemon)\n  if(LOKINET_STRIP)\n    add_custom_command(TARGET ${exe}\n      POST_BUILD\n      COMMAND ${CMAKE_OBJCOPY} ARGS --only-keep-debug $<TARGET_FILE:${exe}> $<TARGET_FILE:${exe}>.debug\n      COMMAND ${CMAKE_STRIP} ARGS --strip-all $<TARGET_FILE:${exe}>)\n  endif()\n  target_include_directories(${exe} PUBLIC \"${PROJECT_SOURCE_DIR}\")\n  if(should_install)\n    if(APPLE)\n      install(TARGETS ${exe}\n        BUNDLE DESTINATION \"${PROJECT_BINARY_DIR}\"\n        RUNTIME DESTINATION \".\"\n        COMPONENT lokinet)\n    else()\n      install(TARGETS ${exe} RUNTIME DESTINATION bin COMPONENT lokinet)\n    endif()\n  endif()\nendforeach()\n\ntarget_link_libraries(lokinet PRIVATE CLI11)\ntarget_link_libraries(lokinet-cntrl PRIVATE CLI11)\n\nif(WITH_SETCAP)\n  install(CODE \"execute_process(COMMAND ${SETCAP} cap_net_admin,cap_net_bind_service=+eip ${CMAKE_INSTALL_PREFIX}/bin/lokinet)\")\nendif()\n\nif(LOKINET_STRIP)\n  add_custom_target(symbols ALL\n    COMMAND ${CMAKE_COMMAND} -E tar cJf ${CMAKE_CURRENT_BINARY_DIR}/debug-symbols.tar.xz $<TARGET_FILE:lokinet>.debug\n    DEPENDS lokinet)\nendif()\n"
  },
  {
    "path": "daemon/lokinet-cntrl.cpp",
    "content": "#include \"utils.hpp\"\n\n#include <CLI/CLI.hpp>\n#include <nlohmann/json.hpp>\n#include <oxenmq/oxenmq.h>\n\nusing namespace llarp;\nusing namespace nlohmann;\n\nnamespace\n{\n    /**\n        Startup CLI options:\n        - verbose\n        - config file pathways\n        - log level\n        - loki controller RPC client URL\n\n        Runtime CLI subcommands:\n        - list\n        - refresh\n        - instance\n            - init\n            - status\n            - close\n            - stop\n     */\n\n    struct cli_opts\n    {\n        bool verbose{false};\n\n        std::vector<std::string> rpc_paths{\n            {\"tcp://127.0.0.1:1190\"}, {\"tcp://127.0.0.1:1189\"}, {\"tcp://127.0.0.1:1188\"}};\n\n        omq::address rpc_url{};\n        std::string log_level{\"info\"};\n        log::Level oxen_log_level{log::Level::info};\n    };\n\n    struct app_data\n    {\n        std::mutex m;\n        std::deque<std::string> input_que{};\n        std::condition_variable cv;\n        std::atomic<bool> running{false};\n    };\n\n}  // namespace\n\nnamespace\n{\n    static std::shared_ptr<app_data> runtime_data;\n\n    template <typename... T>\n    static int exit_now(bool is_error, fmt::format_string<T...> format, T&&... args)\n    {\n        if (is_error)\n            log::error(controller::logcat, format, std::forward<T>(args)...);\n        else\n            log::info(controller::logcat, format, std::forward<T>(args)...);\n\n        runtime_data->running = false;\n        runtime_data->cv.notify_all();\n\n        return is_error ? 1 : 0;\n    }\n\n    auto prefigure = []() -> int {\n        if (not runtime_data)\n            runtime_data = std::make_shared<app_data>();\n\n        return 0;\n    };\n\n    static void app_loop(cli_opts&& options, std::promise<void>&& p)\n    {\n        controller::rpc_controller rpc;\n\n        if (not rpc.start(options.rpc_paths))\n        {\n            log::critical(controller::logcat, \"RPC controller failed to bind; exiting...\");\n            p.set_value_at_thread_exit();\n            return;\n        }\n\n        size_t index{};\n        std::string address{};\n        std::string pubkey{};\n\n        CLI::App app{};\n        auto app_fmt = app.get_formatter();\n        app_fmt->column_width(40);\n        app.set_help_flag(\"\");\n\n        // inner app options\n        auto* hcom = app.add_subcommand(\"help\", \"Print help menu\")->silent();\n        hcom->callback([&]() {\n                app.clear();\n                std::cout << app.help(\"\", CLI::AppFormatMode::Normal) << std::endl;\n                for (auto& com : app.get_subcommands(nullptr))\n                {\n                    std::cout << com->help(\"\", CLI::AppFormatMode::Sub) << std::endl;\n                    for (auto& c : com->get_subcommands(nullptr))\n                        std::cout << c->help(\"\", CLI::AppFormatMode::Sub) << std::endl;\n                }\n            })\n            ->immediate_callback();\n\n        auto* list_subcom =\n            app.add_subcommand(\"list\", \"List all lokinet instances currently running on the local machine\");\n        list_subcom->callback([&]() { rpc.list_all(); })->immediate_callback();\n\n        auto* refresh_subcom = app.add_subcommand(\"refresh\", \"Refresh local lokinet instance information\");\n        refresh_subcom->callback([&]() { rpc.refresh(); });\n\n        auto* instance_subcom =\n            app.add_subcommand(\"instance\", \"Select a lokinet instance\")->require_option(1)->require_subcommand(1);\n        auto* aopt = instance_subcom->add_option(\"-a, --address\", address, \"Local RPC address of lokinet instance\")\n                         ->type_name(\"IP:PORT\");\n        auto* iopt =\n            instance_subcom->add_option(\"-i, --index\", index, \"Index of local lokinet instance (use 'list' to query!)\");\n\n        aopt->excludes(iopt);\n        iopt->excludes(aopt);\n\n        auto* init_subcom =\n            instance_subcom->add_subcommand(\"init\", \"Initiate session to a remote instance\")->require_option(1);\n        init_subcom->add_option(\"-p, --pubkey\", pubkey, \"PubKey of remote lokinet instance\");\n\n        init_subcom->callback([&]() {\n            if (not address.empty())\n                rpc.initiate(omq::address{std::move(address)}, std::move(pubkey));\n            else\n                rpc.initiate(index, std::move(pubkey));\n        });\n\n        auto* status_subcom = instance_subcom->add_subcommand(\"status\", \"Query status of local lokinet instance\");\n\n        status_subcom->callback([&]() {\n            if (not address.empty())\n                rpc.status(omq::address{std::move(address)});\n            else\n                rpc.status(index);\n        });\n\n        auto* close_subcom =\n            instance_subcom->add_subcommand(\"close\", \"Close session to a remote instance\")->require_option(1);\n        close_subcom->add_option(\"-p, --pubkey\", pubkey, \"PubKey of remote lokinet instance\");\n\n        close_subcom->callback([&]() {\n            if (not address.empty())\n                rpc.close(omq::address{std::move(address)}, std::move(pubkey));\n            else\n                rpc.close(index, std::move(pubkey));\n        });\n\n        auto* halt_subcom = instance_subcom->add_subcommand(\"halt\", \"Immediately halt lokinet instance\");\n\n        halt_subcom->callback([&]() {\n            if (not address.empty())\n                rpc.halt(omq::address{std::move(address)});\n            else\n                rpc.halt(index);\n        });\n\n        // notify startup successful\n        runtime_data->running = true;\n        p.set_value();\n\n        while (runtime_data->running)\n        {\n            try\n            {\n                std::deque<std::string> copy{};\n\n                {\n                    std::unique_lock<std::mutex> lock{runtime_data->m, std::defer_lock};\n                    runtime_data->cv.wait(\n                        lock, []() { return !runtime_data->input_que.empty() || !runtime_data->running; });\n                    copy.swap(runtime_data->input_que);\n                }\n\n                if (!copy.empty())\n                {\n                    log::debug(controller::logcat, \"processing input...\");\n                    while (!copy.empty())\n                    {\n                        auto line = copy.front();\n                        copy.pop_front();\n                        log::debug(controller::logcat, \"input: {}\", line);\n                        app.parse(line);\n                    }\n                }\n            }\n            catch (const std::exception& e)\n            {\n                log::warning(controller::logcat, \"Exception: {}\", e.what());\n            }\n\n            app.clear();\n        }\n    }\n\n    static void input_loop()\n    {\n        log::info(controller::logcat, \"input loop started...\");\n\n        std::string input;\n        while (runtime_data->running)\n        {\n            std::getline(std::cin, input);\n\n            if (input == \"exit\")\n            {\n                runtime_data->running = false;\n                runtime_data->cv.notify_all();\n                break;\n            }\n\n            {\n                std::lock_guard<std::mutex> lock{runtime_data->m};\n                runtime_data->input_que.push_back(std::move(input));\n            }\n\n            log::debug(controller::logcat, \"dispatched...\");\n            runtime_data->cv.notify_all();\n        }\n\n        log::info(controller::logcat, \"input loop exiting...\");\n    }\n}  // namespace\n\nint main(int argc, char* argv[])\n{\n    if (auto rv = prefigure(); rv != 0)\n        return rv;\n\n    CLI::App cli{\"loki controller - lokinet instance control utility\", \"lokinet-cntrl\"};\n    cli.get_formatter()->column_width(50);\n    cli_opts options{};\n\n    // initial options\n    cli.add_flag(\"-v, --verbose\", options.verbose, \"Verbose logging (equivalent to passing '--log-level=debug')\");\n    cli.add_option(\n           \"-r, --rpc\",\n           options.rpc_paths,\n           \"Specify RPC bind addresses for loki controller to connect to (accepts multiple args)\")\n        ->type_name(\"PATH(S)\")\n        ->capture_default_str();\n    cli.add_option(\n           \"-l, --log-level\", options.log_level, \"Log verbosity level ('error', 'warn', 'info', 'debug', 'trace')\")\n        ->type_name(\"LEVEL\")\n        ->capture_default_str();\n\n    try\n    {\n        cli.parse(argc, argv);\n    }\n    catch (const CLI::ParseError& e)\n    {\n        return exit_now(true, \"Exception: {}\", e.what());\n    }\n    catch (const std::exception& e)\n    {\n        return exit_now(true, \"Exception: {}\", e.what());\n    }\n\n    options.oxen_log_level = log::level_from_string(options.log_level);\n\n    if (options.verbose)\n        options.oxen_log_level = log::Level::debug;\n\n    log::add_sink(log::Type::Print, \"stderr\");\n    log::reset_level(options.oxen_log_level);\n\n    log::info(controller::logcat, \"initializing...\");\n\n    try\n    {\n        std::promise<void> p;\n        auto f = p.get_future();\n\n        std::thread app_thread{app_loop, std::move(options), std::move(p)};\n\n        f.get();\n\n        std::thread input_thread{input_loop};\n\n        app_thread.join();\n        input_thread.join();\n    }\n    catch (const std::exception& e)\n    {\n        return exit_now(true, \"Exception: {}\", e.what());\n    }\n\n    log::info(controller::logcat, \"exiting...\");\n    return 0;\n}\n"
  },
  {
    "path": "daemon/lokinet.cpp",
    "content": "#include <llarp.hpp>\n#include <llarp/config/config.hpp>  // for ensure_config\n#include <llarp/constants/platform.hpp>\n#include <llarp/constants/version.hpp>\n#include <llarp/util/exceptions.hpp>\n#include <llarp/util/lokinet_init.h>\n#include <llarp/util/thread/threading.hpp>\n\n#include <CLI/CLI.hpp>\n#include <fmt/core.h>\n#include <oxen/log.hpp>\n#include <oxen/quic/loop.hpp>\n\n#include <csignal>\n#include <memory>\n#include <stdexcept>\n#include <thread>\n\n#ifdef _WIN32\n#include <llarp/win32/service_manager.hpp>\n#else\n#include <llarp/util/service_manager.hpp>\n#endif\n\nnamespace\n{\n    struct command_line_options\n    {\n        // bool options\n        bool help = false;\n        bool version = false;\n        bool generate = false;\n        bool generate_embedded = false;\n        bool router = false;\n        bool config = false;\n        bool overwrite = false;\n\n        // string options\n        // TODO: change this to use a std::filesystem::path once we stop using ghc::filesystem on\n        // some platforms\n        std::string configPath;\n\n        // windows options\n        bool win_install = false;\n        bool win_remove = false;\n    };\n\n    // windows-specific function declarations\n    int startWinsock();\n    void install_win32_daemon();\n    void uninstall_win32_daemon();\n\n    // operational function definitions\n    int lokinet_main(int, char**);\n    void handle_signal(int sig);\n    static void start_lokinet(std::optional<std::filesystem::path> confFile, bool snode);\n\n    // variable declarations\n    static auto logcat = llarp::log::Cat(\"daemon\");\n    std::unique_ptr<llarp::Context> ctx;\n\n    // operational function definitions\n    void handle_signal(int sig)\n    {\n        llarp::log::info(logcat, \"Handling signal {}\", sig);\n\n        if (ctx)\n            ctx->signal(sig);\n        else\n            std::cerr << \"Received signal \" << sig << \", but have no context yet. Ignoring!\" << std::endl;\n    }\n\n    // Windows specific code\n#ifdef _WIN32\n    extern \"C\" LONG FAR PASCAL win32_signal_handler(EXCEPTION_POINTERS*);\n    extern \"C\" VOID FAR PASCAL win32_daemon_entry(DWORD, LPTSTR*);\n    VOID insert_description();\n\n    extern \"C\" BOOL FAR PASCAL handle_signal_win32(DWORD fdwCtrlType)\n    {\n        UNREFERENCED_PARAMETER(fdwCtrlType);\n        handle_signal(SIGINT);\n        return TRUE;  // probably unreachable\n    };\n\n    int startWinsock()\n    {\n        WSADATA wsockd;\n        int err;\n        err = ::WSAStartup(MAKEWORD(2, 2), &wsockd);\n        if (err)\n        {\n            perror(\"Failed to start Windows Sockets\");\n            return err;\n        }\n        ::CreateMutex(nullptr, FALSE, \"lokinet_win32_daemon\");\n        return 0;\n    }\n\n    void install_win32_daemon()\n    {\n        SC_HANDLE schSCManager;\n        SC_HANDLE schService;\n        std::array<char, 1024> szPath{};\n\n        if (!GetModuleFileName(nullptr, szPath.data(), MAX_PATH))\n        {\n            llarp::log::error(logcat, \"Cannot install service {}\", GetLastError());\n            return;\n        }\n\n        // Get a handle to the SCM database.\n        schSCManager = OpenSCManager(\n            nullptr,                 // local computer\n            nullptr,                 // ServicesActive database\n            SC_MANAGER_ALL_ACCESS);  // full access rights\n\n        if (nullptr == schSCManager)\n        {\n            llarp::log::error(logcat, \"OpenSCManager failed {}\", GetLastError());\n            return;\n        }\n\n        // Create the service\n        schService = CreateService(\n            schSCManager,               // SCM database\n            strdup(\"lokinet\"),          // name of service\n            \"Lokinet for Windows\",      // service name to display\n            SERVICE_ALL_ACCESS,         // desired access\n            SERVICE_WIN32_OWN_PROCESS,  // service type\n            SERVICE_DEMAND_START,       // start type\n            SERVICE_ERROR_NORMAL,       // error control type\n            szPath.data(),              // path to service's binary\n            nullptr,                    // no load ordering group\n            nullptr,                    // no tag identifier\n            nullptr,                    // no dependencies\n            nullptr,                    // LocalSystem account\n            nullptr);                   // no password\n\n        if (schService == nullptr)\n        {\n            llarp::log::error(logcat, \"CreateService failed {}\", GetLastError());\n            CloseServiceHandle(schSCManager);\n            return;\n        }\n        else\n            llarp::log::info(logcat, \"Service installed successfully\");\n\n        CloseServiceHandle(schService);\n        CloseServiceHandle(schSCManager);\n        insert_description();\n    }\n\n    VOID insert_description()\n    {\n        SC_HANDLE schSCManager;\n        SC_HANDLE schService;\n        SERVICE_DESCRIPTION sd;\n        LPTSTR szDesc = strdup(\n            \"LokiNET is a free, open source, private, \"\n            \"decentralized, \\\"market based sybil resistant\\\" \"\n            \"and IP based onion routing network\");\n        // Get a handle to the SCM database.\n        schSCManager = OpenSCManager(\n            NULL,                    // local computer\n            NULL,                    // ServicesActive database\n            SC_MANAGER_ALL_ACCESS);  // full access rights\n\n        if (nullptr == schSCManager)\n        {\n            llarp::log::error(logcat, \"OpenSCManager failed {}\", GetLastError());\n            return;\n        }\n\n        // Get a handle to the service.\n        schService = OpenService(\n            schSCManager,            // SCM database\n            \"lokinet\",               // name of service\n            SERVICE_CHANGE_CONFIG);  // need change config access\n\n        if (schService == nullptr)\n        {\n            llarp::log::error(logcat, \"OpenService failed {}\", GetLastError());\n            CloseServiceHandle(schSCManager);\n            return;\n        }\n\n        // Change the service description.\n        sd.lpDescription = szDesc;\n\n        if (!ChangeServiceConfig2(\n                schService,                  // handle to service\n                SERVICE_CONFIG_DESCRIPTION,  // change: description\n                &sd))                        // new description\n        {\n            llarp::log::error(logcat, \"ChangeServiceConfig2 failed\");\n        }\n        else\n            llarp::log::info(log_cat, \"Service description updated successfully.\");\n\n        CloseServiceHandle(schService);\n        CloseServiceHandle(schSCManager);\n    }\n\n    void uninstall_win32_daemon()\n    {\n        SC_HANDLE schSCManager;\n        SC_HANDLE schService;\n\n        // Get a handle to the SCM database.\n        schSCManager = OpenSCManager(\n            nullptr,                 // local computer\n            nullptr,                 // ServicesActive database\n            SC_MANAGER_ALL_ACCESS);  // full access rights\n\n        if (nullptr == schSCManager)\n        {\n            llarp::log::error(logcat, \"OpenSCManager failed {}\", GetLastError());\n            return;\n        }\n\n        // Get a handle to the service.\n        schService = OpenService(\n            schSCManager,  // SCM database\n            \"lokinet\",     // name of service\n            0x10000);      // need delete access\n\n        if (schService == nullptr)\n        {\n            llarp::log::error(logcat, \"OpenService failed {}\", GetLastError());\n            CloseServiceHandle(schSCManager);\n            return;\n        }\n\n        // Delete the service.\n        if (!DeleteService(schService))\n        {\n            llarp::log::error(logcat, \"DeleteService failed {}\", GetLastError());\n        }\n        else\n            llarp::log::info(logcat, \"Service deleted successfully\");\n\n        CloseServiceHandle(schService);\n        CloseServiceHandle(schSCManager);\n    }\n\n    /// minidump generation for windows jizz\n    /// will make a coredump when there is an unhandled exception\n    LONG GenerateDump(EXCEPTION_POINTERS* pExceptionPointers)\n    {\n        const auto flags = (MINIDUMP_TYPE)(MiniDumpWithFullMemory | MiniDumpWithFullMemoryInfo | MiniDumpWithHandleData\n                                           | MiniDumpWithUnloadedModules | MiniDumpWithThreadInfo);\n\n        const std::string fname = fmt::format(\"C:\\\\ProgramData\\\\lokinet\\\\crash-{}.dump\", llarp::time_now_ms().count());\n\n        HANDLE hDumpFile;\n        SYSTEMTIME stLocalTime;\n        GetLocalTime(&stLocalTime);\n        MINIDUMP_EXCEPTION_INFORMATION ExpParam{};\n\n        hDumpFile = CreateFile(\n            fname.c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_WRITE | FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0);\n\n        ExpParam.ExceptionPointers = pExceptionPointers;\n        ExpParam.ClientPointers = TRUE;\n\n        MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), hDumpFile, flags, &ExpParam, NULL, NULL);\n\n        return 1;\n    }\n\n    VOID FAR PASCAL SvcCtrlHandler(DWORD dwCtrl)\n    {\n        // Handle the requested control code.\n\n        switch (dwCtrl)\n        {\n            case SERVICE_CONTROL_STOP:\n                // tell service we are stopping\n                llarp::log::debug(logcat, \"Windows service controller gave SERVICE_CONTROL_STOP\");\n                llarp::sys::service_manager->system_changed_our_state(llarp::sys::ServiceState::Stopping);\n                handle_signal(SIGINT);\n                return;\n\n            case SERVICE_CONTROL_INTERROGATE:\n                // report status\n                llarp::log::debug(logcat, \"Got win32 service interrogate signal\");\n                llarp::sys::service_manager->report_changed_state();\n                return;\n\n            default:\n                llarp::log::debug(logcat, \"Got win32 unhandled signal {}\", dwCtrl);\n                break;\n        }\n    }\n\n    // The win32 daemon entry point is where we go when invoked as a windows service; we do the\n    // required service dance and then pretend we were invoked via main().\n    VOID FAR PASCAL win32_daemon_entry(DWORD, LPTSTR* argv)\n    {\n        // Register the handler function for the service\n        auto* svc = dynamic_cast<llarp::sys::SVC_Manager*>(llarp::sys::service_manager);\n        svc->handle = RegisterServiceCtrlHandler(\"lokinet\", SvcCtrlHandler);\n\n        if (svc->handle == nullptr)\n        {\n            llarp::log::error(logcat, \"failed to register daemon control handler\");\n            return;\n        }\n\n        // we hard code the args to lokinet_main.\n        // we yoink argv[0] (lokinet.exe path) and pass in the new args.\n        std::array args = {\n            reinterpret_cast<char*>(argv[0]),\n            reinterpret_cast<char*>(strdup(\"c:\\\\programdata\\\\lokinet\\\\lokinet.ini\")),\n            reinterpret_cast<char*>(0)};\n        lokinet_main(args.size() - 1, args.data());\n    }\n\n#endif\n\n    int lokinet_main(int argc, char** argv)\n    {\n#ifdef _WIN32\n        if (startWinsock())\n            return -1;\n        SetConsoleCtrlHandler(handle_signal_win32, TRUE);\n#endif\n\n        CLI::App cli{\n            \"Lokinet is a free, open source, private, decentralized, market-based sybil resistant \"\n            \"and IP based onion routing network lokinet\"};\n        command_line_options options{};\n\n        // flags: boolean values in command_line_options struct\n        cli.add_flag(\"--version\", options.version, \"Lokinet version\");\n        cli.add_flag(\"-g,--generate\", options.generate, \"Generate default configuration and exit\");\n        cli.add_flag(\"-r,--router\", options.router, \"Run lokinet in routing mode instead of client-only mode\");\n        cli.add_flag(\n            \"-e,--generate-embedded\",\n            options.generate_embedded,\n            \"Generate a default config file for an embedded clients and exit\");\n        cli.add_flag(\"-f,--force\", options.overwrite, \"Force writing config even if file exists\");\n\n        // options: string\n        cli.add_option(\"config,--config\", options.configPath, \"Path to lokinet.ini configuration file\")\n            ->capture_default_str();\n\n        if constexpr (llarp::platform::is_windows)\n        {\n            cli.add_flag(\"--install\", options.win_install, \"Install win32 daemon to SCM\");\n            cli.add_flag(\"--remove\", options.win_remove, \"Remove win32 daemon from SCM\");\n        }\n\n        try\n        {\n            cli.parse(argc, argv);\n        }\n        catch (const CLI::ParseError& e)\n        {\n            return cli.exit(e);\n        }\n\n        std::optional<std::filesystem::path> configFile;\n\n        try\n        {\n            if (options.version)\n            {\n                std::cout << llarp::LOKINET_VERSION_FULL << std::endl;\n                return 0;\n            }\n\n            if constexpr (llarp::platform::is_windows)\n            {\n                if (options.win_install)\n                {\n                    install_win32_daemon();\n                    return 0;\n                }\n                if (options.win_remove)\n                {\n                    uninstall_win32_daemon();\n                    return 0;\n                }\n            }\n\n            if (not options.configPath.empty())\n            {\n                configFile = options.configPath;\n            }\n        }\n        catch (const CLI::OptionNotFound& e)\n        {\n            cli.exit(e);\n        }\n        catch (const CLI::Error& e)\n        {\n            cli.exit(e);\n        }\n\n        auto type = options.generate_embedded ? llarp::config::Type::EmbeddedClient\n            : options.router                  ? llarp::config::Type::Relay\n                                              : llarp::config::Type::FullClient;\n\n        if (configFile.has_value())\n        {\n            // when we have an explicit filepath\n            std::filesystem::path basedir = configFile->parent_path();\n            if (options.generate || options.generate_embedded)\n            {\n                try\n                {\n                    llarp::ensure_config(basedir, *configFile, options.overwrite, type);\n                }\n                catch (std::exception& ex)\n                {\n                    llarp::log::error(logcat, \"cannot generate config at {}: {}\", *configFile, ex.what());\n                    return 1;\n                }\n            }\n            else\n            {\n                try\n                {\n                    if (!exists(*configFile))\n                    {\n                        llarp::log::error(logcat, \"Config file not found {}\", *configFile);\n                        return 1;\n                    }\n                }\n                catch (std::exception& ex)\n                {\n                    llarp::log::error(logcat, \"cannot check if \", *configFile, \" exists: \", ex.what());\n                    return 1;\n                }\n            }\n        }\n        else\n        {\n            try\n            {\n                llarp::ensure_config(\n                    llarp::GetDefaultDataDir(), llarp::GetDefaultConfigPath(), options.overwrite, type);\n            }\n            catch (std::exception& ex)\n            {\n                llarp::log::error(logcat, \"cannot ensure config: {}\", ex.what());\n                return 1;\n            }\n            configFile = llarp::GetDefaultConfigPath();\n        }\n\n        if (options.generate || options.generate_embedded)\n            return 0;\n\n#ifdef _WIN32\n        SetUnhandledExceptionFilter(&GenerateDump);\n#endif\n\n        try\n        {\n            start_lokinet(configFile, options.router);\n        }\n        catch (const std::exception& e)\n        {\n            std::cerr << \"\\nLokinet failed to start: \" << e.what() << \"\\n\\n\";\n            return 1;\n        }\n\n        std::promise<void> watchdog_stop;\n        std::thread watchdog{[ftr = watchdog_stop.get_future()] {\n            llarp::util::SetThreadName(\"llarp-watchdog\");\n            while (ftr.wait_for(1s) != std::future_status::ready)\n            {\n                // do periodic non lokinet related tasks here\n                if (ctx and ctx->is_up() and not ctx->looks_alive())\n                {\n                    auto deadlock_cat = llarp::log::Cat(\"deadlock\");\n                    llarp::log::critical(deadlock_cat, \"Router has deadlocked!\");\n                    llarp::log::flush();\n                    llarp::sys::service_manager->failed();\n                    std::abort();\n                }\n            }\n        }};\n\n        ctx->wait();\n        watchdog_stop.set_value();\n        watchdog.join();\n\n        llarp::log::flush();\n        llarp::sys::service_manager->stopped();\n        ctx.reset();\n        return 0;\n    }\n\n    // this sets up, configures and runs the main context\n    static void start_lokinet(std::optional<std::filesystem::path> confFile, bool snode)\n    {\n        llarp::log::info(logcat, \"starting up {}\", llarp::LOKINET_VERSION_FULL);\n        try\n        {\n            auto type = snode ? llarp::config::Type::Relay : llarp::config::Type::FullClient;\n            std::optional<llarp::Config> conf;\n            try\n            {\n                conf = confFile ? llarp::Config{type, *confFile} : llarp::Config{type, \"\", llarp::GetDefaultDataDir()};\n            }\n            catch (const std::exception& e)\n            {\n                llarp::log::error(logcat, \"Failed to load config: {}\", e.what());\n                throw;\n            }\n\n            ctx = std::make_unique<llarp::Context>(/*embedded=*/false);\n\n            signal(SIGINT, handle_signal);\n            signal(SIGTERM, handle_signal);\n            signal(SIGKILL, handle_signal);\n\n            llarp::util::SetThreadName(\"llarp-main\");\n            ctx->start(std::move(*conf));\n        }\n        catch (llarp::util::bind_socket_error& ex)\n        {\n            auto msg = \"{}; is lokinet already running?\"_format(ex.what());\n            llarp::log::error(logcat, \"{}\", msg);\n            throw std::runtime_error{msg};\n        }\n        catch (const std::exception& ex)\n        {\n            // Don't need to log here: context has already error logged the exception message\n            throw;\n        }\n    }\n\n}  // namespace\n\nint main(int argc, char* argv[])\n{\n    // Set up a default, stderr logging for very early logging; we'll replace this later once we\n    // read the desired log info from config.\n    oxen::log::add_sink(llarp::log::Type::Print, \"stderr\");\n    oxen::log::reset_level(llarp::log::Level::info);\n    // oxen::log::set_level(\"quic\", oxen::log::Level::info);\n\n    // TODO FIXME: this seems to be segfaulting?\n    // llarp::logRingBuffer = std::make_shared<llarp::log::RingBufferSink>(100);\n    // oxen::log::add_sink(llarp::logRingBuffer, llarp::log::DEFAULT_PATTERN_MONO);\n\n#ifndef _WIN32\n    return lokinet_main(argc, argv);\n#else\n    if (auto hntdll = GetModuleHandle(\"ntdll.dll\"))\n    {\n        if (GetProcAddress(hntdll, \"wine_get_version\"))\n        {\n            static const char* text = \"Don't run lokinet in wine, aborting startup\";\n            static const char* title = \"Lokinet Wine Error\";\n            MessageBoxA(NULL, text, title, MB_ICONHAND);\n            std::abort();\n        }\n    }\n\n    SERVICE_TABLE_ENTRY DispatchTable[] = {\n        {strdup(\"lokinet\"), (LPSERVICE_MAIN_FUNCTION)win32_daemon_entry}, {NULL, NULL}};\n\n    // Try first to run as a service; if this works it fires off to win32_daemon_entry and doesn't\n    // return until the service enters STOPPED state.\n    if (StartServiceCtrlDispatcher(DispatchTable))\n        return 0;\n\n    auto error = GetLastError();\n\n    // We'll get this error if not invoked as a service, which is fine: we can just run directly\n    if (error == ERROR_FAILED_SERVICE_CONTROLLER_CONNECT)\n    {\n        llarp::sys::service_manager->disable();\n        return lokinet_main(argc, argv);\n    }\n    else\n    {\n        llarp::log::critical(logcat, \"Error launching service: {}\", std::system_category().message(error));\n        return 1;\n    }\n#endif\n}\n"
  },
  {
    "path": "daemon/lokinet.swift",
    "content": "import AppKit\nimport Foundation\nimport NetworkExtension\nimport SystemExtensions\n\nlet app = NSApplication.shared\n\nlet START = \"--start\"\nlet STOP = \"--stop\"\n\nlet HELP_STRING = \"usage: lokinet {--start|--stop}\"\n\nclass LokinetMain: NSObject, NSApplicationDelegate {\n    var vpnManager = NETunnelProviderManager()\n    var mode = START\n    let netextBundleId = \"org.lokinet.network-extension\"\n\n    func applicationDidFinishLaunching(_: Notification) {\n        if mode == START {\n            startNetworkExtension()\n        } else if mode == STOP {\n            tearDownVPNTunnel()\n        } else {\n            result(msg: HELP_STRING)\n        }\n    }\n\n    func bail() {\n        app.terminate(self)\n    }\n\n    func result(msg: String) {\n        NSLog(msg)\n        // TODO: does lokinet continue after this?\n        bail()\n    }\n\n    func tearDownVPNTunnel() {\n        NSLog(\"Stopping Lokinet\")\n        NETunnelProviderManager.loadAllFromPreferences { [self] (savedManagers: [NETunnelProviderManager]?, error: Error?) in\n            if let error = error {\n                self.result(msg: error.localizedDescription)\n                return\n            }\n\n            if let savedManagers = savedManagers {\n                for manager in savedManagers {\n                    if (manager.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == self.netextBundleId {\n                        manager.connection.stopVPNTunnel()\n                        self.result(msg: \"Lokinet Down\")\n                    }\n                }\n            }\n            self.result(msg: \"Lokinet is not up\")\n        }\n    }\n\n    func startNetworkExtension() {\n        #if MACOS_SYSTEM_EXTENSION\n            NSLog(\"Loading Lokinet network extension\")\n            // Start by activating the system extension\n            let activationRequest = OSSystemExtensionRequest.activationRequest(forExtensionWithIdentifier: netextBundleId, queue: .main)\n            activationRequest.delegate = self\n            OSSystemExtensionManager.shared.submitRequest(activationRequest)\n        #else\n            setupVPNTunnel()\n        #endif\n    }\n\n    func setupVPNTunnel() {\n        NSLog(\"Starting up Lokinet tunnel\")\n        NETunnelProviderManager.loadAllFromPreferences { [self] (savedManagers: [NETunnelProviderManager]?, error: Error?) in\n            if let error = error {\n                self.result(msg: error.localizedDescription)\n                return\n            }\n\n            if let savedManagers = savedManagers {\n                for manager in savedManagers {\n                    if (manager.protocolConfiguration as? NETunnelProviderProtocol)?.providerBundleIdentifier == self.netextBundleId {\n                        NSLog(\"Found saved VPN Manager\")\n                        self.vpnManager = manager\n                    }\n                }\n            }\n            let providerProtocol = NETunnelProviderProtocol()\n            providerProtocol.serverAddress = \"loki.loki\" // Needs to be set to some non-null dummy value\n            providerProtocol.username = \"anonymous\"\n            providerProtocol.providerBundleIdentifier = self.netextBundleId\n            if #available(macOS 11, *) {\n                providerProtocol.enforceRoutes = true\n            }\n            // macos seems to have trouble when this is true, and reports are that this breaks and\n            // doesn't do what it says on the tin in the first place.  Needs more testing.\n            providerProtocol.includeAllNetworks = false\n            self.vpnManager.protocolConfiguration = providerProtocol\n            self.vpnManager.isEnabled = true\n            // self.vpnManager.isOnDemandEnabled = true\n            self.vpnManager.localizedDescription = \"lokinet\"\n            self.vpnManager.saveToPreferences(completionHandler: { [self] error -> Void in\n                if error != nil {\n                    NSLog(\"Error saving to preferences\")\n                    self.result(msg: error!.localizedDescription)\n                } else {\n                    self.vpnManager.loadFromPreferences(completionHandler: { error in\n                        if error != nil {\n                            NSLog(\"Error loading from preferences\")\n                            self.result(msg: error!.localizedDescription)\n                        } else {\n                            do {\n                                NSLog(\"Trying to start\")\n                                self.initializeConnectionObserver()\n                                try self.vpnManager.connection.startVPNTunnel()\n                            } catch let error as NSError {\n                                self.result(msg: error.localizedDescription)\n                            } catch {\n                                self.result(msg: \"There was a fatal error\")\n                            }\n\n                            // Check if we are already connected because, if so, we won't get a\n                            // status change and will just hang waiting for one.\n                            if self.vpnManager.connection.status == .connected {\n                                self.result(msg: \"VPN already connected\");\n                            }\n                        }\n                    })\n                }\n            })\n        }\n    }\n\n    func initializeConnectionObserver() {\n        NotificationCenter.default.addObserver(forName: NSNotification.Name.NEVPNStatusDidChange, object: vpnManager.connection, queue: OperationQueue.main) { [self] _ -> Void in\n            if self.vpnManager.connection.status == .invalid {\n                self.result(msg: \"VPN configuration is invalid\")\n            } else if self.vpnManager.connection.status == .disconnected {\n                self.result(msg: \"VPN is disconnected.\")\n            } else if self.vpnManager.connection.status == .connecting {\n                NSLog(\"VPN is connecting...\")\n            } else if self.vpnManager.connection.status == .reasserting {\n                NSLog(\"VPN is reasserting...\")\n            } else if self.vpnManager.connection.status == .disconnecting {\n                NSLog(\"VPN is disconnecting...\")\n            } else if self.vpnManager.connection.status == .connected {\n                self.result(msg: \"VPN Connected\")\n            }\n        }\n    }\n}\n\n#if MACOS_SYSTEM_EXTENSION\n\n    extension LokinetMain: OSSystemExtensionRequestDelegate {\n        func request(_: OSSystemExtensionRequest, didFinishWithResult result: OSSystemExtensionRequest.Result) {\n            guard result == .completed else {\n                NSLog(\"Unexpected result %d for system extension request\", result.rawValue)\n                return\n            }\n            NSLog(\"Lokinet system extension loaded\")\n            setupVPNTunnel()\n        }\n\n        func request(_: OSSystemExtensionRequest, didFailWithError error: Error) {\n            NSLog(\"System extension request failed: %@\", error.localizedDescription)\n            self.bail()\n        }\n\n        func requestNeedsUserApproval(_ request: OSSystemExtensionRequest) {\n            NSLog(\"Extension %@ requires user approval\", request.identifier)\n        }\n\n        func request(_ request: OSSystemExtensionRequest,\n                     actionForReplacingExtension existing: OSSystemExtensionProperties,\n                     withExtension extension: OSSystemExtensionProperties) -> OSSystemExtensionRequest.ReplacementAction\n        {\n            NSLog(\"Replacing extension %@ version %@ with version %@\", request.identifier, existing.bundleShortVersion, `extension`.bundleShortVersion)\n            return .replace\n        }\n    }\n\n#endif\n\nlet args = CommandLine.arguments\n\n// If we are invoked with no arguments then exec the gui.  This is dumb, but there doesn't seem to\n// be a nicer way to do this on Apple's half-baked platform because:\n// - we have three \"bundles\" we need to manage: the GUI app, the system extension, and the Lokinet\n//   app (this file) which loads the system extension.\n// - if we embed the system extension directly inside the GUI then it fails to launch because the\n//   electron GUI's requirements (needed for JIT) conflict with the ability to load a system\n//   extensions.\n// - if we embed Lokinet.app inside Lokinet-GUI.app and then the system extension inside Lokinet.app\n//   then it works, but macos loses track of the system extension and doesn't remove it when you\n//   remove the application.  (It breaks your system, leaving an impossible-to-remove system\n//   extension, in just the same way it breaks if you don't use Finder to remove the Application.\n//   Apple used to say (around 2 years ago as of writing) that they would fix this situation \"soon\",\n//   but hasn't, and has stopped saying anything about it.)\n// - if we try to use multiple executables (one to launch the system extension, one simple shell\n//   script to execs the embedded GUI app) inside the Lokinet.app and make the GUI the default for\n//   the application then Lokinet gets killed by gatekeeper because code signing only applies the\n//   (required-for-system-extensions) provisioningprofile to the main binary in the app.\n//\n// So we are left needing *one* single binary that isn't the GUI but has to do double-duty for both\n// exec'ing the binary and loading lokinet, depending on how it is called.\n//\n// But of course there is no way to specify command-line arguments to the default binary macOS runs,\n// so we can't use a `--gui` flag or anything so abhorrent to macos purity, thus this nasty\n// solution:\n//   - no args -- exec the GUI\n//   - `--start` -- load the system extension and start lokinet\n//   - `--stop` -- stop lokinet\n//\n// macOS: land of half-baked implementations and nasty hacks to make anything work.\n\nif args.count == 1 {\n    let gui_path = Bundle.main.resourcePath! + \"/../Helpers/Lokinet-GUI.app\"\n    if !FileManager.default.fileExists(atPath: gui_path) {\n        NSLog(\"Could not find gui app at %@\", gui_path)\n        exit(1)\n    }\n    let gui_url = URL(fileURLWithPath: gui_path, isDirectory: false)\n    let gui_app_conf = NSWorkspace.OpenConfiguration()\n    let group = DispatchGroup()\n    group.enter()\n    NSWorkspace.shared.openApplication(at: gui_url, configuration: gui_app_conf,\n            completionHandler: { (app, error) in\n                if error != nil {\n                    NSLog(\"Error launching gui: %@\", error!.localizedDescription)\n                } else {\n                    NSLog(\"Lauched GUI\");\n                }\n                group.leave()\n            })\n    group.wait()\n\n} else if args.count == 2 {\n    let delegate = LokinetMain()\n    delegate.mode = args[1]\n    app.delegate = delegate\n    app.run()\n} else {\n    NSLog(HELP_STRING)\n}\n"
  },
  {
    "path": "daemon/utils.cpp",
    "content": "#include \"utils.hpp\"\n\n#include <llarp/util/formattable.hpp>\n\n#include <nlohmann/json.hpp>\n\nnamespace llarp::controller\n{\n    size_t lokinet_instance::next_id = 0;\n\n    rpc_controller::rpc_controller() : _omq{std::make_shared<omq::OxenMQ>()} {}\n\n    void rpc_controller::_initiate(omq::address src, std::string remote)\n    {\n        log::info(\n            logcat,\n            \"Instructing lokinet instance (bind:{}) to initiate session to remote:{}\",\n            src.full_address(),\n            remote);\n\n        nlohmann::json req;\n        req[\"pk\"] = remote;\n\n        if (auto it = _binds.find(src); it != _binds.end())\n            _omq->request(\n                it->second.cid,\n                \"llarp.session_init\",\n                [&](bool success, std::vector<std::string> data) {\n                    if (success)\n                    {\n                        auto res = nlohmann::json::parse(data[0]);\n                        log::info(logcat, \"RPC call to initiate session succeeded: {}\", res.dump());\n                    }\n                    else\n                        log::critical(logcat, \"RPC call to initiate session failed!\");\n                },\n                req.dump());\n        else\n            log::critical(\n                logcat, \"Could not find connection ID to RPC bind {} for `session_init` command\", src.full_address());\n    }\n\n    void rpc_controller::_status(omq::address src)\n    {\n        log::info(logcat, \"Querying lokinet instance (bind:{}) for router status\", src.full_address());\n\n        if (auto it = _binds.find(src); it != _binds.end())\n            _omq->request(it->second.cid, \"llarp.status\", [&](bool success, std::vector<std::string> data) {\n                if (success)\n                {\n                    auto res = nlohmann::json::parse(data[0]);\n                    log::info(logcat, \"RPC call to query router status succeeded: \\n{}\\n\", res.dump(4));\n                }\n                else\n                    log::critical(logcat, \"RPC call to query router status failed!\");\n            });\n        else\n            log::critical(\n                logcat, \"Could not find connection ID to RPC bind {} for `status` command\", src.full_address());\n    }\n\n    void rpc_controller::_close(omq::address src, std::string remote)\n    {\n        log::info(\n            logcat, \"Querying lokinet instance (bind:{}) to close session to remote:{}\", src.full_address(), remote);\n\n        nlohmann::json req;\n        req[\"pk\"] = remote;\n\n        if (auto it = _binds.find(src); it != _binds.end())\n            _omq->request(\n                it->second.cid,\n                \"llarp.session_close\",\n                [&](bool success, std::vector<std::string> data) {\n                    if (success)\n                    {\n                        auto res = nlohmann::json::parse(data[0]);\n                        log::info(logcat, \"RPC call to close session succeeded: {}\", res.dump());\n                    }\n                    else\n                        log::critical(logcat, \"RPC call to close session failed!\");\n                },\n                req.dump());\n        else\n            log::critical(\n                logcat, \"Could not find connection ID to RPC bind {} for `session_close` command\", src.full_address());\n    }\n\n    void rpc_controller::_halt(omq::address src)\n    {\n        log::info(logcat, \"Instructing lokinet instance (bind:{}) to halt\", src.full_address());\n\n        if (auto it = _binds.find(src); it != _binds.end())\n        {\n            _omq->request(it->second.cid, \"llarp.halt\", [&](bool success, std::vector<std::string> data) {\n                if (success)\n                {\n                    auto res = nlohmann::json::parse(data[0]);\n                    log::info(logcat, \"RPC call to halt instance succeeded: {}\", res.dump());\n                }\n                else\n                    log::critical(logcat, \"RPC call to halt instance failed!\");\n            });\n        }\n        else\n            log::critical(logcat, \"Could not find connection ID to RPC bind {} for `halt` command\", src.full_address());\n    }\n\n    bool rpc_controller::_omq_connect(const std::vector<std::string>& bind_addrs)\n    {\n        int i = 0;\n        std::vector<std::promise<bool>> connect_proms{bind_addrs.size()};\n\n        for (auto& b : bind_addrs)\n        {\n            omq::address bind{b};\n            log::info(logcat, \"RPC controller connecting to RPC bind address ({})\", bind.full_address());\n\n            auto cid = _omq->connect_remote(\n                bind,\n                [&, idx = i](auto) {\n                    log::info(logcat, \"Loki controller successfully connected to RPC bind ({})\", bind.full_address());\n                    connect_proms[idx].set_value(true);\n                },\n                [&, idx = i](auto, std::string_view msg) {\n                    log::info(\n                        logcat, \"Loki controller failed to connect to RPC bind ({}): {}\", bind.full_address(), msg);\n                    connect_proms[idx].set_value(false);\n                });\n\n            auto it = _binds.emplace(bind, lokinet_instance{cid}).first;\n            _indexes.emplace(it->second.ID, it->first);\n            i += 1;\n        }\n\n        bool ret = true;\n\n        for (auto& p : connect_proms)\n            ret &= p.get_future().get();\n\n        return ret;\n    }\n\n    bool rpc_controller::start(std::vector<std::string>& bind_addrs)\n    {\n        _omq->start();\n        return _omq_connect(bind_addrs);\n    }\n\n    void rpc_controller::list_all() const\n    {\n        auto msg = \"\\n\\n\\tLokinet RPC controller connected to {} RPC binds:\\n\"_format(_binds.size());\n        for (auto& [idx, addr] : _indexes)\n            msg += \"\\t\\tID:{} | Address:{}\\n\"_format(idx, addr.full_address());\n\n        log::info(logcat, \"{}\", msg);\n    }\n\n    void rpc_controller::refresh() { log::critical(logcat, \"TODO: implement this!\"); }\n\n    void rpc_controller::initiate(size_t idx, std::string remote)\n    {\n        if (auto it = _indexes.find(idx); it != _indexes.end())\n            _initiate(it->second, std::move(remote));\n        else\n            log::warning(logcat, \"Could not find instance with given index: {}\", idx);\n    }\n\n    void rpc_controller::initiate(omq::address src, std::string remote)\n    {\n        return _initiate(std::move(src), std::move(remote));\n    }\n\n    void rpc_controller::status(omq::address src) { return _status(std::move(src)); };\n\n    void rpc_controller::status(size_t idx)\n    {\n        if (auto it = _indexes.find(idx); it != _indexes.end())\n            _status(it->second);\n        else\n            log::warning(logcat, \"Could not find instance with given index: {}\", idx);\n    }\n\n    void rpc_controller::close(omq::address src, std::string remote)\n    {\n        return _close(std::move(src), std::move(remote));\n    }\n\n    void rpc_controller::close(size_t idx, std::string remote)\n    {\n        if (auto it = _indexes.find(idx); it != _indexes.end())\n            _close(it->second, std::move(remote));\n        else\n            log::warning(logcat, \"Could not find instance with given index: {}\", idx);\n    }\n\n    void rpc_controller::halt(omq::address src) { return _halt(std::move(src)); }\n\n    void rpc_controller::halt(size_t idx)\n    {\n        if (auto it = _indexes.find(idx); it != _indexes.end())\n            _halt(it->second);\n        else\n            log::warning(logcat, \"Could not find instance with given index: {}\", idx);\n    }\n}  // namespace llarp::controller\n"
  },
  {
    "path": "daemon/utils.hpp",
    "content": "#pragma once\n\n#include <llarp.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/logging/buffer.hpp>\n\n#include <oxenmq/oxenmq.h>\n\nnamespace omq = oxenmq;\n\nnamespace llarp::controller\n{\n    static auto logcat = log::Cat(\"rpc-controller\");\n\n    struct rpc_controller;\n\n    struct lokinet_instance\n    {\n        friend struct rpc_controller;\n\n      private:\n        static size_t next_id;\n\n      public:\n        lokinet_instance(omq::ConnectionID c) : ID{++next_id}, cid{std::move(c)} {}\n\n        const size_t ID;\n        omq::ConnectionID cid;\n    };\n\n    struct rpc_controller\n    {\n        rpc_controller();\n\n      private:\n        std::shared_ptr<omq::OxenMQ> _omq;\n        std::unordered_map<omq::address, lokinet_instance> _binds;\n        std::map<size_t, omq::address> _indexes;\n\n        void _initiate(omq::address src, std::string remote);\n        void _status(omq::address src);\n        void _close(omq::address src, std::string remote);\n        void _halt(omq::address src);\n\n        bool _omq_connect(const std::vector<std::string>& bind_addrs);\n\n      public:\n        bool start(std::vector<std::string>& bind_addrs);\n\n        void list_all() const;\n\n        void refresh();\n\n        void initiate(omq::address src, std::string remote);\n        void initiate(size_t idx, std::string remote);\n\n        void status(omq::address src);\n        void status(size_t idx);\n\n        void close(omq::address src, std::string remote);\n        void close(size_t idx, std::string remote);\n\n        void halt(omq::address src);\n        void halt(size_t idx);\n    };\n}  // namespace llarp::controller\n"
  },
  {
    "path": "docs/CMakeLists.txt",
    "content": "find_program(DOXYGEN doxygen)\nif (NOT DOXYGEN)\n    message(STATUS \"Documentation generation disabled (doxygen not found)\")\n    return()\nendif()\n\nfind_program(MKDOCS mkdocs)\nif (NOT MKDOCS)\n    message(STATUS \"Documentation generation disabled (mkdocs not found)\")\n    return()\nendif()\n\nset(lokinet_doc_sources \"${DOCS_SRC}\")\nstring(REPLACE \";\" \" \" lokinet_doc_sources_spaced \"${lokinet_doc_sources}\")\n\nadd_custom_target(clean_xml COMMAND ${CMAKE_COMMAND} -E rm -rf doxyxml)\nadd_custom_target(clean_markdown COMMAND ${CMAKE_COMMAND} -E rm -rf markdown)\n\nadd_custom_command(\n    OUTPUT doxyxml/index.xml\n    COMMAND ${DOXYGEN} Doxyfile\n    DEPENDS\n    clean_xml\n        ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile\n        ${lokinet_doc_sources}\n)\n\n# find doxybook2\nfind_program(DOXYBOOK2 doxybook2)\nif(NOT DOXYBOOK2)\n  if(NOT DOXYBOOK2_ZIP_URL)\n    set(DOXYBOOK2_VERSION v1.4.0 CACHE STRING \"doxybook2 version\")\n    set(DOXYBOOK2_ZIP_URL \"https://github.com/matusnovak/doxybook2/releases/download/${DOXYBOOK2_VERSION}/doxybook2-linux-amd64-${DOXYBOOK2_VERSION}.zip\")\n    set(DOXYBOOK2_ZIP_HASH_OPTS EXPECTED_HASH SHA256=bab9356f5daa550cbf21d8d9b554ea59c8be039716a2caf6e96dee52713fccb0)\n  endif()\n\n  file(DOWNLOAD\n    ${DOXYBOOK2_ZIP_URL}\n    ${CMAKE_CURRENT_BINARY_DIR}/doxybook2.zip\n    ${DOXYBOOK2_ZIP_HASH_OPTS})\n\n  execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf ${CMAKE_CURRENT_BINARY_DIR}/doxybook2.zip\n    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})\n  set(DOXYBOOK2 ${CMAKE_CURRENT_BINARY_DIR}/bin/doxybook2)\n  set(doxybook_localbin ${DOXYBOOK2})\nendif()\n\nadd_custom_command(\n    OUTPUT gen\n    COMMAND ${DOXYBOOK2} --input ${CMAKE_CURRENT_BINARY_DIR}/doxyxml --output ${CMAKE_CURRENT_BINARY_DIR}/gen --config config.json\n    DEPENDS\n    ${doxybook_localbin}\n    ${CMAKE_CURRENT_BINARY_DIR}/gen/index.md\n    ${CMAKE_CURRENT_BINARY_DIR}/config.json\n    ${CMAKE_CURRENT_BINARY_DIR}/doxyxml/index.xml)\n\nadd_custom_target(clean_html COMMAND ${CMAKE_COMMAND} -E rm -rf html)\n\nadd_custom_command(\n  OUTPUT markdown\n  COMMAND find ${CMAKE_CURRENT_BINARY_DIR}/gen/ -type f -name '*.md' -exec ${CMAKE_CURRENT_SOURCE_DIR}/fix-markdown.sh {} \"\\;\" && ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_BINARY_DIR}/gen ${CMAKE_CURRENT_BINARY_DIR}/markdown\n  DEPENDS gen\n)\n\nadd_custom_command(\n  OUTPUT html\n  COMMAND ${MKDOCS} build\n  DEPENDS\n  clean_html\n  ${CMAKE_CURRENT_BINARY_DIR}/markdown)\n\nadd_custom_target(doc DEPENDS markdown)\n\nconfigure_file(Doxyfile.in Doxyfile @ONLY)\nconfigure_file(index.md.in index.md @ONLY)\n\nconfigure_file(config.json config.json COPYONLY)\nconfigure_file(mkdocs.yml mkdocs.yml COPYONLY)\n\n# we seperate this step out so we force clean_markdown to run before markdown target\nadd_custom_command(\n  OUTPUT gen/index.md\n  COMMAND ${CMAKE_COMMAND} -E copy index.md gen/index.md\n  DEPENDS clean_markdown)\n"
  },
  {
    "path": "docs/Doxyfile.in",
    "content": "PROJECT_NAME           = \"Lokinet\"\nPROJECT_NUMBER         = v@lokinet_VERSION@\nPROJECT_BRIEF          = \"Anonymous, decentralized and IP based overlay network for the internet.\"\nOUTPUT_DIRECTORY       = @CMAKE_CURRENT_BINARY_DIR@\nSTRIP_FROM_PATH        = @PROJECT_SOURCE_DIR@ @PROJECT_BINARY_DIR@\nJAVADOC_AUTOBRIEF      = YES\nALIASES                = \"rst=\\verbatim embed:rst\"\nALIASES               += \"endrst=\\endverbatim\"\nBUILTIN_STL_SUPPORT    = YES\nINPUT                  = @lokinet_doc_sources_spaced@\nINCLUDE_PATH           = @PROJECT_SOURCE_DIR@/include @PROJECT_SOURCE_DIR@/llarp @PROJECT_SOURCE_DIR@/external/ghc-filesystem/include/\nRECURSIVE              = YES\nCLANG_ASSISTED_PARSING = NO\n#CLANG_OPTIONS          = -std=c++17 -Wno-pragma-once-outside-header\nGENERATE_HTML          = NO\nHTML_OUTPUT            = doxyhtml\nGENERATE_LATEX         = NO\nGENERATE_XML           = YES\nXML_OUTPUT             = doxyxml\nMACRO_EXPANSION        = YES\n"
  },
  {
    "path": "docs/LICENSE",
    "content": "Low Latency Anonymous Routing Protocol Specification\nWritten in 2017 by Jeff Becker <jeff@i2p.rocks>\n\nTo the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to \nthis protocol specfication to the public domain worldwide. This software is distributed without any warranty.\nYou should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see \n<http://creativecommons.org/publicdomain/zero/1.0/>.\n"
  },
  {
    "path": "docs/architecture.md",
    "content": "# High-Level Architecture\n\n## Path Building\n\n<p align=\"center\">\n    <img src=\"/docs/lokinet_pathbuild_no_steps.png\">\n</p>\n\nStarting from the top, here's a high-level overview of how the lokinet client builds a path to a terminating node\n\n1. Client semi-randomly selects SN's for hops 2 and 3 using Introset Hash Ring (IHR)\n   - First hop is sticky: upon initialization of lokinet, 4-5 first hops are selected\n\n2. Message sent to hop 1\n   - Message consists of eight records in a linked list. Four hops are typically used, leaving the last 4 links as dummy records\n   - Each record contains a TX (upstream) path ID and RX (downstream) path ID\n   - Each record has a pointer to the next record, except for the final hops' record; the pointer here is recursive, signalling the end of the path-build\n\n3. Hop 2 pops top record, appends metadata, and pushes record to the back of linked list\n   - Hop adds metadata to the record, such as optional lifetime, pubkey to derive shared secret, etc\n\n4. Steps 2-3 are repeated for the remaining hops until destination is reached\n   - Final hop reads the recursive pointer signalling the end of the path-build process\n\n5. Upon completion, plain-text reply is propagated backwards, where the client can then decrypt all records\n\n6. Client measures latency\n   - A) Routing message is sequentially encrypted using hop 4's key through hop 1's key\n     - At each iteration, the nonce is permuted by XOR'ing the previous nonce with the hash of the secret key of each hop\n   - B) Routing message is sent s.t. each hop can decrypt, with final hop receiving plain-text\n     - Each hop appends latency and expiration time data, with the final hop interpreting the plain-text as a routing message and sending it back to the client\n\n7. Introset is published to IHR upon successful completion; introset contains:\n   - Path ID's of routers\n   - Latency and expiration time for each hop\n   - DNS SRV records\n   - etc\n\n### Failure Cases\n\n1. Next hop is an invalid SN\n2. Cannot connect to SN\n\nIn either case, the path-build status is sent backwards with an error flag. Once received by the client, metadata related to the prospective path is wiped and the path forgotten\n\n\n"
  },
  {
    "path": "docs/config.json",
    "content": "{\n    \"baseUrl\": \"\",\n    \"indexInFolders\": false,\n    \"linkSuffix\": \".md\",\n    \"mainPageInRoot\": false,\n    \"mainPageName\": \"index\",\n    \"linkLowercase\": false,\n    \"foldersToGenerate\": [\"files\", \"classes\", \"namespaces\"]\n}\n"
  },
  {
    "path": "docs/dns-overview.md",
    "content": "# DNS in Lokinet\n\nLokinet uses dns are its primary interface for resolving, mapping and querying resources inside of lokinet.\nThis was done not because DNS is *good* protocol, but because there is almost no relevent userland applications that are incapable of interacting with DNS, across every platform.\nUsing DNS in lokinet allows for the most zero config setup possible with the current set of standard protocols.\n\nLokinet provides 2 internal gtld, `.loki` and `.snode`\n\n## .snode\n\nThe `.snode` gtld is used to address a lokinet router in the form of `<zbase32 encoded public ed25519 identity key>.snode`.\nTraffic bound to a `.snode` tld will have its source authenticatable only if it originates from another valid lokinet router.\nClients can also send traffic to and from addresses mapped to `.snode` addresses, but the source address on the service node side is ephemeral.\nIn both cases, ip traffic to addresses mapped to `.snode` addresses will have the destination ip rewritten by the lokinet router to be its local interface ip, this ensures traffic stays on the lokinet router' interface for snode traffic and preventing usage as an exit node.\n\n## .loki\n\nThe `.loki` gtld is used to address anonymously published routes to lokinet clients on the network.\n\n<!-- (todo: keyblinding info) -->\n\n## What RR are provided?\n\nAll `.loki` domains by default have the following dns rr synthesized by lokinet:\n\n* `A` record for initiating address mapping\n* `MX` record pointing to the synthesizesd `A` record\n* free wildcard entries for all of the above.\n\nWildard entries are currently only pointing\n\nAll `.snode` domains have by defult just an `A` record for initiating address mapping.\n\nAdditionally both `.loki` and `.snode` can optionally provide multiple `SRV` records to advertise existence of services on or off of the name.\n\n<!-- (//todo: document and verify srv record limitations) -->\n"
  },
  {
    "path": "docs/doxygen.md",
    "content": "\n# Doxygen\n\nbuilding doxygen docs requires the following:\n\n* cmake\n* doxygen\n* sphinx-build\n* sphinx readthedocs theme\n* breathe\n* exhale\n\ninstall packages:\n\n    $ sudo apt install make cmake doxygen python3-sphinx python3-sphinx-rtd-theme python3-breathe python3-pip\n    $ pip3 install --user exhale\n    \nbuild docs:\n\n    $ mkdir -p build-docs\n    $ cd build-docs\n    $ cmake .. && make doc \n\nserve built docs via http, will be served at http://127.0.0.1:8000/\n\n    $ python3 -m http.server -d docs/html\n    \n"
  },
  {
    "path": "docs/exit-setup.md",
    "content": "\nto configure lokinet to be an exit add into `lokinet.ini`:\n\n    [router]\n    min-connections=8\n    max-connections=16\n\n    [network]\n    exit=true\n    keyfile=/var/lib/lokinet/exit.private\n    reachable=1\n    ifaddr=10.0.0.1/16\n    hops=2\n    paths=8\n\n\npost setup for exit (as root) given `eth0` is used to get to the internet:\n\n    # echo 1 > /proc/sys/net/ipv4/ip_forward\n    # iptables -t nat -A POSTROUTING -s 10.0.0.0/16 -o eth0 -j MASQUERADE\n"
  },
  {
    "path": "docs/fix-markdown.sh",
    "content": "#!/bin/bash\n# apply markdown file content quarks\n\n\n# rewrite br tags\nsed -i 's|<br>|<br/>|g' $@ \n"
  },
  {
    "path": "docs/ideal-ux.md",
    "content": "# What does Lokinet actually do?\n\nLokinet is an onion routed authenticated unicast IP network. It exposes an IP tunnel to the user and provides a dns resolver that maps `.loki` and `.snode` gtld onto a user defined ip range.\n\nLokinet allows users to tunnel arbitrary ip ranges to go to a `.loki` address to act as a tunnel broker via another network accessible via another lokinet client. This is commonly known as an \"exit node\" but the way lokinet does this is much more generic so that term is not very accurate given what it actually does.\n\nThe `.snode` gtld refers to a router on the network by its public ed25519 key.\n\nThe `.loki` gtld refers to clients that publish the existence anonymously to the network by their ed25519 public key. (`.loki` also has the ability to use short names resolved via external consensus method, like a blockchain).\n\n# How do I use Lokinet?\n\nset system dns resolver to use the dns resolver provided by lokinet, make sure the upstream dns provider that lokinet uses for non lokinet gtlds is set as desired (see lokinet.ini `[dns]` section)\n\nconfigure exit traffic provider if you want to tunnel ip traffic via lokinet, by default this is off as we cannot provide a sane defualt that makes everyone happy. to enable an exit node, see lokinet.ini `[network]` section, add multiple `exit-node=exitaddrgoeshere.loki` lines for each endpoint you want to use for exit traffic. each `exit-node` entry will be used to randomly stripe across per IP you are sending to. \n\nnote: per flow (ip+proto/port) isolation is trivial on a technical level but currently not implemented at this time.\n\n# Can I run lokinet on a soho router?\n\nYes and that is the best way to run it in practice. \n\n## The \"easy\" way\n\nWe have a community maintained solution for ARM SBCs like rasperry pi: https://github.com/necro-nemesis/LabyrinthAP\n\n## The \"fun\" way (DIY)\n\nIt is quite nice to DIY. if you choose to do so there is some assembly required:\n\non the lokinet side, make sure that the...\n\n* ip ranges for `.loki` and `.snode` are statically set (see lokinet.ini `[network]` section `ifaddr=` option)\n* network interace used by lokinet is statically set (see lokinet.ini `[network]` section `ifname=` option)\n* dns socket is bound to an address the soho router's dns resolver can talk to, see `[dns]` section `bind=` option) \n\non the soho router side:\n\n* route queries for `.loki` and `.snode` gtld to go to lokinet dns on soho router's dns resolver\n* use dhcp options to set dns to use the soho router's dns resolver\n* make sure that the ip ranges for lokinet are reachable via the LAN interface \n* if you are tunneling over an exit ensure that LAN traffic will only forward to go over the lokinet vpn interface\n"
  },
  {
    "path": "docs/index.md.in",
    "content": "# Lokinet @lokinet_VERSION@ (git rev: @GIT_VERSION@)\n\nsummary goes here\n\n## Overview\n\n[code internals](index_namespaces.md)\n"
  },
  {
    "path": "docs/install.md",
    "content": "# Installing\n\nIf you are simply looking to install Lokinet and don't want to compile it yourself we provide several options for platforms to run on:\n\nTier 1:\n\n* [Linux](#linux-install)\n* [Windows](#windows-install)\n* [MacOS](#macos-install)\n\nTier 2:\n\n* [FreeBSD](#freebsd-install)\n\nCurrently Unsupported Platforms: (maintainers welcome)\n\n* [Android](#apk-install)\n* Apple iPhone \n* Homebrew\n* \\[Insert Flavor of the Month windows package manager here\\]\n\n\n## Official Builds\n\n### Windows / MacOS <span id=\"windows-install\" />  <span id=\"macos-install\" />\n\nYou can get the latest stable release for lokinet on windows or macos from https://lokinet.org/ or check the [releases page on github](https://github.com/oxen-io/lokinet/releases).\n\n### Linux <span id=\"linux-install\" />\n\nYou do not have to build from source if you do not wish to, we provide [apt](#deb-install) and [rpm](#rpm-install) repos.\n\n#### APT Repository <span id=\"deb-install\" />\n\nYou can install debian packages from `deb.oxen.io` by adding the apt repo to your system.\n\n    $ sudo curl -so /etc/apt/trusted.gpg.d/oxen.gpg https://deb.oxen.io/pub.gpg\n    $ echo \"deb https://deb.oxen.io $(lsb_release -sc) main\" | sudo tee /etc/apt/sources.list.d/oxen.list\n    \nThis apt repo is also available via lokinet at `http://deb.loki`\n\nOnce added you can install lokinet with:\n\n    $ sudo apt update\n    $ sudo apt install lokinet\n\nWhen running from debian package the following steps are not needed as it is already running and ready to use. You can stop/start/restart it using `systemctl start lokinet`, `systemctl stop lokinet`, etc.\n\n#### RPM <span id=\"rpm-install\" />\n\nWe also provide an RPM repo, see `rpm.oxen.io`, also available on lokinet at `rpm.loki`\n    \n## Bleeding Edge dev builds <span id=\"ci-builds\" />\n\nautomated builds from dev branches for the brave or impatient can be found from our CI pipeline [here](https://oxen.rocks/oxen-io/lokinet/). (warning: these nightly builds may or may not consume your first born child.)\n\n## Building\n\nBuild requirements:\n\n* Git\n* CMake\n* C++ 17 capable C++ compiler\n* libuv >= 1.27.0\n* libsodium >= 1.0.18\n* libssl (for lokinet-bootstrap)\n* libcurl (for lokinet-bootstrap)\n* libunbound\n* libzmq\n* cppzmq\n\n### Linux Compile\n\nIf you want to build from source: <span id=\"linux-compile\" />\n\n    $ sudo apt install build-essential cmake git libcap-dev pkg-config automake libtool libuv1-dev libsodium-dev libzmq3-dev libcurl4-openssl-dev libevent-dev nettle-dev libunbound-dev libssl-dev nlohmann-json3-dev\n    $ git clone --recursive https://github.com/oxen-io/lokinet\n    $ cd lokinet\n    $ mkdir build\n    $ cd build\n    $ cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF\n    $ make -j$(nproc)\n    $ sudo make install\n\nset up the initial configs:\n\n    $ lokinet -g\n    $ lokinet-bootstrap\n\nafter you create default config, run it:\n\n    $ lokinet\n\nThis requires the binary to have the proper capabilities which is usually set by `make install` on the binary. If you have errors regarding permissions to open a new interface this can be resolved using:\n\n    $ sudo setcap cap_net_admin,cap_net_bind_service=+eip /usr/local/bin/lokinet\n\n\n#### Arch Linux <span id=\"mom-cancel-my-meetings-arch-linux-broke-again\" />\n\nDue to [circumstances beyond our control](https://github.com/oxen-io/lokinet/discussions/1823) a working `PKGBUILD` can be found [here](https://raw.githubusercontent.com/oxen-io/lokinet/makepkg/contrib/archlinux/PKGBUILD).\n\n#### Cross Compile For Linux <span id=\"linux-cross\" />\n\ncurrent cross targets:\n\n* aarch64-linux-gnu\n* arm-linux-gnueabihf\n* mips-linux-gnu\n* mips64-linux-gnuabi64\n* mipsel-linux-gnu\n* powerpc64le-linux-gnu\n\ninstall the toolchain (this one is for `aarch64-linux-gnu`, you can provide your own toolchain if you want)\n\n    $ sudo apt install g{cc,++}-aarch64-linux-gnu\n\nbuild 1 or many cross targets:\n\n    $ ./contrib/cross.sh arch_1 arch_2 ... arch_n\n\n### Building For Windows <span id=\"win32-cross\" />\n\nwindows builds are cross compiled from debian/ubuntu linux\n\nadditional build requirements:\n\n* nsis\n* cpack\n* rsvg-convert (`librsvg2-bin` package on Debian/Ubuntu)\n\nsetup:\n\n    $ sudo apt install build-essential cmake git pkg-config mingw-w64 nsis cpack automake libtool\n    $ sudo update-alternatives --set x86_64-w64-mingw32-gcc /usr/bin/x86_64-w64-mingw32-gcc-posix\n    $ sudo update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix\n\nbuilding:\n\n    $ git clone --recursive https://github.com/oxen-io/lokinet\n    $ cd lokinet\n    $ ./contrib/windows.sh\n    \n### Compiling for MacOS <span id=\"mac-compile\" />\n\nSource code compilation of Lokinet by end users is not supported or permitted by apple on their platforms, see [this](../contrib/macos/README.txt) for more information.\n\nIf you find this disagreeable consider using a platform that permits compiling from source.\n\n### FreeBSD <span id=\"freebsd-install\" />\n\nCurrently has no VPN Platform code, see issue `#1513`\n\nbuild:\n\n    $ pkg install cmake git pkgconf\n    $ git clone --recursive https://github.com/oxen-io/lokinet\n    $ cd lokinet\n    $ mkdir build\n    $ cd build\n    $ cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON -DBUILD_STATIC_DEPS=ON ..\n    $ make\n\ninstall (root):\n\n    # make install\n    \n### Android <span id=\"apk-install\" />\n\nWe have an Android APK for lokinet VPN via android VPN API. \n\nComing to F-Droid whenever that happens. [[issue]](https://github.com/oxen-io/lokinet-flutter-app/issues/8)\n\n* [source code](https://github.com/oxen-io/lokinet-flutter-app)\n"
  },
  {
    "path": "docs/liblokinet-dev-guide.md",
    "content": "# Embedding Lokinet into an existing application\n\nWhen all else fails and you want to deploy lokinet inside your app without the OS caring you can embed an entire lokinet client and a few of the upper network layers into your application manually.\n\n## Why you should avoid this route \n\n`// TODO: this`\n\n## When you cannot avoid this route, how do i use it?\n\n`// TODO: this`\n"
  },
  {
    "path": "docs/macos-signing.txt",
    "content": "If you are reading this to try to build Lokinet for yourself for an Apple operating system and\nsimultaneously care about open source, privacy, or freedom then you, my friend, are a walking\ncontradiction: you are trying to get Lokinet to work on a platform that actively despises open\nsource, privacy, and freedom.  Even Windows is a better choice in all of these categories than\nApple.\n\nThis directory contains the magical incantations and random voodoo symbols needed to coax an Apple\nbuild.  There's no reason builds have to be this stupid, except that Apple wants to funnel everyone\ninto the no-CI, no-help, undocumented, non-toy-apps-need-not-apply modern Apple culture.\n\nThis is disgusting.\n\nBut it gets worse.\n\nThe following two files, in particular, are the very worst manifestations of this already toxic\nApple cancer: they are required for proper permissions to run on macOS, are undocumented, and can\nonly be regenerated through the entirely closed source Apple Developer backend, for which you have\nto pay money first to get a team account (a personal account will not work), and they lock the\nresulting binaries to only run on individually selected Apple computers selected at the time the\nprofile is provisioned (with no ability to allow it to run anywhere).\n\n    lokinet.dev.provisionprofile\n    lokinet-extension.dev.provisionprofile\n\nThis is actively hostile to open source development, but that is nothing new for Apple.\n\nThere are also release provisioning profiles\n\n    lokinet.release.provisionprofile\n    lokinet-extension.release.provisionprofile\n\nThese ones allow distribution of the app, but only if notarized, and again require notarization plus\nsigning by a (paid) Apple developer account.\n\nIn order to make things work, you'll have to replace these provisioning profiles with your own\n(after paying Apple for the privilege of developing on their platform, of course) and change all the\nteam/application/bundle IDs to reference your own team, matching the provisioning profiles.  The dev\nprovisioning profiles must be a \"macOS Development\" provisioning profile, and must include the\nsigning keys and the authorized devices on which you want to run it.  (The profiles bundled in this\nrepository contains the lokinet team's \"Apple Development\" keys associated with the Oxen project,\nand mac dev boxes.  This is *useless* for anyone else).\n\nFor release builds, you still need a provisioning profile, but it must be a \"Distribution: Developer\nID\" provisioning profile, and are tied to a (paid) Developer ID.  The ones in the repository are\nattached to the Oxen Project Developer ID and are useless to anyone else.\n\nOnce you have that in place, you need to build and sign the package using a certificate matching\nyour provisioning profile before your Apple system will allow it to run.  (That's right, your $2000\nbox won't let you run programs you build from source on it unless you also subscribe to a $100/year\nApple developer account).\n\nOkay, so now that you have paid Apple more money for the privilege of using your own computer,\nhere's how you make a signed lokinet app:\n\n1) Decide which type of build you are doing: a lokinet system extension, or an app extension.  The\n   former must be signed and notarized and will only work when placed in the /Applications folder,\n   but will not work as a dev build and cannot be distributed outside the Mac App Store.  The latter\n   is usable as a dev build, but still requires a signature and Apple-provided provisioningprofile\n   listing the limited number of devices on which it is allowed to run.\n\n   For system extension builds you want to add the -DMACOS_SYSTEM_EXTENSION=ON flag to cmake.\n\n2) Figure out the certificate to use for signing and make sure you have it installed.  For a\n   distributable system extension build you need a \"Developer ID Application\" key and certificate,\n   issued by your paid developer.apple.com account.  For dev builds you need a \"Apple Development\"\n   certificate.\n\n   In most cases you don't need to specify these; the default cmake script will figure them out.\n   (If it can't, e.g. because you have multiple of the right type installed, it will error with the\n   keys it found).\n\n   To be explicit, use `security find-identity -v` to list your keys, then list the key identity\n   with -DCODESIGN_ID=.....\n\n3) If you are doing a system extension build you will need to provide notarization login information by adding:\n\n   -DMACOS_NOTARIZE_ASC=XYZ123 -DMACOS_NOTARIZE_USER=me@example.com -DMACOS_NOTARIZE_PASS=@keychain:codesigning-password\n\n   a) The first value (XYZ123) needs to be the organization-specific unique value, and is printed in\n      brackets in the certificate description.  For example:\n\n          15095CD1E6AF441ABC69BDC52EE186A18200A49F \"Developer ID Application: Some Developer (ABC123XYZ9)\"\n\n      would require ABC123XYZ9 for this field.\n\n   b) The USER field is your Apple Developer login e-mail address.\n\n   c) The PASS field is a keychain reference holding your \"Application-Specific Password\".  To set\n      up such a password for your account, consult Apple documentation.  Once you have it, load it\n      into your keychain via:\n\n          export HISTFILE=''  # Don't want to store this in the shell history\n          xcrun altool --store-password-in-keychain-item \"codesigning-password\" -u \"user\" -p \"password\"\n\n      You can change \"codesigning-password\" to whatever you want (just make sure it agrees with the\n      -DMACOS_NOTARIZE_PASS option you build with).  \"user\" and \"password\" should be your developer\n      account device-specific login credentials provided by Apple.\n\n   To make your life easier, stash these settings into a `~/.notarization.cmake` file inside your\n   home directory; if you have not specified them in the build, and this file exists, lokinet's\n   cmake will load it:\n\n          set(MACOS_NOTARIZE_USER \"me@example.com\")\n          set(MACOS_NOTARIZE_PASS \"@keychain:codesigning-password\")\n          set(MACOS_NOTARIZE_ASC \"ABC123XYZ9\")\n\n4) Build and sign the package; there is a script `contrib/mac.sh` that can help (extra cmake options\n   you need can be appended to the end), or you can build yourself in a build directory.  See the\n   script for the other cmake options that are typically needed.  Note that `-G Ninja` (as well as a\n   working ninja builder) are required.\n\n   If you get an error `errSecInternalComponent` this is Apple's highly descriptive way of telling\n   you that you need to unlock your keychain, which you can do by running `security unlock`.\n\n   If doing it yourself, `ninja sign` will build and then sign the app.\n\n   If you need to also notarize (e.g. for a system extension build) run `./notarize.py` from the\n   build directory (or alternatively `ninja notarize`, but the former gives you status output while\n   it runs).\n\n5) Packaging the app: you want to use `-DLOKINET_PACKAGE=ON` when configuring with cmake and then,\n   once all signing and notarization is complete, run `cpack` which will give you a .dmg and a .zip\n   containing the release.\n"
  },
  {
    "path": "docs/mkdocs.yml",
    "content": "site_name: Lokinet\ntheme:\n  name: 'readthedocs'\ndocs_dir: markdown\nsite_dir: html\n"
  },
  {
    "path": "docs/net-comparisons.md",
    "content": "# How is lokinet different than ...\n\n## Tor Browser\n\nTor browser is a hardened Firefox Web Browser meant exclusively to surf http(s) sites via Tor. It is meant to be a complete self contained browser you open and run to surf the Web (not the internet) anonymously.\nLokinet does not provide a web browser at this time because that is not a small task to do at all, and an even larger task to do in a way that is secure, robust and private. Community Contribuitions Welcomed.\n\n## Tor/Onion Services\n\nWhile Tor Browser is the main user facing product made by Tor Project, the main powerhouse is Tor itself. Tor provides a way to anonymize tcp connections made by an initiator and optionally additionally the recipient, when using a .onion address. Lokinet provides a similar feature set but can carry anything that can be encapsulated in an IP packet (currently only unicast traffic). \n\nThe Lokinet UX differs greatly from that of Tor. By default we do not provide exit connectivity. Because each user's threat model greatly varies in scope and breadth, there exists no one size fits all way to do exit connectivity. Users obtain their exit node information out-of-band at the moment. In the future we want to add decentralized network wide service discovery not limited to just exit providers, but this is currently unimplemented. We think that by being hands-off with respect to exit node requirements a far more diverse set of exit nodes can exist. In addition to having totally open unrestrcited exits, there is merit to permitting \"specialized\" exit providers that are allowed to do excessive filtering or geo blocking for security theatre checkbox compliance.\n\nLokinet additionally encourages the manual selection and pinning of edge connections to fit each user's threat model.\n\n## I2P\n\nIntegrating applications to utilize i2p's network layer is painful and greatly stunts mainstream adoption.\nLokinet takes the inverse approach of i2p: make app integration in lokinet should require zero custom shims or modifications to code to make it work.\n\n## DVPNs / Commercial VPN Proxies\n\nOne Hop VPNs can see your real IP and all of the traffic you tunnel over them. They are able to turn your data over to authorities even if they claim to not log.\n\nLokinet can see only 1 of these 2 things, but NEVER both:\n\n* Encrypted data coming from your real IP going to the first hop Lokinet Router forwarded to another Lokinet Router.\n* A lokinet exit sees traffic coming from a `.loki` address but has no idea what the real IP is for it.\n\nOne Hop Commericial VPN Tunnels are no log by **policy**.  You just have to trust that they are telling the truth.\n\nLokinet is no log by **design** it doesn't have a choice in this matter because the technology greatly hampers efforts to do so.\n\nAny Lokinet Client can be an exit if they want to and it requires no service node stakes. Exits are able to charge users for exiting to the internet, tooling for orechestrating this is in development.\n"
  },
  {
    "path": "docs/project-structure.md",
    "content": "# Lokinet Project Structure \n\nthis codebase is a bit large. this is a high level map of the current code structure.\n\n## Lokinet executable main functions `(/daemon)`\n\n* `lokinet.cpp`: lokinet daemon executable\n* `lokinet.swift`: macos sysex/appex executable\n* `lokinet-vpn.cpp`: lokinet rpc tool for controlling exit node usage\n* `lokinet-bootstrap.cpp`: legacy util for windows, downloads a bootstrap file via https\n\n\n## Lokinet public headers `(/include)`\n\n`lokinet.h and lokinet/*.h`: C headers for embedded lokinet \n\n`llarp.hpp`: semi-internal C++ header for lokinet executables\n\n\n## Lokinet core library `(/llarp)` \n\n* `/llarp`: contains a few straggling compilation units\n* `/llarp/android`: android platform compat shims\n* `/llarp/apple`: all apple platform specific code\n* `/llarp/config`: configuration structs, generation/parsing/validating of config files\n* `/llarp/consensus`: network consenus and inter relay testing \n* `/llarp/constants`: contains all compile time constants \n* `/llarp/crypto`: cryptography interface and implementation, includes various secure helpers\n* `/llarp/dht`: dht message structs, parsing, validation and handlers of dht related parts of the protocol \n* `/llarp/dns`: dns subsytem, dns udp wire parsers, resolver, server, rewriter/interceptor, the works\n* `/llarp/ev`: event loop interfaces and implementations \n* `/llarp/exit`: `.snode` endpoint \"backend\"\n* `/llarp/handlers`: packet endpoint \"frontends\"\n* `/llarp/iwp`: \"internet wire protocol\", hacky homegrown durable udp wire protocol used in lokinet\n* `/llarp/link`: linklayer (node to node) communcation subsystem\n* `/llarp/messages`: linklayer message parsing and handling \n* `/llarp/net`: wrappers and helpers for ip addresses / ip ranges / sockaddrs, hides platform specific implemenation details\n* `/llarp/path`: onion routing path logic, both client and relay side, path selection algorithms.\n* `/llarp/peerstats`: deprecated\n* `/llarp/quic`: plainquic shims for quic protocol inside lokinet \n* `/llarp/router`: the relm of the god objects\n* `/llarp/routing`: routing messages (onion routed messages sent over paths), parsing, validation and handler interfaces.\n* `/llarp/rpc`: lokinet zmq rpc server and zmq client for externalizing logic (like with blockchain state and custom `.loki` endpoint orchestration)\n* `/llarp/service`: `.loki` endpoint \"backend\"\n* `/llarp/simulation`: network simulation shims\n* `/llarp/tooling`: network simulation tooling\n* `/llarp/util`: utility function dumping ground\n* `/llarp/vpn`: vpn tunnel implemenation for each supported platform\n* `/llarp/win32`: windows specific code\n\n\n## Component relations\n\n### `/llarp/service` / `/llarp/handlers` / `/llarp/exit`\n\nfor all codepaths for traffic over lokinet, there is 2 parts, the \"frontend\" and the \"backend\".\n\nthe \"backend\" is responsible for sending and recieving data inside lokinet using our internal formats via paths, it handles flow management, lookups, timeouts, handover, and all state we have inside lokinet.\n\nthe \"fontend\", is a translation layer that takes in IP Packets from the OS, and send it to the backend to go where ever it wants to go, and recieves data from the \"backend\" and sends it to the OS as an IP Packet.\n\nthere are 2 'backends': `.snode` and `.loki`\n\nthere are 2 'frontends': \"tun\" (generic OS vpn interface) and \"null\" (does nothing)\n\n* `//TODO: the backends need to be split up into multiple sub components as they are a kitchen sink.`\n* `//TODO: the frontends blend into the backend too much and need to have their boundery clearer.`\n\n\n### `/llarp/ev` /  `/llarp/net` / `/llarp/vpn`\n\nthese contain most of the os/platform specific bits\n\n* `//TODO: untangle these`\n\n\n### `/llarp/link` /  `/llarp/iwp`\n\nnode to node traffic logic and wire protocol dialects \n\n* `//TODO: make better definitions of interfaces`\n* `//TODO: separte implementation details from interfaces`\n\n\n## Platform contrib code `(/contrib)`\n\ngrab bag directory for non core related platform specific non source code\n\n* `/contrib/format.sh`: clang-format / jsonnetfmt / swiftformat helper, will check or correct code style.\n\nsystem layer and packaging related:\n \n* `/contrib/NetworkManager`\n* `/contrib/apparmor`\n* `/contrib/systemd-resolved`\n* `/contrib/lokinet-resolvconf`\n* `/contrib/bootstrap`\n\nbuild shims / ci helpers\n\n* `/contrib/ci`\n* `/contrib/patches`\n* `/contrib/cross`\n* `/contrib/android.sh`\n* `/contrib/android-configure.sh`\n* `/contrib/windows.sh`\n* `/contrib/windows-configure.sh`\n* `/contrib/mac.sh`\n* `/contrib/ios.sh`\n* `/contrib/cross.sh`\n"
  },
  {
    "path": "docs/readme.md",
    "content": "# Lokinet Docs\n\nThis is where Lokinet documentation lives.\n\n## Contents:\n\n### Local Environment Set-Up\n  - [Installing Lokinet](install.md)\n  - [Using Lokinet](ideal-ux.md)\n\n\n### High Level Overview\n  - [Lokinet versus \\[insert network technology name here\\]](net-comparisons.md)\n  - [Lokinet architecture](architecture.md)\n  - [Lokinet and DNS](dns-overview.md)\n  - [Limitations of Lokinet](we-cannot-make-sandwiches.md)\n\n\n### Lokinet Internals\n  - [Git repo layout and project structure](project-structure.md)\n  - [Building Doxygen Docs for internals](doxygen.md)\n\n\n### Lokinet (SN)Application Developer Portal\n  - [SNapps development overview](snapps-dev-guide.md)\n  - [Embedded Lokinet](liblokinet-dev-guide.md)\n"
  },
  {
    "path": "docs/refactor_notes.md",
    "content": "# High Level Iterative Approach\n\nthe desired outcome of this refactor will be splitting the existing code up into a stack of new components.\na layer hides all functionality of the layer below it to reduce the complexity like the OSI stack intends to.\nthe refactor starts at the top layer, wiring up the old implementation piecewise to the top layer.\nonce the top layer is wired up to the old implementation we will move down to the next layer.\nthis will repeat until we reach the bottom layer.\nonce the old implementation is wired up into these new clearly defined layers, we can fixup or replace different parts of each layer one at a time as needed.\n\nworking down from each layer will let us pick apart the old implementation (if needed) that we would wire up to the new base classes for that layer we are defining now without worrying about what is below it (yet).\n\nthis refactor is very able to be split up into small work units that (ideally) do not confict with each other.\n\n\nPDU: https://en.wikipedia.org/wiki/Protocol_data_unit\n\n# The New Layers\n\nfrom top to bottom the new layers are:\n\n* Platform Layer\n* Flow Layer\n* Routing Layer\n* Onion Layer\n* Link Layer\n* Wire Layer\n\n\n## Platform Layer\n\nthis is the top layer, it is responsibile ONLY to act as a handler of reading data from the \"user\" (via tun interface or whatever) to forward to the flow layer as desired, and to take data from the flow layer and send it to the \"user\".\nany kind of IP/dns mapping or traffic isolation details are done here. embedded lokinet would be implemented in this layer as well, as it is without a full tun interface.\n\nPlatform layer PDU are what the OS gives us and we internally convert them into flow layer PDU and hand them off to the flow layer.\n\n\n## Flow Layer\n\nthis layer is tl;dr mean to multiplex data from the platform layer across the routing layer and propagating PDU from the routing to the platform layer if needed.\n\nthe flow layer is responsible for sending platform layer PDU across path we have already established.\nthis layer is informed by the routing layer below it of state changes in what paths are available for use.\nthe flow layer requests from the layer below to make new paths if it wishes to get new ones on demand.\nthis layer will recieve routing layer PDU from the routing layer and apply any congestion control needed to buffer things to the os if it is needed at all.\n\nflow layer PDU are (data, ethertype, src-pubkey, dst-pubkey, isolation-metric) tuples.\ndata is the datum we are tunneling over lokinet. ethertype tells us what kind of datum this is, e.g. plainquic/ipv4/ipv6/auth/etc.\nsrc-pubkey and dst-pubkey are public the ed25519 public keys of each end of the flow in use.\nthe isolation metric is a piece of metadata we use to distinguish unique flows (convotag). in this new seperation convotags explicitly do not hand over across paths.\n\n\n## Routing Layer\n\nthis layer is tl;dr meant for path management but not path building.\n\nthe routing layer is responsible for sending/recieving flow layer PDU, DHT requests/responses, latency testing PDU and any other kind of PDU we send/recieve over the onion layer.\nthis layer will be responsible for managing paths we have already built across lokinet.\nthe routing layer will periodically measure path status/latency, and do any other kinds of perioidic path related tasks post build.\nthis layer when asked for a new path from the flow layer will use one that has been prebuilt already and if the number of prebuilt paths is below a threshold we will tell the onion layer to build more paths.\nthe routing layer will recieve path build results be their success/fail/timeout from the onion layer that were requested and apply any congestion control needed at the pivot router.\n\nrouting layer PDU are (data, src-path, dst-path) tuples.\ndata is the datum we are transferring between paths.\nsrc-path and dst-path are (pathid, router id) tuples, the source being which path this routing layer PDU originated from, destination being which path it is going to.\nin the old model, router id is always the router that recieves it as the pivot router, this remains the same unless we explicitly provide router-id.\nthis lets us propagate hints to DHT related PDU held inside the datum.\n\n\n## Onion Layer\n\nthe onion layer is repsonsible for path builds, path selection logic and low level details of encrypted/decrypting PDU that are onion routed over paths.\nthis layer is requested by the routing layer to build a path to a pivot router with an optional additional constraints (e.g. unique cidr/operator/geoip/etc, latency constaints, hop length, path lifetime).\nthe onion layer will encrypt PDU and send them to link layer as (frame/edge router id) tuples, and recieve link layer frames from edge routers, decrypt them and propagate them as needed to the routing layer.\nthis layer also handles transit onion traffic and transit path build responsibilities as a snode and apply congestion control as needed per transit path.\n\nthe onion layer PDU are (data, src-path, dst-path) tuples.\nsrc-path and dst-path are (router-id, path-id) tuples which contain the ed25519 pubkey of the node and the 128 bit path-id it was associated with.\ndata is some datum we are onion routing that we would apply symettric encryption as needed before propagating to upper or lower layers.\n\n\n## Link Layer\n\nthe link layer is responsbile for transmission of frames between nodes.\nthis layer will handle queuing and congestion control between wire proto sessions between nodes.\nthe link layer is will initate and recieve wire session to/from remote nodes.\n\nthe link layer PDU is (data, src-router-id, dst-router-id) tuples.\ndata is a datum of a link layer frame.\nsrc-router-id and dst-router-id are (ed25519-pubkey, net-addr, wire-proto-info) tuples.\nthe ed25519 pubkey is a .snode address, (clients have these too but they are ephemeral).\nnet-addr is an (ip, port) tuple the node is reachable via the wire protocol.\nwire-proto-info is dialect specific wire protocol specific info.\n\n## Wire Layer\n\nthe wire layer is responsible for transmitting link layer frames between nodes.\nall details here are specific to each wire proto dialect.\n"
  },
  {
    "path": "docs/snapps-dev-guide.md",
    "content": "# (SN)Apps Development Guide\n\n\n## Our approach\n\n`// TODO: this`\n\n## Differences From Other approaches\n\n`// TODO: this`\n\n### SOCKS Proxy/HTTP Tunneling\n\n`// TODO: this`\n\n### Embedding a network stack\n\n`// TODO: this`\n\n## High Level Code Practices\n\n`// TODO: this`\n\n### Goodisms\n\n`// TODO: this`\n\n### Badisms\n\n`// TODO: this`\n"
  },
  {
    "path": "docs/spanish/LICENSE",
    "content": "Especificaciones del Protocolo de Enrutado Anónimo de Baja Latencia - Low Latency Anonymous Routing Protocol\nEscrito en el 2017 por Jeff Becker <jeff@i2p.rocks>\n\nEn el alcance de lo posible dentro de la ley, el (los) autor(es) dedica(ron) al dominio público mundial todo \nel derecho de copia y los derechos relacionados y semejantes relacionados a esta especificación del protocolo.\nEste software se distribuido sin garantia alguna. Usted deberia tener una copia de la Dedicación de Dominio Público\nCC0 junto con este software. En caso contrario, vea <https://creativecommons.org/publicdomain/zero/1.0/deed.es>.\n"
  },
  {
    "path": "docs/spanish/README",
    "content": "Carpeta de las Especificaciones del Protocolo\n\nTodo los documentos de esta carpeta están licenciados como CC0 y son del dominio público.\n\nPor favor tome nota que la implementación de referencia de LokiNET está licenciada bajo la licencia ZLIB.\n"
  },
  {
    "path": "docs/spanish/vision-general.txt",
    "content": "LLARP - Low Latency Anon Routing Protocol -  Protocolo de Enrutado Anónimo de Baja Latencia\n\n\tResumen TL;DR: un router onion con una interfaz tun para transportar paquetes ip\n\tde forma anónima entre usted y la internet, y desde dentro de ella hacia otros usuarios.\n\t\n\tEste documento describe la visión general de LLARP, detalla todas las metas\n\tdel proyecto, lo que no son metas y la arquitectura de la red desde una perspectiva\n\tgeneral.\t\n\t\n\tPrefacio\t\n\t\n\tTrabajar en I2P ha sido experiencia realmente grande para todo el que se involucra.\n\tDespués bastante deliberación, yo decidí empezar a construir un protocolo onion de\n\t\"proxima generacion\". LLARP es específicamente (en la actualidad) un proyecto de\n\tinvestigación para explorar la siguiente cuestión: \n\n\n\t\"¿Que hubiera sido si I2P fuera construido en el año presente (2018)? ¿Que hubiera\n\tsido diferente?\"\t\n\n\t\n\tLo que No son Metas del Proyecto: \n\t\n\n\tEste proyecto no intenta resolver la correlación por la forma del tráfico o ataques a\n\tla red patrocinadas por el estado. Lo primero es una propiedad inherente de las redes\n\tcomputacionales de baja latencia que yo personalmente no pienso que es posible\n\tcompletamente \"resolver\" de forma apropiada. Lo segundo es una amenaza que por el\n\tmomento se encuentra fuera de los alcances de las herramientas actuales que me están \n\tdisponibles. \n\t\n\tEste proyecto no pretende ser la aplicación mágica de nivel curalotodo para la\n\taplicación o la seguridad del usuario final. Después de todo, eso es un problema que\n\texiste entre la silla y el teclado.\n\n\t\n\tLa Única Meta del Proyecto:\n\t\n\t\n\tLLARP en una suite de protocolos que pretende mantener anónima la IP mediante el\n\tofrecimiento de un agente de túnel anónimo a nivel red (IPv4/IPv6) tanto para\n\t\"servicios ocultos\" y la comunicación de regreso a la \"red transparente\" (la Internet \n\tcomun). Tanto las comunicación del servicio oculto y la red transparente DEBEN\n\tpermitir tanto el trafico de salida y el tráfico de entrada a nivel red sin implementar\n\tNAT alguna (con excepción de la IPv4 de la cual la NAT es permitida debido a la\n\tescasez de direcciones).\n\t\n\t\n\tEn concreto, Queremos permitir tanto la salida y la entrada anonima del trafico\n\ta nivel red entre las redes habilitadas por LLARP y la Internet.\n\t\n\tLas razones de porque empezar desde cero:\n\n\tA pesar de los mejores esfuerzos del Proyecto Tor para popularizar el uso de Tor,\n\tTor2Web parece ser ampliamente popular para las personas que no desean optar estar\n\tdentro del ecosistema. Mi solucion propuesta seria permitir el tráfico de entrada desde\n\tlos \"nodos de salida\" en adición de permitir el tráfico de salida. No tengo idea\n\ten cómo pudiera hacer esto los protocolos actuales en Tor, o si es posible o\n\trecomendable intentar tal cosa ya que no estoy familiarizado con su ecosistema.\n\t\n\t\n\tI2P se hubiera usado como un medio para transito anonimo IP cifrado pero la red actual\n\ttiene problemas con la latencia y el rendimiento. Avanzar I2P sobre criptografía\n\tmoderna está en proceso dentro de I2P, proceso que ya lleva por lo menos 5 años y con\n\tmenos progreso que lo deseado. Así como algunos antes de mi, yo he llegado a la \n\tconclusión que seria mas rapido rehacer todo el stack \"de la forma correcta\" que estar \n\tesperando a que I2P termine sus avances. Dicho esto, nada está previniendo a I2P en ser \n\tusado para el transito de trafico anonimo IP cifrado dentro de un futuro en que I2P\n\ttermine sus migraciones de protocolo, pero yo no quiero esperarlo.\n\t\n\t\n\t\n\tEn concreto, yo quiero tomar las \"mejores partes\" de Tor e I2P, y hacer una nueva\n\tsuite de protocolos.\n\n\t\n\tPara ambos, tanto Tor e I2P, les tengo 2 categorías de sus atributos.\n\t\n\t\n\tlo bueno\t\n\tlo malo y lo feo\n\t\n\t\n\tLo bueno (I2P):\n\t\n\t\n\tI2P apunta a proveer una capa de red anónima de carga balanceada insuplantable.\n\t\n\t\n\tYo quiero esta característica\n\t\n\t\n\tI2P tiene agilidad de confianza, no necesita tener alguna confianza integrada en el\n\tcódigo dentro de su arquitectura de red. Incluso la fase de arranque de la red puede \n\trealizarse desde un solo router, si el usuario lo desea (aunque esto esté desaconsejado)\n\t\n\n\tYo quiero esta característica\t\n\t\n\n\tLo bueno (Tor):\t\n\t\n\t\n\tTor abarca la realidad de la actual infraestructura de la Internet al tener una\n\tarquitectura cliente/servidor. Esto permite tener barreras de acceso muy bajas\n\tpara usar la red Tor, y barreras más altas de acceso para contribuir a la \n\tinfraestructura de enrutado. Esto promueve una forma saludable de red con servidores\n\tde alta capacidad ofreciendo servicios a clientes de baja capacidad que \"se cuelgan \n\tdel extremo\" de la red.\n\n\t\n\tYo quiero esta característica\n\t\n\t\n\tLo malo y lo feo (I2P):\n\t\n\t\n\tMalo: I2P usa criptografia vieja, en especial ElGamal de 2048 bits usando primos no\n\testandares. El uso de ElGamal es tan constante a traves del stack del protocolo I2P\n\tque existe en cada nivel suyo. Removerlo es una tarea masiva que está tomando\n\tmucho MUCHO tiempo.\n\t\n\t\n\tYo no quiero esta característica\n\t\n\n\tFeo: I2P no puede actualmente mitigar la mayoría de los ataques Sybil, con su\n\tactual arquitectura de red. I2P recientemente añadió algunas soluciones de listas de \n\tbloqueo que están firmadas por los firmantes de lanzamiento, pero esto es probable que\n\tno escale en el evento de una ataque \"grande\". Además I2P tampoco tiene el personal \n\tpara ese tipo de ataques.\n\t\n\t\n\tEste es un problema difícil de resolver en que la red Loki pudiera ayudar con la \n\tcreación de una barrera financiera para correr múltiples relays.\n\t\n\t\n\tLo malo y lo feo (Tor):\n\t\n\t\n\tMalo: Tor está estrictamente orientado al TCP.\n\t\n\tYo no quiero esta característica\n\n\n\n"
  },
  {
    "path": "docs/tcp-over-quic.md",
    "content": "# \"liblokinet\" TCP-over-QUIC\n\nIn order for lokinet to work in an embedded version (which I will call \"liblokinet\" in this\ndocument), which lokinet cannot create TUN device (either because the host OS doesn't support them,\nor because lokinet needs to run without permissions to manage them) lokinet needs a solution for\nsending TCP data from the device to a remote lokinet client (i.e. a snapp, a snode, or another\nliblokinet client).  Since the vast majority of network connectivity relies on TCP stream\nconnections, not supporting them would be a severe limitation of a lokinet library that would make\nit nearly useless.\n\nTraditional \"full\" lokinet does not need to solve this problem: it creates virtual IPs on the TUN\ninterface that map to every looked-up `.loki` address and then the host system's in-kernel TCP layer\nhandles the intricacies of TCP including acknowledgement, retry, and so on.  While there are\nuser-space TCP implementations available, they are generally incomplete, unmaintained, or both,\nwhich would mean substantial work and ongoing maintenance for us to adopt or reimplement such a\nuser-space TCP layer, for which we would most likely be the only user and contributor.\n\nInstead this proposal is for lokinet to support a tunneled TCP stream mode where TCP traffic is\ncarried over lokinet via a subset of the\n[QUIC](https://datatracker.ietf.org/doc/draft-ietf-quic-transport/) protocol.  Unlike TCP, QUIC has\nseveral well-maintained user-space implementations which allow us to use, rather than create, a\nwell-maintained QUIC implementation.\n\n## Overview\n\nThe high-level strategy of how we handle such a stream connection is to have TCP connections\nestablished only within the local device.  A liblokinet application would invoke a lokinet call to\nestablish such a connection to proxy to a remote host by lokinet name and TCP port.  This would\nfirst establish a lokinet connection to the remote host, then open a QUIC connection over it and\nstart listening for TCP connections on a local port.  When a new TCP connection is established on\nthis port lokinet will establish a new QUIC stream over the existing connection, specifying the\ndestination port while initializing the stream.  (The client is free to establish as many TCP\nconnections as it wants: each one becomes a separate QUIC stream).\n\nThe situation is similar for the receiving lokinet client: it would listen for incoming QUIC\nconnections on the local lokinet IP and, when establishing a QUIC stream, would establish a local\nTCP connection to the requested port on the lokinet IP.  Any incoming stream data is then forwarded\ninto this TCP connection, and any responses are sent back via the QUIC stream.\n\n## Example\n\nFor example, suppose `snap7.loki` is a lokinet snapp with a web server listening on port 80 and a\nliblokinet client `omg42.loki` wants to connect to it to retrieve a cat photo.  With a full\nlokinet client, the DNS request for `omg56789.loki` triggers creation of a virtual IP on the TUN\ndevice, returns the IP to the system, and any TCP packets sent to this IP are forwarded to the\nprimary lokinet IP of `azfoj123.loki`, where an HTTP server is ready and waiting to provide cat\nphotos.\n\nWith a liblokinet client, this process will looks a little different: the client will first make a\ncall to the liblokinet library (rather than a DNS request) specifying the lokinet host name and TCP\nport it wants to connect to (note that this is pseudo-code; the actual implementation calls will\nhave to deal with various details such as connection delays and timeouts that are omitted here):\n\n    result = lokinet_stream_connect(lokinet_addr, port)\n    if result->connection_established:\n        http_get(\"http://\" + result->local_address + \":\" + result->local_port + \"/cat.jpg\")\n\nHere `http_get` would need no knowledge of lokinet at all: it will simply connect via TCP to an\naddress such as `127.0.0.1:4716` for the HTTP request.  It will send the request, and receive it,\nover this localhost TCP socket.\n\nInternally, lokinet will have established a QUIC connection to the remote host, and started\nlistening for TCP connections on the localhost port.  When `http_get` establishes a TCP connection\non this local port it will create a QUIC stream on the established QUIC connection and forward all\nstream data received from the TCP connection into the QUIC stream, and any data that comes back over\nthe QUIC stream will similarly be copied into the localhost TCP connection.\n\nEffectively the data path of data send from the app on omg42.loki to the HTTP snapp on omg42.loki\nlooks like this:\n\n    ┌omg42.loki────────────┐                             ┌snap7.loki───────────┐\n    │ Main app thread      │                             │ HTTP                │\n    │ TCP localhost:4567 ─>│─┐                           │ TCP 172.16.0.1:80 <─│─┐\n    ├──────────────────────┤ │                           ╞═════════════════════╡ │\n    │ liblokinet (in app)  │ │                           │ lokinet (on host)   │ │\n    │ TCP localhost:4567 <─│─┘                         ┌>│─> QUIC UDP          │ │\n    │           QUIC UDP ─>│───... Lokinet routers ...─┘ │ TCP 172.16.0.1:80 ─>│─┘\n    └──────────────────────┘                             └─────────────────────┘\n\n(These connections are all bi-direction, so any TCP stream data replied from omg42.loki follows the\nsame path in reverse.)\n\n## Implementation details/notes\n\nImplementation library: `ngtcp2` is a robust, maintained library that fits our needs well.\n\n### Not a general QUIC server/client\n\nThe QUIC tunnel described here is *only* for Lokinet TCP streams; it is not intended to be\ninteroperable with general QUIC clients, which allows us some leeway to not support some aspects of\nQUIC that are of no advantage over a lokinet conversation.\n\n### No encryption\n\nSince lokinet traffic is itself encrypted and private, the built-in TLS layers of QUIC are something\nthat we don't need or want.  Thus the QUIC implementation used will simply use no-op encryption to\npass data and avoid/ignore certificates.  (`ngtcp2`, in particular, allows pluggable authentication\nto allow this).\n\n### No address verification\n\nQUIC recommends address verification (among other things, to avoid amplification attacks).  Lokinet\nconnections already provide this and so we can safely not use it.\n\n### Stream establishing\n\nEstablishing a QUIC stream requires sending additional information during connection: namely the\ntarget connection port.  Thus establishing a new stream will require some additional data to be\npassed, likely as the initial stream data.  (Specification of how this data is to be encoded is not\nyet specified).\n\n### Incoming TCP-over-QUIC connections\n\nHandling of incoming connections to a liblokinet client will require a similar process, but in\nreverse:\n\n- the client starts listening on a localhost TCP port\n- the client makes a call to liblokinet to inform it of this available listening port\n- incoming QUIC tunneled streams attempting to connect to that registered port are accepted and\n  establish a new TCP connection as long as the stream stays open; data is forwarded between the two\n  connections.\n- Should the client require end-point verification liblokinet will provide a function that can look\n  up the remote lokinet address based on the source port of the TCP connection.  (This is different\n  from but analogous to a snapp doing a reverse DNS lookup on the source address to determine the\n  remote address).\n\nNote: to be externally reachable by other lokinet clients, a liblokinet client would have to publish\nan introset; this introset would also include an additional flag indicating that TCP connections\nmust be tunneled through a TCP-over-QUIC connection.\n\nNote 2: we additionally may want to signal during connection that new TCP connections back to us\nshould be done over a QUIC tunnel, which requires also adding a flag when establishing the\nlokinet conversation.\n\n### Non-tunneled incoming TCP connections\n\nWithout a controllable TCP stack we have no ability to accept these, however since the introset (and\nconversation initiation) indicates that TCP should be tunneled, we should just drop these packets.\n\n## Lokinet implementation notes\n\n### Outbound connections -- liblokinet\n\nThe application makes a liblokinet library call such as\n\n    lokinet_stream_result res;\n    lokinet_outbound_stream(&res, \"some-snapp.loki\", 2345);\n\nThis initiates an outbound connection to the given lokinet remote, asking to connect to port 2345 on\nthe remote.  Plainquic begins listening on a random localhost port, and returns this via an entry in\n`res`.  New connections establishes to this localhost port initiate new streams on the quic\nconnection which are tunneled to the remote end.\n\n### Inbound connections -- liblokinet\n\nThe application needs to start listening on one or more TCP ports (e.g. on localhost, but doesn't\nhave to be) and then registers a callback with lokinet about the availability of this port for\nincoming plainquic connections by setting up a callback:\n\n```C\n    int accept_inbound(const char *lokinet_addr, uint16_t port, sockaddr *addr, void *context) {\n        // lokinet_addr is the remote lokinet client trying to establish a stream\n        // port is the port they are trying to reach\n        // If the client is allowed then set the local TCP socket address that the tunnel should\n        // connect to in `addr` (which is big enough to allow either sockaddr_in or sockaddr_in6)\n        sockaddr_in* a = (sockaddr_in*)addr;\n        a->sin_family = AF_INET;\n        a->sin_addr = INADDR_LOOPBACK;\n        a->sin_port = htons(5678); // NB: Doesn't have to be the passed-in `port`\n        return 0;\n        // If this callback doesn't handle the requested port (will try other callbacks):\n        return -1;\n        // If this callback does handle it and the connection should be refused:\n        return -2;\n        // (Return values other than 0/-1/-2 are reserved and should not be used).\n    }\n    lokinet_inbound_stream(&accept_inbound, NULL /*context*/);\n```\nor, for the very simple case where connections should be available on some localhost port:\n```C\n    // All incoming tunneled connections for port 5678 should go to localhost:5678\n    lokinet_inbound_stream_simple(5678);\n```\n\nWhen a new plainquic connection arrives, if such a callback has been registered it will be called to\ndetermine whether the connection should be accepted and, if it is, where streams opened on that\nconnection should be sent.  (For the simple version, all inbound connections on port 5678 would be\naccepted and would be forwarded to localhost:5678; inbound connections for other ports would be\nrefused).\n\nEach new plainquic stream initiated by the remote connection then establishes a new TCP connection\nto the IP/port set by the callback.\n\n### Outbound connections - full lokinet\n\nWhen attempting to connect to a client who has indicated in its introset that it requires plainquic\nconnections then plainquic will bind to and listen on the virtual (tun) TCP/IP port and establish a\nplainquic connection to the remote liblokinet on the given port.  (Future connections to this port\nwill establish new streams on the existing connection).\n\nSetting up the initial listener involves intercepting the initial TCP connection attempt (i.e. the\nSYN packet), starting to listen on it while simultaneously initiating the plainquic connection over\nlokinet.  It may work sufficiently well (investigation required) to simply drop this initial SYN\npacket and let the initiator retry in a few moments to attack to the new listener which now goes\ninto the plainquic listener which establishes a new stream.\n\nThereafter the local application simply talks to this local listener and all stream data gets\ntunneled over lokinet to the remote liblokinet.\n\n### Inbound connections - full lokinet\n\nThis is fairly simple: when incoming quic-tunneled packets arrive we start up a plainquic server (if\nnot already running), deliver the packets into it, and it tunnels incoming stream data into TCP\nconnections to the primary lokinet IP (using the IP mapped to the lokinet endpoint as the source\naddress).\n\n\nTODO:\n- Add quic protocol type to llarp/service/protocol_types.hpp\n- Convert stuff in plainquic code to use lokinet structures (e.g. logging, address encapsulation)\n- Add handler for QUIC packets to llarp/handlers/tun.cpp that see that protocol type and forward the\n  packet off to the quic server to handle.\n- Get at the uvw event loop from the quic code so that we can put the plainquic stuff onto it rather\n  than spinning up its own event loop.  I was thinking about something like:\n  `virtual std::shared_ptr<void> get_uvw_loop() { return nullptr; }` in ev.h, and an override that\n  returns the uvw event loop in the ev_libuv.h subclass (the type erasure through the shared_ptr<void>\n  means ev.h doesn't have to depend on any uvw.h headers).  Then the quic code can just do something\n  like:\n    auto uv_loop = std::static_pointer_cast<uvw::Loop>(ev->get_uvw_loop());\n    if (not uv_loop) { die(\"horribly\"); }\n- convert the crap in the `main` functions copied from plainquic test code to exposed library calls.\n- decide whether we start up a quic server and/or client on demand, or just always start it.\n\n\nOutgoing conns:\n- Add \"supported protocols\" item to introset and (for liblokinet) leave off IPv4/v6 flags, but add\n  quic protocol flag.\n\n"
  },
  {
    "path": "docs/we-cannot-make-sandwiches.md",
    "content": "# What Lokinet can't do \n\nLokinet does a few things very well, but obviously can't do everything.\n\n## Anonymize OS/Application Fingerprints\n\nMitigating OS/Application Fingerprinting is the responsibility of the OS and Applications. Lokinet is an Unspoofable IP Packet Onion router, tuning OS fingerprints to be uniform would be a great thing to have in general even outside of the context of Lokinet. The creation of such an OS bundle is a great idea, but outside the scope of Lokinet. We welcome others to develop a solution for this.\n\n## Malware\n\nLokinet cannot prevent running of malicious programs. Computer security unfortunately cannot be solved unilaterally by networking software without simply dropping all incoming and outgoing traffic.\n\n## Phoning Home\n\nLokinet cannot prevent software which sends arbitrary usage data or private information to Microsoft/Apple/Google/Amazon/Facebook/etc. If you are using a service that requires the ability to phone home in order to work, that is a price you pay to use that service.\n\n## Make Sandwiches\n\nAt its core, Lokinet is technology that anonymizes and authenticates IP traffic. At this current time Lokinet cannot make you a sandwich. No, not even as root.\n"
  },
  {
    "path": "external/CMakeLists.txt",
    "content": "option(SUBMODULE_CHECK \"Enables checking that vendored library submodules are up to date\" ON)\nif(SUBMODULE_CHECK)\n  find_package(Git)\n  if(GIT_FOUND)\n    function(check_submodule relative_path)\n      execute_process(COMMAND git rev-parse \"HEAD\" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${relative_path} OUTPUT_VARIABLE localHead)\n      execute_process(COMMAND git rev-parse \"HEAD:external/${relative_path}\" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE checkedHead)\n      string(COMPARE EQUAL \"${localHead}\" \"${checkedHead}\" upToDate)\n      if (upToDate)\n        message(STATUS \"Submodule 'external/${relative_path}' is up-to-date\")\n      else()\n        message(FATAL_ERROR \"Submodule 'external/${relative_path}' is not up-to-date. Please update with\\ngit submodule update --init --recursive\\nor run cmake with -DSUBMODULE_CHECK=OFF\")\n      endif()\n\n      # Extra arguments check nested submodules\n      foreach(submod ${ARGN})\n        execute_process(COMMAND git rev-parse \"HEAD\" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${relative_path}/${submod} OUTPUT_VARIABLE localHead)\n        execute_process(COMMAND git rev-parse \"HEAD:${submod}\" WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${relative_path} OUTPUT_VARIABLE checkedHead)\n        string(COMPARE EQUAL \"${localHead}\" \"${checkedHead}\" upToDate)\n        if (NOT upToDate)\n            message(FATAL_ERROR \"Nested submodule '${relative_path}/${submod}' is not up-to-date. Please update with\\ngit submodule update --init --recursive\\nor run cmake with -DSUBMODULE_CHECK=OFF\")\n        endif()\n      endforeach()\n    endfunction ()\n\n    message(STATUS \"Checking submodules\")\n    check_submodule(CLI11)\n    check_submodule(nlohmann)\n    check_submodule(oxen-libquic)\n    check_submodule(oxen-mq)\n    check_submodule(pybind11)\n    check_submodule(sqlite_orm)\n  endif()\nendif()\n\nmacro(system_or_submodule BIGNAME smallname target pkgconf subdir)\n  if(NOT TARGET ${target})\n    option(FORCE_${BIGNAME}_SUBMODULE \"force using ${smallname} submodule\" OFF)\n    if(NOT BUILD_STATIC_DEPS AND NOT FORCE_${BIGNAME}_SUBMODULE AND NOT FORCE_ALL_SUBMODULES)\n      pkg_check_modules(${BIGNAME} ${pkgconf} IMPORTED_TARGET)\n    endif()\n    if(${BIGNAME}_FOUND)\n      add_library(${smallname} INTERFACE)\n      if(NOT TARGET PkgConfig::${BIGNAME} AND CMAKE_VERSION VERSION_LESS \"3.21\")\n        # Work around cmake bug 22180 (PkgConfig::THING not set if no flags needed)\n      else()\n        target_link_libraries(${smallname} INTERFACE PkgConfig::${BIGNAME})\n      endif()\n      message(STATUS \"Found system ${smallname} ${${BIGNAME}_VERSION}\")\n    else()\n      message(STATUS \"using ${smallname} submodule ${subdir}\")\n      add_subdirectory(${subdir} EXCLUDE_FROM_ALL)\n    endif()\n    if(NOT TARGET ${target})\n      add_library(${target} ALIAS ${smallname})\n    endif()\n  endif()\nendmacro()\n\nsystem_or_submodule(OXENC oxenc oxenc::oxenc liboxenc>=1.5.0 oxen-libquic/external/oxen-encoding)\nsystem_or_submodule(OXENLOGGING oxen-logging oxen::logging liboxen-logging>=1.2.0 oxen-libquic/external/oxen-logging)\nif(LOKINET_FULL)\n    system_or_submodule(OXENMQ oxenmq oxenmq::oxenmq liboxenmq>=1.3 oxen-mq)\nendif()\nsystem_or_submodule(FMT fmt fmt::fmt fmt>=9 oxen-libquic/external/oxen-logging/fmt)\n\nset(JSON_BuildTests OFF CACHE INTERNAL \"\")\nset(JSON_Install OFF CACHE INTERNAL \"\")\nsystem_or_submodule(NLOHMANN nlohmann_json nlohmann_json::nlohmann_json nlohmann_json>=3.7.0 nlohmann)\n\n\nif(LOKINET_HIVE)\n  add_subdirectory(pybind11 EXCLUDE_FROM_ALL)\nendif()\n\n\nsystem_or_submodule(CLI11 CLI11 CLI11::CLI11 CLI11>=2.3.0 CLI11)\n\n\nif(LOKINET_PEERSTATS)\n  add_library(sqlite_orm INTERFACE)\n  target_include_directories(sqlite_orm SYSTEM INTERFACE sqlite_orm/include)\n  if(NOT TARGET sqlite3)\n    add_library(sqlite3 INTERFACE)\n    pkg_check_modules(SQLITE3 REQUIRED IMPORTED_TARGET sqlite3)\n    target_link_libraries(sqlite3 INTERFACE PkgConfig::SQLITE3)\n  endif()\n  target_link_libraries(sqlite_orm INTERFACE sqlite3)\nendif()\n\n\nset(LIBQUIC_BUILD_TESTS OFF CACHE BOOL \"\")\nsystem_or_submodule(OXENQUIC quic oxen::quic liboxenquic>=1.6 oxen-libquic)\n\n\n# libcrypt defaults, only on with macos and non static linux\nset(default_libcrypt OFF)\n\nif(CMAKE_SYSTEM_NAME MATCHES \"Linux\" AND NOT STATIC_LINK)\n  pkg_check_modules(LIBCRYPT libcrypt IMPORTED_TARGET)\n  if(LIBCRYPTO_FOUND)\n      set(default_libcrypt ON)\n  endif()\nendif()\nif(MACOS)\n  set(default_libcrypt ON)\nendif()\n\noption(WITH_LIBCRYPT \"enable fast password hash with libcrypt\" ${default_libcrypt})\n\nadd_library(lokinet-libcrypt INTERFACE)\nif(WITH_LIBCRYPT)\n  pkg_check_modules(LIBCRYPT libcrypt IMPORTED_TARGET REQUIRED)\n  target_compile_definitions(lokinet-libcrypt INTERFACE LOKINET_HAVE_CRYPT)\n  target_link_libraries(lokinet-libcrypt INTERFACE PkgConfig::LIBCRYPT)\n  message(STATUS \"using libcrypt ${LIBCRYPT_VERSION}\")\nelse()\n  # TODO static build lib crypt?\n  message(STATUS \"not building with libcrypt\")\nendif()\n"
  },
  {
    "path": "include/llarp.hpp",
    "content": "#pragma once\n\n#include <future>\n#include <memory>\n#include <mutex>\n\nnamespace oxen::quic\n{\n    class Loop;\n}\n\nnamespace llarp\n{\n    namespace vpn\n    {\n        class Platform;\n    }\n\n    struct Config;\n    class Router;\n\n    // Helper \"context\" that aids in starting up Lokinet.\n    //\n    // Note that this class is *not* thread-safe: only one thread should attempt to hold and\n    // interact with it to manage Lokinet.\n    //\n    // TODO FIXME this class seems unnecessary, we should get rid of it.\n    struct Context\n    {\n        std::unique_ptr<Router> router;\n\n        explicit Context(bool embedded);\n        ~Context();\n\n        // Starts Lokinet; returns as soon as Lokinet is up and running (or throws if startup\n        // fails).  The loop may be provided in order to use an existing loop, but otherwise a new\n        // one will be started.\n        void start(Config conf, std::shared_ptr<oxen::quic::Loop> loop = nullptr);\n\n        // Waits for Lokinet to finish.  Note that this does not *trigger* such a shutdown; for that\n        // you would call `stop()` before this.\n        void wait();\n\n        // Call this to deliver a signal, such as SIGTERM or SIGINT to stop Lokinet if currently\n        // running.  (This can be called from any thread).\n        void signal(int sig);\n\n        // Initiates Lokinet shutdown, and returns immediately (without waiting for shutdown).  Call\n        // `wait()` after this if you also want to wait for shutdown to complete.\n        void stop();\n\n        bool is_up() const;\n\n        bool is_stopping() const;\n\n        // Returns true if Lokinet has stopped and `wait()` needs to be called to finish\n        // destruction.\n        bool is_waiting() const;\n\n        bool looks_alive() const;\n\n        int androidFD = -1;\n\n      private:\n        bool embedded;\n        std::future<void> lifetime_waiter;\n    };\n}  // namespace llarp\n"
  },
  {
    "path": "include/lokinet/addr.h",
    "content": "#pragma once\n#include \"context.h\"\n\n#ifdef __cplusplus\nextern \"C\"\n{\n#endif\n\n    /// get a free()-able null terminated string that holds our .loki address\n    /// returns NULL if we dont have one right now\n    char* EXPORT lokinet_address(struct lokinet_context*);\n#ifdef __cplusplus\n}\n#endif\n"
  },
  {
    "path": "include/lokinet/context.h",
    "content": "#pragma once\n\n#include \"export.h\"\n\n#include <stdbool.h>\n#include <stdint.h>\n#include <unistd.h>\n\n#ifdef __cplusplus\nextern \"C\"\n{\n#endif\n\n    struct lokinet_context;\n\n    /// allocate a new lokinet context\n    struct lokinet_context* EXPORT lokinet_context_new();\n\n    /// free a context allocated by lokinet_context_new\n    void EXPORT lokinet_context_free(struct lokinet_context*);\n\n    /// spawn all the threads needed for operation and start running\n    /// return 0 on success\n    /// return non zero on fail\n    int EXPORT lokinet_context_start(struct lokinet_context*);\n\n    /// return 0 if we our endpoint has published on the network and is ready to send\n    /// return -1 if we don't have enough paths ready\n    /// retrun -2 if we look deadlocked\n    /// retrun -3 if context was null or not started yet\n    int EXPORT lokinet_status(struct lokinet_context*);\n\n    /// wait at most N milliseconds for lokinet to build paths and get ready\n    /// return 0 if we are ready\n    /// return nonzero if we are not ready\n    int EXPORT lokinet_wait_for_ready(int N, struct lokinet_context*);\n\n    /// stop all operations on this lokinet context\n    void EXPORT lokinet_context_stop(struct lokinet_context*);\n\n    /// load a bootstrap RC from memory\n    /// return 0 on success\n    /// return non zero on fail\n    int EXPORT lokinet_add_bootstrap_rc(const char*, size_t, struct lokinet_context*);\n\n#ifdef __cplusplus\n}\n#endif\n"
  },
  {
    "path": "include/lokinet/export.h",
    "content": "#pragma once\n\n#ifdef _WIN32\n#define EXPORT __cdecl\n#else\n#define EXPORT\n#endif\n"
  },
  {
    "path": "include/lokinet/misc.h",
    "content": "#pragma once\n#include \"export.h\"\n#ifdef __cplusplus\nextern \"C\"\n{\n#endif\n\n    /// change our network id globally across all contexts\n    void EXPORT lokinet_set_netid(const char* netid);\n\n    /// get our current netid\n    /// must be free()'d after use\n    const char* EXPORT lokinet_get_netid();\n\n    /// set log level\n    /// possible values: trace, debug, info, warn, error, critical, none\n    /// return 0 on success\n    /// return non zero on fail\n    int EXPORT lokinet_log_level(const char* level);\n\n    /// Function pointer to invoke with lokinet log messages\n    typedef void (*lokinet_logger_func)(const char* message, void* context);\n\n    /// Optional function to call when flushing lokinet log messages; can be NULL if flushing is not\n    /// meaningful for the logging system.\n    typedef void (*lokinet_logger_sync)(void* context);\n\n    /// set a custom logger function; it is safe (and often desirable) to call this before calling\n    /// initializing lokinet via lokinet_context_new.\n    void EXPORT lokinet_set_syncing_logger(lokinet_logger_func func, lokinet_logger_sync sync, void* context);\n\n    /// shortcut for calling `lokinet_set_syncing_logger` with a NULL sync\n    void EXPORT lokinet_set_logger(lokinet_logger_func func, void* context);\n\n    /// @brief take in hex and turn it into base32z\n    /// @return value must be free()'d later\n    char* EXPORT lokinet_hex_to_base32z(const char* hex);\n\n#ifdef __cplusplus\n}\n#endif\n"
  },
  {
    "path": "include/lokinet/srv.h",
    "content": "#pragma once\n\n#include \"context.h\"\n\n#ifdef __cplusplus\nextern \"C\"\n{\n#endif\n\n    // a single srv record\n    struct lokinet_srv_record\n    {\n        /// the srv priority of the record\n        uint16_t priority;\n        /// the weight of this record\n        uint16_t weight;\n        /// null terminated string of the hostname\n        char target[256];\n        /// the port to use\n        int port;\n    };\n\n    /// private members of a srv lookup\n    struct lokinet_srv_lookup_private;\n\n    /// the result of an srv lookup\n    struct lokinet_srv_lookup_result\n    {\n        /// set to zero on success otherwise is the error code\n        int error;\n        /// pointer to internal members\n        /// dont touch me\n        struct lokinet_srv_lookup_private* internal;\n    };\n\n    /// do a srv lookup on host for service\n    /// caller MUST call lokinet_srv_lookup_done when they are done handling the result\n    int EXPORT lokinet_srv_lookup(\n        char* host, char* service, struct lokinet_srv_lookup_result* result, struct lokinet_context* ctx);\n\n    /// a hook function to handle each srv record in a srv lookup result\n    /// passes in NULL when we are at the end of iteration\n    /// passes in void * user data\n    /// hook should NOT free the record\n    typedef void (*lokinet_srv_record_iterator)(struct lokinet_srv_record*, void*);\n\n    /// iterate over each srv record in a lookup result\n    /// user is passes into hook and called for each result and then with NULL as the result on the\n    /// end of iteration\n    void EXPORT\n    lokinet_for_each_srv_record(struct lokinet_srv_lookup_result* result, lokinet_srv_record_iterator iter, void* user);\n\n    /// free internal members of a srv lookup result after use of the result\n    void EXPORT lokinet_srv_lookup_done(struct lokinet_srv_lookup_result* result);\n\n#ifdef __cplusplus\n}\n#endif\n"
  },
  {
    "path": "include/lokinet/stream.h",
    "content": "#pragma once\n\n#include \"context.h\"\n\n#ifdef __cplusplus\nextern \"C\"\n{\n#endif\n\n    /// the result of a lokinet stream mapping attempt\n    struct lokinet_stream_result\n    {\n        /// set to zero on success otherwise the error that happened\n        /// use strerror(3) to get printable string of this error\n        int error;\n\n        /// the local ip address we mapped the remote endpoint to\n        /// null terminated\n        char local_address[256];\n        /// the local port we mapped the remote endpoint to\n        int local_port;\n        /// the id of the stream we created\n        int stream_id;\n    };\n\n    /// connect out to a remote endpoint\n    /// remoteAddr is in the form of \"name:port\"\n    /// localAddr is either NULL for any or in the form of \"ip:port\" to bind to an explicit address\n    void EXPORT lokinet_outbound_stream(\n        struct lokinet_stream_result* result,\n        const char* remoteAddr,\n        const char* localAddr,\n        struct lokinet_context* context);\n\n    /// stream accept filter determines if we should accept a stream or not\n    /// return 0 to accept\n    /// return -1 to explicitly reject\n    /// return -2 to silently drop\n    typedef int (*lokinet_stream_filter)(const char* remote, uint16_t port, void* userdata);\n\n    /// set stream accepter filter\n    /// passes user parameter into stream filter as void *\n    /// returns stream id\n    int EXPORT\n    lokinet_inbound_stream_filter(lokinet_stream_filter acceptFilter, void* user, struct lokinet_context* context);\n\n    /// simple stream acceptor\n    /// simple variant of lokinet_inbound_stream_filter that maps port to localhost:port\n    int EXPORT lokinet_inbound_stream(uint16_t port, struct lokinet_context* context);\n\n    void EXPORT lokinet_close_stream(int stream_id, struct lokinet_context* context);\n\n#ifdef __cplusplus\n}\n#endif\n"
  },
  {
    "path": "include/lokinet/udp.h",
    "content": "#pragma once\n\n#include \"context.h\"\n\n#ifdef __cplusplus\nextern \"C\"\n{\n#endif\n\n    /// information about a udp flow\n    struct lokinet_udp_flowinfo\n    {\n        /// remote endpoint's .loki or .snode address\n        char remote_host[256];\n        /// remote endpont's port\n        uint16_t remote_port;\n        /// the socket id for this flow used for i/o purposes and closing this socket\n        int socket_id;\n    };\n\n    /// a result from a lokinet_udp_bind call\n    struct lokinet_udp_bind_result\n    {\n        /// a socket id used to close a lokinet udp socket\n        int socket_id;\n    };\n\n    /// flow acceptor hook, return 0 success, return nonzero with errno on failure\n    typedef int (*lokinet_udp_flow_filter)(\n        void* userdata, const struct lokinet_udp_flowinfo* remote_address, void** flow_userdata, int* timeout_seconds);\n\n    /// callback to make a new outbound flow\n    typedef void(lokinet_udp_create_flow_func)(void* userdata, void** flow_userdata, int* timeout_seconds);\n\n    /// hook function for handling packets\n    typedef void (*lokinet_udp_flow_recv_func)(\n        const struct lokinet_udp_flowinfo* remote_address,\n        const char* pkt_data,\n        size_t pkt_length,\n        void* flow_userdata);\n\n    /// hook function for flow timeout\n    typedef void (*lokinet_udp_flow_timeout_func)(\n        const struct lokinet_udp_flowinfo* remote_address, void* flow_userdata);\n\n    /// inbound listen udp socket\n    /// expose udp port exposePort to the void\n    ////\n    /// @param filter MUST be non null, pointing to a flow filter for accepting new udp flows,\n    /// called with user data\n    ///\n    /// @param recv MUST be non null, pointing to a packet handler function for each flow, called\n    /// with per flow user data provided by filter function if accepted\n    ///\n    /// @param timeout MUST be non null,\n    /// pointing to a cleanup function to clean up a stale flow, staleness determined by the value\n    /// given by the filter function returns 0 on success\n    ///\n    /// @returns nonzero on error in which it is an errno value\n    int EXPORT lokinet_udp_bind(\n        uint16_t exposedPort,\n        lokinet_udp_flow_filter filter,\n        lokinet_udp_flow_recv_func recv,\n        lokinet_udp_flow_timeout_func timeout,\n        void* user,\n        struct lokinet_udp_bind_result* result,\n        struct lokinet_context* ctx);\n\n    /// @brief establish a udp flow to remote endpoint\n    ///\n    /// @param create_flow the callback to create the new flow if we establish one\n    ///\n    /// @param user passed to new_flow as user data\n    ///\n    /// @param remote the remote address to establish to\n    ///\n    /// @param ctx the lokinet context to use\n    ///\n    /// @return 0 on success, non zero errno on fail\n    int EXPORT lokinet_udp_establish(\n        lokinet_udp_create_flow_func create_flow,\n        void* user,\n        const struct lokinet_udp_flowinfo* remote,\n        struct lokinet_context* ctx);\n\n    /// @brief send on an established flow to remote endpoint\n    /// blocks until we have sent the packet\n    ///\n    /// @param flowinfo remote flow to use for sending\n    ///\n    /// @param ptr pointer to data to send\n    ///\n    /// @param len the length of the data\n    ///\n    /// @param ctx the lokinet context to use\n    ///\n    /// @returns 0 on success and non zero errno on fail\n    int EXPORT lokinet_udp_flow_send(\n        const struct lokinet_udp_flowinfo* remote, const void* ptr, size_t len, struct lokinet_context* ctx);\n\n    /// @brief close a bound udp socket\n    /// closes all flows immediately\n    ///\n    /// @param socket_id the bound udp socket's id\n    ///\n    /// @param ctx lokinet context\n    void EXPORT lokinet_udp_close(int socket_id, struct lokinet_context* ctx);\n\n#ifdef __cplusplus\n}\n#endif\n"
  },
  {
    "path": "include/lokinet.h",
    "content": "#pragma once\n\n#include \"lokinet/addr.h\"\n#include \"lokinet/context.h\"\n#include \"lokinet/misc.h\"\n#include \"lokinet/srv.h\"\n#include \"lokinet/stream.h\"\n#include \"lokinet/udp.h\"\n"
  },
  {
    "path": "include/lokinet.hpp",
    "content": "#pragma once\n\n#include <cstdint>\n#include <filesystem>\n#include <functional>\n#include <memory>\n#include <thread>\n#include <type_traits>\n\nnamespace llarp\n{\n    struct Context;\n    struct Config;\n}  // namespace llarp\n\nnamespace oxen::quic\n{\n    class Loop;\n}\n\nnamespace lokinet\n{\n    enum class Network\n    {\n        MAINNET,\n        TESTNET\n    };\n\n    /// Metadata returned when establishing a session for a TCP or UDP tunnel.\n    struct tunnel_info\n    {\n        /// The requested remote address.  If an ONS entry was requested, this will be the resolved\n        /// \"fulladdress.loki\" rather than the ONS entry value.\n        std::string remote;\n\n        /// The requested remote port.  Packets sent to the `local_port` are delivered to this\n        /// remote port, and returning packets from that remote back to the incoming source are\n        /// routed back to the client and delivered to the source port that sent the original\n        /// packet.  Multiple connections to the same address are possible: each different source\n        /// port establishes a separate connection (actual TCP connections for TCP, a remembered\n        /// mapping for UDP).\n        uint16_t remote_port;\n\n        /// The bound local port.  After establishing a lokinet session, clients connect (TCP) or\n        /// send (UDP) to this port (on address 127.0.0.1) to reach the destination through lokinet.\n        uint16_t local_port;\n\n        /// A suggested maximum MTU for the connection.  If the application supports a configurable\n        /// MTU, this value is the recommended value that avoids some additional overhead from\n        /// packet splitting, which can slightly reduce latency and jitter.  If the application\n        /// doesn't support MTU configuration then this value can simply be ignored and Lokinet will\n        /// split any \"too large\" packets into two.\n        uint16_t suggested_mtu;\n    };\n\n    class Lokinet\n    {\n        std::unique_ptr<llarp::Context> context;\n\n        struct path_ctor\n        {};\n        Lokinet(path_ctor, const std::filesystem::path& p, std::shared_ptr<oxen::quic::Loop> loop);\n\n      public:\n        // Starts an embedded lokinet that loads the given string contents as a config file.\n        explicit Lokinet(std::string config, std::shared_ptr<oxen::quic::Loop> existing_loop = nullptr);\n\n        // Starts an embedded lokinet instance with extra configuration specified in the given\n        // config file.  (Templatized to avoid ambiguous implicit conversion from std::string\n        // conflicting with the constructor above.)\n        template <std::same_as<std::filesystem::path> FSPath>\n        explicit Lokinet(const FSPath& config, std::shared_ptr<oxen::quic::Loop> existing_loop = nullptr)\n            : Lokinet{path_ctor{}, config, std::move(existing_loop)}\n        {}\n\n        // Starts an embedded lokinet with default config that runs on the given network with\n        // default settings.\n        explicit Lokinet(Network network, std::shared_ptr<oxen::quic::Loop> existing_loop = nullptr);\n\n        // Destructor stops the lokinet instance.  The destructor blocks until shutdown is complete.\n        ~Lokinet();\n\n        // Schedules the given callback to be fired when Lokinet edge connections are mostly\n        // established (and thus Lokinet is ready to start building paths).  If lokinet is already\n        // established, this will schedule an immediate invocation of the callback.\n        //\n        // If persist is true then the callback will be stored and called *each* time Lokinet enters\n        // the connected state (i.e. it will be called again if Lokinet loses all connectivity and\n        // then regains connections).\n        void on_connected(std::function<void()> callback, bool persist = false);\n\n        // Schedules the given callback to be fired when Lokinet becomes fully disconnected, i.e.\n        // loses all established edge connections.  If persist is true then the callback will be\n        // fired *each* time Lokinet transitions from connected to disconnected state.  If Lokinet\n        // is not currently connected then the callback will be scheduled immediately.\n        void on_disconnected(std::function<void()> callback, bool persist = false);\n\n        // Establishes a UDP session to the given remote (.loki or .snode) and port.  When the\n        // lokinet session to the remote is established, the callback is invoked with the info\n        // corresponding to the session and tunnel.  This call can happen instantly (before this\n        // function call returns) if a session to the given address is already established, but\n        // otherwise the callback will be called at some future point when the callback is\n        // established.\n        //\n        // If a connection cannot be established for whatever reason, the `on_failed` callback is\n        // invoked with a string giving a descriptive reason.  Like `on_established`, it is possible\n        // for this to fire immediately, such as for an unparseable address or if Lokinet can\n        // determine immediately that the connection will fail.\n        //\n        // The callbacks must not block as they are called from Lokinet's logic thread (and so any\n        // blocking will stall Lokinet).\n        void establish_udp(\n            std::string_view remote_view,\n            uint16_t port,\n            std::function<void(tunnel_info info)> on_established,\n            std::function<void(std::string errmsg)> on_failed);\n\n        // Simple synchronous wrapper around the above: this blocks until either `on_established` or\n        // `on_failed` is called then returns the tunnel info (success) or throws the error message\n        // (failure).  This is provided for quick-and-dirty implementation code; generally code\n        // should prefer the callback-based async version, above.\n        tunnel_info establish_udp_blocking(std::string_view remote, uint16_t port);\n    };\n\n    template Lokinet::Lokinet(const std::filesystem::path&, std::shared_ptr<oxen::quic::Loop>);\n\n}  // namespace lokinet\n"
  },
  {
    "path": "jni/CMakeLists.txt",
    "content": "add_library(lokinet-android\n    SHARED\n    lokinet_config.cpp\n    lokinet_daemon.cpp)\ntarget_link_libraries(lokinet-android lokinet-amalgum)\n"
  },
  {
    "path": "jni/java/src/network/loki/lokinet/LokinetConfig.java",
    "content": "package network.loki.lokinet;\n\nimport java.nio.ByteBuffer;\n\npublic class LokinetConfig\n{\n  static {\n    System.loadLibrary(\"lokinet-android\");\n  }\n\n  private static native ByteBuffer Obtain(String dataDir);\n  private static native void Free(ByteBuffer buf);\n\n  /*** load config file from disk */\n  public native boolean Load();\n  /*** save chages to disk */\n  public native boolean Save();\n\n  \n  /** override default config value before loading from config file */\n  public native void AddDefaultValue(String section, String key, String value);\n  \n  private final ByteBuffer impl;\n\n  public LokinetConfig(String dataDir)\n  {\n    impl = Obtain(dataDir);\n    if(impl == null)\n      throw new RuntimeException(\"cannot obtain config from \"+dataDir);\n  }\n\n  public void finalize()\n  {\n    if (impl != null)\n    {\n      Free(impl);\n    }\n  }\n}\n"
  },
  {
    "path": "jni/java/src/network/loki/lokinet/LokinetDaemon.java",
    "content": "package network.loki.lokinet;\n\nimport java.lang.Thread;\nimport java.nio.ByteBuffer;\nimport java.io.File;\n\nimport android.net.VpnService;\nimport android.util.Log;\nimport android.content.Intent;\nimport android.os.ParcelFileDescriptor;\n\npublic class LokinetDaemon extends VpnService\n{\n  static {\n    System.loadLibrary(\"lokinet-android\");\n  }\n\n  private static native ByteBuffer Obtain();\n  private static native void Free(ByteBuffer buf);\n  public native boolean Configure(LokinetConfig config);\n  public native int Mainloop();\n  public native boolean IsRunning();\n  public native boolean Stop();\n  public native void InjectVPNFD();\n  public native int GetUDPSocket();\n\n  private static native String DetectFreeRange();\n\n  public native String DumpStatus();\n\n\n  public static final String LOG_TAG = \"LokinetDaemon\";\n\n  ByteBuffer impl = null;\n  ParcelFileDescriptor iface;\n  int m_FD = -1;\n  int m_UDPSocket = -1;\n\n  @Override\n    public void onCreate()\n    {\n      super.onCreate();\n    }\n\n  @Override\n    public void onDestroy()\n    {\n      super.onDestroy();\n\n      if (IsRunning())\n      {\n        Stop();\n      }\n      if (impl != null)\n      {\n        Free(impl);\n        impl = null;\n      }\n    }\n\n  public int onStartCommand(Intent intent, int flags, int startID)\n  {\n    Log.d(LOG_TAG, \"onStartCommand()\");\n\n    if (!IsRunning())\n    {\n      if (impl != null)\n      {\n        Free(impl);\n        impl = null;\n      }\n      impl = Obtain();\n      if (impl == null)\n      {\n        Log.e(LOG_TAG, \"got nullptr when creating llarp::Context in jni\");\n        return START_NOT_STICKY;\n      }\n\n      String dataDir = getFilesDir().toString();\n      LokinetConfig config;\n      try\n      {\n        config = new LokinetConfig(dataDir);\n      }\n      catch(RuntimeException ex)\n      {\n        Log.e(LOG_TAG, ex.toString());\n        return START_NOT_STICKY;\n      }\n\n      // FIXME: make these configurable\n      String exitNode = \"exit.loki\";\n      String upstreamDNS = \"1.1.1.1\";\n      String ourRange = DetectFreeRange();\n\n      if(ourRange.isEmpty())\n      {\n        Log.e(LOG_TAG, \"cannot detect free range\");\n        return START_NOT_STICKY;\n      }\n\n\n      // set up config values\n      config.AddDefaultValue(\"network\", \"exit-node\", exitNode);\n      config.AddDefaultValue(\"network\", \"ifaddr\", ourRange);\n      config.AddDefaultValue(\"dns\", \"upstream\", upstreamDNS);\n\n\n      if (!config.Load())\n      {\n        Log.e(LOG_TAG, \"failed to load (or create) config file at: \" + dataDir + \"/lokinet.ini\");\n        return START_NOT_STICKY;\n      }\n\n      VpnService.Builder builder = new VpnService.Builder();\n\n      builder.setMtu(1500);\n\n      String[] parts = ourRange.split(\"/\");\n      String ourIPv4 = parts[0];\n      int ourMask = Integer.parseInt(parts[1]);\n\n      // set ip4\n      builder.addAddress(ourIPv4, ourMask);\n      builder.addRoute(\"0.0.0.0\", 0);\n      // set ip6\n      // TODO: convert ipv4 to fd00::/8 range for ipv6\n      // builder.addAddress(ourIPv6, ourMask + 96);\n      // builder.addRoute(\"::\", 0);\n\n      builder.addDnsServer(upstreamDNS);\n      builder.setSession(\"Lokinet\");\n      builder.setConfigureIntent(null);\n\n      iface = builder.establish();\n      if (iface == null)\n      {\n        Log.e(LOG_TAG, \"VPN Interface from builder.establish() came back null\");\n        return START_NOT_STICKY;\n      }\n\n      m_FD = iface.detachFd();\n\n      InjectVPNFD();\n\n      new Thread(() -> {\n          Configure(config);\n          m_UDPSocket = GetUDPSocket();\n          protect(m_UDPSocket);\n          Mainloop();\n          }).start();\n\n      Log.d(LOG_TAG, \"started successfully!\");\n    }\n    else\n    {\n      Log.d(LOG_TAG, \"already running\");\n    }\n\n    return START_STICKY;\n  }\n}\n"
  },
  {
    "path": "jni/lokinet_config.cpp",
    "content": "#include \"lokinet_jni_common.hpp\"\n#include \"network_loki_lokinet_LokinetConfig.h\"\n\n#include <llarp.hpp>\n#include <llarp/config/config.hpp>\n\nextern \"C\"\n{\n    JNIEXPORT jobject JNICALL Java_network_loki_lokinet_LokinetConfig_Obtain(JNIEnv* env, jclass, jstring dataDir)\n    {\n        auto conf = VisitStringAsStringView<llarp::Config*>(\n            env, dataDir, [](std::string_view val) -> llarp::Config* { return new llarp::Config{val}; });\n\n        if (conf == nullptr)\n            return nullptr;\n        return env->NewDirectByteBuffer(conf, sizeof(llarp::Config));\n    }\n\n    JNIEXPORT void JNICALL Java_network_loki_lokinet_LokinetConfig_Free(JNIEnv* env, jclass, jobject buf)\n    {\n        auto ptr = FromBuffer<llarp::Config>(env, buf);\n        delete ptr;\n    }\n\n    JNIEXPORT jboolean JNICALL Java_network_loki_lokinet_LokinetConfig_Load(JNIEnv* env, jobject self)\n    {\n        auto conf = GetImpl<llarp::Config>(env, self);\n        if (conf == nullptr)\n            return JNI_FALSE;\n        if (conf->Load())\n        {\n            return JNI_TRUE;\n        }\n        return JNI_FALSE;\n    }\n\n    JNIEXPORT jboolean JNICALL Java_network_loki_lokinet_LokinetConfig_Save(JNIEnv* env, jobject self)\n    {\n        auto conf = GetImpl<llarp::Config>(env, self);\n        if (conf == nullptr)\n            return JNI_FALSE;\n        try\n        {\n            conf->Save();\n        }\n        catch (...)\n        {\n            return JNI_FALSE;\n        }\n        return JNI_TRUE;\n    }\n\n    JNIEXPORT void JNICALL Java_network_loki_lokinet_LokinetConfig_AddDefaultValue(\n        JNIEnv* env, jobject self, jstring section, jstring key, jstring value)\n    {\n        auto convert = [](std::string_view str) -> std::string { return std::string{str}; };\n\n        const auto sect = VisitStringAsStringView<std::string>(env, section, convert);\n        const auto k = VisitStringAsStringView<std::string>(env, key, convert);\n        const auto v = VisitStringAsStringView<std::string>(env, value, convert);\n\n        auto conf = GetImpl<llarp::Config>(env, self);\n        if (conf)\n        {\n            conf->AddDefault(sect, k, v);\n        }\n    }\n}\n"
  },
  {
    "path": "jni/lokinet_daemon.cpp",
    "content": "#include \"lokinet_jni_common.hpp\"\n#include \"network_loki_lokinet_LokinetDaemon.h\"\n\n#include <llarp.hpp>\n#include <llarp/config/config.hpp>\n#include <llarp/router/router.hpp>\n\nextern \"C\"\n{\n    JNIEXPORT jobject JNICALL Java_network_loki_lokinet_LokinetDaemon_Obtain(JNIEnv* env, jclass)\n    {\n        auto* ptr = new llarp::Context();\n        if (ptr == nullptr)\n            return nullptr;\n        return env->NewDirectByteBuffer(ptr, sizeof(llarp::Context));\n    }\n\n    JNIEXPORT void JNICALL Java_network_loki_lokinet_LokinetDaemon_Free(JNIEnv* env, jclass, jobject buf)\n    {\n        auto ptr = FromBuffer<llarp::Context>(env, buf);\n        delete ptr;\n    }\n\n    JNIEXPORT jboolean JNICALL\n    Java_network_loki_lokinet_LokinetDaemon_Configure(JNIEnv* env, jobject self, jobject conf)\n    {\n        auto ptr = GetImpl<llarp::Context>(env, self);\n        auto config = GetImpl<llarp::Config>(env, conf);\n        if (ptr == nullptr || config == nullptr)\n            return JNI_FALSE;\n        try\n        {\n            llarp::RuntimeOptions opts{};\n\n            // janky make_shared deep copy because jni + shared pointer = scary\n            ptr->Configure(std::make_shared<llarp::Config>(*config));\n            ptr->Setup(opts);\n        }\n        catch (...)\n        {\n            return JNI_FALSE;\n        }\n        return JNI_TRUE;\n    }\n\n    JNIEXPORT jint JNICALL Java_network_loki_lokinet_LokinetDaemon_Mainloop(JNIEnv* env, jobject self)\n    {\n        auto ptr = GetImpl<llarp::Context>(env, self);\n        if (ptr == nullptr)\n            return -1;\n        llarp::RuntimeOptions opts{};\n        return ptr->Run(opts);\n    }\n\n    JNIEXPORT jboolean JNICALL Java_network_loki_lokinet_LokinetDaemon_IsRunning(JNIEnv* env, jobject self)\n    {\n        auto ptr = GetImpl<llarp::Context>(env, self);\n        return (ptr != nullptr && ptr->IsUp()) ? JNI_TRUE : JNI_FALSE;\n    }\n\n    JNIEXPORT jboolean JNICALL Java_network_loki_lokinet_LokinetDaemon_Stop(JNIEnv* env, jobject self)\n    {\n        auto ptr = GetImpl<llarp::Context>(env, self);\n        if (ptr == nullptr)\n            return JNI_FALSE;\n        if (not ptr->IsUp())\n            return JNI_FALSE;\n        ptr->CloseAsync();\n        ptr->Wait();\n        return ptr->IsUp() ? JNI_FALSE : JNI_TRUE;\n    }\n\n    JNIEXPORT void JNICALL Java_network_loki_lokinet_LokinetDaemon_InjectVPNFD(JNIEnv* env, jobject self)\n    {\n        if (auto ptr = GetImpl<llarp::Context>(env, self))\n            ptr->androidFD = GetObjectMemberAsInt<int>(env, self, \"m_FD\");\n    }\n\n    JNIEXPORT jint JNICALL Java_network_loki_lokinet_LokinetDaemon_GetUDPSocket(JNIEnv* env, jobject self)\n    {\n        if (auto ptr = GetImpl<llarp::Context>(env, self); ptr and ptr->router)\n            return ptr->router->outbound_socket();\n        return -1;\n    }\n\n    JNIEXPORT jstring JNICALL Java_network_loki_lokinet_LokinetDaemon_DetectFreeRange(JNIEnv* env, jclass)\n    {\n        std::string rangestr{};\n        if (auto maybe = llarp::net::Platform::Default_ptr()->FindFreeRange())\n        {\n            rangestr = maybe->ToString();\n        }\n        return env->NewStringUTF(rangestr.c_str());\n    }\n\n    JNIEXPORT jstring JNICALL Java_network_loki_lokinet_LokinetDaemon_DumpStatus(JNIEnv* env, jobject self)\n    {\n        std::string status{};\n        if (auto ptr = GetImpl<llarp::Context>(env, self))\n        {\n            if (ptr->IsUp())\n            {\n                std::promise<std::string> result;\n                ptr->CallSafe([&result, router = ptr->router]() {\n                    const auto status = router->ExtractStatus();\n                    result.set_value(status.dump());\n                });\n                status = result.get_future().get();\n            }\n        }\n        return env->NewStringUTF(status.c_str());\n    }\n}\n"
  },
  {
    "path": "jni/lokinet_jni_common.hpp",
    "content": "#pragma once\n\n#include <jni.h>\n\n#include <functional>\n#include <string_view>\n\n/// visit string as native bytes\n/// jvm uses some unholy encoding internally so we convert it to utf-8\ntemplate <typename T, typename V>\nstatic T VisitStringAsStringView(JNIEnv* env, jobject str, V visit)\n{\n    const jclass stringClass = env->GetObjectClass(str);\n    const jmethodID getBytes = env->GetMethodID(stringClass, \"getBytes\", \"(Ljava/lang/String;)[B\");\n\n    const jstring charsetName = env->NewStringUTF(\"UTF-8\");\n    const jbyteArray stringJbytes = (jbyteArray)env->CallObjectMethod(str, getBytes, charsetName);\n    env->DeleteLocalRef(charsetName);\n\n    const size_t length = env->GetArrayLength(stringJbytes);\n    jbyte* pBytes = env->GetByteArrayElements(stringJbytes, NULL);\n\n    T result = visit(std::string_view((const char*)pBytes, length));\n\n    env->ReleaseByteArrayElements(stringJbytes, pBytes, JNI_ABORT);\n    env->DeleteLocalRef(stringJbytes);\n\n    return result;\n}\n\n/// cast jni buffer to T *\ntemplate <typename T>\nstatic T* FromBuffer(JNIEnv* env, jobject o)\n{\n    if (o == nullptr)\n        return nullptr;\n    return static_cast<T*>(env->GetDirectBufferAddress(o));\n}\n\n/// get T * from object member called membername\ntemplate <typename T>\nstatic T* FromObjectMember(JNIEnv* env, jobject self, const char* membername)\n{\n    jclass cl = env->GetObjectClass(self);\n    jfieldID name = env->GetFieldID(cl, membername, \"Ljava/nio/ByteBuffer;\");\n    jobject buffer = env->GetObjectField(self, name);\n    return FromBuffer<T>(env, buffer);\n}\n\n/// visit object string member called membername as bytes\ntemplate <typename T, typename V>\nstatic T VisitObjectMemberStringAsStringView(JNIEnv* env, jobject self, const char* membername, V v)\n{\n    jclass cl = env->GetObjectClass(self);\n    jfieldID name = env->GetFieldID(cl, membername, \"Ljava/lang/String;\");\n    jobject str = env->GetObjectField(self, name);\n    return VisitStringAsStringView<T, V>(env, str, v);\n}\n\n/// get object member int called membername\ntemplate <typename Int_t>\nInt_t GetObjectMemberAsInt(JNIEnv* env, jobject self, const char* membername)\n{\n    jclass cl = env->GetObjectClass(self);\n    jfieldID name = env->GetFieldID(cl, membername, \"I\");\n    return env->GetIntField(self, name);\n}\n\n/// get implementation on jni type\ntemplate <typename T>\nT* GetImpl(JNIEnv* env, jobject self)\n{\n    return FromObjectMember<T>(env, self, \"impl\");\n}\n"
  },
  {
    "path": "jni/network_loki_lokinet_LokinetConfig.h",
    "content": "/* DO NOT EDIT THIS FILE - it is machine generated */\n#include <jni.h>\n/* Header for class network_loki_lokinet_LokinetConfig */\n\n#ifndef _Included_network_loki_lokinet_LokinetConfig\n#define _Included_network_loki_lokinet_LokinetConfig\n#ifdef __cplusplus\nextern \"C\"\n{\n#endif\n    /*\n     * Class:     network_loki_lokinet_LokinetConfig\n     * Method:    Obtain\n     * Signature: (Ljava/lang/String;)Ljava/nio/ByteBuffer;\n     */\n    JNIEXPORT jobject JNICALL Java_network_loki_lokinet_LokinetConfig_Obtain(JNIEnv*, jclass, jstring);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetConfig\n     * Method:    Free\n     * Signature: (Ljava/nio/ByteBuffer;)V\n     */\n    JNIEXPORT void JNICALL Java_network_loki_lokinet_LokinetConfig_Free(JNIEnv*, jclass, jobject);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetConfig\n     * Method:    Load\n     * Signature: ()Z\n     */\n    JNIEXPORT jboolean JNICALL Java_network_loki_lokinet_LokinetConfig_Load(JNIEnv*, jobject);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetConfig\n     * Method:    Save\n     * Signature: ()Z\n     */\n    JNIEXPORT jboolean JNICALL Java_network_loki_lokinet_LokinetConfig_Save(JNIEnv*, jobject);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetConfig\n     * Method:    AddDefaultValue\n     * Signature: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V\n     */\n    JNIEXPORT void JNICALL\n    Java_network_loki_lokinet_LokinetConfig_AddDefaultValue(JNIEnv*, jobject, jstring, jstring, jstring);\n\n#ifdef __cplusplus\n}\n#endif\n#endif\n"
  },
  {
    "path": "jni/network_loki_lokinet_LokinetDaemon.h",
    "content": "/* DO NOT EDIT THIS FILE - it is machine generated */\n#include <jni.h>\n/* Header for class network_loki_lokinet_LokinetDaemon */\n\n#ifndef _Included_network_loki_lokinet_LokinetDaemon\n#define _Included_network_loki_lokinet_LokinetDaemon\n#ifdef __cplusplus\nextern \"C\"\n{\n#endif\n    /*\n     * Class:     network_loki_lokinet_LokinetDaemon\n     * Method:    Obtain\n     * Signature: ()Ljava/nio/ByteBuffer;\n     */\n    JNIEXPORT jobject JNICALL Java_network_loki_lokinet_LokinetDaemon_Obtain(JNIEnv*, jclass);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetDaemon\n     * Method:    Free\n     * Signature: (Ljava/nio/ByteBuffer;)V\n     */\n    JNIEXPORT void JNICALL Java_network_loki_lokinet_LokinetDaemon_Free(JNIEnv*, jclass, jobject);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetDaemon\n     * Method:    Configure\n     * Signature: (Lnetwork/loki/lokinet/LokinetConfig;)Z\n     */\n    JNIEXPORT jboolean JNICALL Java_network_loki_lokinet_LokinetDaemon_Configure(JNIEnv*, jobject, jobject);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetDaemon\n     * Method:    Mainloop\n     * Signature: ()I\n     */\n    JNIEXPORT jint JNICALL Java_network_loki_lokinet_LokinetDaemon_Mainloop(JNIEnv*, jobject);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetDaemon\n     * Method:    IsRunning\n     * Signature: ()Z\n     */\n    JNIEXPORT jboolean JNICALL Java_network_loki_lokinet_LokinetDaemon_IsRunning(JNIEnv*, jobject);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetDaemon\n     * Method:    Stop\n     * Signature: ()Z\n     */\n    JNIEXPORT jboolean JNICALL Java_network_loki_lokinet_LokinetDaemon_Stop(JNIEnv*, jobject);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetDaemon\n     * Method:    InjectVPNFD\n     * Signature: ()V\n     */\n    JNIEXPORT void JNICALL Java_network_loki_lokinet_LokinetDaemon_InjectVPNFD(JNIEnv*, jobject);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetDaemon\n     * Method:    GetUDPSocket\n     * Signature: ()I\n     */\n    JNIEXPORT jint JNICALL Java_network_loki_lokinet_LokinetDaemon_GetUDPSocket(JNIEnv*, jobject);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetDaemon\n     * Method:    DetectFreeRange\n     * Signature: ()Ljava/lang/String;\n     */\n    JNIEXPORT jstring JNICALL Java_network_loki_lokinet_LokinetDaemon_DetectFreeRange(JNIEnv*, jclass);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetDaemon\n     * Method:    DumpStatus\n     * Signature: ()Ljava/lang/String;\n     */\n    JNIEXPORT jstring JNICALL Java_network_loki_lokinet_LokinetDaemon_DumpStatus(JNIEnv*, jobject);\n\n#ifdef __cplusplus\n}\n#endif\n#endif\n"
  },
  {
    "path": "jni/network_loki_lokinet_LokinetVPN.h",
    "content": "/* DO NOT EDIT THIS FILE - it is machine generated */\n#include <jni.h>\n/* Header for class network_loki_lokinet_LokinetVPN */\n\n#ifndef _Included_network_loki_lokinet_LokinetVPN\n#define _Included_network_loki_lokinet_LokinetVPN\n#ifdef __cplusplus\nextern \"C\"\n{\n#endif\n    /*\n     * Class:     network_loki_lokinet_LokinetVPN\n     * Method:    PacketSize\n     * Signature: ()I\n     */\n    JNIEXPORT jint JNICALL Java_network_loki_lokinet_LokinetVPN_PacketSize(JNIEnv*, jclass);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetVPN\n     * Method:    Alloc\n     * Signature: ()Ljava/nio/Buffer;\n     */\n    JNIEXPORT jobject JNICALL Java_network_loki_lokinet_LokinetVPN_Alloc(JNIEnv*, jclass);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetVPN\n     * Method:    Free\n     * Signature: (Ljava/nio/Buffer;)V\n     */\n    JNIEXPORT void JNICALL Java_network_loki_lokinet_LokinetVPN_Free(JNIEnv*, jclass, jobject);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetVPN\n     * Method:    Stop\n     * Signature: ()V\n     */\n    JNIEXPORT void JNICALL Java_network_loki_lokinet_LokinetVPN_Stop(JNIEnv*, jobject);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetVPN\n     * Method:    ReadPkt\n     * Signature: (Ljava/nio/ByteBuffer;)I\n     */\n    JNIEXPORT jint JNICALL Java_network_loki_lokinet_LokinetVPN_ReadPkt(JNIEnv*, jobject, jobject);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetVPN\n     * Method:    WritePkt\n     * Signature: (Ljava/nio/ByteBuffer;)Z\n     */\n    JNIEXPORT jboolean JNICALL Java_network_loki_lokinet_LokinetVPN_WritePkt(JNIEnv*, jobject, jobject);\n\n    /*\n     * Class:     network_loki_lokinet_LokinetVPN\n     * Method:    SetInfo\n     * Signature: (Lnetwork/loki/lokinet/LokinetVPN/VPNInfo;)V\n     */\n    JNIEXPORT void JNICALL Java_network_loki_lokinet_LokinetVPN_SetInfo(JNIEnv*, jobject, jobject);\n#ifdef __cplusplus\n}\n#endif\n#endif\n"
  },
  {
    "path": "jni/network_loki_lokinet_LokinetVPN_VPNInfo.h",
    "content": "/* DO NOT EDIT THIS FILE - it is machine generated */\n#include <jni.h>\n/* Header for class network_loki_lokinet_LokinetVPN_VPNInfo */\n\n#ifndef _Included_network_loki_lokinet_LokinetVPN_VPNInfo\n#define _Included_network_loki_lokinet_LokinetVPN_VPNInfo\n#ifdef __cplusplus\nextern \"C\"\n{\n#endif\n#ifdef __cplusplus\n}\n#endif\n#endif\n"
  },
  {
    "path": "jni/network_loki_lokinet_Lokinet_JNI.h",
    "content": "/* DO NOT EDIT THIS FILE - it is machine generated */\n#include <jni.h>\n/* Header for class network_loki_lokinet_Lokinet_JNI */\n\n#ifndef _Included_network_loki_lokinet_Lokinet_JNI\n#define _Included_network_loki_lokinet_Lokinet_JNI\n#ifdef __cplusplus\nextern \"C\"\n{\n#endif\n    /*\n     * Class:     network_loki_lokinet_Lokinet_JNI\n     * Method:    getABICompiledWith\n     * Signature: ()Ljava/lang/String;\n     */\n    JNIEXPORT jstring JNICALL Java_network_loki_lokinet_Lokinet_1JNI_getABICompiledWith(JNIEnv*, jclass);\n\n    /*\n     * Class:     network_loki_lokinet_Lokinet_JNI\n     * Method:    startLokinet\n     * Signature: (Ljava/lang/String;)Ljava/lang/String;\n     */\n    JNIEXPORT jstring JNICALL Java_network_loki_lokinet_Lokinet_1JNI_startLokinet(JNIEnv*, jclass, jstring);\n\n    JNIEXPORT jstring JNICALL Java_network_loki_lokinet_Lokinet_1JNI_getIfAddr(JNIEnv*, jclass);\n\n    JNIEXPORT jint JNICALL Java_network_loki_lokinet_Lokinet_1JNI_getIfRange(JNIEnv*, jclass);\n\n    /*\n     * Class:     network_loki_lokinet_Lokinet_JNI\n     * Method:    stopLokinet\n     * Signature: ()V\n     */\n    JNIEXPORT void JNICALL Java_network_loki_lokinet_Lokinet_1JNI_stopLokinet(JNIEnv*, jclass);\n\n    JNIEXPORT void JNICALL Java_network_loki_lokinet_Lokinet_1JNI_setVPNFileDescriptor(JNIEnv*, jclass, jint, jint);\n\n    /*\n     * Class:     network_loki_lokinet_Lokinet_JNI\n     * Method:    onNetworkStateChanged\n     * Signature: (Z)V\n     */\n    JNIEXPORT void JNICALL Java_network_loki_lokinet_Lokinet_1JNI_onNetworkStateChanged(JNIEnv*, jclass, jboolean);\n\n#ifdef __cplusplus\n}\n#endif\n#endif\n"
  },
  {
    "path": "jni/readme.md",
    "content": "jni binding for lokinet vpn using android vpn api\n"
  },
  {
    "path": "llarp/CMakeLists.txt",
    "content": "include(Version)\n\n# Add an internal lokinet static library target, enables LTO (if enabled) on the target,\n# and links it to the common lokinet-base interface.\n# Invoke with the target/library name (e.g. \"lokinet-foo\") and list of source files, e.g.\n#     lokinet_add_library(lokinet-foo foo/source1.cpp foo/source2.cpp)\nfunction(lokinet_add_library libname)\n    add_library(${libname} STATIC ${ARGN})\n    target_link_libraries(${libname} PUBLIC lokinet-base PRIVATE lokinet-base-internal)\n    enable_lto(${libname})\nendfunction()\n\nlokinet_add_library(lokinet-cryptography\n  crypto/crypto.cpp\n  crypto/key_manager.cpp\n  crypto/keys.cpp\n  crypto/types.cpp\n)\n\nif(LOKINET_FULL)\n  lokinet_add_library(lokinet-exit-auth\n    auth/auth.cpp\n    auth/file.cpp\n    auth/session.cpp\n  )\nendif()\n\n# Functional objects use by lokinet-core and other libraries\n# needed by vpn/ router/ rpc/ handlers/ net/ link/\nlokinet_add_library(lokinet-core-utils\n  handlers/session.cpp\n\n  messages/common.cpp\n  messages/dht.cpp\n  messages/fetch.cpp\n  messages/path.cpp\n)\n\nif (LOKINET_FULL)\n  target_sources(lokinet-core-utils PRIVATE handlers/tun.cpp vpn/egres_packet_router.cpp)\nendif()\n\nlokinet_add_library(lokinet-core\n  context.cpp\n  \n  link/connection.cpp\n  link/endpoint.cpp\n  link/link_manager.cpp\n\n  router/router.cpp\n\n  session/session.cpp\n)\n\nif (LOKINET_FULL)\n  target_sources(lokinet-core\n    PRIVATE\n    router/route_poker.cpp\n    consensus/reachability_testing.cpp\n  )\nendif()\n\nif (LOKINET_FULL)\n  lokinet_add_library(lokinet-rpc\n    rpc/json_binary_proxy.cpp\n    rpc/json_conversions.cpp\n    rpc/oxend_rpc.cpp\n    rpc/rpc_request_parser.cpp\n    rpc/rpc_server.cpp\n  )\nendif()\n\n# config, crypto, timeplace, nodedb, net, router\nlokinet_add_library(lokinet-utils\n  ${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp\n  util/buffer.cpp\n  util/file.cpp\n  util/logging.cpp\n  util/mem.cpp\n  util/str.cpp\n  util/thread/queue_manager.cpp  \n  util/thread/threading.cpp\n  util/time.cpp\n  util/zstd.cpp\n)\n\nadd_dependencies(lokinet-utils genversion)\n\nlokinet_add_library(lokinet-contact\n  contact/client_contact.cpp\n  contact/client_intro.cpp\n  contact/contactdb.cpp\n  contact/relay_contact.cpp\n  contact/router_id.cpp\n  contact/sns.cpp\n)\n\n# Network interface files used for mediating ip traffic\nlokinet_add_library(lokinet-ip\n  net/ip_packet.cpp\n  net/policy.cpp\n  net/utils.cpp\n)\n\n# Addressing and event loop files used by lokinet-core and other libraries\n# needed by rpc/ link/ config/ path/ dht/\nlokinet_add_library(lokinet-addressing\n  address/address.cpp\n  address/ip_range.cpp\n  address/utils.cpp\n\n  ev/fd_poller.cpp\n  ev/tcp.cpp\n)\n\nlokinet_add_library(lokinet-dns\n  dns/srv_data.cpp\n)\n\nif (LOKINET_FULL)\n  # lokinet-platform holds all platform specific code\n  lokinet_add_library(lokinet-platform\n    vpn/packet_router.cpp\n    vpn/platform.cpp\n  )\n\n  if (ANDROID)\n    target_sources(lokinet-platform PRIVATE android/ifaddrs.c util/nop_service_manager.cpp)\n  endif()\n\n  if(CMAKE_SYSTEM_NAME MATCHES \"Linux\")\n    target_sources(lokinet-platform PRIVATE linux/dbus.cpp)\n    if(WITH_SYSTEMD)\n      target_sources(lokinet-platform PRIVATE linux/sd_service_manager.cpp)\n    else()\n      target_sources(lokinet-platform PRIVATE util/nop_service_manager.cpp)\n    endif()\n  endif()\n\n  if (WIN32)\n    target_sources(lokinet-platform PRIVATE\n      net/win32.cpp\n      vpn/win32.cpp\n      win32/service_manager.cpp\n      win32/exec.cpp\n      win32/dll.cpp\n      win32/exception.cpp\n      win32/wintun.cpp\n      win32/windivert.cpp)\n    target_include_directories(lokinet-platform PRIVATE ${CMAKE_BINARY_DIR}/wintun/include/ ${CMAKE_BINARY_DIR}/WinDivert-${WINDIVERT_VERSION}/include/)\n  else()\n    target_sources(lokinet-platform PRIVATE\n      net/posix.cpp)\n  endif()\n\n  if(APPLE)\n    add_subdirectory(apple)\n    target_sources(lokinet-platform PRIVATE util/nop_service_manager.cpp)\n  endif()\n\n  # lokinet-dns is the dns parsing and hooking library that we use to\n  # parse modify and reconstitute dns wire proto, dns queries and RR\n  target_sources(lokinet-dns PRIVATE\n    dns/message.cpp   # dns/server\n    dns/name.cpp      # srv_data, question, rr\n    dns/platform.cpp\n    dns/question.cpp  # message\n    dns/rr.cpp\n    dns/serialize.cpp\n    dns/server.cpp\n  )\n\n  # platform specific bits and bobs for setting dns\n  if(WITH_SYSTEMD)\n    target_sources(lokinet-dns PRIVATE dns/nm_platform.cpp dns/sd_platform.cpp)\n  endif()\nendif()\n\n# lokinet-nodedb holds all types and logic for storing parsing and constructing\n# nodedb data published to the network and versions of it stored locally\nlokinet_add_library(lokinet-nodedb\n  nodedb.cpp\n  profiling.cpp # path, router, service::endpoint\n)\n\nset(BOOTSTRAP_FALLBACKS)\nforeach(bs IN ITEMS MAINNET TESTNET)\n  if(LOKINET_BOOTSTRAP_FALLBACK_${bs})\n    message(STATUS \"Building with ${bs} fallback bootstrap path \\\"${LOKINET_BOOTSTRAP_FALLBACK_${bs}}\\\"\")\n    file(READ \"${LOKINET_BOOTSTRAP_FALLBACK_${bs}}\" bs_data HEX)\n    string(REGEX REPLACE \"([0-9a-f][0-9a-f])\" \"\\\\\\\\x\\\\1\" bs_data \"${bs_data}\")\n    set(BOOTSTRAP_FALLBACKS \"${BOOTSTRAP_FALLBACKS}        {NetID::${bs}, \\\"${bs_data}\\\"sv},\\n\")\n  endif()\nendforeach()\nconfigure_file(\"nodedb-bootstraps.cpp.in\" \"${CMAKE_CURRENT_BINARY_DIR}/nodedb-bootstraps.cpp\" @ONLY)\ntarget_sources(lokinet-nodedb PRIVATE \"${CMAKE_CURRENT_BINARY_DIR}/nodedb-bootstraps.cpp\")\n\n# lokinet-config is for all configuration types and parsers\nlokinet_add_library(lokinet-config\n  config/config.cpp\n  config/definition.cpp\n  config/ini.cpp\n)\n\n# All path objects; link directly to lokinet-core\nlokinet_add_library(lokinet-path\n  path/build_stats.cpp\n  path/hopid.cpp\n  path/path.cpp\n  path/path_context.cpp\n  path/path_handler.cpp\n  path/transit_hop.cpp\n)\n\nif(LOKINET_DEBUG_PATH_SEED)\n  target_compile_definitions(lokinet-nodedb PRIVATE LOKINET_DEBUG_PATH_SEED)\n  target_compile_definitions(lokinet-config PRIVATE LOKINET_DEBUG_PATH_SEED)\n  target_compile_definitions(lokinet-path PRIVATE LOKINET_DEBUG_PATH_SEED)\nendif()\n\n# Link libraries to their internals\ntarget_link_libraries(lokinet-nodedb PUBLIC lokinet-addressing lokinet-cryptography)\ntarget_link_libraries(lokinet-contact PUBLIC lokinet-dns)\nif(LOKINET_FULL)\n  target_link_libraries(lokinet-rpc PUBLIC lokinet-contact lokinet-config)\nendif()\ntarget_link_libraries(lokinet-addressing PUBLIC lokinet-utils lokinet-cryptography lokinet-contact lokinet-ip)\ntarget_link_libraries(lokinet-config PUBLIC lokinet-cryptography lokinet-addressing)\nif(LOKINET_FULL)\n  target_link_libraries(lokinet-platform PUBLIC lokinet-cryptography lokinet-ip)\n  target_link_libraries(lokinet-config PUBLIC lokinet-exit-auth)\nendif()\ntarget_link_libraries(lokinet-dns\n  PUBLIC\n  lokinet-cryptography\n  lokinet-config\n)\nif(LOKINET_FULL)\n  target_link_libraries(lokinet-dns PUBLIC lokinet-platform)\nendif()\n\ntarget_link_libraries(lokinet-path\n  PUBLIC\n  lokinet-addressing\n)\n\ntarget_link_libraries(lokinet-core-utils \n  PUBLIC \n  lokinet-config\n  lokinet-dns\n)\n\nif(LOKINET_FULL)\n  target_link_libraries(lokinet-core-utils\n    PUBLIC\n    lokinet-rpc\n    lokinet-platform\n  )\nendif()\n\ntarget_link_libraries(lokinet-cryptography\n  PUBLIC\n  lokinet-libcrypt\n)\n\nif(LOKINET_FULL)\n  target_link_libraries(lokinet-exit-auth PUBLIC lokinet-core-utils lokinet-cryptography)\nendif()\n\ntarget_link_libraries(lokinet-utils\n  PUBLIC\n  lokinet-cryptography\n)\n\ntarget_link_libraries(lokinet-core \n  PUBLIC \n  lokinet-core-utils\n  lokinet-nodedb\n  lokinet-path\n)\nif(LOKINET_FULL)\n  target_link_libraries(lokinet-core PUBLIC lokinet-exit-auth)\nendif()\n\n\ntarget_link_libraries(lokinet-base \n  INTERFACE \n\n  Threads::Threads\n\n  oxenc::oxenc \n  oxen::logging \n  \n  sodium\n)\n\n\nif(LOKINET_FULL)\n  target_link_libraries(lokinet-base INTERFACE libunbound oxenmq::oxenmq)\nendif()\n\n# libevent\nif(NOT TARGET libevent::core)\n  add_library(libevent_core INTERFACE)\n  pkg_check_modules(LIBEVENT_core libevent_core>=2.1 IMPORTED_TARGET REQUIRED)\n  target_link_libraries(libevent_core INTERFACE PkgConfig::LIBEVENT_core)\n  add_library(libevent::core ALIAS libevent_core)\nendif()\ntarget_link_libraries(lokinet-addressing PRIVATE libevent::core)\n\n\n# libzstd\nif(TARGET libzstd::static)  # static dep or libsession-util's embedded static target\n    target_link_libraries(lokinet-utils PRIVATE libzstd::static)\nelse()\n    pkg_check_modules(LIBZSTD libzstd>=1.3 IMPORTED_TARGET REQUIRED)\n    target_link_libraries(lokinet-utils PRIVATE PkgConfig::LIBZSTD)\nendif()\n\n\nadd_library(liblokinet lokinet.cpp)\ntarget_link_libraries(liblokinet PUBLIC lokinet-core)\n\nset_target_properties(liblokinet PROPERTIES OUTPUT_NAME lokinet)\nif(BUILD_SHARED_LIBS AND LOKINET_VERSION_SO)\n  set_target_properties(liblokinet PROPERTIES\n    VERSION \"${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}\"\n    SOVERSION \"${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}\")\nendif()\n\nif(WIN32)\n  target_link_libraries(liblokinet PUBLIC ws2_32 iphlpapi -fstack-protector)\n  install(TARGETS liblokinet DESTINATION bin COMPONENT liblokinet)\nelseif(NOT APPLE)\n  include(GNUInstallDirs)\n  install(TARGETS liblokinet LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} COMPONENT liblokinet)\nendif()\n\nadd_library(lokinet::liblokinet ALIAS liblokinet)\n\nfile(GLOB_RECURSE docs_SRC */*.hpp *.hpp)\nset(DOCS_SRC ${docs_SRC} PARENT_SCOPE)\n"
  },
  {
    "path": "llarp/address/address.cpp",
    "content": "#include \"address.hpp\"\n\n#include <llarp/util/formattable.hpp>\n\n#include <oxenc/base32z.h>\n\n#include <stdexcept>\n\nnamespace llarp\n{\n    NetworkAddress::NetworkAddress(std::string_view arg)\n    {\n        if (arg.ends_with(TLD::SNODE))\n        {\n            _is_client = false;\n            arg.remove_suffix(TLD::SNODE.size());\n        }\n        else if (arg.ends_with(TLD::LOKI))\n        {\n            _is_client = true;\n            arg.remove_suffix(TLD::LOKI.size());\n        }\n        else\n        {\n            throw std::invalid_argument{\n                \"Invalid network address '{}': expected *{} or *{}\"_format(arg, TLD::LOKI, TLD::SNODE)};\n        }\n        if (!_pubkey.from_base32z(arg))\n            throw std::invalid_argument{\"Invalid network address '{}{}': expected full pubkey\"_format(\n                arg, _is_client ? TLD::LOKI : TLD::SNODE)};\n    }\n\n    NetworkAddress::NetworkAddress(std::string_view arg, bool is_client) : _is_client{is_client}\n    {\n        if (!_pubkey.from_base32z(arg))\n            throw std::invalid_argument{\"Invalid pubkey passed to NetworkAddress constructor: {}\"_format(arg)};\n    }\n\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/address/address.hpp",
    "content": "#pragma once\n\n#include \"utils.hpp\"\n\n#include <llarp/contact/router_id.hpp>\n#include <llarp/contact/sns.hpp>\n#include <llarp/util/aligned.hpp>\n\nnamespace llarp\n{\n    /// Combines a pubkey and client/snode flag to represent a generic (client or snode) address.\n    struct NetworkAddress\n    {\n      private:\n        RouterID _pubkey{};\n        bool _is_client{false};\n\n      public:\n        NetworkAddress() = default;\n        // Constructs from a full network address ending in '.loki' or '.snode' (but *not* an ONS\n        // entry).  Throws std::invalid_argument if invalid.\n        explicit NetworkAddress(std::string_view addr);\n        // Constructs from a full network address (base32z-encoded pubkey) *not* ending in .loki or\n        // .snode.  The client or snode status is determined by the bool.\n        NetworkAddress(std::string_view addr, bool is_client);\n        // Constructs from a pubkey and flag indicating whether this is a client (true) or snode\n        // (false).\n        NetworkAddress(const RouterID& rid, bool is_client) : _pubkey{rid}, _is_client{is_client} {}\n\n        bool operator==(const NetworkAddress& other) const\n        {\n            return std::tie(_pubkey, _is_client) == std::tie(other._pubkey, other._is_client);\n        }\n\n        bool empty() const { return _pubkey.is_zero(); }\n\n        bool client() const { return _is_client; }\n\n        bool relay() const { return !_is_client; }\n\n        const RouterID& router_id() const { return _pubkey; }\n\n        // Returns a log proxy object that prints a shortened part of the pubkey:\n        auto short_name() const { return _pubkey.short_string(); }\n\n        std::string name() const { return _pubkey.to_string(); }\n\n        std::string to_string() const { return name().append(_is_client ? TLD::LOKI : TLD::SNODE); }\n        static constexpr bool to_string_formattable = true;\n    };\n\n}  // namespace llarp\n\nnamespace std\n{\n    template <>\n    struct hash<llarp::NetworkAddress>\n    {\n        size_t operator()(const llarp::NetworkAddress& r) const { return llarp::AlignedHasher{}(r.router_id()); }\n    };\n}  //  namespace std\n"
  },
  {
    "path": "llarp/address/ip_range.cpp",
    "content": "#include \"ip_range.hpp\"\n\n#include <llarp/util/logging.hpp>\n#include <llarp/util/str.hpp>\n\n#include <algorithm>\n#include <stdexcept>\n#include <type_traits>\n#include <variant>\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"iprange\");\n\n    template <bool is_ipv4>\n    static std::conditional_t<is_ipv4, ipv4_net, ipv6_net> parse_ip_net(\n        std::string_view address, std::optional<uint8_t> default_mask)\n    {\n        auto mask_pos = address.find('/');\n        std::string addr{address.substr(0, mask_pos)};\n        quic::Address a{addr, 0};\n        if (a.is_ipv4() != is_ipv4)\n            throw std::invalid_argument{\"Cannot construct an IPv{} range from a IPv{} address\"_format(\n                is_ipv4 ? \"4\" : \"6\", is_ipv4 ? \"6\" : \"4\")};\n\n        uint8_t mask;\n        if (mask_pos == std::string::npos)\n        {\n            if (default_mask)\n                mask = *default_mask;\n            else\n                throw std::invalid_argument{\"Invalid IP range: /N network mask is required\"};\n        }\n        else if (!parse_int(address.substr(mask_pos + 1), mask) || mask > (is_ipv4 ? 32 : 128))\n        {\n            throw std::invalid_argument{\"Invalid IP range: {} is not a valid IPv{} network mask\"_format(\n                address.substr(mask_pos), is_ipv4 ? \"4\" : \"6\")};\n        }\n\n        if constexpr (is_ipv4)\n            return {a.to_ipv4(), mask};\n        else\n            return {a.to_ipv6(), mask};\n    }\n\n    ipv4_net parse_ipv4_net(std::string_view address, std::optional<uint8_t> default_mask)\n    {\n        return parse_ip_net<true>(address, default_mask);\n    }\n    ipv6_net parse_ipv6_net(std::string_view address, std::optional<uint8_t> default_mask)\n    {\n        return parse_ip_net<false>(address, default_mask);\n    }\n    ipv4_range parse_ipv4_range(std::string_view address, std::optional<uint8_t> default_mask)\n    {\n        return parse_ipv4_net(address, default_mask).to_range();\n    }\n    ipv6_range parse_ipv6_range(std::string_view address, std::optional<uint8_t> default_mask)\n    {\n        return parse_ipv6_net(address, default_mask).to_range();\n    }\n\n    // Encoded as ABCDM for each byte of A.B.C.D/M\n    std::string encode(const ipv4_range& r)\n    {\n        std::string enc;\n        enc.resize(5);\n        oxenc::write_host_as_big(r.ip.addr, enc.data());\n        enc[4] = static_cast<char>(r.mask);\n        return enc;\n    }\n    ipv4_range decode_ipv4_range(std::string_view encoded)\n    {\n        if (encoded.size() != 5)\n            throw std::invalid_argument{\"Invalid ipv4 range encoding\"};\n        ipv4_range result;\n        result.mask = static_cast<uint8_t>(encoded[4]);\n        if (result.mask > 32)\n            throw std::invalid_argument{\"Invalid ipv4 range encoded netmask\"};\n        result.ip.addr = oxenc::load_big_to_host<uint32_t>(encoded.data());\n        return result;\n    }\n    // Encode as *either* the full IPv6 address (16 bytes) followed by the netmask byte, or the\n    // first 8 bytes of the IPv6 address followed by the netmask byte.  In the latter case, the\n    // lower 8 bytes are implied to be 0.\n    std::string encode(const ipv6_range& r)\n    {\n        std::string enc;\n        enc.resize(r.ip.lo ? 17 : 9);\n        oxenc::write_host_as_big(r.ip.hi, enc.data());\n        if (r.ip.lo)\n            oxenc::write_host_as_big(r.ip.lo, enc.data() + 8);\n        enc.back() = static_cast<char>(r.mask);\n        return enc;\n    }\n    ipv6_range decode_ipv6_range(std::string_view encoded)\n    {\n        if (encoded.size() != 9 && encoded.size() != 17)\n            throw std::invalid_argument{\"Invalid ipv6 range encoding\"};\n        ipv6_range result;\n        result.mask = static_cast<uint8_t>(encoded.back());\n        if (result.mask > 128)\n            throw std::invalid_argument{\"Invalid ipv6 range encoded netmask\"};\n        result.ip.hi = oxenc::load_big_to_host<uint64_t>(encoded.data());\n        result.ip.lo = encoded.size() == 17 ? oxenc::load_big_to_host<uint64_t>(encoded.data() + 8) : 0;\n        return result;\n    }\n    std::variant<ipv4_range, ipv6_range> decode_ip_range(std::string_view encoded)\n    {\n        switch (encoded.size())\n        {\n            case 5:\n                return decode_ipv4_range(encoded);\n            case 9:\n            case 17:\n                return decode_ipv6_range(encoded);\n            default:\n                throw std::invalid_argument{\"Invalid encoded ip range\"};\n        }\n    }\n\n    ipv4_net to_ipv4_net(const quic::Address& addr, uint8_t mask)\n    {\n        if (!addr.is_ipv4())\n            throw std::invalid_argument{\"Cannot construct an ipv4_net from an IPv6 address\"};\n        if (mask > 32)\n            throw std::invalid_argument{\"{} is not a valid IPv4 network mask\"_format(mask)};\n        return {addr.to_ipv4(), mask};\n    }\n    ipv6_net to_ipv6_net(quic::Address addr, uint8_t mask)\n    {\n        if (!addr.is_ipv6())\n            throw std::invalid_argument{\"Cannot construct an ipv6_net from an IPv4 address\"};\n        if (mask > 128)\n            throw std::invalid_argument{\"{} is not a valid IPv6 network mask\"_format(mask)};\n        return {addr.to_ipv6(), mask};\n    }\n\n    static std::optional<ipv4_net> find_ipv4_net(\n        uint32_t start, uint32_t end, uint8_t mask, const std::vector<ipv4_range>& exclude)\n    {\n        uint32_t step = uint32_t{1} << (32 - mask);\n        auto ret = std::make_optional<ipv4_net>();\n        auto& net = *ret;\n        auto& addr = net.ip.addr;\n        ret->mask = mask;\n        for (addr = start + 1; addr < end; addr += step)\n            if (std::ranges::none_of(\n                    exclude, [&net](const auto& e) { return e.contains(net.ip) || net.contains(e.ip); }))\n                return ret;\n        ret.reset();\n        return ret;\n    }\n\n    std::optional<ipv4_net> find_private_ipv4_net(const std::vector<ipv4_range>& exclude, uint8_t mask)\n    {\n        if (mask < 8)\n        {\n            log::warning(logcat, \"Cannot auto-detect IPv4 private networks larger than /8\");\n            return std::nullopt;\n        }\n\n        // Cut off the smallest block we search to /20 (4096 addresses) just so that this doesn't\n        // take an excessive amount of time if there are no free address spaces.  We still give back\n        // whatever you requested, if smaller, we just don't search the smaller spaces.  That is, if\n        // you request a /24 and 10.0.0.0/24 is already assigned then the next thing we would try is\n        // 10.0.16.0/24 rather than 10.0.1.0/24.  (There are 4368 distinct private range /20\n        // blocks).\n        auto search_mask = std::min<uint8_t>(18, mask);\n        std::optional<ipv4_net> ret;\n        // Start looking in the 172.16.x.x - 172.31.x.x range first as it is the least commonly\n        // used, then 10.x.x.x, then finally the tiny 192.168.x.x range.\n        if (mask >= 12)\n            ret = find_ipv4_net((172 << 24) | (16 << 16), (172 << 24) | (32 << 16), search_mask, exclude);\n\n        if (!ret && mask >= 8)\n            ret = find_ipv4_net((10 << 24), (11 << 24), search_mask, exclude);\n\n        if (!ret && mask >= 16)\n            ret = find_ipv4_net((192 << 24) | (168 << 16), (192 << 24) | (169 << 16), search_mask, exclude);\n\n        if (ret)\n            ret->mask = mask;  // In case we used a larger search_mask\n\n        return ret;\n    }\n\n    std::optional<ipv6_net> find_private_ipv6_net(const std::vector<ipv6_range>& exclude, uint8_t mask)\n    {\n        // This /48 is registered for Lokinet in the IPv6 ULA registry (https://ula.ungleich.ch/):\n        constexpr uint64_t start = 0xfd2e'6c6f'6b69'0000;  // 2e 6c 6f 6b 69 == . l o k i\n        constexpr uint64_t end = start + 0x1'0000;\n\n        if (mask < 48)\n        {\n            log::warning(logcat, \"Cannot auto-detect IPv6 private networks larger than /48\");\n            return std::nullopt;\n        }\n\n        // First do a preliminary check that none of the excluded ranges covers the entire /48,\n        // because if so, we simply can't succeed.\n        {\n            ipv6 min, max;\n            min.hi = start;\n            max.hi = end - 1;\n            max.lo = 0xffff'ffff'ffff'ffff;\n            if (std::ranges::any_of(\n                    exclude, [&min, &max](const auto& e) { return e.contains(min) && e.contains(max); }))\n            {\n                log::warning(\n                    logcat, \"Failed to find a free private /64 IPv6 range: the entire {}/48 range is unavailable\", min);\n                return std::nullopt;\n            }\n        }\n\n        // Cut off the smallest block we search to /64 just so that this doesn't take an excessive\n        // amount of time, and because we can do it with just uint64_t math.  We still give back\n        // whatever you requested, if smaller, we just don't search the smaller spaces.\n        auto search_mask = std::min<uint8_t>(64, mask);\n        uint64_t hi_step = 1 << (64 - search_mask);\n\n        auto ret = std::make_optional<ipv6_net>();\n        auto& net = *ret;\n        net.ip.lo = 1;\n        auto& hi = net.ip.hi;\n        for (hi = start; hi < end; hi += hi_step)\n        {\n            if (std::ranges::none_of(\n                    exclude, [&net](const auto& e) { return e.contains(net.ip) || net.contains(e.ip); }))\n            {\n                ret->mask = mask;  // In case we enlarged it for searching\n                return ret;\n            }\n        }\n\n        ret.reset();\n        return ret;\n    }\n\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/address/ip_range.hpp",
    "content": "#pragma once\n\n#include \"types.hpp\"\n\n#include <oxen/quic/address.hpp>\n\nnamespace llarp\n{\n    // Takes an address with an embedded \"/mask\" at the end and splits it into address and mask.\n    // Throws std::invalid_argument if not parseable as an ADDR/MASK value.  If default_mask is\n    // given then the mask is optional, and the given default will be used if the mask is omitted\n    // from the string.  (Otherwise the mask must be provided in the string).\n    //\n    // Throws on invalid input.\n    ipv4_net parse_ipv4_net(std::string_view address, std::optional<uint8_t> default_mask = std::nullopt);\n    ipv6_net parse_ipv6_net(std::string_view address, std::optional<uint8_t> default_mask = std::nullopt);\n\n    // Exactly the same as above but returns a range (which differs from a net in that it ignores\n    // the IP and always uses the base IP).\n    ipv4_range parse_ipv4_range(std::string_view address, std::optional<uint8_t> default_mask = std::nullopt);\n    ipv6_range parse_ipv6_range(std::string_view address, std::optional<uint8_t> default_mask = std::nullopt);\n\n    // Extracts the ipv4 address from a quic::Address, applies the netmask, and returns in an\n    // ipv{4,6}_{net,range}.  Throws if the Address contains the wrong type (ipv6 when ipv4 wanted or vice\n    // versa).\n    ipv4_net to_ipv4_net(const quic::Address& addr, uint8_t mask);\n    ipv6_net to_ipv6_net(const quic::Address& addr, uint8_t mask);\n    ipv4_net to_ipv4_range(const quic::Address& addr, uint8_t mask);\n    ipv6_net to_ipv6_range(const quic::Address& addr, uint8_t mask);\n\n    // binary encoding of an IP range, such as for exit policy ranges in a CC.\n    std::string encode(const ipv4_range& r);\n    std::string encode(const ipv6_range& r);\n    ipv4_range decode_ipv4_range(std::string_view encoded);\n    ipv6_range decode_ipv6_range(std::string_view decoded);\n    std::variant<ipv4_range, ipv6_range> decode_ip_range(std::string_view encoded);\n\n    /// IPRangeIterator - walks through an IP range\n    template <bool IPv4 = true>\n    struct IPRangeIterator\n    {\n        static constexpr bool is_ipv4 = IPv4;\n        using ip_t = std::conditional_t<is_ipv4, ipv4, ipv6>;\n        using ip_net_t = std::conditional_t<is_ipv4, ipv4_net, ipv6_net>;\n\n      private:\n        ip_t _curr, _base, _last;\n\n      public:\n        IPRangeIterator() = default;\n\n        // Creates a range that starts at the current IP of range, increments up to the max\n        // pre-broadcast address, and resets to the \".1\" address of the range.\n        explicit IPRangeIterator(const ip_net_t& net)\n            : _curr{net.ip}, _base{_curr.to_base(net.mask)}, _last{net.max_ip()}\n        {}\n\n        // Returns the next ip address in the iterating range; returns nullopt if range is exhausted\n        std::optional<ip_t> next_ip()\n        {\n            if (_curr == _last)\n                return std::nullopt;\n            if (auto next = _curr.next_ip())\n                return _curr = *next;\n            return std::nullopt;\n        }\n\n        // Resets the range to the base IP in the range so that next_ip() starts over from the\n        // beginning of the range.\n        void reset() { _curr = _base; }\n\n        bool range_exhausted() const { return _curr == _last; }\n    };\n    IPRangeIterator(const ipv4_net&) -> IPRangeIterator<true>;\n    IPRangeIterator(const ipv6_net&) -> IPRangeIterator<false>;\n\n    using IPv4RangeIterator = IPRangeIterator<true>;\n    using IPv6RangeIterator = IPRangeIterator<false>;\n\n    // Finds a private IP /N range that does not overlap with any ranges in `excluding`.  Returns\n    // the ipv{4,6}_net with ip set to the first usable address in the range (i.e. the \".1\" or \"::1\"\n    // for an IPv4 mask_size of 24 or smaller).  Returns nullopt if no suitable range can be found.\n    std::optional<ipv4_net> find_private_ipv4_net(const std::vector<ipv4_range>& excluding, uint8_t mask_size);\n    std::optional<ipv6_net> find_private_ipv6_net(const std::vector<ipv6_range>& excluding, uint8_t mask_size);\n\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/address/map.hpp",
    "content": "#pragma once\n\n#include \"address.hpp\"\n\n#include <llarp/util/formattable.hpp>\n#include <llarp/util/thread/threading.hpp>\n\nnamespace llarp\n{\n    template <typename LocalAddrT>\n    struct address_map\n    {\n      protected:\n        std::unordered_map<LocalAddrT, NetworkAddress> _local_to_remote;\n        std::unordered_map<NetworkAddress, LocalAddrT> _remote_to_local;\n        std::unordered_map<std::string, NetworkAddress> _name_to_remote;\n\n        using Lock_t = util::NullLock;\n        mutable util::NullMutex addr_mutex;\n\n      public:\n        /** This functions exactly as std::unordered_map's ::insert_or_assign method. If a key equivalent\n            to `local` or `remote` already exists, then they will be assigned to the corresponding value.\n            Otherwise, the values will be inserted.\n\n            The returned `bool` is true if the insertion took place and `false` if assignment occurred.\n        */\n        bool insert_or_assign(const LocalAddrT& local, const NetworkAddress& remote)\n        {\n            Lock_t l{addr_mutex};\n\n            auto [_1, ins1] = _local_to_remote.insert_or_assign(local, remote);\n            auto [_2, ins2] = _remote_to_local.insert_or_assign(remote, local);\n            auto [_3, ins3] = _name_to_remote.insert_or_assign(remote.name(), remote);\n\n            return ins1 && ins2 && ins3;\n        }\n\n        std::optional<NetworkAddress> get_remote(const LocalAddrT& local) const\n        {\n            Lock_t l{addr_mutex};\n\n            if (auto itr = _local_to_remote.find(local); itr != _local_to_remote.end())\n                return itr->second;\n            return std::nullopt;\n        }\n\n        std::optional<NetworkAddress> get_remote(const std::string& name) const\n        {\n            Lock_t l{addr_mutex};\n\n            if (auto itr = _name_to_remote.find(name); itr != _name_to_remote.end())\n                return itr->second;\n            return std::nullopt;\n        }\n        std::optional<NetworkAddress> operator[](const LocalAddrT& local) const { return get_remote(local); }\n\n        std::optional<LocalAddrT> get_local(const NetworkAddress& remote) const\n        {\n            Lock_t l{addr_mutex};\n\n            if (auto itr = _remote_to_local.find(remote); itr != _remote_to_local.end())\n                return itr->second;\n            return std::nullopt;\n        }\n        std::optional<LocalAddrT> operator[](const NetworkAddress& remote) const { return get_local(remote); }\n\n        std::optional<LocalAddrT> get_local(const std::string& name) const\n        {\n            Lock_t l{addr_mutex};\n\n            if (auto itr = _name_to_remote.find(name); itr != _name_to_remote.end())\n                return get_local(itr->second);\n            return std::nullopt;\n        }\n\n        bool has_local(const LocalAddrT& local) const\n        {\n            Lock_t l{addr_mutex};\n\n            return _local_to_remote.contains(local);\n        }\n\n        bool has_remote(const NetworkAddress& remote) const\n        {\n            Lock_t l{addr_mutex};\n\n            return _remote_to_local.contains(remote);\n        }\n\n        void unmap(const NetworkAddress& remote)\n        {\n            Lock_t l{addr_mutex};\n\n            if (auto it = _remote_to_local.find(remote); it != _remote_to_local.end())\n            {\n                _local_to_remote.erase(it->second);\n                _remote_to_local.erase(it);\n            }\n            _name_to_remote.erase(remote.name());\n        }\n\n        void unmap(const LocalAddrT& local)\n        {\n            Lock_t l{addr_mutex};\n\n            if (auto it_a = _local_to_remote.find(local); it_a != _local_to_remote.end())\n            {\n                if (auto it_b = _remote_to_local.find(it_a->second); it_b != _remote_to_local.end())\n                {\n                    _name_to_remote.erase(it_b->first.name());\n                    _remote_to_local.erase(it_b);\n                }\n                _local_to_remote.erase(it_a);\n            }\n        }\n\n        void unmap(const std::string& name)\n        {\n            Lock_t l{addr_mutex};\n\n            if (auto it_a = _name_to_remote.find(name); it_a != _name_to_remote.end())\n            {\n                if (auto it_b = _remote_to_local.find(it_a->second); it_b != _remote_to_local.end())\n                {\n                    _local_to_remote.erase(it_b->second);\n                    _remote_to_local.erase(it_b);\n                }\n                _name_to_remote.erase(it_a);\n            }\n        }\n    };\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/address/types.hpp",
    "content": "#pragma once\n\n#include <llarp/util/formattable.hpp>\n\n#include <oxen/quic/ip.hpp>\n\nnamespace llarp\n{\n    namespace quic = oxen::quic;\n    using quic::ipv4;\n    using quic::ipv4_net;\n    using quic::ipv4_range;\n    using quic::ipv6;\n    using quic::ipv6_net;\n    using quic::ipv6_range;\n\n    // Used for hash combining, below\n    inline constexpr size_t inverse_golden_ratio = sizeof(size_t) >= 8 ? 0x9e37'79b9'7f4a'7c15 : 0x9e37'79b9;\n}  //   namespace llarp\n\ntemplate <>\nstruct std::hash<llarp::ipv4>\n{\n    size_t operator()(const llarp::ipv4& obj) const noexcept { return hash<uint32_t>{}(obj.addr); }\n};\n\ntemplate <>\nstruct std::hash<llarp::ipv6>\n{\n    size_t operator()(const llarp::ipv6& obj) const noexcept\n    {\n        std::hash<uint64_t> subhash{};\n        auto h = subhash(obj.hi);\n        h ^= subhash(obj.lo) + llarp::inverse_golden_ratio + (h << 6) + (h >> 2);\n        return h;\n    }\n};\n"
  },
  {
    "path": "llarp/address/utils.cpp",
    "content": "#include \"utils.hpp\"\n\n#include <llarp/crypto/constants.hpp>\n#include <llarp/util/formattable.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/str.hpp>\n\n#include <stdexcept>\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"address-utils\");\n\n    namespace detail\n    {\n        std::optional<std::string> parse_addr_string(std::string_view arg, std::string_view tld)\n        {\n            std::optional<std::string> ret = std::nullopt;\n\n            if (auto pos = arg.find_first_of('.'); pos != std::string_view::npos)\n            {\n                auto _prefix = arg.substr(0, pos);\n                // check the pubkey prefix is the right length\n                if (_prefix.length() != PUBKEYSIZE)\n                    return ret;\n\n                // verify the tld is allowed\n                auto _tld = arg.substr(pos);\n\n                if (_tld == tld and TLD::allowed(_tld))\n                    ret = _prefix;\n            }\n\n            return ret;\n        };\n\n        std::pair<std::string, uint16_t> parse_addr(std::string_view addr, std::optional<uint16_t> default_port)\n        {\n            std::pair<std::string, uint16_t> result;\n            auto &[host, port] = result;\n\n            if (auto p = addr.rfind(':'); p != std::string_view::npos)\n            {\n                if (!parse_int(addr.substr(p + 1), port))\n                    throw std::invalid_argument{\"Invalid address: could not parse port\"};\n                addr.remove_suffix(addr.size() - p);\n            }\n            else if (default_port.has_value())  // use ::has_value() in case default_port is set but is == 0\n            {\n                port = *default_port;\n            }\n            else\n                throw std::invalid_argument{\n                    \"Invalid address: argument contains no port and no default was specified (input:{})\"_format(addr)};\n\n            bool had_sq_brackets = false;\n\n            if (!addr.empty() && addr.front() == '[' && addr.back() == ']')\n            {\n                addr.remove_prefix(1);\n                addr.remove_suffix(1);\n                had_sq_brackets = true;\n            }\n\n            if (auto p = addr.find_first_not_of(\"0123456789.\"sv); p != std::string_view::npos)\n            {\n                if (auto q = addr.find_first_not_of(\"0123456789ABCDEFabcdef:.\"); q != std::string_view::npos)\n                    throw std::invalid_argument{\"Invalid address: does not look like IPv4 or IPv6!\"};\n                if (!had_sq_brackets)\n                    throw std::invalid_argument{\"Invalid address: IPv6 addresses require [...] square brackets\"};\n            }\n\n            host = addr;\n            return result;\n        }\n    }  // namespace detail\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/address/utils.hpp",
    "content": "#pragma once\n\n#include <cstdint>\n#include <optional>\n#include <string_view>\n\nnamespace llarp\n{\n    using namespace std::literals;\n\n    namespace TLD\n    {\n        inline constexpr auto SNODE = \".snode\"sv;\n        inline constexpr auto LOKI = \".loki\"sv;\n\n        inline constexpr bool allowed(std::string_view dot_tld) { return dot_tld == SNODE || dot_tld == LOKI; }\n    }  //  namespace TLD\n\n    namespace detail\n    {\n        std::optional<std::string> parse_addr_string(std::string_view arg, std::string_view tld);\n\n        std::pair<std::string, uint16_t> parse_addr(std::string_view addr, std::optional<uint16_t> default_port);\n\n    }  //  namespace detail\n\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/android/ifaddrs.c",
    "content": "/*\nCopyright (c) 2013, Kenneth MacKay\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without modification,\nare permitted provided that the following conditions are met:\n * Redistributions of source code must retain the above copyright notice, this\n   list of conditions and the following disclaimer.\n * Redistributions in binary form must reproduce the above copyright notice,\n   this list of conditions and the following disclaimer in the documentation\n   and/or other materials provided with the distribution.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\nANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\nWARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR\nANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\nLOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\nANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n*/\n\n#include \"ifaddrs.h\"\n\n#include <errno.h>\n#include <linux/netlink.h>\n#include <linux/rtnetlink.h>\n#include <net/if_arp.h>\n#include <netinet/in.h>\n#include <netpacket/packet.h>\n#include <stddef.h>\n#include <stdlib.h>\n#include <string.h>\n#include <sys/socket.h>\n#include <unistd.h>\n\ntypedef struct NetlinkList\n{\n    struct NetlinkList* m_next;\n    struct nlmsghdr* m_data;\n    unsigned int m_size;\n} NetlinkList;\n\nstatic int netlink_socket(void)\n{\n    int l_socket = socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE);\n    if (l_socket < 0)\n    {\n        return -1;\n    }\n\n    struct sockaddr_nl l_addr;\n    memset(&l_addr, 0, sizeof(l_addr));\n    l_addr.nl_family = AF_NETLINK;\n    if (bind(l_socket, (struct sockaddr*)&l_addr, sizeof(l_addr)) < 0)\n    {\n        close(l_socket);\n        return -1;\n    }\n\n    return l_socket;\n}\n\nstatic int netlink_send(int p_socket, int p_request)\n{\n    struct\n    {\n        struct nlmsghdr m_hdr;\n        struct rtgenmsg m_msg;\n    } l_data;\n\n    memset(&l_data, 0, sizeof(l_data));\n\n    l_data.m_hdr.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtgenmsg));\n    l_data.m_hdr.nlmsg_type = p_request;\n    l_data.m_hdr.nlmsg_flags = NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST;\n    l_data.m_hdr.nlmsg_pid = 0;\n    l_data.m_hdr.nlmsg_seq = p_socket;\n    l_data.m_msg.rtgen_family = AF_UNSPEC;\n\n    struct sockaddr_nl l_addr;\n    memset(&l_addr, 0, sizeof(l_addr));\n    l_addr.nl_family = AF_NETLINK;\n    return (sendto(p_socket, &l_data.m_hdr, l_data.m_hdr.nlmsg_len, 0, (struct sockaddr*)&l_addr, sizeof(l_addr)));\n}\n\nstatic int netlink_recv(int p_socket, void* p_buffer, size_t p_len)\n{\n    struct msghdr l_msg;\n    struct iovec l_iov = {p_buffer, p_len};\n    struct sockaddr_nl l_addr;\n\n    for (;;)\n    {\n        l_msg.msg_name = (void*)&l_addr;\n        l_msg.msg_namelen = sizeof(l_addr);\n        l_msg.msg_iov = &l_iov;\n        l_msg.msg_iovlen = 1;\n        l_msg.msg_control = NULL;\n        l_msg.msg_controllen = 0;\n        l_msg.msg_flags = 0;\n        int l_result = recvmsg(p_socket, &l_msg, 0);\n\n        if (l_result < 0)\n        {\n            if (errno == EINTR)\n            {\n                continue;\n            }\n            return -2;\n        }\n\n        if (l_msg.msg_flags & MSG_TRUNC)\n        {  // buffer was too small\n            return -1;\n        }\n        return l_result;\n    }\n}\n\nstatic struct nlmsghdr* getNetlinkResponse(int p_socket, int* p_size, int* p_done)\n{\n    size_t l_size = 4096;\n    void* l_buffer = NULL;\n\n    for (;;)\n    {\n        free(l_buffer);\n        l_buffer = malloc(l_size);\n        if (l_buffer == NULL)\n        {\n            return NULL;\n        }\n\n        int l_read = netlink_recv(p_socket, l_buffer, l_size);\n        *p_size = l_read;\n        if (l_read == -2)\n        {\n            free(l_buffer);\n            return NULL;\n        }\n        if (l_read >= 0)\n        {\n            pid_t l_pid = getpid();\n            struct nlmsghdr* l_hdr;\n            for (l_hdr = (struct nlmsghdr*)l_buffer; NLMSG_OK(l_hdr, (unsigned int)l_read);\n                 l_hdr = (struct nlmsghdr*)NLMSG_NEXT(l_hdr, l_read))\n            {\n                if ((pid_t)l_hdr->nlmsg_pid != l_pid || (int)l_hdr->nlmsg_seq != p_socket)\n                {\n                    continue;\n                }\n\n                if (l_hdr->nlmsg_type == NLMSG_DONE)\n                {\n                    *p_done = 1;\n                    break;\n                }\n\n                if (l_hdr->nlmsg_type == NLMSG_ERROR)\n                {\n                    free(l_buffer);\n                    return NULL;\n                }\n            }\n            return l_buffer;\n        }\n\n        l_size *= 2;\n    }\n}\n\nstatic NetlinkList* newListItem(struct nlmsghdr* p_data, unsigned int p_size)\n{\n    NetlinkList* l_item = malloc(sizeof(NetlinkList));\n    if (l_item == NULL)\n    {\n        return NULL;\n    }\n\n    l_item->m_next = NULL;\n    l_item->m_data = p_data;\n    l_item->m_size = p_size;\n    return l_item;\n}\n\nstatic void freeResultList(NetlinkList* p_list)\n{\n    NetlinkList* l_cur;\n    while (p_list)\n    {\n        l_cur = p_list;\n        p_list = p_list->m_next;\n        free(l_cur->m_data);\n        free(l_cur);\n    }\n}\n\nstatic NetlinkList* getResultList(int p_socket, int p_request)\n{\n    if (netlink_send(p_socket, p_request) < 0)\n    {\n        return NULL;\n    }\n\n    NetlinkList* l_list = NULL;\n    NetlinkList* l_end = NULL;\n    int l_size;\n    int l_done = 0;\n    while (!l_done)\n    {\n        struct nlmsghdr* l_hdr = getNetlinkResponse(p_socket, &l_size, &l_done);\n        if (!l_hdr)\n        {  // error\n            freeResultList(l_list);\n            return NULL;\n        }\n\n        NetlinkList* l_item = newListItem(l_hdr, l_size);\n        if (!l_item)\n        {\n            freeResultList(l_list);\n            return NULL;\n        }\n        if (!l_list)\n        {\n            l_list = l_item;\n        }\n        else\n        {\n            l_end->m_next = l_item;\n        }\n        l_end = l_item;\n    }\n    return l_list;\n}\n\nstatic size_t maxSize(size_t a, size_t b) { return (a > b ? a : b); }\n\nstatic size_t calcAddrLen(sa_family_t p_family, int p_dataSize)\n{\n    switch (p_family)\n    {\n        case AF_INET:\n            return sizeof(struct sockaddr_in);\n        case AF_INET6:\n            return sizeof(struct sockaddr_in6);\n        case AF_PACKET:\n            return maxSize(sizeof(struct sockaddr_ll), offsetof(struct sockaddr_ll, sll_addr) + p_dataSize);\n        default:\n            return maxSize(sizeof(struct sockaddr), offsetof(struct sockaddr, sa_data) + p_dataSize);\n    }\n}\n\nstatic void makeSockaddr(sa_family_t p_family, struct sockaddr* p_dest, void* p_data, size_t p_size)\n{\n    switch (p_family)\n    {\n        case AF_INET:\n            memcpy(&((struct sockaddr_in*)p_dest)->sin_addr, p_data, p_size);\n            break;\n        case AF_INET6:\n            memcpy(&((struct sockaddr_in6*)p_dest)->sin6_addr, p_data, p_size);\n            break;\n        case AF_PACKET:\n            memcpy(((struct sockaddr_ll*)p_dest)->sll_addr, p_data, p_size);\n            ((struct sockaddr_ll*)p_dest)->sll_halen = p_size;\n            break;\n        default:\n            memcpy(p_dest->sa_data, p_data, p_size);\n            break;\n    }\n    p_dest->sa_family = p_family;\n}\n\nstatic void addToEnd(struct ifaddrs** p_resultList, struct ifaddrs* p_entry)\n{\n    if (!*p_resultList)\n    {\n        *p_resultList = p_entry;\n    }\n    else\n    {\n        struct ifaddrs* l_cur = *p_resultList;\n        while (l_cur->ifa_next)\n        {\n            l_cur = l_cur->ifa_next;\n        }\n        l_cur->ifa_next = p_entry;\n    }\n}\n\nstatic int interpretLink(struct nlmsghdr* p_hdr, struct ifaddrs** p_resultList)\n{\n    struct ifinfomsg* l_info = (struct ifinfomsg*)NLMSG_DATA(p_hdr);\n\n    size_t l_nameSize = 0;\n    size_t l_addrSize = 0;\n    size_t l_dataSize = 0;\n\n    size_t l_rtaSize = NLMSG_PAYLOAD(p_hdr, sizeof(struct ifinfomsg));\n    struct rtattr* l_rta;\n    for (l_rta = IFLA_RTA(l_info); RTA_OK(l_rta, l_rtaSize); l_rta = RTA_NEXT(l_rta, l_rtaSize))\n    {\n        void* l_rtaData = RTA_DATA(l_rta);\n        (void)l_rtaData;\n        size_t l_rtaDataSize = RTA_PAYLOAD(l_rta);\n        switch (l_rta->rta_type)\n        {\n            case IFLA_ADDRESS:\n            case IFLA_BROADCAST:\n                l_addrSize += NLMSG_ALIGN(calcAddrLen(AF_PACKET, l_rtaDataSize));\n                break;\n            case IFLA_IFNAME:\n                l_nameSize += NLMSG_ALIGN(l_rtaSize + 1);\n                break;\n            case IFLA_STATS:\n                l_dataSize += NLMSG_ALIGN(l_rtaSize);\n                break;\n            default:\n                break;\n        }\n    }\n\n    struct ifaddrs* l_entry = malloc(sizeof(struct ifaddrs) + sizeof(int) + l_nameSize + l_addrSize + l_dataSize);\n    if (l_entry == NULL)\n    {\n        return -1;\n    }\n    memset(l_entry, 0, sizeof(struct ifaddrs));\n    l_entry->ifa_name = \"\";\n\n    char* l_index = ((char*)l_entry) + sizeof(struct ifaddrs);\n    char* l_name = l_index + sizeof(int);\n    char* l_addr = l_name + l_nameSize;\n    char* l_data = l_addr + l_addrSize;\n\n    // save the interface index so we can look it up when handling the addresses.\n    memcpy(l_index, &l_info->ifi_index, sizeof(int));\n\n    l_entry->ifa_flags = l_info->ifi_flags;\n\n    l_rtaSize = NLMSG_PAYLOAD(p_hdr, sizeof(struct ifinfomsg));\n    for (l_rta = IFLA_RTA(l_info); RTA_OK(l_rta, l_rtaSize); l_rta = RTA_NEXT(l_rta, l_rtaSize))\n    {\n        void* l_rtaData = RTA_DATA(l_rta);\n        size_t l_rtaDataSize = RTA_PAYLOAD(l_rta);\n        switch (l_rta->rta_type)\n        {\n            case IFLA_ADDRESS:\n            case IFLA_BROADCAST:\n            {\n                size_t l_addrLen = calcAddrLen(AF_PACKET, l_rtaDataSize);\n                makeSockaddr(AF_PACKET, (struct sockaddr*)l_addr, l_rtaData, l_rtaDataSize);\n                ((struct sockaddr_ll*)l_addr)->sll_ifindex = l_info->ifi_index;\n                ((struct sockaddr_ll*)l_addr)->sll_hatype = l_info->ifi_type;\n                if (l_rta->rta_type == IFLA_ADDRESS)\n                {\n                    l_entry->ifa_addr = (struct sockaddr*)l_addr;\n                }\n                else\n                {\n                    l_entry->ifa_broadaddr = (struct sockaddr*)l_addr;\n                }\n                l_addr += NLMSG_ALIGN(l_addrLen);\n                break;\n            }\n            case IFLA_IFNAME:\n                strncpy(l_name, l_rtaData, l_rtaDataSize);\n                l_name[l_rtaDataSize] = '\\0';\n                l_entry->ifa_name = l_name;\n                break;\n            case IFLA_STATS:\n                memcpy(l_data, l_rtaData, l_rtaDataSize);\n                l_entry->ifa_data = l_data;\n                break;\n            default:\n                break;\n        }\n    }\n\n    addToEnd(p_resultList, l_entry);\n    return 0;\n}\n\nstatic struct ifaddrs* findInterface(int p_index, struct ifaddrs** p_links, int p_numLinks)\n{\n    int l_num = 0;\n    struct ifaddrs* l_cur = *p_links;\n    while (l_cur && l_num < p_numLinks)\n    {\n        char* l_indexPtr = ((char*)l_cur) + sizeof(struct ifaddrs);\n        int l_index;\n        memcpy(&l_index, l_indexPtr, sizeof(int));\n        if (l_index == p_index)\n        {\n            return l_cur;\n        }\n\n        l_cur = l_cur->ifa_next;\n        ++l_num;\n    }\n    return NULL;\n}\n\nstatic int interpretAddr(struct nlmsghdr* p_hdr, struct ifaddrs** p_resultList, int p_numLinks)\n{\n    struct ifaddrmsg* l_info = (struct ifaddrmsg*)NLMSG_DATA(p_hdr);\n    struct ifaddrs* l_interface = findInterface(l_info->ifa_index, p_resultList, p_numLinks);\n\n    if (l_info->ifa_family == AF_PACKET)\n    {\n        return 0;\n    }\n\n    size_t l_nameSize = 0;\n    size_t l_addrSize = 0;\n\n    int l_addedNetmask = 0;\n\n    size_t l_rtaSize = NLMSG_PAYLOAD(p_hdr, sizeof(struct ifaddrmsg));\n    struct rtattr* l_rta;\n    for (l_rta = IFA_RTA(l_info); RTA_OK(l_rta, l_rtaSize); l_rta = RTA_NEXT(l_rta, l_rtaSize))\n    {\n        void* l_rtaData = RTA_DATA(l_rta);\n        (void)l_rtaData;\n        size_t l_rtaDataSize = RTA_PAYLOAD(l_rta);\n\n        switch (l_rta->rta_type)\n        {\n            case IFA_ADDRESS:\n            case IFA_LOCAL:\n                if ((l_info->ifa_family == AF_INET || l_info->ifa_family == AF_INET6) && !l_addedNetmask)\n                {  // make room for netmask\n                    l_addrSize += NLMSG_ALIGN(calcAddrLen(l_info->ifa_family, l_rtaDataSize));\n                    l_addedNetmask = 1;\n                }\n            case IFA_BROADCAST:\n                l_addrSize += NLMSG_ALIGN(calcAddrLen(l_info->ifa_family, l_rtaDataSize));\n                break;\n            case IFA_LABEL:\n                l_nameSize += NLMSG_ALIGN(l_rtaSize + 1);\n                break;\n            default:\n                break;\n        }\n    }\n\n    struct ifaddrs* l_entry = malloc(sizeof(struct ifaddrs) + l_nameSize + l_addrSize);\n    if (l_entry == NULL)\n    {\n        return -1;\n    }\n    memset(l_entry, 0, sizeof(struct ifaddrs));\n    l_entry->ifa_name = (l_interface ? l_interface->ifa_name : \"\");\n\n    char* l_name = ((char*)l_entry) + sizeof(struct ifaddrs);\n    char* l_addr = l_name + l_nameSize;\n\n    l_entry->ifa_flags = l_info->ifa_flags;\n    if (l_interface)\n    {\n        l_entry->ifa_flags |= l_interface->ifa_flags;\n    }\n\n    l_rtaSize = NLMSG_PAYLOAD(p_hdr, sizeof(struct ifaddrmsg));\n    for (l_rta = IFA_RTA(l_info); RTA_OK(l_rta, l_rtaSize); l_rta = RTA_NEXT(l_rta, l_rtaSize))\n    {\n        void* l_rtaData = RTA_DATA(l_rta);\n        size_t l_rtaDataSize = RTA_PAYLOAD(l_rta);\n        switch (l_rta->rta_type)\n        {\n            case IFA_ADDRESS:\n            case IFA_BROADCAST:\n            case IFA_LOCAL:\n            {\n                size_t l_addrLen = calcAddrLen(l_info->ifa_family, l_rtaDataSize);\n                makeSockaddr(l_info->ifa_family, (struct sockaddr*)l_addr, l_rtaData, l_rtaDataSize);\n                if (l_info->ifa_family == AF_INET6)\n                {\n                    if (IN6_IS_ADDR_LINKLOCAL((struct in6_addr*)l_rtaData)\n                        || IN6_IS_ADDR_MC_LINKLOCAL((struct in6_addr*)l_rtaData))\n                    {\n                        ((struct sockaddr_in6*)l_addr)->sin6_scope_id = l_info->ifa_index;\n                    }\n                }\n\n                if (l_rta->rta_type == IFA_ADDRESS)\n                {  // apparently in a point-to-point network IFA_ADDRESS contains the\n                   // dest address and IFA_LOCAL contains the local address\n                    if (l_entry->ifa_addr)\n                    {\n                        l_entry->ifa_dstaddr = (struct sockaddr*)l_addr;\n                    }\n                    else\n                    {\n                        l_entry->ifa_addr = (struct sockaddr*)l_addr;\n                    }\n                }\n                else if (l_rta->rta_type == IFA_LOCAL)\n                {\n                    if (l_entry->ifa_addr)\n                    {\n                        l_entry->ifa_dstaddr = l_entry->ifa_addr;\n                    }\n                    l_entry->ifa_addr = (struct sockaddr*)l_addr;\n                }\n                else\n                {\n                    l_entry->ifa_broadaddr = (struct sockaddr*)l_addr;\n                }\n                l_addr += NLMSG_ALIGN(l_addrLen);\n                break;\n            }\n            case IFA_LABEL:\n                strncpy(l_name, l_rtaData, l_rtaDataSize);\n                l_name[l_rtaDataSize] = '\\0';\n                l_entry->ifa_name = l_name;\n                break;\n            default:\n                break;\n        }\n    }\n\n    if (l_entry->ifa_addr && (l_entry->ifa_addr->sa_family == AF_INET || l_entry->ifa_addr->sa_family == AF_INET6))\n    {\n        unsigned l_maxPrefix = (l_entry->ifa_addr->sa_family == AF_INET ? 32 : 128);\n        unsigned l_prefix = (l_info->ifa_prefixlen > l_maxPrefix ? l_maxPrefix : l_info->ifa_prefixlen);\n        char l_mask[16] = {0};\n        unsigned i;\n        for (i = 0; i < (l_prefix / 8); ++i)\n        {\n            l_mask[i] = 0xff;\n        }\n        if (l_prefix % 8)\n        {\n            l_mask[i] = 0xff << (8 - (l_prefix % 8));\n        }\n\n        makeSockaddr(l_entry->ifa_addr->sa_family, (struct sockaddr*)l_addr, l_mask, l_maxPrefix / 8);\n        l_entry->ifa_netmask = (struct sockaddr*)l_addr;\n    }\n\n    addToEnd(p_resultList, l_entry);\n    return 0;\n}\n\nstatic int interpretLinks(int p_socket, NetlinkList* p_netlinkList, struct ifaddrs** p_resultList)\n{\n    int l_numLinks = 0;\n    pid_t l_pid = getpid();\n    for (; p_netlinkList; p_netlinkList = p_netlinkList->m_next)\n    {\n        unsigned int l_nlsize = p_netlinkList->m_size;\n        struct nlmsghdr* l_hdr;\n        for (l_hdr = p_netlinkList->m_data; NLMSG_OK(l_hdr, l_nlsize); l_hdr = NLMSG_NEXT(l_hdr, l_nlsize))\n        {\n            if ((pid_t)l_hdr->nlmsg_pid != l_pid || (int)l_hdr->nlmsg_seq != p_socket)\n            {\n                continue;\n            }\n\n            if (l_hdr->nlmsg_type == NLMSG_DONE)\n            {\n                break;\n            }\n\n            if (l_hdr->nlmsg_type == RTM_NEWLINK)\n            {\n                if (interpretLink(l_hdr, p_resultList) == -1)\n                {\n                    return -1;\n                }\n                ++l_numLinks;\n            }\n        }\n    }\n    return l_numLinks;\n}\n\nstatic int interpretAddrs(int p_socket, NetlinkList* p_netlinkList, struct ifaddrs** p_resultList, int p_numLinks)\n{\n    pid_t l_pid = getpid();\n    for (; p_netlinkList; p_netlinkList = p_netlinkList->m_next)\n    {\n        unsigned int l_nlsize = p_netlinkList->m_size;\n        struct nlmsghdr* l_hdr;\n        for (l_hdr = p_netlinkList->m_data; NLMSG_OK(l_hdr, l_nlsize); l_hdr = NLMSG_NEXT(l_hdr, l_nlsize))\n        {\n            if ((pid_t)l_hdr->nlmsg_pid != l_pid || (int)l_hdr->nlmsg_seq != p_socket)\n            {\n                continue;\n            }\n\n            if (l_hdr->nlmsg_type == NLMSG_DONE)\n            {\n                break;\n            }\n\n            if (l_hdr->nlmsg_type == RTM_NEWADDR)\n            {\n                if (interpretAddr(l_hdr, p_resultList, p_numLinks) == -1)\n                {\n                    return -1;\n                }\n            }\n        }\n    }\n    return 0;\n}\n\nint getifaddrs(struct ifaddrs** ifap)\n{\n    if (!ifap)\n    {\n        return -1;\n    }\n    *ifap = NULL;\n\n    int l_socket = netlink_socket();\n    if (l_socket < 0)\n    {\n        return -1;\n    }\n\n    NetlinkList* l_linkResults = getResultList(l_socket, RTM_GETLINK);\n    if (!l_linkResults)\n    {\n        close(l_socket);\n        return -1;\n    }\n\n    NetlinkList* l_addrResults = getResultList(l_socket, RTM_GETADDR);\n    if (!l_addrResults)\n    {\n        close(l_socket);\n        freeResultList(l_linkResults);\n        return -1;\n    }\n\n    int l_result = 0;\n    int l_numLinks = interpretLinks(l_socket, l_linkResults, ifap);\n    if (l_numLinks == -1 || interpretAddrs(l_socket, l_addrResults, ifap, l_numLinks) == -1)\n    {\n        l_result = -1;\n    }\n\n    freeResultList(l_linkResults);\n    freeResultList(l_addrResults);\n    close(l_socket);\n    return l_result;\n}\n\nvoid freeifaddrs(struct ifaddrs* ifa)\n{\n    struct ifaddrs* l_cur;\n    while (ifa)\n    {\n        l_cur = ifa;\n        ifa = ifa->ifa_next;\n        free(l_cur);\n    }\n}\n"
  },
  {
    "path": "llarp/android/ifaddrs.h",
    "content": "/*\n * Copyright (c) 1995, 1999\n *\tBerkeley Software Design, Inc.  All rights reserved.\n *\n * Redistribution and use in source and binary forms, with or without\n * modification, are permitted provided that the following conditions\n * are met:\n * 1. Redistributions of source code must retain the above copyright\n *    notice, this list of conditions and the following disclaimer.\n *\n * THIS SOFTWARE IS PROVIDED BY Berkeley Software Design, Inc. ``AS IS'' AND\n * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n * ARE DISCLAIMED.  IN NO EVENT SHALL Berkeley Software Design, Inc. BE LIABLE\n * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\n * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\n * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\n * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY\n * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF\n * SUCH DAMAGE.\n *\n *\tBSDI ifaddrs.h,v 2.5 2000/02/23 14:51:59 dab Exp\n */\n\n#pragma once\n\nstruct ifaddrs\n{\n    struct ifaddrs* ifa_next;\n    char* ifa_name;\n    unsigned int ifa_flags;\n    struct sockaddr* ifa_addr;\n    struct sockaddr* ifa_netmask;\n    struct sockaddr* ifa_dstaddr;\n    void* ifa_data;\n};\n\n/*\n * This may have been defined in <net/if.h>.  Note that if <net/if.h> is\n * to be included it must be included before this header file.\n */\n#ifndef ifa_broadaddr\n#define ifa_broadaddr ifa_dstaddr /* broadcast address interface */\n#endif\n\n#include <sys/cdefs.h>\n\n__BEGIN_DECLS\nextern int getifaddrs(struct ifaddrs** ifap);\nextern void freeifaddrs(struct ifaddrs* ifa);\n__END_DECLS\n"
  },
  {
    "path": "llarp/app.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<assembly xmlns=\"urn:schemas-microsoft-com:asm.v1\" manifestVersion=\"1.0\" xmlns:asmv3=\"urn:schemas-microsoft-com:asm.v3\">\n  <trustInfo xmlns=\"urn:schemas-microsoft-com:asm.v3\">\n    <security>\n      <requestedPrivileges>\n        <requestedExecutionLevel level=\"asInvoker\"/>\n      </requestedPrivileges>\n    </security>\n  </trustInfo>\n  \n  <compatibility xmlns=\"urn:schemas-microsoft-com:compatibility.v1\">\n    <application>\n      <!--The ID below indicates application support for Windows Vista -->\n      <supportedOS Id=\"{e2011457-1546-43c5-a5fe-008deee3d3f0}\"/>\n      <!--The ID below indicates application support for Windows 7 -->\n      <supportedOS Id=\"{35138b9a-5d96-4fbd-8e2d-a2440225f93a}\"/>\n      <!--The ID below indicates application support for Windows 8 -->\n      <supportedOS Id=\"{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}\"/>\n      <!--The ID below indicates application support for Windows 8.1 -->\n      <supportedOS Id=\"{1f676c76-80e1-4239-95bb-83d0f6d0da78}\"/> \n      <!--The ID below indicates application support for Windows 10 -->\n      <supportedOS Id=\"{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}\"/> \n    </application>\n  </compatibility>\n  <dependency>\n    <dependentAssembly>\n      <assemblyIdentity\n          type=\"win32\"\n          name=\"Microsoft.Windows.Common-Controls\"\n          version=\"6.0.0.0\"\n          processorArchitecture=\"*\"\n          publicKeyToken=\"6595b64144ccf1df\"\n          language=\"*\"\n        />\n    </dependentAssembly>\n  </dependency>\n</assembly>"
  },
  {
    "path": "llarp/apple/CMakeLists.txt",
    "content": "\n# 3.13+ so that we can add link libraries to parent targets\ncmake_minimum_required(VERSION 3.13)\n\nif (LOKINET_DAEMON AND (BUILD_SHARED_LIBS OR NOT BUILD_STATIC_DEPS OR NOT STATIC_LINK))\n  message(FATAL_ERROR \"macOS full builds require a full static build; perhaps use the contrib/mac.sh script to build?\")\nendif()\n\n# god (steve jobs) made apple so that man may suffer\nfind_library(FOUNDATION Foundation REQUIRED)\n\ntarget_link_libraries(lokinet-base INTERFACE ${FOUNDATION})\n\nif (LOKINET_DAEMON)\n\n  target_sources(lokinet-platform PRIVATE vpn_platform.cpp vpn_interface.cpp route_manager.cpp context_wrapper.cpp)\n\n  find_library(NETEXT NetworkExtension REQUIRED)\n  find_library(COREFOUNDATION CoreFoundation REQUIRED)\n\n  add_executable(lokinet-extension MACOSX_BUNDLE\n    PacketTunnelProvider.m\n    DNSTrampoline.m\n  )\n\n  enable_lto(lokinet-extension)\n\n  # -fobjc-arc enables automatic reference counting for objective-C code\n  # -e _NSExtensionMain because the appex has that instead of a `main` function entry point, of course.\n  target_compile_options(lokinet-extension PRIVATE -fobjc-arc)\n  if(MACOS_SYSTEM_EXTENSION)\n    target_compile_definitions(lokinet-extension PRIVATE MACOS_SYSTEM_EXTENSION)\n    target_compile_definitions(lokinet-base INTERFACE MACOS_SYSTEM_EXTENSION)\n  else()\n    target_link_options(lokinet-extension PRIVATE -e _NSExtensionMain)\n  endif()\n\n  if(MACOS_SYSTEM_EXTENSION)\n      set(bundle_ext systemextension)\n      set(product_type com.apple.product-type.system-extension)\n  else()\n      set(bundle_ext appex)\n      set(product_type com.apple.product-type.app-extension)\n  endif()\n\n  target_link_libraries(lokinet-extension PRIVATE\n    lokinet-core\n    ${COREFOUNDATION}\n    ${NETEXT})\n\n  set_target_properties(lokinet-extension PROPERTIES\n    BUNDLE TRUE\n    BUNDLE_EXTENSION ${bundle_ext}\n    OUTPUT_NAME org.lokinet.network-extension\n    MACOSX_BUNDLE_INFO_PLIST ${PROJECT_SOURCE_DIR}/contrib/macos/lokinet-extension.Info.plist.in\n    XCODE_PRODUCT_TYPE ${product_type}\n    )\n\n  if(CODESIGN AND CODESIGN_EXT_PROFILE)\n    add_custom_command(TARGET lokinet-extension\n      POST_BUILD\n      COMMAND ${CMAKE_COMMAND} -E copy_if_different\n        ${CODESIGN_EXT_PROFILE}\n        $<TARGET_BUNDLE_DIR:lokinet-extension>/Contents/embedded.provisionprofile\n    )\n  endif()\n\nendif()\n"
  },
  {
    "path": "llarp/apple/DNSTrampoline.h",
    "content": "#pragma once\n#include <NetworkExtension/NetworkExtension.h>\n#include <uv.h>\n\nextern NSString* error_domain;\n\n/**\n * \"Trampoline\" class that listens for UDP DNS packets when we have exit mode enabled.  These arrive\n * on localhost:1053 coming from lokinet's embedded libunbound (when exit mode is enabled), wraps\n * them via NetworkExtension's crappy UDP API, then sends responses back to libunbound to be\n * parsed/etc.  This class knows nothing about DNS, it is basically just a UDP packet forwarder, but\n * using Apple magic reinvented wheel wrappers that are oh so wonderful like everything Apple.\n *\n * So for a lokinet configuration of \"upstream=1.1.1.1\", when exit mode is OFF:\n * - DNS requests go unbound either to 127.0.0.1:53 directly (system extension) or bounced through\n *   TUNNELIP:53 (app extension), which forwards them (directly) to the upstream DNS server(s).\n * With exit mode ON:\n * - DNS requests go to unbound, as above, and unbound forwards them to 127.0.0.1:1053, which\n *   encapsulates them in Apple's god awful crap, then (on a response) sends them back to\n *   libunbound to be delivered back to the requestor.\n * (This assumes a non-lokinet DNS; .loki and .snode get handled before either of these).\n */\n@interface LLARPDNSTrampoline : NSObject\n{\n    // The socket libunbound talks with:\n    uv_udp_t request_socket;\n    // The reply address.  This is a bit hacky: we configure libunbound to just use single address\n    // (rather than a range) so that we don't have to worry about tracking different reply\n    // addresses.\n  @public\n    struct sockaddr reply_addr;\n    // UDP \"session\" aimed at the upstream DNS\n  @public\n    NWUDPSession* upstream;\n    // Apple docs say writes could take time *and* the crappy Apple datagram write methods aren't\n    // callable again until the previous write finishes.  Deal with this garbage API by queuing\n    // everything than using a uv_async to process the queue.\n  @public\n    int write_ready;\n  @public\n    NSMutableArray<NSData*>* pending_writes;\n    uv_async_t write_trigger;\n}\n- (void)startWithUpstreamDns:(NWUDPSession*)dns\n                    listenIp:(NSString*)listenIp\n                  listenPort:(uint16_t)listenPort\n                      uvLoop:(uv_loop_t*)loop\n           completionHandler:(void (^)(NSError* error))completionHandler;\n\n- (void)flushWrites;\n\n- (void)dealloc;\n\n@end\n"
  },
  {
    "path": "llarp/apple/DNSTrampoline.m",
    "content": "#include \"DNSTrampoline.h\"\n\n#include <uv.h>\n\nNSString* error_domain = @\"org.lokinet\";\n\n// Receiving an incoming packet, presumably from libunbound.  NB: this is called from the libuv\n// event loop.\nstatic void on_request(\n    uv_udp_t* socket, ssize_t nread, const uv_buf_t* buf, const struct sockaddr* addr, unsigned flags)\n{\n    (void)flags;\n    if (nread < 0)\n    {\n        NSLog(@\"Read error: %s\", uv_strerror(nread));\n        free(buf->base);\n        return;\n    }\n\n    if (nread == 0 || !addr)\n    {\n        if (buf)\n            free(buf->base);\n        return;\n    }\n\n    LLARPDNSTrampoline* t = (__bridge LLARPDNSTrampoline*)socket->data;\n\n    // We configure libunbound to use just one single port so we'll just send replies to the last\n    // port to talk to us.  (And we're only listening on localhost in the first place).\n    t->reply_addr = *addr;\n\n    // NSData takes care of calling free(buf->base) for us with this constructor:\n    [t->pending_writes addObject:[NSData dataWithBytesNoCopy:buf->base length:nread]];\n\n    [t flushWrites];\n}\n\nstatic void on_sent(uv_udp_send_t* req, int status)\n{\n    (void)status;\n    NSArray<NSData*>* datagrams = (__bridge_transfer NSArray<NSData*>*)req->data;\n    (void)datagrams;\n    free(req);\n}\n\n// NB: called from the libuv event loop (so we don't have to worry about the above and this one\n// running at once from different threads).\nstatic void write_flusher(uv_async_t* async)\n{\n    LLARPDNSTrampoline* t = (__bridge LLARPDNSTrampoline*)async->data;\n    if (t->pending_writes.count == 0)\n        return;\n\n    NSArray<NSData*>* data = [NSArray<NSData*> arrayWithArray:t->pending_writes];\n    [t->pending_writes removeAllObjects];\n    __weak LLARPDNSTrampoline* weakSelf = t;\n    [t->upstream writeMultipleDatagrams:data\n                      completionHandler:^(NSError* error) {\n                        if (error)\n                            NSLog(@\"Failed to send request to upstream DNS: %@\", error);\n\n                        // Trigger another flush in case anything built up while Apple was doing its\n                        // things.  Just call it unconditionally (rather than checking the queue)\n                        // because this handler is probably running in some other thread.\n                        [weakSelf flushWrites];\n                      }];\n}\n\nstatic void alloc_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf)\n{\n    (void)handle;\n    buf->base = malloc(suggested_size);\n    buf->len = suggested_size;\n}\n\n@implementation LLARPDNSTrampoline\n\n- (void)startWithUpstreamDns:(NWUDPSession*)dns\n                    listenIp:(NSString*)listenIp\n                  listenPort:(uint16_t)listenPort\n                      uvLoop:(uv_loop_t*)loop\n           completionHandler:(void (^)(NSError* error))completionHandler\n{\n    NSLog(@\"Setting up trampoline\");\n    pending_writes = [[NSMutableArray<NSData*> alloc] init];\n    write_trigger.data = (__bridge void*)self;\n    uv_async_init(loop, &write_trigger, write_flusher);\n\n    request_socket.data = (__bridge void*)self;\n    uv_udp_init(loop, &request_socket);\n    struct sockaddr_in recv_addr;\n    uv_ip4_addr(listenIp.UTF8String, listenPort, &recv_addr);\n    int ret = uv_udp_bind(&request_socket, (const struct sockaddr*)&recv_addr, UV_UDP_REUSEADDR);\n    if (ret < 0)\n    {\n        NSString* errstr = [NSString stringWithFormat:@\"Failed to start DNS trampoline: %s\", uv_strerror(ret)];\n        NSError* err = [NSError errorWithDomain:error_domain code:ret userInfo:@{@\"Error\": errstr}];\n        NSLog(@\"%@\", err);\n        return completionHandler(err);\n    }\n    uv_udp_recv_start(&request_socket, alloc_buffer, on_request);\n\n    NSLog(@\"Starting DNS trampoline\");\n\n    upstream = dns;\n    __weak LLARPDNSTrampoline* weakSelf = self;\n    [upstream\n        setReadHandler:^(NSArray<NSData*>* datagrams, NSError* error) {\n          // Reading a reply back from the UDP socket used to talk to upstream\n          if (error)\n          {\n              NSLog(@\"Reader handler failed: %@\", error);\n              return;\n          }\n          LLARPDNSTrampoline* strongSelf = weakSelf;\n          if (!strongSelf || datagrams.count == 0)\n              return;\n\n          uv_buf_t* buffers = malloc(datagrams.count * sizeof(uv_buf_t));\n          size_t buf_count = 0;\n          for (NSData* packet in datagrams)\n          {\n              buffers[buf_count].base = (void*)packet.bytes;\n              buffers[buf_count].len = packet.length;\n              buf_count++;\n          }\n          uv_udp_send_t* uvsend = malloc(sizeof(uv_udp_send_t));\n          uvsend->data = (__bridge_retained void*)datagrams;\n          int ret =\n              uv_udp_send(uvsend, &strongSelf->request_socket, buffers, buf_count, &strongSelf->reply_addr, on_sent);\n          free(buffers);\n          if (ret < 0)\n              NSLog(@\"Error returning DNS responses to unbound: %s\", uv_strerror(ret));\n        }\n          maxDatagrams:NSUIntegerMax];\n\n    completionHandler(nil);\n}\n\n- (void)flushWrites\n{\n    uv_async_send(&write_trigger);\n}\n\n- (void)dealloc\n{\n    NSLog(@\"Stopping DNS trampoline\");\n    uv_close((uv_handle_t*)&request_socket, NULL);\n    uv_close((uv_handle_t*)&write_trigger, NULL);\n}\n\n@end\n"
  },
  {
    "path": "llarp/apple/PacketTunnelProvider.m",
    "content": "#include \"context_wrapper.h\"\n#include \"DNSTrampoline.h\"\n\n#include <Foundation/Foundation.h>\n#include <NetworkExtension/NetworkExtension.h>\n\n#define LLARP_APPLE_PACKET_BUF_SIZE 64\n\n@interface LLARPPacketTunnel : NEPacketTunnelProvider\n{\n    void* lokinet;\n    llarp_incoming_packet packet_buf[LLARP_APPLE_PACKET_BUF_SIZE];\n  @public\n    NEPacketTunnelNetworkSettings* settings;\n  @public\n    NEIPv4Route* tun_route4;\n  @public\n    NEIPv6Route* tun_route6;\n    LLARPDNSTrampoline* dns_tramp;\n}\n\n- (void)startTunnelWithOptions:(NSDictionary<NSString*, NSObject*>*)options\n             completionHandler:(void (^)(NSError* error))completionHandler;\n\n- (void)stopTunnelWithReason:(NEProviderStopReason)reason completionHandler:(void (^)(void))completionHandler;\n\n- (void)handleAppMessage:(NSData*)messageData completionHandler:(void (^)(NSData* responseData))completionHandler;\n\n- (void)readPackets;\n\n- (void)updateNetworkSettings;\n\n@end\n\nstatic void nslogger(const char* msg) { NSLog(@\"%s\", msg); }\n\nstatic void packet_writer(int af, const void* data, size_t size, void* ctx)\n{\n    if (ctx == nil || data == nil)\n        return;\n\n    NSData* buf = [NSData dataWithBytesNoCopy:(void*)data length:size freeWhenDone:NO];\n    LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*)ctx;\n    [t.packetFlow writePackets:@[buf] withProtocols:@[[NSNumber numberWithInt:af]]];\n}\n\nstatic void start_packet_reader(void* ctx)\n{\n    if (ctx == nil)\n        return;\n\n    LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*)ctx;\n    [t readPackets];\n}\n\nstatic void add_ipv4_route(const char* addr, const char* netmask, void* ctx)\n{\n    NSLog(@\"Adding IPv4 route %s:%s to packet tunnel\", addr, netmask);\n    NEIPv4Route* route = [[NEIPv4Route alloc] initWithDestinationAddress:[NSString stringWithUTF8String:addr]\n                                                              subnetMask:[NSString stringWithUTF8String:netmask]];\n\n    LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*)ctx;\n    for (NEIPv4Route* r in t->settings.IPv4Settings.includedRoutes)\n        if ([r.destinationAddress isEqualToString:route.destinationAddress] &&\n            [r.destinationSubnetMask isEqualToString:route.destinationSubnetMask])\n            return;  // Already in the settings, nothing to add.\n\n    t->settings.IPv4Settings.includedRoutes = [t->settings.IPv4Settings.includedRoutes arrayByAddingObject:route];\n\n    [t updateNetworkSettings];\n}\n\nstatic void del_ipv4_route(const char* addr, const char* netmask, void* ctx)\n{\n    NSLog(@\"Removing IPv4 route %s:%s to packet tunnel\", addr, netmask);\n    NEIPv4Route* route = [[NEIPv4Route alloc] initWithDestinationAddress:[NSString stringWithUTF8String:addr]\n                                                              subnetMask:[NSString stringWithUTF8String:netmask]];\n\n    LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*)ctx;\n    NSMutableArray<NEIPv4Route*>* routes = [NSMutableArray arrayWithArray:t->settings.IPv4Settings.includedRoutes];\n    for (size_t i = 0; i < routes.count; i++)\n    {\n        if ([routes[i].destinationAddress isEqualToString:route.destinationAddress] &&\n            [routes[i].destinationSubnetMask isEqualToString:route.destinationSubnetMask])\n        {\n            [routes removeObjectAtIndex:i];\n            i--;\n        }\n    }\n\n    if (routes.count != t->settings.IPv4Settings.includedRoutes.count)\n    {\n        t->settings.IPv4Settings.includedRoutes = routes;\n        [t updateNetworkSettings];\n    }\n}\n\nstatic void add_ipv6_route(const char* addr, int prefix, void* ctx)\n{\n    NEIPv6Route* route = [[NEIPv6Route alloc] initWithDestinationAddress:[NSString stringWithUTF8String:addr]\n                                                     networkPrefixLength:[NSNumber numberWithInt:prefix]];\n\n    LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*)ctx;\n    for (NEIPv6Route* r in t->settings.IPv6Settings.includedRoutes)\n        if ([r.destinationAddress isEqualToString:route.destinationAddress] &&\n            [r.destinationNetworkPrefixLength isEqualToNumber:route.destinationNetworkPrefixLength])\n            return;  // Already in the settings, nothing to add.\n\n    t->settings.IPv6Settings.includedRoutes = [t->settings.IPv6Settings.includedRoutes arrayByAddingObject:route];\n\n    [t updateNetworkSettings];\n}\n\nstatic void del_ipv6_route(const char* addr, int prefix, void* ctx)\n{\n    NEIPv6Route* route = [[NEIPv6Route alloc] initWithDestinationAddress:[NSString stringWithUTF8String:addr]\n                                                     networkPrefixLength:[NSNumber numberWithInt:prefix]];\n\n    LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*)ctx;\n    NSMutableArray<NEIPv6Route*>* routes = [NSMutableArray arrayWithArray:t->settings.IPv6Settings.includedRoutes];\n    for (size_t i = 0; i < routes.count; i++)\n    {\n        if ([routes[i].destinationAddress isEqualToString:route.destinationAddress] &&\n            [routes[i].destinationNetworkPrefixLength isEqualToNumber:route.destinationNetworkPrefixLength])\n        {\n            [routes removeObjectAtIndex:i];\n            i--;\n        }\n    }\n\n    if (routes.count != t->settings.IPv6Settings.includedRoutes.count)\n    {\n        t->settings.IPv6Settings.includedRoutes = routes;\n        [t updateNetworkSettings];\n    }\n}\n\nstatic void add_default_route(void* ctx)\n{\n    NSLog(@\"Making the tunnel the default route\");\n    LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*)ctx;\n\n    t->settings.IPv4Settings.includedRoutes = @[NEIPv4Route.defaultRoute];\n    t->settings.IPv6Settings.includedRoutes = @[NEIPv6Route.defaultRoute];\n\n    [t updateNetworkSettings];\n}\n\nstatic void del_default_route(void* ctx)\n{\n    NSLog(@\"Removing default route from tunnel\");\n    LLARPPacketTunnel* t = (__bridge LLARPPacketTunnel*)ctx;\n\n    t->settings.IPv4Settings.includedRoutes = @[t->tun_route4];\n    t->settings.IPv6Settings.includedRoutes = @[t->tun_route6];\n\n    [t updateNetworkSettings];\n}\n\n@implementation LLARPPacketTunnel\n\n- (void)readPackets\n{\n    [self.packetFlow readPacketObjectsWithCompletionHandler:^(NSArray<NEPacket*>* packets) {\n      if (lokinet == nil)\n          return;\n\n      size_t size = 0;\n      for (NEPacket* p in packets)\n      {\n          packet_buf[size].bytes = p.data.bytes;\n          packet_buf[size].size = p.data.length;\n          size++;\n          if (size >= LLARP_APPLE_PACKET_BUF_SIZE)\n          {\n              llarp_apple_incoming(lokinet, packet_buf, size);\n              size = 0;\n          }\n      }\n      if (size > 0)\n          llarp_apple_incoming(lokinet, packet_buf, size);\n\n      [self readPackets];\n    }];\n}\n\n- (void)startTunnelWithOptions:(NSDictionary<NSString*, NSObject*>*)options\n             completionHandler:(void (^)(NSError*))completionHandler\n{\n    NSString* default_bootstrap = [NSBundle.mainBundle pathForResource:@\"bootstrap\" ofType:@\"signed\"];\n    NSString* home = NSHomeDirectory();\n\n    llarp_apple_config conf = {\n        .config_dir = home.UTF8String,\n        .default_bootstrap = default_bootstrap.UTF8String,\n        .ns_logger = nslogger,\n        .packet_writer = packet_writer,\n        .start_reading = start_packet_reader,\n        .route_callbacks =\n            {.add_ipv4_route = add_ipv4_route,\n             .del_ipv4_route = del_ipv4_route,\n             .add_ipv6_route = add_ipv6_route,\n             .del_ipv6_route = del_ipv6_route,\n             .add_default_route = add_default_route,\n             .del_default_route = del_default_route},\n    };\n\n    lokinet = llarp_apple_init(&conf);\n    if (!lokinet)\n    {\n        NSError* init_failure = [NSError errorWithDomain:error_domain\n                                                    code:500\n                                                userInfo:@{@\"Error\": @\"Failed to initialize lokinet\"}];\n        NSLog(@\"%@\", [init_failure localizedDescription]);\n        return completionHandler(init_failure);\n    }\n\n    NSString* ip = [NSString stringWithUTF8String:conf.tunnel_ipv4_ip];\n    NSString* mask = [NSString stringWithUTF8String:conf.tunnel_ipv4_netmask];\n\n    // We don't have a fixed address so just stick some bogus value here:\n    settings = [[NEPacketTunnelNetworkSettings alloc] initWithTunnelRemoteAddress:@\"127.3.2.1\"];\n\n#ifdef MACOS_SYSTEM_EXTENSION\n    NSString* dns_ip = [NSString stringWithUTF8String:conf.dns_bind_ip];\n#else\n    // TODO: placeholder\n    NSString* dns_ip = ip;\n#endif\n    NSLog(@\"setting dns to %@\", dns_ip);\n    NEDNSSettings* dns = [[NEDNSSettings alloc] initWithServers:@[dns_ip]];\n    dns.domainName = @\"localhost.loki\";\n    dns.matchDomains = @[@\"\"];\n    // In theory, matchDomains is supposed to be set to DNS suffixes that we resolve.  This seems\n    // highly unreliable, though: often it just doesn't work at all (perhaps only if we make\n    // ourselves the default route?), and even when it does work, it seems there are secret reasons\n    // that some domains (such as instagram.com) still won't work because there's some magic sauce\n    // in the OS that Apple engineers don't want to disclose (\"This is what I expected, actually.\n    // Although I will not comment on what I believe is happening here\", from\n    // https://developer.apple.com/forums/thread/685410).\n    //\n    // So the documentation sucks and the feature doesn't appear to work, so as much as it would be\n    // nice to capture only .loki and .snode when not in exit mode, we can't, so capture everything\n    // and use our default upstream.\n    dns.matchDomains = @[@\"\"];\n    dns.matchDomainsNoSearch = true;\n    dns.searchDomains = @[];\n    settings.DNSSettings = dns;\n\n    NWHostEndpoint* upstreamdns_ep;\n    if (strlen(conf.upstream_dns))\n        upstreamdns_ep = [NWHostEndpoint endpointWithHostname:[NSString stringWithUTF8String:conf.upstream_dns]\n                                                         port:@(conf.upstream_dns_port).stringValue];\n\n    NEIPv4Settings* ipv4 = [[NEIPv4Settings alloc] initWithAddresses:@[ip] subnetMasks:@[mask]];\n    tun_route4 = [[NEIPv4Route alloc] initWithDestinationAddress:ip subnetMask:mask];\n    ipv4.includedRoutes = @[tun_route4];\n    settings.IPv4Settings = ipv4;\n\n    NSString* ip6 = [NSString stringWithUTF8String:conf.tunnel_ipv6_ip];\n    NSNumber* ip6_prefix = [NSNumber numberWithUnsignedInt:conf.tunnel_ipv6_prefix];\n    NEIPv6Settings* ipv6 = [[NEIPv6Settings alloc] initWithAddresses:@[ip6] networkPrefixLengths:@[ip6_prefix]];\n    tun_route6 = [[NEIPv6Route alloc] initWithDestinationAddress:ip6 networkPrefixLength:ip6_prefix];\n    ipv6.includedRoutes = @[tun_route6];\n    settings.IPv6Settings = ipv6;\n\n    __weak LLARPPacketTunnel* weakSelf = self;\n    [self setTunnelNetworkSettings:settings\n                 completionHandler:^(NSError* err) {\n                   if (err)\n                   {\n                       NSLog(@\"Failed to configure lokinet tunnel: %@\", err);\n                       return completionHandler(err);\n                   }\n                   LLARPPacketTunnel* strongSelf = weakSelf;\n                   if (!strongSelf)\n                       return completionHandler(nil);\n\n                   int start_ret = llarp_apple_start(strongSelf->lokinet, (__bridge void*)strongSelf);\n                   if (start_ret != 0)\n                   {\n                       NSError* start_failure = [NSError errorWithDomain:error_domain\n                                                                    code:start_ret\n                                                                userInfo:@{@\"Error\": @\"Failed to start lokinet\"}];\n                       NSLog(@\"%@\", start_failure);\n                       lokinet = nil;\n                       return completionHandler(start_failure);\n                   }\n\n                   NSString* dns_tramp_ip = @\"127.0.0.1\";\n                   NSLog(\n                       @\"Starting DNS exit mode trampoline to %@ on %@:%d\",\n                       upstreamdns_ep,\n                       dns_tramp_ip,\n                       dns_trampoline_port);\n                   NWUDPSession* upstreamdns = [strongSelf createUDPSessionThroughTunnelToEndpoint:upstreamdns_ep\n                                                                                      fromEndpoint:nil];\n                   strongSelf->dns_tramp = [LLARPDNSTrampoline alloc];\n                   [strongSelf->dns_tramp startWithUpstreamDns:upstreamdns\n                                                      listenIp:dns_tramp_ip\n                                                    listenPort:dns_trampoline_port\n                                                        uvLoop:llarp_apple_get_uv_loop(strongSelf->lokinet)\n                                             completionHandler:^(NSError* error) {\n                                               if (error)\n                                                   NSLog(@\"Error starting dns trampoline: %@\", error);\n                                               return completionHandler(error);\n                                             }];\n                 }];\n}\n\n- (void)stopTunnelWithReason:(NEProviderStopReason)reason completionHandler:(void (^)(void))completionHandler\n{\n    if (lokinet)\n    {\n        llarp_apple_shutdown(lokinet);\n        lokinet = nil;\n    }\n    completionHandler();\n}\n\n- (void)handleAppMessage:(NSData*)messageData completionHandler:(void (^)(NSData* responseData))completionHandler\n{\n    NSData* response = [NSData dataWithBytesNoCopy:\"ok\" length:3 freeWhenDone:NO];\n    completionHandler(response);\n}\n\n- (void)updateNetworkSettings\n{\n    self.reasserting = YES;\n    __weak LLARPPacketTunnel* weakSelf = self;\n    // Apple documentation says that setting network settings to nil isn't required before setting\n    // it to a new value.  Apple lies: both end up with a routing table that looks exactly the same\n    // (from both `netstat -rn` and from everything that happens in `route -n monitor`), but if we\n    // don't call with nil first then everything fails to route to either lokinet *and* clearnet\n    // through the exit, so there is apparently some special magic internal Apple state that\n    // actually *does* require the tunnel settings being reset with nil first.\n    //\n    // Thanks for the accurate documentation, Apple.\n    //\n    [self setTunnelNetworkSettings:nil\n                 completionHandler:^(NSError* err) {\n                   if (err)\n                       NSLog(@\"Failed to clear lokinet tunnel settings: %@\", err);\n                   LLARPPacketTunnel* strongSelf = weakSelf;\n                   if (strongSelf)\n                   {\n                       [weakSelf setTunnelNetworkSettings:strongSelf->settings\n                                        completionHandler:^(NSError* err) {\n                                          LLARPPacketTunnel* strongSelf = weakSelf;\n                                          if (strongSelf)\n                                              strongSelf.reasserting = NO;\n                                          if (err)\n                                              NSLog(\n                                                  @\"Failed to reconfigure lokinet tunnel settings: \"\n                                                  @\"%@\",\n                                                  err);\n                                        }];\n                   }\n                 }];\n}\n\n@end\n\n#ifdef MACOS_SYSTEM_EXTENSION\n\nint main()\n{\n    [NEProvider startSystemExtensionMode];\n    dispatch_main();\n}\n\n#endif\n"
  },
  {
    "path": "llarp/apple/context.hpp",
    "content": "#pragma once\n\n#include \"route_manager.hpp\"\n#include \"vpn_platform.hpp\"\n\n#include <llarp.hpp>\n\nnamespace llarp::apple\n{\n    struct Context : public llarp::Context\n    {\n        std::shared_ptr<vpn::Platform> make_vpn_platform() override\n        {\n            return std::make_shared<VPNPlatform>(\n                *this, m_PacketWriter, m_OnReadable, route_callbacks, callback_context);\n        }\n\n        // Callbacks that must be set for packet handling *before* calling Setup/Configure/Run; the\n        // main point of these is to get passed through to VPNInterface, which will be called during\n        // setup, after construction.\n        VPNInterface::packet_write_callback m_PacketWriter;\n        VPNInterface::on_readable_callback m_OnReadable;\n        llarp_route_callbacks route_callbacks{};\n        void* callback_context = nullptr;\n    };\n\n}  // namespace llarp::apple\n"
  },
  {
    "path": "llarp/apple/context_wrapper.cpp",
    "content": "#include \"context_wrapper.h\"\n\n#include \"context.hpp\"\n#include \"vpn_interface.hpp\"\n\n#include <llarp/config/config.hpp>\n#include <llarp/constants/apple.hpp>\n#include <llarp/net/ip_packet.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/logging/buffer.hpp>\n#include <llarp/util/logging/callback_sink.hpp>\n\n#include <cassert>\n#include <cstdint>\n#include <cstring>\n\nnamespace\n{\n    static auto logcat = oxen::log::Cat(\"apple.ctx_wrapper\");\n\n    struct instance_data\n    {\n        llarp::apple::Context context;\n        std::thread runner;\n        packet_writer_callback packet_writer;\n        start_reading_callback start_reading;\n\n        std::weak_ptr<llarp::apple::VPNInterface> iface;\n    };\n\n}  // namespace\n\n// Expose this with C linkage so that objective-c can use it\nextern \"C\" const uint16_t dns_trampoline_port = llarp::apple::dns_trampoline_port;\n\nvoid* llarp_apple_init(llarp_apple_config* appleconf)\n{\n    llarp::log::clear_sinks();\n    llarp::log::add_sink(std::make_shared<llarp::logging::CallbackSink_mt>(\n        [](const char* msg, void* nslog) { reinterpret_cast<ns_logger_callback>(nslog)(msg); },\n        nullptr,\n        reinterpret_cast<void*>(appleconf->ns_logger)));\n    llarp::logRingBuffer = std::make_shared<llarp::log::RingBufferSink>(100);\n    llarp::log::add_sink(llarp::logRingBuffer, llarp::log::DEFAULT_PATTERN_MONO);\n\n    try\n    {\n        auto config_dir = std::filesystem::u8path(appleconf->config_dir);\n        auto config = std::make_shared<llarp::Config>(config_dir);\n        std::filesystem::path config_path = config_dir / \"lokinet.ini\";\n        if (!exists(config_path))\n            llarp::ensure_config(config_dir, config_path, false, llarp::config::Type::FullClient);\n        config->load(config_path);\n\n        // If no range is specified then go look for a free one, set that in the config, and then\n        // return it to the caller via the char* parameters.\n        auto& range = config->network.if_addr;\n        if (!range.addr.h)\n        {\n            if (auto maybe = llarp::net::Platform::Default_ptr()->find_free_range())\n                range = *maybe;\n            else\n                throw std::runtime_error{\"Could not find any free IP range\"};\n        }\n        auto addr = llarp::net::TruncateV6(range.addr).to_string();\n        auto mask = llarp::net::TruncateV6(range.netmask_bits).to_string();\n        if (addr.size() > 15 || mask.size() > 15)\n            throw std::runtime_error{\"Unexpected non-IPv4 tunnel range configured\"};\n        std::strncpy(appleconf->tunnel_ipv4_ip, addr.c_str(), sizeof(appleconf->tunnel_ipv4_ip));\n        std::strncpy(appleconf->tunnel_ipv4_netmask, mask.c_str(), sizeof(appleconf->tunnel_ipv4_netmask));\n\n        // TODO: in the future we want to do this properly with our pubkey (see issue #1705), but\n        // that's going to take a bit more work because we currently can't *get* the (usually)\n        // ephemeral pubkey at this stage of lokinet configuration.  So for now we just stick our\n        // IPv4 address into it until #1705 gets implemented.\n        llarp::huint128_t ipv6{llarp::uint128_t{0xfd2e'6c6f'6b69'0000, llarp::net::TruncateV6(range.addr).h}};\n        std::strncpy(appleconf->tunnel_ipv6_ip, ipv6.to_string().c_str(), sizeof(appleconf->tunnel_ipv6_ip));\n        appleconf->tunnel_ipv6_prefix = 48;\n\n        appleconf->upstream_dns[0] = '\\0';\n        for (auto& upstream : config->dns.upstream_dns)\n        {\n            if (upstream.isIPv4())\n            {\n                std::strcpy(appleconf->upstream_dns, upstream.hostString().c_str());\n                appleconf->upstream_dns_port = upstream.getPort();\n                break;\n            }\n        }\n\n#ifdef MACOS_SYSTEM_EXTENSION\n        std::strncpy(\n            appleconf->dns_bind_ip, config->dns.m_bind.front().hostString().c_str(), sizeof(appleconf->dns_bind_ip));\n#endif\n\n        // If no explicit bootstrap then set the system default one included with the app bundle\n        if (config->bootstrap.files.empty())\n            config->bootstrap.files.push_back(std::filesystem::u8path(appleconf->default_bootstrap));\n\n        auto inst = std::make_unique<instance_data>();\n        inst->context.Configure(std::move(config));\n        inst->context.route_callbacks = appleconf->route_callbacks;\n\n        inst->packet_writer = appleconf->packet_writer;\n        inst->start_reading = appleconf->start_reading;\n\n        return inst.release();\n    }\n    catch (const std::exception& e)\n    {\n        oxen::log::error(logcat, \"Failed to initialize lokinet from config: {}\", e.what());\n    }\n    return nullptr;\n}\n\nint llarp_apple_start(void* lokinet, void* callback_context)\n{\n    auto* inst = static_cast<instance_data*>(lokinet);\n\n    inst->context.callback_context = callback_context;\n\n    inst->context.m_PacketWriter = [inst, callback_context](int af_family, void* data, size_t size) {\n        inst->packet_writer(af_family, data, size, callback_context);\n        return true;\n    };\n\n    inst->context.m_OnReadable = [inst, callback_context](llarp::apple::VPNInterface& iface) {\n        inst->iface = iface.weak_from_this();\n        inst->start_reading(callback_context);\n    };\n\n    std::promise<void> result;\n    inst->runner = std::thread{[inst, &result] {\n        const llarp::RuntimeOptions opts{};\n        try\n        {\n            inst->context.Setup(opts);\n        }\n        catch (...)\n        {\n            result.set_exception(std::current_exception());\n            return;\n        }\n        result.set_value();\n        inst->context.Run(opts);\n    }};\n\n    try\n    {\n        result.get_future().get();\n    }\n    catch (const std::exception& e)\n    {\n        oxen::log::error(logcat, \"Failed to initialize lokinet: {}\", e.what());\n        return -1;\n    }\n\n    return 0;\n}\n\nuv_loop_t* llarp_apple_get_uv_loop(void* lokinet)\n{\n    auto& inst = *static_cast<instance_data*>(lokinet);\n    auto uvw = inst.context.loop->MaybeGetUVWLoop();\n    assert(uvw);\n    return uvw->raw();\n}\n\nint llarp_apple_incoming(void* lokinet, const llarp_incoming_packet* packets, size_t size)\n{\n    auto& inst = *static_cast<instance_data*>(lokinet);\n\n    auto iface = inst.iface.lock();\n    if (!iface)\n        return -1;\n\n    int count = 0;\n    for (size_t i = 0; i < size; i++)\n    {\n        llarp_buffer_t buf{static_cast<const uint8_t*>(packets[i].bytes), packets[i].size};\n        if (iface->OfferReadPacket(buf))\n            count++;\n        else\n            oxen::log::error(logcat, \"invalid IP packet: {}\", llarp::buffer_printer(buf));\n    }\n\n    iface->MaybeWakeUpperLayers();\n    return count;\n}\n\nvoid llarp_apple_shutdown(void* lokinet)\n{\n    auto* inst = static_cast<instance_data*>(lokinet);\n\n    inst->context.CloseAsync();\n    inst->context.Wait();\n    inst->runner.join();\n    delete inst;\n}\n"
  },
  {
    "path": "llarp/apple/context_wrapper.h",
    "content": "#pragma once\n\n// C-linkage wrappers for interacting with a lokinet context, so that we can call them from Swift\n// code (which currently doesn't support C++ interoperability at all).\n\n#ifdef __cplusplus\nextern \"C\"\n{\n#endif\n\n#include <sys/socket.h>\n#include <unistd.h>\n#include <uv.h>\n\n    // Port (on localhost) for our DNS trampoline for bouncing DNS requests through the exit route\n    // when in exit mode.\n    extern const uint16_t dns_trampoline_port;\n\n    /// C callback function for us to invoke when we need to write a packet\n    typedef void (*packet_writer_callback)(int af, const void* data, size_t size, void* ctx);\n\n    /// C callback function to invoke once we are ready to start receiving packets\n    typedef void (*start_reading_callback)(void* ctx);\n\n    /// C callback that bridges things into NSLog\n    typedef void (*ns_logger_callback)(const char* msg);\n\n    /// C callbacks to add/remove specific and default routes to the tunnel\n    typedef void (*llarp_route_ipv4_callback)(const char* addr, const char* netmask, void* ctx);\n    typedef void (*llarp_route_ipv6_callback)(const char* addr, int prefix, void* ctx);\n    typedef void (*llarp_default_route_callback)(void* ctx);\n    typedef struct llarp_route_callbacks\n    {\n        /// Callback invoked to set up an IPv4 range that should be routed through the tunnel\n        /// interface.  Called with the address and netmask.\n        llarp_route_ipv4_callback add_ipv4_route;\n\n        /// Callback invoked to set the tunnel as the default IPv4 route.\n        llarp_default_route_callback add_ipv4_default_route;\n\n        /// Callback invoked to remove a specific range from the tunnel IPv4 routes.  Called with\n        /// the address and netmask.\n        llarp_route_ipv4_callback del_ipv4_route;\n\n        /// Callback invoked to set up an IPv6 range that should be routed through the tunnel\n        /// interface.  Called with the address and netmask.\n        llarp_route_ipv6_callback add_ipv6_route;\n\n        /// Callback invoked to remove a specific range from the tunnel IPv6 routes.  Called with\n        /// the address and netmask.\n        llarp_route_ipv6_callback del_ipv6_route;\n\n        /// Callback invoked to set the tunnel as the default IPv4/IPv6 route.\n        llarp_default_route_callback add_default_route;\n\n        /// Callback invoked to remove the tunnel as the default IPv4/IPv6 route.\n        llarp_default_route_callback del_default_route;\n    } llarp_route_callbacks;\n\n    /// Pack of crap to be passed into llarp_apple_init to initialize\n    typedef struct llarp_apple_config\n    {\n        /// lokinet configuration directory, expected to be the application-specific \"home\"\n        /// directory, which is where state files are stored and the lokinet.ini will be loaded (or\n        /// created if it doesn't exist).\n        const char* config_dir;\n        /// path to the default bootstrap.signed file included in installation, which will be used\n        /// by default when no specific bootstrap is in the config file.\n        const char* default_bootstrap;\n        /// llarp_apple_init writes the IP address for the primary tunnel IP address here,\n        /// null-terminated.\n        char tunnel_ipv4_ip[INET_ADDRSTRLEN];\n        /// llarp_apple_init writes the netmask of the tunnel address here, null-terminated.\n        char tunnel_ipv4_netmask[INET_ADDRSTRLEN];\n        /// Writes the IPv6 address for the tunnel here, null-terminated.\n        char tunnel_ipv6_ip[INET6_ADDRSTRLEN];\n        /// IPv6 address prefix.\n        uint16_t tunnel_ipv6_prefix;\n\n        /// The first upstream DNS server's IPv4 address the OS should use when in exit mode.\n        /// (Currently on mac in exit mode we only support querying the first such configured\n        /// server).\n        char upstream_dns[INET_ADDRSTRLEN];\n        uint16_t upstream_dns_port;\n\n#ifdef MACOS_SYSTEM_EXTENSION\n        /// DNS bind IP; llarp_apple_init writes the lokinet config value here so that we know (in\n        /// Apple API code) what to set DNS to when lokinet gets turned on.  Null terminated.\n        char dns_bind_ip[INET_ADDRSTRLEN];\n#endif\n\n        /// \\defgroup callbacks Callbacks\n        /// Callbacks we invoke for various operations that require glue into the Apple network\n        /// extension APIs.  All of these except for ns_logger are passed the pointer provided to\n        /// llarp_apple_start when invoked.\n        /// @{\n\n        /// simple wrapper around NSLog for lokinet message logging\n        ns_logger_callback ns_logger;\n\n        /// C function callback that will be called when we need to write a packet to the packet\n        /// tunnel.  Will be passed AF_INET or AF_INET6, a void pointer to the data, and the size of\n        /// the data in bytes.\n        packet_writer_callback packet_writer;\n\n        /// C function callback that will be called when lokinet is setup and ready to start\n        /// receiving packets from the packet tunnel.  This should set up the read handler to\n        /// deliver packets via llarp_apple_incoming.\n        start_reading_callback start_reading;\n\n        /// Callbacks invoked to add/remove routes to the tunnel.\n        llarp_route_callbacks route_callbacks;\n\n        /// @}\n    } llarp_apple_config;\n\n    /// Initializes a lokinet instance by initializing various objects and loading the configuration\n    /// (if <config_dir>/lokinet.ini exists).  Does not actually start lokinet (call\n    /// llarp_apple_start for that).\n    ///\n    /// Returns NULL if there was a problem initializing/loading the configuration, otherwise\n    /// returns an opaque void pointer that should be passed into the other llarp_apple_* functions.\n    ///\n    /// \\param config pointer to a llarp_apple_config where we get the various settings needed\n    /// and return the ip/mask/dns fields needed for the tunnel.\n    void* llarp_apple_init(llarp_apple_config* config);\n\n    /// Starts the lokinet instance in a new thread.\n    ///\n    /// \\param lokinet the void pointer returned by llarp_apple_init\n    ///\n    /// \\param callback_context Opaque pointer that is passed into the various callbacks provided to\n    /// llarp_apple_init.  This code does nothing with this pointer aside from passing it through to\n    /// callbacks.\n    ///\n    /// \\returns 0 on succesful startup, -1 on failure.\n    int llarp_apple_start(void* lokinet, void* callback_context);\n\n    /// Returns a pointer to the uv event loop.  Must have called llarp_apple_start already.\n    uv_loop_t* llarp_apple_get_uv_loop(void* lokinet);\n\n    /// Struct of packet data; a C array of tests gets passed to llarp_apple_incoming\n    typedef struct llarp_incoming_packet\n    {\n        const void* bytes;\n        size_t size;\n    } llarp_incoming_packet;\n\n    /// Called to deliver one or more incoming packets from the apple layer into lokinet.  Takes a C\n    /// array of `llarp_incoming_packets` with pointers/sizes set to the individual new packets that\n    /// have arrived.\n    ///\n    /// Returns the number of valid packets on success (which can be less than the number of\n    /// provided packets, if some failed to parse), or -1 if there is no current active VPNInterface\n    /// associated with the lokinet instance (which generally means llarp_apple_start wasn't called\n    /// or failed, or lokinet is in the process of shutting down).\n    int llarp_apple_incoming(void* lokinet, const llarp_incoming_packet* packets, size_t size);\n\n    /// Stops a lokinet instance created with `llarp_apple_initialize`.  This waits for lokinet to\n    /// shut down and rejoins the thread.  After this call the given pointer is no longer valid.\n    void llarp_apple_shutdown(void* lokinet);\n\n#ifdef __cplusplus\n}  // extern \"C\"\n#endif\n"
  },
  {
    "path": "llarp/apple/route_manager.cpp",
    "content": "#include \"route_manager.hpp\"\n\n#include <llarp.hpp>\n#include <llarp/handlers/tun.hpp>\n\n#include <memory>\n\nnamespace llarp::apple\n{\n    static auto logcat = log::Cat(\"apple.route_manager\");\n\n    void RouteManager::check_trampoline(bool enable)\n    {\n        if (trampoline_active == enable)\n            return;\n        auto router = context.router;\n        if (!router)\n        {\n            log::error(logcat, \"Cannot reconfigure to use DNS trampoline: no router\");\n            return;\n        }\n\n        auto& tun = router->tun_endpoint();\n        if (!tun)\n        {\n            log::error(logcat, \"Cannot reconfigure to use DNS trampoline: no tun endpoint found (!?)\");\n            return;\n        }\n\n        if (enable)\n            tun.reconfigure_dns({oxen::quic::Address{\"127.0.0.1\", dns_trampoline_port}});\n        else\n            tun->reconfigure_dns(router->config()->dns._upstream_dns);\n\n        trampoline_active = enable;\n    }\n\n    void RouteManager::add_default_route_via_interface(vpn::NetworkInterface&)\n    {\n        check_trampoline(true);\n        if (callback_context and route_callbacks.add_default_route)\n            route_callbacks.add_default_route(callback_context);\n    }\n\n    void RouteManager::delete_default_route_via_interface(vpn::NetworkInterface&)\n    {\n        check_trampoline(false);\n        if (callback_context and route_callbacks.del_default_route)\n            route_callbacks.del_default_route(callback_context);\n    }\n\n    void RouteManager::add_route_via_interface(vpn::NetworkInterface&, ipv4_range range)\n    {\n        check_trampoline(true);\n        if (callback_context)\n        {\n            if (route_callbacks.add_ipv4_route)\n                route_callbacks.add_ipv4_route(\n                    range.BaseAddressString().c_str(), std::string{range.mask}.c_str(), callback_context);\n        }\n    }\n\n    void RouteManager::add_route_via_interface(vpn::NetworkInterface&, ipv6_range range)\n    {\n        check_trampoline(true);\n        if (callback_context)\n        {\n            if (route_callbacks.add_ipv6_route)\n                route_callbacks.add_ipv6_route(range.BaseAddressString().c_str(), range.mask, callback_context);\n        }\n    }\n\n    void RouteManager::delete_route_via_interface(vpn::NetworkInterface&, ipv4_range range)\n    {\n        check_trampoline(false);\n        if (callback_context)\n        {\n            if (route_callbacks.del_ipv4_route)\n                route_callbacks.del_ipv4_route(\n                    range.ip.to_string().c_str(), std::string{range.mask}.c_str(), callback_context);\n        }\n    }\n\n    void RouteManager::delete_route_via_interface(vpn::NetworkInterface&, ipv6_range range)\n    {\n        check_trampoline(false);\n        if (callback_context)\n        {\n            if (route_callbacks.del_ipv6_route)\n                route_callbacks.del_ipv6_route(range.ip.to_string().c_str(), range.mask, callback_context);\n        }\n    }\n\n}  // namespace llarp::apple\n"
  },
  {
    "path": "llarp/apple/route_manager.hpp",
    "content": "#pragma once\n\n#include \"context_wrapper.h\"\n\n#include <llarp/router/router.hpp>\n#include <llarp/vpn/platform.hpp>\n\nnamespace llarp::apple\n{\n    class RouteManager final : public llarp::vpn::AbstractRouteManager\n    {\n      public:\n        RouteManager(llarp::Context& ctx, llarp_route_callbacks rcs, void* callback_context)\n            : context{ctx}, callback_context{callback_context}, route_callbacks{std::move(rcs)}\n        {}\n\n        /// These are called for poking route holes, but we don't have to do that at all on macos\n        /// because the appex isn't subject to its own rules.\n        void add_route(quic::Address /*ip*/, quic::Address /*gateway*/) override {}\n\n        void delete_route(quic::Address /*ip*/, quic::Address /*gateway*/) override {}\n\n        void add_default_route_via_interface(vpn::NetworkInterface& vpn) override;\n\n        void delete_default_route_via_interface(vpn::NetworkInterface& vpn) override;\n\n        void add_route_via_interface(vpn::NetworkInterface& vpn, ipv4_range range) override;\n        void add_route_via_interface(vpn::NetworkInterface& vpn, ipv6_range range) override;\n\n        void delete_route_via_interface(vpn::NetworkInterface& vpn, ipv4_range range) override;\n        void delete_route_via_interface(vpn::NetworkInterface& vpn, ipv6_range range) override;\n\n        std::vector<quic::Address> get_non_interface_gateways(vpn::NetworkInterface& /*vpn*/) override\n        {\n            // We can't get this on mac from our sandbox, but we don't actually need it because we\n            // ignore the gateway for AddRoute/DelRoute anyway, so just return a zero IP.\n            return std::vector<quic::Address>{};\n        }\n\n      private:\n        llarp::Context& context;\n        bool trampoline_active = false;\n        void check_trampoline(bool enable);\n\n        void* callback_context = nullptr;\n        llarp_route_callbacks route_callbacks;\n    };\n\n}  // namespace llarp::apple\n"
  },
  {
    "path": "llarp/apple/vpn_interface.cpp",
    "content": "\n#include \"vpn_interface.hpp\"\n\n#include \"context.hpp\"\n\n#include <llarp/router/router.hpp>\n\nnamespace llarp::apple\n{\n    VPNInterface::VPNInterface(\n        Context& ctx, packet_write_callback packet_writer, on_readable_callback on_readable, Router* router)\n        : vpn::NetworkInterface{},\n          _pkt_writer{std::move(packet_writer)},\n          _on_readable{std::move(on_readable)},\n          _router{router}\n    {\n        ctx._loop->call_soon([this] { _on_readable(*this); });\n    }\n\n    bool VPNInterface::OfferReadPacket(const llarp_buffer_t& buf)\n    {\n        IPPacket pkt;\n        if (!pkt.load(buf.copy()))\n            return false;\n        _read_que.tryPushBack(std::move(pkt));\n        return true;\n    }\n\n    void VPNInterface::MaybeWakeUpperLayers() const\n    {\n        //\n    }\n\n    int VPNInterface::PollFD() const { return -1; }\n\n    IPPacket VPNInterface::read_next_packet()\n    {\n        IPPacket pkt{};\n        if (not _read_que.empty())\n            pkt = _read_que.popFront();\n        return pkt;\n    }\n\n    bool VPNInterface::write_packet(IPPacket pkt)\n    {\n        // TODO: replace this with IPPacket::to_udp\n        (void)pkt;\n        // int af_family = pkt() ? AF_INET6 : AF_INET;\n        // return _pkt_writer(af_family, pkt.data(), pkt.size());\n        return true;\n    }\n\n}  // namespace llarp::apple\n"
  },
  {
    "path": "llarp/apple/vpn_interface.hpp",
    "content": "#pragma once\n\n#include <llarp.hpp>\n#include <llarp/util/thread/queue.hpp>\n#include <llarp/vpn/platform.hpp>\n\n#include <memory>\n\nnamespace llarp::apple\n{\n    struct Context;\n\n    class VPNInterface final : public vpn::NetworkInterface, public std::enable_shared_from_this<VPNInterface>\n    {\n      public:\n        using packet_write_callback = std::function<bool(int af_family, void* data, int size)>;\n        using on_readable_callback = std::function<void(VPNInterface&)>;\n\n        explicit VPNInterface(\n            Context& ctx, packet_write_callback packet_writer, on_readable_callback on_readable, Router* router);\n\n        // Method to call when a packet has arrived to deliver the packet to lokinet\n        bool OfferReadPacket(const llarp_buffer_t& buf);\n\n        int PollFD() const override;\n\n        IPPacket read_next_packet() override;\n\n        bool write_packet(IPPacket pkt) override;\n\n        void MaybeWakeUpperLayers() const override;\n\n      private:\n        // Function for us to call when we have a packet to emit.  Should return true if the packet\n        // was handed off to the OS successfully.\n        packet_write_callback _pkt_writer;\n\n        // Called when we are ready to start reading packets\n        on_readable_callback _on_readable;\n\n        inline static constexpr auto PacketQueueSize = 1024;\n\n        thread::Queue<IPPacket> _read_que{PacketQueueSize};\n\n        Router* const _router;\n    };\n\n}  // namespace llarp::apple\n"
  },
  {
    "path": "llarp/apple/vpn_platform.cpp",
    "content": "#include \"vpn_platform.hpp\"\n\n#include \"context.hpp\"\n\nnamespace llarp::apple\n{\n    VPNPlatform::VPNPlatform(\n        Context& ctx,\n        VPNInterface::packet_write_callback packet_writer,\n        VPNInterface::on_readable_callback on_readable,\n        llarp_route_callbacks route_callbacks,\n        void* callback_context)\n        : _context{ctx},\n          _route_manager{ctx, std::move(route_callbacks), callback_context},\n          _packet_writer{std::move(packet_writer)},\n          _read_cb{std::move(on_readable)}\n    {}\n\n    std::shared_ptr<vpn::NetworkInterface> VPNPlatform::obtain_interface(vpn::InterfaceInfo, Router* router)\n    {\n        return std::make_shared<VPNInterface>(_context, _packet_writer, _read_cb, router);\n    }\n}  // namespace llarp::apple\n"
  },
  {
    "path": "llarp/apple/vpn_platform.hpp",
    "content": "#pragma once\n\n#include \"route_manager.hpp\"\n#include \"vpn_interface.hpp\"\n\n#include <llarp/vpn/platform.hpp>\n\nnamespace llarp::apple\n{\n    class VPNPlatform final : public vpn::Platform\n    {\n      public:\n        explicit VPNPlatform(\n            Context& ctx,\n            VPNInterface::packet_write_callback packet_writer,\n            VPNInterface::on_readable_callback on_readable,\n            llarp_route_callbacks route_callbacks,\n            void* callback_context);\n\n        std::shared_ptr<vpn::NetworkInterface> obtain_interface(vpn::InterfaceInfo, Router*) override;\n\n        vpn::AbstractRouteManager& RouteManager() override { return _route_manager; }\n\n      private:\n        Context& _context;\n        apple::RouteManager _route_manager;\n        VPNInterface::packet_write_callback _packet_writer;\n        VPNInterface::on_readable_callback _read_cb;\n    };\n\n}  // namespace llarp::apple\n"
  },
  {
    "path": "llarp/auth/auth.cpp",
    "content": "#include \"auth.hpp\"\n\n#include <oxenmq/oxenmq.h>\n\nnamespace llarp::auth\n{\n    static const std::unordered_map<std::string_view, AuthCode> codes = {\n        {\"OKAY\"sv, AuthCode::ACCEPTED},\n        {\"REJECT\"sv, AuthCode::REJECTED},\n        {\"PAYME\"sv, AuthCode::PAYMENT_REQUIRED},\n        {\"LIMITED\"sv, AuthCode::RATE_LIMIT}};\n\n    /// maybe get auth result from string\n    std::optional<AuthCode> parse_code(std::string_view data)\n    {\n        if (auto it = codes.find(data); it != codes.end())\n            return it->second;\n        return std::nullopt;\n    }\n\n    static const std::unordered_map<std::string_view, AuthType> types = {\n        {\"file\"sv, AuthType::FILE},\n        {\"lmq\"sv, AuthType::OMQ},\n        {\"whitelist\"sv, AuthType::WHITELIST},\n        {\"none\"sv, AuthType::NONE}};\n\n}  // namespace llarp::auth\n"
  },
  {
    "path": "llarp/auth/auth.hpp",
    "content": "#pragma once\n\n#include <llarp/address/address.hpp>\n#include <llarp/contact/router_id.hpp>\n#include <llarp/crypto/types.hpp>\n#include <llarp/util/str.hpp>\n#include <llarp/util/thread/threading.hpp>\n\n#include <functional>\n#include <optional>\n#include <string>\n#include <unordered_set>\n\nnamespace llarp\n{\n    class Router;\n}\nnamespace llarp::auth\n{\n    /// authentication status code\n    enum class AuthCode : uint64_t\n    {\n        /// explicitly accepted\n        ACCEPTED = 0,\n        /// explicitly rejected\n        REJECTED = 1,\n        /// attempt failed\n        FAILED = 2,\n        /// attempt rate limited\n        RATE_LIMIT = 3,\n        /// need mo munny\n        PAYMENT_REQUIRED = 4\n    };\n\n    /// auth result object with code and reason\n    struct AuthResult\n    {\n        AuthCode code;\n        std::string reason;\n    };\n\n    /// info needed by clients in order to authenticate to a remote endpoint\n    struct AuthInfo\n    {\n        std::string token;\n    };\n\n    /// what kind of backend to use for auth\n    enum class AuthType\n    {\n        /// no authentication\n        NONE,\n        /// manual whitelist\n        WHITELIST,\n        /// OMQ server\n        OMQ,\n        /// static file\n        FILE,\n    };\n\n    struct AuthPolicy\n    {\n      protected:\n        Router& _router;\n\n      public:\n        AuthPolicy(Router& r) : _router{r} {}\n\n        virtual ~AuthPolicy() = default;\n\n        const Router& router() const { return _router; }\n\n        Router& router() { return _router; }\n    };\n\n    /// maybe get auth result from string\n    std::optional<AuthCode> parse_code(std::string_view data);\n\n}  // namespace llarp::auth\n"
  },
  {
    "path": "llarp/auth/file.cpp",
    "content": "#include \"auth.hpp\"\n\n#include <llarp/router/router.hpp>\n#include <llarp/util/str.hpp>\n\nnamespace llarp::auth\n{\n    AuthResult FileAuthPolicy::check_files(const AuthInfo& info) const\n    {\n        for (const auto& f : _files)\n        {\n            std::ifstream i{f};\n            std::string line{};\n            while (std::getline(i, line))\n            {\n                // split off comments\n                const auto parts = split_any(line, \"#;\", true);\n                if (auto part = parts[0]; not parts.empty() and not parts[0].empty())\n                {\n                    // split off whitespaces and check password\n                    if (check_passwd(std::string{TrimWhitespace(part)}, info.token))\n                        return AuthResult{AuthCode::ACCEPTED, \"accepted by whitelist\"};\n                }\n            }\n        }\n        return AuthResult{AuthCode::REJECTED, \"rejected by whitelist\"};\n    }\n\n    bool FileAuthPolicy::check_passwd(std::string hash, std::string challenge) const\n    {\n        switch (_type)\n        {\n            case AuthFileType::PLAIN:\n                return hash == challenge;\n            case AuthFileType::HASHES:\n#ifdef LOKINET_HAVE_CRYPT\n                return crypto::check_passwd_hash(std::move(hash), std::move(challenge));\n#else\n                return false;\n#endif\n        }\n        return false;\n    }\n\n}  // namespace llarp::auth\n"
  },
  {
    "path": "llarp/auth/file.hpp",
    "content": "#pragma once\n\n#include \"auth.hpp\"\n\n#include <filesystem>\n#include <functional>\n#include <optional>\n#include <string>\n#include <unordered_set>\n#include <vector>\n\nnamespace llarp\n{\n    class Router;\n}\nnamespace llarp::auth\n{\n    /// how to interpret an file for auth\n    enum class AuthFileType\n    {\n        PLAIN,\n        HASHES,\n    };\n\n    struct FileAuthPolicy final : public AuthPolicy\n    {\n        FileAuthPolicy(Router& r, std::vector<std::filesystem::path> files, AuthFileType filetype)\n            : AuthPolicy{r}, _files{std::move(files)}, _type{filetype}\n        {}\n\n      private:\n        const std::vector<std::filesystem::path> _files;\n        const AuthFileType _type;\n        mutable util::Mutex _m;\n        /// returns an auth result for a auth info challange, opens every file until it finds a\n        /// token matching it this is expected to be done in the IO thread\n        AuthResult check_files(const AuthInfo& info) const;\n\n        bool check_passwd(std::string hash, std::string challenge) const;\n    };\n\n}  // namespace llarp::auth\n"
  },
  {
    "path": "llarp/auth/rpc.cpp",
    "content": "#include \"rpc.hpp\"\n\n#include <llarp/router/router.hpp>\n\n#include <oxenmq/oxenmq.h>\n\nnamespace llarp::auth\n{\n    static auto logcat = log::Cat(\"rpc.auth\");\n\n    RPCAuthPolicy::RPCAuthPolicy(Router& r, std::string url, std::string method, oxenmq::OxenMQ& omq)\n        : AuthPolicy{r},\n          _endpoint{std::move(url)},\n          _method{std::move(method)},\n          //   _whitelist{std::move(whitelist_addrs)},\n          //   _static_tokens{std::move(whitelist_tokens)},\n          _omq{omq}\n    {\n        if (_endpoint.empty() or _method.empty())\n            throw std::invalid_argument{\n                \"RPC AuthPolicy must be initialized with an endpoint to query and a method to invoke!\"};\n    }\n\n    RPCAuthPolicy::~RPCAuthPolicy() = default;\n\n    void RPCAuthPolicy::start()\n    {\n        _omq.connect_remote(\n            _endpoint,\n            [this](oxenmq::ConnectionID c) {\n                _omq_conn = std::make_unique<oxenmq::ConnectionID>(std::move(c));\n                log::info(logcat, \"OMQ connected to endpoint auth server\");\n            },\n            [this](oxenmq::ConnectionID, std::string_view fail) {\n                log::warning(logcat, \"OMQ failed to connect to endpoint auth server: {}\", fail);\n                _router.loop.call_later(1s, [this] { start(); });\n            });\n    }\n\n}  // namespace llarp::auth\n"
  },
  {
    "path": "llarp/auth/rpc.hpp",
    "content": "#pragma once\n\n#include \"auth.hpp\"\n\n#include <memory>\n#include <string>\n#include <unordered_set>\n\nnamespace oxenmq\n{\n    class OxenMQ;\n    struct ConnectionID;\n}  // namespace oxenmq\n\nnamespace llarp::auth\n{\n    struct RPCAuthPolicy final : public AuthPolicy\n    {\n        explicit RPCAuthPolicy(Router& r, std::string url, std::string method, oxenmq::OxenMQ& omq);\n\n        ~RPCAuthPolicy() override;\n\n        void start();\n\n      private:\n        const std::string _endpoint;\n        const std::string _method;\n        // const std::unordered_set<NetworkAddress> _whitelist;\n        // const std::unordered_set<std::string> _static_tokens;\n\n        oxenmq::OxenMQ& _omq;\n        std::unique_ptr<oxenmq::ConnectionID> _omq_conn;\n        std::unordered_set<session_tag> _pending_sessions;\n    };\n\n}  // namespace llarp::auth\n"
  },
  {
    "path": "llarp/auth/session.cpp",
    "content": "#include \"session.hpp\"\n\n#include <llarp/crypto/key_manager.hpp>\n#include <llarp/router/router.hpp>\n\nnamespace llarp::auth\n{\n    static auto logcat = log::Cat(\"auth_policy\");\n\n    SessionAuthPolicy::SessionAuthPolicy(Router& r, RouterID& remote, bool is_snode, bool is_exit)\n        : AuthPolicy{r}, _is_snode_service{is_snode}, _is_exit_service{is_exit}, _remote{remote, not _is_snode_service}\n    {\n        // These can both be false but CANNOT both be true\n        if (_is_exit_service and _is_snode_service)\n            throw std::runtime_error{\"Cannot create SessionAuthPolicy for a remote exit and remote service!\"};\n\n        if (_is_snode_service)\n            _session_key = _router.secret_key();\n        else\n            _session_key = crypto::generate_ed25519();\n    }\n\n    std::optional<std::string_view> SessionAuthPolicy::fetch_auth_token()\n    {\n        std::optional<std::string_view> ret = std::nullopt;\n        auto& exit_auths = _router.config().network.exit_auths;\n\n        if (auto itr = exit_auths.find(_remote); itr != exit_auths.end())\n            ret = itr->second;\n\n        return ret;\n    }\n\n    bool SessionAuthPolicy::load_key_from_file(const char* fname)\n    {\n        try\n        {\n            KeyManager::load_from_file(_session_key, fname);\n            return true;\n        }\n        catch (const std::exception& e)\n        {\n            log::error(logcat, \"Failed to load secret key from {}: {}\", fname, e.what());\n        }\n        return false;\n    }\n\n}  // namespace llarp::auth\n"
  },
  {
    "path": "llarp/auth/session.hpp",
    "content": "#pragma once\n\n#include \"auth.hpp\"\n\nnamespace llarp\n{\n    class Router;\n}\nnamespace llarp::auth\n{\n    struct SessionAuthPolicy final : public AuthPolicy\n    {\n      private:\n        const bool _is_snode_service{false};\n        const bool _is_exit_service{false};\n\n        Ed25519SecretKey _session_key;\n        NetworkAddress _remote;\n\n      public:\n        SessionAuthPolicy(Router& r, RouterID& remote, bool is_snode, bool is_exit = false);\n\n        bool load_key_from_file(const char* fname);\n\n        std::optional<std::string_view> fetch_auth_token();\n\n        const Ed25519SecretKey& session_key() const { return _session_key; }\n\n        bool is_snode_service() const { return _is_snode_service; }\n\n        bool is_exit_service() const { return _is_exit_service; }\n    };\n\n}  // namespace llarp::auth\n"
  },
  {
    "path": "llarp/config/config.cpp",
    "content": "#include \"config.hpp\"\n\n#include \"definition.hpp\"\n#include \"ini.hpp\"\n\n#include <llarp/constants/path.hpp>\n#include <llarp/constants/platform.hpp>\n#include <llarp/constants/version.hpp>\n#include <llarp/contact/sns.hpp>\n#include <llarp/path/path_handler.hpp>\n#include <llarp/util/file.hpp>\n#include <llarp/util/formattable.hpp>\n#include <llarp/util/logging/buffer.hpp>\n\n#include <filesystem>\n#include <stdexcept>\n\n#ifndef LOKINET_EMBEDDED_ONLY\n#include <oxenmq/address.h>\n#endif\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"config\");\n\n    static bool check_path_op(std::optional<std::filesystem::path>& path)\n    {\n        if (not path.has_value())\n        {\n            log::info(logcat, \"Path input failed to parse...\");\n        }\n        else if (path->empty())\n        {\n            log::warning(logcat, \"Path contents ({}) empty...\", path->c_str());\n            path.reset();\n        }\n        else\n        {\n            log::debug(logcat, \"Valid path parsed ({})\", path->c_str());\n            return true;\n        }\n\n        return false;\n    }\n\n    using namespace config;\n\n    const llarp::net::Platform* ConfigGenParameters::net_ptr()\n    {\n#ifndef LOKINET_EMBEDDED_ONLY\n        if (type != config::Type::EmbeddedClient)\n            return llarp::net::Platform::Default_ptr();\n#endif\n        return nullptr;\n    }\n\n    static auto public_ip_loader(\n        std::optional<quic::Address>& into, std::string conf_name, std::string deprecated_for = \"\"s)\n    {\n        return [&into, conf_name = std::move(conf_name), deprecated_for = std::move(deprecated_for)](std::string ip) {\n            if (!deprecated_for.empty())\n                log::warning(logcat, \"{} is deprecated; use {} instead\", conf_name, deprecated_for);\n            try\n            {\n                quic::Address a{ip, into ? into->port() : uint16_t{0}};\n\n                if (!a.is_ipv4())\n                    throw std::invalid_argument{\"IP must be an IPv4 address\"};\n                if (!a.is_public_ip())\n                    throw std::invalid_argument{\"IP is not public\"};\n\n                into = std::move(a);\n            }\n            catch (const std::exception& e)\n            {\n                throw std::invalid_argument{\"Invalid {}: {}\"_format(conf_name, e.what())};\n            }\n        };\n    }\n    static auto public_port_loader(\n        std::optional<quic::Address>& into, std::string conf_name, std::string deprecated_for = \"\"s)\n    {\n        return [&into, conf_name = std::move(conf_name), deprecated_for = std::move(deprecated_for)](uint16_t port) {\n            if (!deprecated_for.empty())\n                log::warning(logcat, \"{} is deprecated; use {} instead\", conf_name, deprecated_for);\n            if (port == 0)\n                throw std::invalid_argument{\"{} cannot be 0\"_format(conf_name)};\n            if (!into)\n                into.emplace();\n            into->set_port(port);\n        };\n    }\n\n    void RouterConfig::define_config_options(ConfigDefinition& conf, const ConfigGenParameters& params)\n    {\n        conf.add_section_comments(\n            \"router\",\n            {\n                \"Configuration for routing activity.\",\n            });\n\n        conf.define_option<int>(\"router\", \"job-queue-size\", Default{1024 * 8}, Hidden, [this](int arg) {\n            if (arg < 1024)\n                throw std::invalid_argument(\"job-queue-size must be 1024 or greater\");\n\n            job_que_size = arg;\n        });\n\n        conf.define_option<std::string>(\n            \"router\",\n            \"netid\",\n            Default{\"{}\"_format(NetID::MAINNET)},\n            Comment{\"Network ID; this is '{}' for mainnet, '{}' for testnet.\"_format(NetID::MAINNET, NetID::TESTNET)},\n            [this](std::string arg) { net_id = netid_from_string(arg); });\n\n        conf.define_option<int>(\"router\", \"relay-connections\", Deprecated);\n\n        conf.define_option<int>(\"router\", \"min-connections\", Deprecated);\n\n        conf.define_option<int>(\"router\", \"max-connections\", Deprecated);\n\n        conf.define_option<std::string>(\"router\", \"nickname\", Deprecated);\n\n        conf.define_option<std::filesystem::path>(\n            \"router\",\n            \"data-dir\",\n            Default{params.default_data_dir},\n            Comment{\n                \"Optional directory for containing lokinet runtime data. This includes generated\",\n                \"private keys.\",\n            },\n            [this](std::filesystem::path arg) {\n                if (arg.empty())\n                    arg = std::filesystem::path{\".\"};\n                if (not exists(arg))\n                    throw std::runtime_error{\"Specified [router]:data-dir {} does not exist\"_format(arg)};\n\n                data_dir = std::move(arg);\n            });\n\n        conf.define_option<std::string>(\n            \"router\",\n            \"public-ip\",\n            RelayOnly,\n            Comment{\n                \"For complex network configurations where the detected IP is incorrect or non-public\",\n                \"this setting specifies the public IPv4 address at which this router reachable.\",\n            },\n            public_ip_loader(public_addr, \"[router]:public-address\"));\n\n        conf.define_option<std::string>(\"router\", \"public-address\", Hidden, [](std::string) {\n            throw std::invalid_argument{\n                \"[router]:public-address option no longer supported, use [router]:public-ip and \"\n                \"[router]:public-port instead\"};\n        });\n\n        conf.define_option<uint16_t>(\n            \"router\",\n            \"public-port\",\n            RelayOnly,\n            Comment{\n                \"When specifying public-ip=, this specifies the public UDP port at which this lokinet\",\n                \"router is reachable. Defaults to the [bind]:listen port when public-ip is specified.\",\n            },\n            public_port_loader(public_addr, \"[router]:public-port\"));\n\n        conf.add_options_validator([this] {\n            if (public_addr and not public_addr->is_public_ip())\n                throw std::invalid_argument{\"[router]:public-ip is required when specifying [router]:public-port\"};\n        });\n\n        // FIXME: this option isn't currently used!\n        conf.define_option<int>(\n            \"router\",\n            \"worker-threads\",\n            Default{0},\n            Comment{\n                \"The number of threads available for performing cryptographic functions.\",\n                \"The minimum is one thread, but network performance may increase with more.\",\n                \"threads. Should not exceed the number of logical CPU cores.\",\n                \"0 means use the number of logical CPU cores detected at startup.\",\n            },\n            [this](int arg) {\n                if (arg < 0)\n                    throw std::invalid_argument(\"worker-threads must be >= 0\");\n\n                worker_threads = arg;\n            });\n\n        // Hidden option because this isn't something that should ever be turned off occasionally\n        // when doing dev/testing work.\n        conf.define_option<bool>(\"router\", \"block-bogons\", Default{true}, Hidden, assignment_acceptor(block_bogons));\n\n        conf.define_option<std::string>(\"router\", \"contact-file\", Deprecated);\n\n        conf.define_option<std::string>(\"router\", \"encryption-privkey\", Deprecated);\n\n        conf.define_option<std::string>(\"router\", \"ident-privkey\", Deprecated);\n\n        conf.define_option<std::string>(\"router\", \"transport-privkey\", RelayOnly, Deprecated);\n\n        // Deprecated options:\n\n        // these weren't even ever used!\n        conf.define_option<std::string>(\"router\", \"max-routers\", Deprecated);\n        conf.define_option<std::string>(\"router\", \"min-routers\", Deprecated);\n\n        // TODO: this may have been a synonym for [router]worker-threads\n        conf.define_option<std::string>(\"router\", \"threads\", Deprecated);\n        conf.define_option<std::string>(\"router\", \"net-threads\", Deprecated);\n\n        is_relay = params.type == config::Type::Relay;\n    }\n\n    void ExitConfig::define_config_options(ConfigDefinition& conf, const ConfigGenParameters&)\n    {\n        conf.define_option<std::string>(\n            \"exit\",\n            \"auth\",\n            FullClientOnly,\n            MultiValue,\n            Comment{\n                \"Specify an optional authentication token required to use a non-public exit node.\",\n                \"For example:\",\n                \"    auth=myfavouriteexit.loki:abc\",\n                \"uses the authentication code `abc` whenever myfavouriteexit.loki is accessed.\",\n                \"Can be specified multiple times to store codes for different exit nodes.\",\n            },\n            [this](std::string arg) {\n                if (arg.empty())\n                    throw std::invalid_argument{\"Empty argument passed to '[exit]:auth'\"};\n\n                const auto pos = arg.find(\":\");\n\n                if (pos == std::string::npos)\n                {\n                    throw std::invalid_argument(\n                        \"[exit]:auth invalid format, expects exit-address.loki:auth-token-goes-here\");\n                }\n\n                const auto addr = arg.substr(0, pos);\n                auto auth = arg.substr(pos + 1);\n\n                if (is_valid_sns(addr))\n                {\n                    sns_auth_tokens.emplace(std::move(addr), std::move(auth));\n                    return;\n                }\n                try\n                {\n                    NetworkAddress exit{addr};\n                    if (!exit.client())\n                        throw std::invalid_argument{\"only .loki addresses can be used for exits\"};\n                    auth_tokens.emplace(std::move(exit), std::move(auth));\n                }\n                catch (const std::exception& e)\n                {\n                    throw std::invalid_argument(\"[exit]:auth invalid exit address: {}\"_format(e.what()));\n                }\n            });\n\n        conf.define_option<bool>(\n            \"exit\",\n            \"enable\",\n            FullClientOnly,\n            Default{false},\n            assignment_acceptor(exit_enabled),\n            Comment{\n                \"Enable exit-node functionality for local lokinet instance.\",\n            });\n\n        conf.define_option<std::string>(\n            \"exit\",\n            \"policy\",\n            FullClientOnly,\n            MultiValue,\n            Comment{\n                \"Specifies the IP traffic accepted by the local exit node traffic policy. If any are\",\n                \"specified then only matched traffic will be allowed and all other traffic will be\",\n                \"dropped. Examples:\",\n                \"    policy=tcp\",\n                \"would allow all TCP/IP packets (regardless of port);\",\n                \"    policy=0x69\",\n                \"would allow IP traffic with IP protocol 0x69;\",\n                \"    policy=udp/53\",\n                \"would allow UDP port 53; and\",\n                \"    policy=tcp/smtp\",\n                \"would allow TCP traffic on the standard smtp port (21).\",\n            },\n            [this](std::string arg) {\n                // this will throw on error\n                exit_policy.protocols.insert(net::ProtocolInfo::from_config(arg));\n            });\n\n        conf.define_option<std::string>(\n            \"exit\",\n            \"reserved-range\",\n            FullClientOnly,\n            MultiValue,\n            Comment{\n                \"Reserve an ip range to use as an exit broker for a `.loki` address\",\n                \"Specify a `.loki` address and a reserved ip range to use as an exit broker.\",\n                \"Examples:\",\n                \"    reserved-range=whatever.loki\",\n                \"would route all exit traffic through whatever.loki; and\",\n                \"    reserved-range=stuff.loki:100.0.0.0/24\",\n                \"would route the IP range 100.0.0.0/24 through stuff.loki.\",\n                \"This option can be specified multiple times (to map different IP ranges).\",\n            },\n            [this](std::string arg) {\n                if (arg.empty())\n                    return;\n\n                std::variant<ipv4_range, ipv6_range> range;\n\n                const auto pos = arg.find(\":\");\n\n                if (pos == std::string::npos)\n                    range = ipv4{0} / 0;\n                else\n                {\n                    try\n                    {\n                        std::string input = arg.substr(pos + 1);\n                        if (input.find(\":\") != std::string::npos)  // ipv6\n                            range = parse_ipv6_range(input, 128);\n                        else\n                            range = parse_ipv4_range(input, 32);\n                    }\n                    catch (const std::exception& e)\n                    {\n                        throw std::invalid_argument{\"[exit]:reserved-range invalid ip range: {}\"_format(e.what())};\n                    }\n\n                    arg.resize(pos);\n                }\n\n                if (is_valid_sns(arg))\n                    sns_ranges[arg].push_back(std::move(range));\n                else\n                {\n                    try\n                    {\n                        ranges[NetworkAddress{arg}].push_back(std::move(range));\n                    }\n                    catch (const std::exception& e)\n                    {\n                        throw std::invalid_argument{\"[exit]:reserved-range invalid address: {}\"_format(arg)};\n                    }\n                }\n            });\n\n        conf.define_option<std::string>(\n            \"exit\",\n            \"routed-range\",\n            FullClientOnly,\n            MultiValue,\n            Comment{\n                \"Advertise that exit node routes exit traffic to the specified IP range. If omitted, the\",\n                \"default is ALL public ranges.  Can be set to public to indicate that this exit\",\n                \"routes traffic to the public internet.\",\n                \"For example:\",\n                \"    routed-range=10.0.0.0/16\",\n                \"    routed-range=public\",\n                \"to advertise that this exit routes traffic to both the public internet, and to\",\n                \"10.0.x.y addresses.\",\n                \"\",\n                \"Note that this option does not automatically configure network routing; that\",\n                \"must be configured separately on the exit system to handle lokinet traffic.\",\n            },\n            [this](std::string arg) {\n                if (arg == \"public\")\n                    exit_policy.ranges.push_back(ipv4{0} / 0);\n                else\n                {\n                    try\n                    {\n                        if (arg.find(':') != std::string::npos)\n                            exit_policy.ranges_v6.push_back(parse_ipv6_range(arg, 128));\n                        else\n                            exit_policy.ranges.push_back(parse_ipv4_range(arg, 32));\n                    }\n                    catch (const std::exception& e)\n                    {\n                        throw std::invalid_argument{\"[exit]:routed-range invalid range '{}': {}\"_format(arg, e.what())};\n                    }\n                }\n            });\n    }\n\n    void NetworkConfig::define_config_options(ConfigDefinition& conf, const ConfigGenParameters& params)\n    {\n        conf.add_section_comments(\n            \"network\",\n            {\n                \"Network settings related to network devices and communications.\",\n            });\n\n        conf.define_option<bool>(\n            \"network\",\n            \"save-profiles\",\n            Default{params.type != config::Type::EmbeddedClient},\n            Hidden,\n            assignment_acceptor(save_profiles));\n\n        conf.define_option<bool>(\"network\", \"profiling\", Default{true}, Hidden, assignment_acceptor(enable_profiling));\n\n        conf.define_option<std::string>(\"network\", \"profiles\", Deprecated);\n\n        conf.define_option<std::string>(\n            \"network\",\n            \"keyfile\",\n            ClientOnly,\n            [this](std::string arg) {\n                if (arg.empty())\n                    return;\n\n                keyfile = arg;\n\n                if (check_path_op(keyfile))\n                    log::info(logcat, \"Client configured to try private key file at path: {}\", keyfile->c_str());\n                else\n                    log::warning(logcat, \"Bad input for client private key file ({}); using ephemeral...\", arg);\n            },\n            Comment{\n                \"The private key to persist address with. If not specified the address will be\",\n                \"ephemerally generated.\",\n            });\n\n        conf.define_option<std::string>(\n            \"network\",\n            \"auth-type\",\n            FullClientOnly,\n            Comment{\n                \"Set the endpoint authentication type.\",\n                \"none/whitelist/lmq/file\",\n            },\n            [this](std::string arg) {\n                if (arg == \"file\")\n                    auth_type = auth::AuthType::FILE;\n                else if (arg == \"lmq\" || arg == \"omq\" || arg == \"zmq\")\n                    auth_type = auth::AuthType::OMQ;\n                else if (arg == \"whitelist\")\n                    auth_type = auth::AuthType::WHITELIST;\n                else if (arg == \"\" || arg == \"none\")\n                    auth_type = auth::AuthType::NONE;\n                else\n                    throw std::invalid_argument{\"invalid [network]:auth-type value: '{}'\"_format(arg)};\n            });\n\n        conf.define_option<std::string>(\n            \"network\",\n            \"omq-auth-endpoint\",\n            FullClientOnly,\n            assignment_acceptor(auth_endpoint),\n            Comment{\n                \"OMQ endpoint to talk to for authenticating new sessions\",\n                \"ipc:///var/lib/lokinet/auth.socket\",\n                \"tcp://127.0.0.1:5555\",\n            });\n\n        conf.define_option<std::string>(\n            \"network\",\n            \"omq-auth-method\",\n            FullClientOnly,\n            Default{\"llarp.auth\"},\n            Comment{\n                \"OMQ function to call for authenticating new sessions\",\n                \"llarp.auth\",\n            },\n            [this](std::string arg) {\n                if (arg.empty())\n                    return;\n                auth_method = std::move(arg);\n            });\n\n        conf.define_option<std::string>(\n            \"network\",\n            \"auth-whitelist\",\n            FullClientOnly,\n            MultiValue,\n            Comment{\n                \"manually add a remote endpoint by .loki address to the access whitelist\",\n            },\n            [this](std::string arg) {\n                try\n                {\n                    auth_whitelist.insert(NetworkAddress{arg});\n                }\n                catch (const std::exception& e)\n                {\n                    throw std::invalid_argument{\n                        \"[network]:auth-whitelist: invalid .loki address '{}': {}\"_format(arg, e.what())};\n                }\n            });\n\n        conf.define_option<std::filesystem::path>(\n            \"network\",\n            \"auth-file\",\n            FullClientOnly,\n            MultiValue,\n            Comment{\n                \"Read auth tokens from file to accept endpoint auth\",\n                \"Can be provided multiple times\",\n            },\n            [this, rel_base = params.default_data_dir](std::filesystem::path arg) {\n                if (!arg.empty() && arg.is_relative())\n                    arg = rel_base / arg;\n                if (not exists(arg))\n                    throw std::invalid_argument{\"cannot load auth file {}: file does not exist\"_format(arg)};\n                auth_files.push_back(std::move(arg));\n            });\n\n        conf.define_option<std::string>(\n            \"network\",\n            \"auth-file-type\",\n            FullClientOnly,\n            Comment{\n                \"How to interpret the contents of an auth file.\",\n#ifdef LOKINET_HAVE_CRYPT\n                \"Possible values: hash, plaintext\",\n#else\n                \"Possible values: plaintext\",\n#endif\n            },\n            [this](std::string arg) {\n                if (arg == \"plain\" || arg == \"plaintext\")\n                    auth_file_type = auth::AuthFileType::PLAIN;\n                else if (arg == \"hashed\" || arg == \"hashes\" || arg == \"hash\")\n                {\n#ifndef LOKINET_HAVE_CRYPT\n                    throw std::invalid_argument{\"Hashed auth files are not supported by this Lokinet build\"};\n#endif\n                    auth_file_type = auth::AuthFileType::HASHES;\n                }\n                else\n                    throw std::invalid_argument{\"Invalid auth file type '{}'\"_format(arg)};\n            });\n\n        conf.define_option<std::string>(\n            \"network\",\n            \"auth-static\",\n            FullClientOnly,\n            MultiValue,\n            Comment{\n                \"Manually add a static auth code to accept for endpoint auth\",\n                \"Can be provided multiple times\",\n            },\n            [this](std::string arg) { auth_static_tokens.emplace(std::move(arg)); });\n\n        conf.define_option<bool>(\n            \"network\",\n            \"reachable\",\n            FullClientOnly,\n            Default{true},\n            assignment_acceptor(is_reachable),\n            Comment{\n                \"Determines whether we will pubish our service's ClientContact to the network (client default: TRUE)\",\n            });\n\n        conf.define_option<int>(\"network\", \"hops\", ClientOnly, Hidden, [](int) {\n            log::warning(\n                logcat,\n                \"[network]:hops is no longer supported; default path lengths applied. See the path options in the \"\n                \"[paths] section instead\");\n        });\n\n        conf.define_option<int>(\"network\", \"paths\", ClientOnly, Hidden, [](int) {\n            log::error(\n                logcat,\n                \"[network]:paths is no longer supported; default path numbers applied. See the path options in the \"\n                \"[paths] section instead\");\n        });\n\n        conf.define_option<bool>(\n            \"network\",\n            \"auto-routing\",\n            FullClientOnly,\n            Default{true},\n            Comment{\n                \"Enable / disable automatic route configuration.\",\n                \"When this is enabled and an exit is used Lokinet will automatically configure the\",\n                \"operating system routes to route public internet traffic through the exit node.\",\n                \"This is enabled by default, but can be disabled if advanced/manual exit routing\",\n                \"configuration is desired.\"},\n            assignment_acceptor(enable_route_poker));\n\n        conf.define_option<bool>(\n            \"network\",\n            \"blackhole-routes\",\n            FullClientOnly,\n            Default{true},\n            Comment{\n                \"Enable / disable route configuration blackholes.\",\n                \"When enabled lokinet will drop IPv4 and IPv6 traffic (when in exit mode) that is \"\n                \"not\",\n                \"handled in the exit configuration.  Enabled by default.\"},\n            assignment_acceptor(blackhole_routes));\n\n        conf.define_option<std::string>(\n            \"network\",\n            \"ifname\",\n            NotEmbedded,\n            Comment{\n                \"Interface name for lokinet traffic. If unset lokinet will look for a free name\",\n                \"matching 'lokitunN', starting at N=0 (e.g. lokitun0, lokitun1, ...).\",\n#ifdef __linux__\n                \"\",\n                \"On Linux, you can use '%d' in the name as a pattern to have the OS automatically choose\",\n                \"a device name by replacing '%d' with a number to construct an unused interface name\",\n#endif\n            },\n            assignment_acceptor(_if_name));\n\n        conf.define_option<std::string>(\n            \"network\",\n            \"ifaddr\",\n            NotEmbedded,\n            Comment{\n                \"Local IP and netmask for lokinet traffic. For example, 172.16.0.1/16 to use\",\n                \"172.16.0.1 for this lokinet instance and 172.16.x.y for remote peers. If omitted\",\n                \"then lokinet will attempt to automatically select an unused private range.\",\n                \"If you specify an all-0 address with range (e.g. 0.0.0.0/12) then lokinet will\",\n                \"auto-select a private range of the given size.\",\n            },\n            [this](std::string arg) {\n                try\n                {\n                    _local_ip_net = parse_ipv4_net(arg);\n                }\n                catch (const std::exception& e)\n                {\n                    throw std::invalid_argument{\"[network]:ifaddr invalid value '{}': {}\"_format(arg, e.what())};\n                }\n            });\n\n        conf.define_option<std::string>(\n            \"network\",\n            \"ipv6-network\",\n            NotEmbedded,\n            Hidden,\n            Comment{\n                \"Enables internal IPv6 traffic for lokinet.  Can be set to:\",\n                \"  - false to disable IPv6 support.  This is the default if omitted\",\n                \"  - true to enable IPv6 support and auto-detect a free private /64 network range\",\n                \"  - ::/80 to auto-detect a free private range of netmask 80 (change as needed) \",\n                \"    instead of the default 64\",\n                \"  - An explicit private address and range to use, such as: fd00:abcd:1234::1/56\",\n                \"\",\n                \"Currently experimental and not supported.\",\n            },\n            [this](std::string arg) {\n                if (arg.empty())\n                {\n                    enable_ipv6 = false;\n                    return;\n                }\n                if (auto b = parse_boolean(arg))\n                {\n                    enable_ipv6 = *b;\n                    return;\n                }\n                try\n                {\n                    _local_ipv6_net = parse_ipv6_net(arg);\n                    enable_ipv6 = true;\n                }\n                catch (const std::exception& e)\n                {\n                    throw std::invalid_argument{\"[network]:ipv6-addr invalid value '{}': {}\"_format(arg, e.what())};\n                }\n            });\n\n        conf.define_option<std::string>(\n            \"network\",\n            \"mapaddr\",\n            FullClientOnly,\n            MultiValue,\n            Comment{\n                \"Map a remote `.loki` address to always use a fixed local IP. For example:\",\n                \"    mapaddr=<pubkey>.loki:172.16.0.10\",\n                \"maps `<pubkey>.loki` to `172.16.0.10` instead of using the next available IP.\",\n                \"The given IP address must be inside the range configured by ifaddr=, and the\",\n                \"remote `.loki` cannot be an ONS address\"},\n            [this](std::string arg) {\n                if (arg.empty())\n                    return;\n\n                const auto pos = arg.find(\":\");\n\n                if (pos == std::string::npos)\n                    throw std::invalid_argument{\n                        \"[endpoint]:mapaddr invalid entry '{}'; expected 'ADDR:IP'\"_format(arg)};\n\n                auto addr_arg = std::string_view{arg}.substr(0, pos);\n                auto ip_arg = arg.substr(pos + 1);\n\n                try\n                {\n                    NetworkAddress raddr{addr_arg};\n                    // ipv6\n                    if (ip_arg.find(':') != std::string_view::npos)\n                        _reserved_local_ipv6.emplace(raddr, ip_arg);\n                    else\n                        _reserved_local_ipv4.emplace(raddr, ip_arg);\n                }\n                catch (const std::exception& e)\n                {\n                    throw std::invalid_argument{\"[endpoint]:mapaddr invalid entry '{}': {}\"_format(arg, e.what())};\n                }\n            });\n\n        // TODO: support SRV records for routers, but for now client only\n        conf.define_option<std::string>(\n            \"network\",\n            \"srv\",\n            FullClientOnly,\n            MultiValue,\n            Comment{\n                \"Specify SRV Records for services hosted on the SNApp for protocols that use SRV\",\n                \"records for service discovery. Each line specifies a single SRV record as:\",\n                \"    srv=_service._protocol priority weight port target.loki\",\n                \"and can be specified multiple times as needed.\",\n                \"For more info see\",\n                \"https://docs.oxen.io/products-built-on-oxen/lokinet/snapps/hosting-snapps\",\n                \"and general description of DNS SRV record configuration.\",\n            },\n            [this](std::string arg) {\n                auto maybe_srv = dns::SRVData::from_srv_string(arg);\n\n                if (not maybe_srv)\n                    throw std::invalid_argument{\"Invalid SRV Record string: {}\"_format(arg)};\n\n                srv_records.emplace(std::move(*maybe_srv));\n            });\n\n        conf.define_option<int>(\"network\", \"path-alignment-timeout\", Deprecated);\n\n        conf.define_option<std::filesystem::path>(\n            \"network\",\n            \"persist-addrmap-file\",\n            FullClientOnly,\n            Comment{\n                \"If given this specifies a file in which to record mapped local tunnel addresses so\",\n                \"the same local address will be used for the same lokinet address on reboot. If this\",\n                \"is not specified then the local IP of remote lokinet targets will not persist across\",\n                \"restarts of lokinet.\",\n            },\n            [this, rel_base = params.default_data_dir](std::filesystem::path file) {\n                if (!file.empty() && file.is_relative())\n                    file = rel_base / file;\n                static constexpr auto addrmap_errorstr = \"Invalid entry in persist-addrmap-file\"sv;\n                if (file.empty())\n                    throw std::invalid_argument(\"persist-addrmap-file cannot be empty\");\n\n                if (not exists(file))\n                    throw std::invalid_argument(\"persist-addrmap-file path invalid: {}\"_format(file));\n\n                bool load_file = true;\n                {\n                    constexpr auto ADDR_PERSIST_MODIFY_WINDOW = 1min;\n                    const auto last_write_time = std::filesystem::last_write_time(file);\n                    const auto now = decltype(last_write_time)::clock::now();\n\n                    if (now < last_write_time or now - last_write_time > ADDR_PERSIST_MODIFY_WINDOW)\n                    {\n                        load_file = false;\n                    }\n                }\n\n                std::string data;\n                if (auto maybe = util::OpenFileStream<std::ifstream>(file, std::ios_base::binary); maybe and load_file)\n                {\n                    log::debug(logcat, \"Config loading persisting address map file from path:{}\", file);\n                    maybe->seekg(0, std::ios_base::end);\n                    const auto len = maybe->tellg();\n                    maybe->seekg(0, std::ios_base::beg);\n                    data.resize(len);\n                    maybe->read(data.data(), len);\n                }\n                else\n                {\n                    auto err = \"Config could not load persisting address map file from path:{}\"_format(file);\n                    log::warning(logcat, \"{} {}\", err, load_file ? \"NOT FOUND\" : \"STALE\");\n                }\n\n                if (not data.empty())\n                {\n                    log::trace(logcat, \"Config parsing address map data: {}\", llarp::buffer_printer{data});\n\n                    const auto parsed = oxenc::bt_deserialize<oxenc::bt_dict>(data);\n\n                    for (const auto& [key, value] : parsed)\n                    {\n                        try\n                        {\n                            quic::Address addr{key, 0};\n\n                            std::variant<ipv4, ipv6> ip;\n\n                            auto check_ip_okay = []<typename Range>(const std::optional<Range>& range, const auto& ip) {\n                                if (range)\n                                {\n                                    bool bad = ip == range->ip || ip == range->to_range().ip;\n                                    if constexpr (std::same_as<Range, ipv4_net>)\n                                        bad = bad || ip == range->broadcast();\n                                    if (bad)\n                                    {\n                                        log::warning(\n                                            logcat, \"{}: ignore invalid address map IP {}\", addrmap_errorstr, ip);\n                                        return false;\n                                    }\n                                    if (!range->contains(ip))\n                                    {\n                                        log::warning(\n                                            logcat,\n                                            \"{}: IP {} is outside the configured local range {}\",\n                                            addrmap_errorstr,\n                                            ip,\n                                            range->to_range());\n                                        return false;\n                                    }\n                                }\n                                return true;\n                            };\n\n                            if (addr.is_ipv4())\n                            {\n                                if (!check_ip_okay(_local_ip_net, ip.emplace<ipv4>(addr.to_ipv4())))\n                                    continue;\n                            }\n                            else\n                            {\n                                if (!check_ip_okay(_local_ipv6_net, ip.emplace<ipv6>(addr.to_ipv6())))\n                                    continue;\n                            }\n\n                            const auto* arg = std::get_if<std::string>(&value);\n                            if (not arg)\n                            {\n                                log::warning(logcat, \"{}: {}\", addrmap_errorstr, \"not a string!\");\n                                continue;\n                            }\n\n                            if (is_valid_sns(*arg))\n                            {\n                                log::warning(logcat, \"{}: {}\", addrmap_errorstr, \"cannot accept ONS names!\");\n                                continue;\n                            }\n\n                            try\n                            {\n                                NetworkAddress netaddr{*arg};\n                                if (auto* ip4 = std::get_if<ipv4>(&ip))\n                                    _reserved_local_ipv4.emplace(std::move(netaddr), std::move(*ip4));\n                                else\n                                    _reserved_local_ipv6.emplace(std::move(netaddr), std::move(std::get<ipv6>(ip)));\n                            }\n                            catch (const std::exception& e)\n                            {\n                                log::warning(logcat, \"{}: invalid value {}: {}\", addrmap_errorstr, *arg, e.what());\n                                continue;\n                            }\n                        }\n                        catch (const std::exception& e)\n                        {\n                            log::warning(\n                                logcat,\n                                \"Exception caught parsing key:value (key:{}) pair in addr persist file:{}\",\n                                key,\n                                e.what());\n                        }\n                    }\n                }\n\n                addr_map_persist_file = file;\n            });\n\n        // Deprecated options:\n        conf.define_option<std::string>(\"network\", \"enabled\", Deprecated);\n    }\n\n    void DnsConfig::define_config_options(ConfigDefinition& conf, const ConfigGenParameters& params)\n    {\n        conf.add_section_comments(\n            \"dns\",\n            {\n                \"DNS configuration\",\n            });\n\n        // Most non-linux platforms have loopback as 127.0.0.1/32, but linux uses 127.0.0.1/8 so\n        // that we can bind to other 127.* IPs to avoid conflicting with something else that may be\n        // listening on 127.0.0.1:53.\n        constexpr std::array DefaultDNSBind{\n#ifdef __linux__\n#ifdef WITH_SYSTEMD\n            // when we have systemd support add a random high port on loopback as well\n            // see https://github.com/oxen-io/lokinet/issues/1887#issuecomment-1091897282\n            Default{\"127.0.0.1:0\"},\n#endif\n            Default{\"127.3.2.1:53\"},\n#else\n            Default{\"127.0.0.1:53\"},\n#endif\n        };\n\n        auto parse_addr_for_dns = [](const std::string& arg) {\n            std::optional<quic::Address> addr = std::nullopt;\n            std::string_view arg_v{arg}, port;\n            std::string host;\n            uint16_t p{DEFAULT_DNS_PORT};\n\n            if (auto pos = arg_v.find(':'); pos != arg_v.npos)\n            {\n                host = arg_v.substr(0, pos);\n                port = arg_v.substr(pos + 1);\n\n                if (not llarp::parse_int<uint16_t>(port, p))\n                    log::info(logcat, \"Failed to parse port in arg:{}, defaulting to DNS port 53\", port);\n\n                addr = quic::Address{host, p};\n            }\n\n            return addr;\n        };\n\n        conf.define_option<std::string>(\n            \"dns\",\n            \"upstream\",\n            FullClientOnly,\n            MultiValue,\n            Comment{\n                \"Upstream resolver(s) to use as fallback for non-loki addresses.\",\n                \"Multiple values accepted.\",\n            },\n            [this, parse_addr_for_dns](std::string arg) {\n                if (not arg.empty())\n                {\n                    if (auto maybe_addr = parse_addr_for_dns(arg))\n                        _upstream_dns.push_back(std::move(*maybe_addr));\n                    else\n                        log::warning(logcat, \"Failed to parse upstream DNS resolver address:{}\", arg);\n                }\n            });\n\n        conf.define_option<bool>(\n            \"dns\",\n            \"l3-intercept\",\n            FullClientOnly,\n            Default{\n                platform::is_windows or platform::is_android or (platform::is_macos and not platform::is_apple_sysex)},\n            Comment{\"Intercept all dns traffic (udp/53) going into our lokinet network interface \"\n                    \"instead of binding a local udp socket\"},\n            assignment_acceptor(l3_intercept));\n\n        conf.define_option<std::string>(\n            \"dns\",\n            \"query-bind\",\n            FullClientOnly,\n#if defined(_WIN32)\n            Default{\"0.0.0.0:0\"},\n#else\n            Hidden,\n#endif\n            Comment{\n                \"Address to bind to for sending upstream DNS requests.\",\n            },\n            [this, parse_addr_for_dns](std::string arg) {\n                if (not arg.empty())\n                {\n                    if (auto maybe_addr = parse_addr_for_dns(arg))\n                        _query_bind = std::move(*maybe_addr);\n                    else\n                        log::warning(logcat, \"Failed to parse bind address for DNS queries:{}\", arg);\n                }\n            });\n\n        conf.define_option<std::string>(\n            \"dns\",\n            \"bind\",\n            NotEmbedded,\n            DefaultDNSBind,\n            MultiValue,\n            Comment{\n                \"Address to bind to for handling DNS requests.\",\n            },\n            [this, parse_addr_for_dns](std::string arg) {\n                if (not arg.empty())\n                {\n                    if (auto maybe_addr = parse_addr_for_dns(arg))\n                    {\n                        _bind_addrs.push_back(std::move(*maybe_addr));\n                    }\n                    else\n                        log::warning(logcat, \"Failed to parse bind address for handling DNS requests:{}\", arg);\n                }\n            });\n\n        conf.define_option<std::filesystem::path>(\n            \"dns\",\n            \"add-hosts\",\n            FullClientOnly,\n            Comment{\"Add a hosts file to the dns resolver\", \"For use with client side dns filtering\"},\n            [this, rel_base = params.default_data_dir](std::filesystem::path path) {\n                if (path.empty())\n                    return;\n                if (path.is_relative())\n                    path = rel_base / path;\n                if (not exists(path))\n                    throw std::invalid_argument{\"cannot add hosts file {} as it does not exist\"_format(path)};\n                hostfiles.emplace_back(std::move(path));\n            });\n\n        // Ignored option (used by the systemd service file to disable resolvconf configuration).\n        conf.define_option<bool>(\n            \"dns\",\n            \"no-resolvconf\",\n            FullClientOnly,\n            Comment{\n                \"Can be uncommented and set to 1 to disable resolvconf configuration of lokinet \"\n                \"DNS.\",\n                \"(This is not used directly by lokinet itself, but by the lokinet init scripts\",\n                \"on systems which use resolveconf)\",\n            });\n\n        // forward the rest to libunbound\n        conf.add_undeclared_handler(\n            \"dns\", [this](auto, std::string_view key, std::string_view val) { extra_opts.emplace(key, val); });\n    }\n\n    void LinksConfig::define_config_options(ConfigDefinition& conf, const ConfigGenParameters&)\n    {\n        conf.add_section_comments(\n            \"bind\",\n            {\n                \"This section allows specifying the IPs that lokinet uses for incoming and outgoing\",\n                \"connections.  For simple setups it can usually be left blank, but may be required\",\n                \"for routers with multiple IPs, or routers that must listen on a private IP with\",\n                \"forwarded public traffic.  It can also be useful for clients that want to use a\",\n                \"consistent outgoing port for which firewall rules can be configured.\",\n            });\n\n        conf.define_option<std::string>(\n            \"bind\",\n            \"public-ip\",\n            Hidden,\n            RelayOnly,\n            public_ip_loader(public_addr, \"[bind]:public-ip\", \"[router]:public-ip\"));\n\n        conf.define_option<uint16_t>(\n            \"bind\",\n            \"public-port\",\n            Hidden,\n            RelayOnly,\n            public_port_loader(public_addr, \"[bind]:public-port\", \"[router]:public-port\"));\n\n        auto parse_addr_for_link = [](std::string_view arg) {\n            quic::Address a = quic::Address::parse(arg, 0);\n            if (a.is_loopback())\n                throw std::invalid_argument{\"Invalid listen address: {} is a loopback address\"_format(arg)};\n            if (a.is_ipv6() && a.is_any_addr())\n                a = quic::Address{ipv4{0, 0, 0, 0}, a.port()};\n            else if (a.is_ipv6() && a.is_ipv4_mapped_ipv6())\n                a.unmap_ipv4_from_ipv6();\n            else if (a.is_ipv6())\n                throw std::invalid_argument{\"Invalid listen address: IPv6 addresses are not currently supported\"};\n            return a;\n        };\n\n        conf.define_option<std::string>(\n            \"bind\",\n            \"listen\",\n            Comment{\n                \"IP and/or port for lokinet to bind to for inbound/outbound connections.\",\n                \"\",\n                \"If IP is omitted then lokinet will search for a local network interface with a\",\n                \"public IP address and use that IP (and will exit with an error if no such IP is found\",\n                \"on the system).  If port is omitted then lokinet defaults to 1090 (routers) or 1091 (clients).\",\n                \"\",\n                \"Examples:\",\n                \"    listen=15.5.29.5:443\",\n                \"    listen=10.0.2.2\",\n                \"    listen=:1234\",\n                \"\",\n                \"Note that, when running as a relay, a private range IP address (like the second example\",\n                \"above) requires also using [router]:public-ip/-port to specify the public IP address at\",\n                \"which this router can be reached, and requires that traffic on that port is redirected to\",\n                \"the listening internal address.\",\n            },\n            [this, parse_addr_for_link](const std::string& arg) {\n                if (listen_addr)\n                    throw std::runtime_error{\n                        \"Multiple listen addresses found.  If upgrading from an older lokinet, delete extra \"\n                        \"[bind]:inbound and [bind]:IP and use only one [bind]:listen\"};\n                listen_addr = parse_addr_for_link(arg);\n            });\n\n        conf.define_option<std::string>(\n            \"bind\", \"inbound\", RelayOnly, MultiValue, Hidden, [this, parse_addr_for_link](const std::string& arg) {\n                if (listen_addr)\n                    throw std::runtime_error{\n                        \"Multiple listen addresses found.  If upgrading from an older lokinet, delete extra \"\n                        \"[bind]:inbound and [bind]:IP and use only one [bind]:listen\"};\n                listen_addr = parse_addr_for_link(arg);\n                log::warning(\n                    logcat,\n                    \"Loaded listen address {} from deprecated [bind]:inbound option; please update your config to \"\n                    \"use [bind]:listen instead\",\n                    *listen_addr);\n            });\n\n        conf.define_option<std::string>(\"bind\", \"outbound\", MultiValue, Deprecated, Hidden);\n\n        conf.add_undeclared_handler(\"bind\", [this](std::string_view, std::string_view key, std::string_view val) {\n            // special case: old lokinet used '*' for outbound port, which now does nothing\n            if (key == \"*\")\n            {\n                log::warning(\n                    logcat, \"[bind]:*=PORT is deprecated and no longer does anything in this version of Lokinet\");\n                return;\n            }\n\n            log::warning(\n                logcat, \"[bind]:{} is deprecated: Please update your config to use [bind]:listen instead\", key);\n\n            // Otherwise you could have either `A.B.C.D=PORT` or `IFNAME=port`.  The latter was\n            // almost never used, and so we only look for the format and error on the latter.\n            if (listen_addr)\n                throw std::runtime_error{\n                    \"Multiple listen addresses found.  If upgrading from an older lokinet, replace extra \"\n                    \"[bind]:inbound=/IP= settings with a single [bind]:listen=\"};\n\n            uint16_t port{0};\n\n            quic::Address temp;\n            try\n            {\n                if (!llarp::parse_int<uint16_t>(val, port))\n                    throw std::runtime_error{\"Could not parse port\"};\n                temp = quic::Address{std::string{key}, port};\n            }\n            catch (const std::exception&)\n            {\n                throw std::runtime_error{\n                    \"Invalid [bind] deprecated config item: {}={}. \"\n                    \"Please replace with a [bind]:listen=... directive\"_format(key, val)};\n            }\n\n            listen_addr = std::move(temp);\n\n            log::warning(\n                logcat,\n                \"[bind]:{0}={1} is deprecated; please replace with [bind] config entry: listen={0}:{1}\",\n                key,\n                val);\n        });\n    }\n\n    void ApiConfig::define_config_options(ConfigDefinition& conf, const ConfigGenParameters& params)\n    {\n        conf.add_section_comments(\n            \"api\",\n            {\n                \"JSON API settings\",\n            });\n\n        constexpr std::array DefaultRPCBind{\n            Default{\"tcp://127.0.0.1:1190\"},\n#ifndef _WIN32\n            Default{\"ipc://rpc.sock\"},\n#endif\n        };\n\n        conf.define_option<bool>(\n            \"api\",\n            \"enabled\",\n            NotEmbedded,\n            Default{params.type == config::Type::FullClient},\n            assignment_acceptor(enable_rpc_server),\n            Comment{\n                \"Determines whether or not the OMQ JSON API is enabled. By default this is enabled for clients, \"\n                \"disabled for relays\",\n            });\n\n        conf.define_option<std::string>(\n            \"api\",\n            \"bind\",\n            NotEmbedded,\n            DefaultRPCBind,\n            MultiValue,\n            [this, first = true](std::string arg) mutable {\n                if (first)\n                {\n                    rpc_bind_addrs.clear();\n                    first = false;\n                }\n                if (arg.find(\"://\") == std::string::npos)\n                {\n                    arg = \"tcp://\" + arg;\n                }\n                rpc_bind_addrs.push_back(std::move(arg));\n            },\n            Comment{\n                \"IP addresses and ports to bind to.\",\n                \"Recommend localhost-only for security purposes.\",\n            });\n\n        conf.define_option<std::string>(\"api\", \"authkey\", Deprecated);\n\n        // TODO: this was from pre-refactor:\n        // TODO: add pubkey to whitelist\n    }\n\n    void LokidConfig::define_config_options(ConfigDefinition& conf, const ConfigGenParameters&)\n    {\n        conf.add_section_comments(\n            \"lokid\",\n            {\n                \"Settings for communicating with oxend\",\n            });\n\n        conf.define_option<bool>(\n            \"lokid\",\n            \"disable-testing\",\n            Default{false},\n            Hidden,\n            RelayOnly,\n            Comment{\"Development option: set to true to disable reachability testing when using\", \"testnet\"},\n            assignment_acceptor(disable_testing));\n\n        conf.define_option<std::string>(\n            \"lokid\",\n            \"rpc\",\n            RelayOnly,\n            Required,\n            Comment{\n                \"oxenmq control address for for communicating with oxend. Depends on oxend's\",\n                \"lmq-local-control configuration option. By default this value should be\",\n                \"ipc://OXEND-DATA-DIRECTORY/oxend.sock, such as:\",\n                \"    rpc=ipc:///var/lib/oxen/oxend.sock\",\n                \"    rpc=ipc:///home/USER/.oxen/oxend.sock\",\n                \"but can use (non-default) TCP if oxend is configured that way:\",\n                \"    rpc=tcp://127.0.0.1:5678\",\n            },\n            [this](std::string arg) {\n#ifndef LOKINET_EMBEDDED_ONLY\n                oxenmq::address test_valid{arg};\n#endif\n                rpc_addr = std::move(arg);\n            });\n\n        // Deprecated options:\n        conf.define_option<std::string>(\"lokid\", \"jsonrpc\", RelayOnly, Hidden, [](std::string arg) {\n            if (arg.empty())\n                return;\n            throw std::invalid_argument(\n                \"the [lokid]:jsonrpc option is no longer supported; please use the [lokid]:rpc config \"\n                \"option instead with oxend's lmq-local-control address -- typically a value such as \"\n                \"rpc=ipc:///var/lib/oxen/oxend.sock or rpc=ipc:///home/snode/.oxen/oxend.sock\");\n        });\n        conf.define_option<bool>(\"lokid\", \"enabled\", RelayOnly, Deprecated);\n        conf.define_option<std::string>(\"lokid\", \"username\", Deprecated);\n        conf.define_option<std::string>(\"lokid\", \"password\", Deprecated);\n        conf.define_option<std::string>(\"lokid\", \"service-node-seed\", Deprecated);\n    }\n\n    void BootstrapConfig::define_config_options(ConfigDefinition& conf, const ConfigGenParameters&)\n    {\n        conf.add_section_comments(\n            \"bootstrap\",\n            {\n                \"Configure nodes that will bootstrap us onto the network\",\n            });\n\n        conf.define_option<std::string>(\n            \"bootstrap\",\n            \"add-node\",\n            MultiValue,\n            Comment{\n                \"Specify a bootstrap file containing a list of signed RelayContacts of service nodes\",\n                \"which can act as a bootstrap. Can be specified multiple times. If set this overrides\",\n                \"the built-in seed node list.\",\n            },\n            [this](std::string arg) {\n                if (arg.empty())\n                    throw std::invalid_argument(\"cannot use empty filename as bootstrap\");\n\n                files.emplace_back(std::move(arg));\n\n                if (not exists(files.back()))\n                    throw std::invalid_argument(\"file does not exist: \" + arg);\n            });\n    }\n\n    void LoggingConfig::define_config_options(ConfigDefinition& conf, const ConfigGenParameters& params)\n    {\n        conf.add_section_comments(\n            \"logging\",\n            {\n                \"Logging settings\",\n            });\n\n        conf.define_option<std::string>(\n            \"logging\",\n            \"type\",\n            Default{\n                params.type == config::Type::EmbeddedClient      ? \"none\"\n                    : platform::is_android or platform::is_apple ? \"system\"\n                                                                 : \"print\"},\n            [this](std::string arg) {\n                if (arg == \"none\")\n                    type = std::nullopt;\n                else\n                    type = log::type_from_string(arg);\n            },\n            Comment{\n                \"Log type (format). Valid options are:\",\n                \"  print - print logs to standard output\",\n                \"  system - logs directed to the system logger (syslog/eventlog/etc.)\",\n                \"  file - plaintext formatting to a file\",\n                (params.type == config::Type::EmbeddedClient ? \"  none - do not reset the logging system (for embedded \"\n                                                               \"use with external oxen::logging)\"\n                                                             : \"\"),\n            });\n\n        conf.define_option<std::string>(\n            \"logging\",\n            \"level\",\n            Default{\n                params.type == config::Type::Relay            ? \"warn\"\n                    : params.type == config::Type::FullClient ? \"info\"\n                                                              : \"\"},\n            [this](std::string arg) { levels = std::move(arg); },\n            Comment{\n                \"Minimum log severity level to print. Logging below this level will be ignored.\",\n                \"Can also be set to a comma-separated list of individual categories, such as:\",\n                \"    *=warn, logcat123=debug\",\n                \"\",\n                \"Valid log levels, in ascending order, are:\",\n                \"  trace, debug, info, warn, error, critical, off\",\n            });\n\n        conf.define_option<std::string>(\n            \"logging\",\n            \"file\",\n            Default{\"\"},\n            assignment_acceptor(file),\n            Comment{\n                \"When using type=file this is the output filename.\",\n            });\n    }\n\n    void PathConfig::define_config_options(ConfigDefinition& conf, const ConfigGenParameters&)\n    {\n        conf.add_section_comments(\n            \"paths\",\n            {\n                \"Settings related to path selection such as number of hops and selection criteria\",\n            });\n\n        conf.define_option<int>(\n            \"paths\",\n            \"edge-connections\",\n            Default{CLIENT_ROUTER_CONNECTIONS},\n            ClientOnly,\n            Comment{\n                \"Minimum number of routers lokinet client will attempt to maintain direct (i.e. \\\"edge\\\")\",\n                \"connections to.  All paths will start through one of these edges.\",\n                \"\",\n                \"Lokinet may use more than this number of edges in single-hop connection mode\",\n                \"(see [paths]:client-hops) and may use fewer connections if limited by [paths]:strict-edge.\"},\n            lower_bounded_assignment_acceptor(edge_connections, 1, \"[paths]:edge-connections\"));\n\n        conf.define_option<int>(\n            \"paths\",\n            \"outbound-paths\",\n            ClientOnly,\n            Default{2},\n            Comment{\n                \"Number of paths to maintain per active outbound connection to a remote client or relay.\",\n                \"Only one path is actively used at a time, but others are used for regular path rotation\",\n                \"and as a fallback for path failure.\",\n                \"\",\n                \"Note that this value applies to EACH outbound connection separately: if you have active\",\n                \"connections to 5 clients and 3 snodes, lokinet will maintain 16 outbound paths (at the\",\n                \"default setting of 2).\",\n                \"\",\n                \"Setting this value to 1 is allowed, but will result in brief periods of packet loss\",\n                \"whenever paths expire due to the lack of allowed backup path.\",\n            },\n            bounded_assignment_acceptor(outbound_paths, 1, 4, \"[paths]:outbound-paths\"));\n\n        conf.define_option<int>(\n            \"paths\",\n            \"client-hops\",\n            ClientOnly,\n            Default{3},\n            Comment{\n                \"Number of hops to use when establishing a connection to a service node relay to\",\n                \"communicate through that relay to another client.\",\n                \"\",\n                \"The overall number of hops to the remote client is this value PLUS the number of inbound\",\n                \"hops the other client has configured for their inbound hops (via [paths]:inbound-hops).\",\n                \"\",\n                \"Setting this value to 1 puts lokinet into single-hop mode for the connection from this\",\n                \"client to the aligned pivot router, which potentially weakens connection privacy as\",\n                \"your public IP will be observable to any service node listed as a pivot for any remote\",\n                \"client that you connect to.\"},\n            bounded_assignment_acceptor(client_hops, 1, path::BUILD_LENGTH, \"[paths]:client-hops\"));\n\n        conf.define_option<int>(\n            \"paths\",\n            \"relay-hops\",\n            ClientOnly,\n            Comment{\n                \"Number of hops to use when establishing a connection to talk to a service node.\",\n                \"\",\n                \"A value of 1 results in establishing direct connection to the snode (i.e. only\",\n                \"encryption but no onion routing); 2 would select one intermediate snode to onion\",\n                \"route through; 4 uses three intermediates, and so on, up to the maximum of 8.\",\n                \"\",\n                \"Additional hops increases privacy but also increase latency and reduces network\",\n                \"performance through the path.\",\n                \"\",\n                \"If not set, this default to one greater than the value of [paths]:client-hops.\",\n                \"\",\n                \"Setting this value to 1 puts lokinet into single-hop mode for the connection from this\",\n                \"client to service node (i.e. `.snode` addresses) which potentially weakens connection\",\n                \"privacy as any service nodes you connect to will be able to observe your public IP.\"},\n            bounded_assignment_acceptor(relay_hops_, 1, path::BUILD_LENGTH, \"[paths]:relay-hops\"));\n\n        conf.define_option<int>(\n            \"paths\",\n            \"inbound-paths\",\n            ClientOnly,\n            Default{4},\n            Comment{\n                \"Number of local paths that Lokinet maintains for both network reachability (i.e. remote\",\n                \"clients connecting to this instance) and network communication such as looking up\",\n                \"client lto maintain for network reachability and for general network requests\",\n                \"\",\n                \"This value does NOT apply to paths that are built to reach external clients or relays.\",\n            },\n            bounded_assignment_acceptor(inbound_paths, 1, 10, \"[paths]:local-paths\"));\n\n        conf.define_option<int>(\n            \"paths\",\n            \"inbound-paths-extra\",\n            FullClientOnly,\n            Hidden,\n            Default{0},\n            Comment{\n                \"Extra inbound paths to use for Lokinet connectivity.  This option is hidden as it is not\",\n                \"meant for normal Lokinet use, and may be removed or replaced without warning in the future\",\n            },\n            lower_bounded_assignment_acceptor(inbound_paths_extra, 0, \"[paths]:inbound-paths-extra\"));\n\n        conf.define_option<int>(\n            \"paths\",\n            \"inbound-hops\",\n            ClientOnly,\n            Comment{\n                \"Number of hops to use for inbound and general request connections (see [paths]:inbound-paths).\",\n                \"\",\n                \"When a remote Lokinet is connecting to this instance, this controls the path length of the\",\n                \"local side of the full client-to-client path (i.e. from the common relay \\\"pivot\\\" to this\",\n                \"Lokinet instance).\",\n                \"\",\n                \"If not set, this value defaults to the same value as [paths]:client-hops.\",\n            },\n            bounded_assignment_acceptor(inbound_hops_, 1, path::BUILD_LENGTH, \"[paths]:inbound-hops\"));\n\n        conf.define_option<int>(\n            \"paths\",\n            \"inbound-pivot-reuse\",\n            ClientOnly,\n            Default{1},\n            Comment{\n                \"This configures the maximum number of times a single relay may be used as a path pivot.\",\n                \"The default is one, meaning each inbound path is built to a distinct pivot, but special\",\n                \"cases (such as a one-hop reachable endpoint using a small number of pivots) may want to\",\n                \"increase this to allow multiple pivots to be used at once.  Leaving this at the default\",\n                \"of 1 is recommended for most cases.\",\n            },\n            lower_bounded_assignment_acceptor(inbound_pivot_reuse, 1, \"[paths]:inbound-pivot-reuse\"));\n\n        conf.define_option<int>(\n            \"paths\",\n            \"unique-range-size\",\n            Default{24},\n            ClientOnly,\n            [this](int arg) {\n                if (arg > 32 or (arg < 4 and arg != 0))\n                    throw std::invalid_argument{\"[paths]:unique-range-size must be between 4 and 32, or 0\"};\n\n                unique_hop_netmask = static_cast<uint8_t>(arg);\n            },\n            Comment{\n                \"Netmask for router path selection; each router must be from a distinct IPv4 subnet\",\n                \"of the given size.  Defaults to 24.\",\n                \"\",\n                \"For instance, setting this to 16 selects routers for each path that have distinct\",\n                \"x.y.*.* IP addresses; 32 merely requires that each router have a unique IP.  Setting\",\n                \"this to 0 disables IP uniqueness entirely (i.e. paths can be selected that go through\",\n                \"different Lokinet routers on the same IP)\",\n            });\n\n        conf.define_option<std::chrono::seconds>(\n            \"paths\",\n            \"acceptable-expiry\",\n            Default{300s},\n            ClientOnly,\n            Comment{\n                \"The minimum expiry time a path/pivot must have for it to be eligible when switching\",\n                \"to a new path.  Inactive paths older than this will be replaced with new paths.\",\n            },\n            bounded_assignment_acceptor(acceptable_expiry, 0s, path::MAX_LIFETIME / 2, \"acceptable-expiry\"));\n\n        conf.define_option<std::chrono::seconds>(\n            \"paths\",\n            \"min-expiry\",\n            Default{60s},\n            ClientOnly,\n            Comment{\n                \"The minimum allowed path/pivot expiry time (in seconds) of a currently active outbound path.\",\n                \"When an active path reaches an expiry less than this value then the path to the remote will\",\n                \"be rotated immediately to use a newer path.\",\n                \"\",\n                \"This value cannot be larger than acceptable-expiry.\",\n            },\n            bounded_assignment_acceptor(min_expiry, 0s, path::MAX_LIFETIME / 2, \"min-expiry\"));\n\n        conf.define_option<std::chrono::milliseconds>(\n            \"paths\",\n            \"build-timeout\",\n            ClientOnly,\n            Default{10s},\n            Comment{\n                \"How long to wait for a session or path to establish before timing out the attempt.\",\n                \"Value is in seconds, or milliseconds with an ms suffix (e.g. 2500ms).\",\n            },\n            bounded_assignment_acceptor(build_timeout, 1ms, 1min, \"[paths]:build-timeout\"));\n\n        conf.add_options_validator([this] {\n            if (min_expiry > acceptable_expiry)\n                throw std::invalid_argument{\"[paths]:min-expiry cannot be longer than [paths]:acceptable-expiry\"};\n        });\n\n        conf.define_option<std::chrono::seconds>(\n            \"paths\",\n            \"ping-interval\",\n            Default{5s},\n            ClientOnly,\n            Comment{\"How frequently to send pings along built paths to test that they are still alive.\"},\n            lower_bounded_assignment_acceptor(ping_interval, 1s, \"[paths]:ping-interval\"));\n\n        conf.define_option<int>(\n            \"paths\",\n            \"max-missed-pings\",\n            Default{5},\n            ClientOnly,\n            Comment{\n                \"The maximum number of consecutive missed pings (see ping-interval) allowed for a path.  If a path\",\n                \"misses more than this, the path will be considered to have died and be replaced.\"},\n            lower_bounded_assignment_acceptor(max_missed_pings, 0, \"[paths]:max-missed-pings\"));\n\n#ifdef LOKINET_DEBUG_PATH_SEED\n        conf.define_option<uint64_t>(\n            \"paths\", \"debug-path-seed\", ClientOnly, Hidden, assignment_acceptor(debug_path_seed));\n#endif\n\n        conf.define_option<std::string>(\n            \"paths\",\n            \"strict-edge\",\n            ClientOnly,\n            MultiValue,\n            [this](std::string value) {\n                RouterID router;\n                if (value.size() == 64 && oxenc::is_hex(value))\n                    oxenc::from_hex(value.begin(), value.end(), router.begin());\n                else if (not router.from_relay_address(value))\n                    throw std::invalid_argument{\"[paths]:strict-edge: Invalid .snode pubkey: {}\"_format(value)};\n\n                if (not strict_edges.insert(router).second)\n                    throw std::invalid_argument{\n                        \"[paths]:strict-edge: Duplicate strict connect .snode value: {}\"_format(value)};\n            },\n            Comment{\n                R\"(List of service node public keys of \"edge\" nodes (also known as \"first hops\") that)\",\n                \"Lokinet will exclusively use when establishing paths through the network.  You can use\",\n                \"this to always use closer (i.e. lower latency) first hops, or to limit which network\",\n                \"nodes see connections from your IP address.\",\n                \"\",\n                \"Public keys can be provided either in native lokinet address format (ADDR.snode), or using\",\n                \"the 64-character hexademical pubkey notation common used for Session service nodes.\",\n                \"Specify this option multiple times to specify multiple allowed edge nodes.\",\n                \"\",\n                \"Note that only registered service node pubkeys will be used, and so connectivity will be\",\n                \"lost entirely if all of the listed pubkeys are or become deregistered.\",\n                \"\",\n                \"This option is incompatible with single-hop outbound path mode (see `[paths]:client-hops`\",\n                \"and `[paths]:relay-hops`).\",\n                \"\",\n                \"Note that if bootstrapping is needed a connection will be made to the configured bootstrap\",\n                \"nodes to obtain an initial router list.  See [bootstrap]:add-node if you want to also\",\n                \"override the nodes used for bootstrapping.\"});\n\n        conf.add_options_validator([this] {\n            if (strict_edges.empty())\n                return;\n            if (client_hops == 1)\n                throw std::invalid_argument{\n                    \"[paths]:strict-edge cannot be used with [paths]:client-hops=1 single hop mode\"};\n            if (relay_hops_ and *relay_hops_ == 1)\n                throw std::invalid_argument{\n                    \"[paths]:strict-edge cannot be used with [paths]:relay-hops=1 single hop mode\"};\n        });\n\n        conf.define_option<std::string>(\n            \"paths\",\n            \"blacklist-snode\",\n            ClientOnly,\n            MultiValue,\n            Comment{\n                \"Adds a lokinet relay `.snode` address to the list of relays to avoid when\",\n                \"connecting to edges or building paths. Can be specified multiple times.\",\n            },\n            [this](std::string arg) {\n                RouterID id;\n                if (not id.from_relay_address(arg))\n                    throw std::invalid_argument{\"Invalid RouterID: {}\"_format(arg)};\n\n                auto itr = snode_blacklist.emplace(std::move(id));\n                if (not itr.second)\n                    throw std::invalid_argument{\"Duplicate blacklist-snode: {}\"_format(arg)};\n            });\n\n#ifdef WITH_GEOIP\n        conf.defineOption<std::string>(\n            \"paths\",\n            \"exclude-country\",\n            ClientOnly,\n            MultiValue,\n            [this](std::string arg) { m_ExcludeCountries.emplace(lowercase_ascii_string(std::move(arg))); },\n            Comment{\n                \"Exclude a country given its 2 letter country code from being used in path builds.\",\n                \"For example:\",\n                \"    exclude-country=DE\",\n                \"would avoid building paths through routers with IPs in Germany.\",\n                \"This option can be specified multiple times to exclude multiple countries\",\n                \"Note that this option does not affect the final relay or pivot in outgoing paths\",\n            });\n#endif\n    }\n\n    std::unique_ptr<ConfigGenParameters> Config::make_gen_params() const\n    {\n        auto cgp = std::make_unique<ConfigGenParameters>();\n        cgp->default_data_dir = data_dir;\n        cgp->type = type;\n        return cgp;\n    }\n\n    Config::Config(config::Type type, std::filesystem::path conf_file) : data_dir{conf_file.parent_path()}, type{type}\n    {\n        auto ini = util::file_to_string(conf_file);\n        load_config_data(std::move(ini), std::move(conf_file));\n    }\n\n    Config::Config(config::Type type, std::string ini, std::filesystem::path default_data_dir)\n        : data_dir{std::move(default_data_dir)}, type{type}\n    {\n        load_config_data(std::move(ini));\n    }\n\n    static std::filesystem::path overrides_dir(const std::filesystem::path& datadir) { return datadir / \"conf.d\"; }\n\n    void Config::save()\n    {\n        const auto overridesDir = overrides_dir(data_dir);\n        if (not exists(overridesDir))\n            create_directories(overridesDir);\n        parser.save();\n    }\n\n    void Config::override(std::string section, std::string key, std::string value)\n    {\n        parser.add_override(overrides_dir(data_dir) / \"overrides.ini\", section, key, value);\n    }\n\n    void Config::load_overrides(ConfigDefinition& conf) const\n    {\n        ConfigParser parser;\n        const auto overridesDir = overrides_dir(data_dir);\n        if (exists(overridesDir))\n        {\n            for (const auto& f : std::filesystem::directory_iterator{overridesDir})\n            {\n                if (not f.is_regular_file() or f.path().extension() != \".ini\")\n                    continue;\n                ConfigParser parser;\n                try\n                {\n                    parser.load_file(f.path());\n                }\n                catch (const std::exception& e)\n                {\n                    throw std::runtime_error{\"Failed to load config file {}: {}\"_format(f.path().string(), e.what())};\n                }\n\n                parser.iter_all_sections([&](std::string_view section, const SectionValues& values) {\n                    for (const auto& [k, v] : values)\n                        conf.add_config_value(section, k, v);\n                });\n            }\n        }\n    }\n\n    void Config::add_default(std::string section, std::string key, std::string val)\n    {\n        additional.emplace_back(std::array<std::string, 3>{section, key, val});\n    }\n\n    void Config::load_config_data(std::string ini, std::optional<std::filesystem::path> filename)\n    {\n#ifdef LOKINET_EMBEDDED_ONLY\n        if (type != Type::EmbeddedClient)\n            throw std::runtime_error{\n                \"This lokinet build only supports embedded clients, not {}\"_format(to_string(type))};\n#endif\n        auto params = make_gen_params();\n        ConfigDefinition conf{type};\n        add_backcompat_opts(conf);\n        init_config(conf, *params);\n\n        for (const auto& item : additional)\n        {\n            conf.add_config_value(item[0], item[1], item[2]);\n        }\n\n        parser.clear();\n\n        if (filename)\n            parser.set_filename(*filename);\n        else\n            parser.set_filename(std::filesystem::path{});\n\n        parser.load_from_str(std::move(ini));\n\n        parser.iter_all_sections([&](std::string_view section, const SectionValues& values) {\n            for (const auto& pair : values)\n            {\n                conf.add_config_value(section, pair.first, pair.second);\n            }\n        });\n\n        load_overrides(conf);\n\n        conf.process();\n    }\n\n    void Config::init_config(ConfigDefinition& conf, const ConfigGenParameters& params)\n    {\n        router.define_config_options(conf, params);\n        exit.define_config_options(conf, params);\n        network.define_config_options(conf, params);\n        paths.define_config_options(conf, params);\n        dns.define_config_options(conf, params);\n        links.define_config_options(conf, params);\n        api.define_config_options(conf, params);\n        lokid.define_config_options(conf, params);\n        bootstrap.define_config_options(conf, params);\n        logging.define_config_options(conf, params);\n    }\n\n    void Config::add_backcompat_opts(ConfigDefinition& conf)\n    {\n        // These config sections don't exist anymore:\n\n        conf.define_option<std::string>(\"system\", \"user\", Deprecated);\n        conf.define_option<std::string>(\"system\", \"group\", Deprecated);\n        conf.define_option<std::string>(\"system\", \"pidfile\", Deprecated);\n\n        conf.define_option<std::string>(\"netdb\", \"dir\", Deprecated);\n\n        conf.define_option<std::string>(\"metrics\", \"json-metrics-path\", Deprecated);\n    }\n\n    void ensure_config(std::filesystem::path dataDir, std::filesystem::path confFile, bool overwrite, config::Type type)\n    {\n        // fail to overwrite if not instructed to do so\n        if (exists(confFile) && !overwrite)\n        {\n            log::info(logcat, \"Config file already exists; NOT creating new config\");\n            return;\n        }\n\n        const auto parent = confFile.parent_path();\n\n        // create parent dir if it doesn't exist\n        if ((not parent.empty()) and (not exists(parent)))\n        {\n            create_directory(parent);\n        }\n\n        log::info(logcat, \"Attempting to create config file for {} at file path:{}\", to_string(type), confFile);\n\n        llarp::Config config{type, \"\", dataDir};\n        auto confStr = config.generate_config_base();\n\n        try\n        {\n            util::buffer_to_file(confFile, confStr);\n        }\n        catch (const std::exception& e)\n        {\n            throw std::runtime_error{\"Failed to write config data to {}: {}\"_format(confFile, e.what())};\n        }\n\n        log::info(logcat, \"Generated new config (path: {})\", confFile);\n    }\n\n    std::string Config::generate_config_base()\n    {\n        auto params = make_gen_params();\n\n        llarp::ConfigDefinition def{type};\n        init_config(def, *params);\n\n        return def.generate_ini_config(true);\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/config/config.hpp",
    "content": "#pragma once\n\n#include \"definition.hpp\"\n#include \"ini.hpp\"\n\n#include <llarp/address/address.hpp>\n#include <llarp/address/ip_range.hpp>\n#include <llarp/auth/auth.hpp>\n#include <llarp/auth/file.hpp>\n#include <llarp/constants/files.hpp>\n#include <llarp/constants/path.hpp>\n#include <llarp/contact/relay_contact.hpp>\n#include <llarp/crypto/types.hpp>\n#include <llarp/dns/srv_data.hpp>\n#include <llarp/net/platform.hpp>\n#include <llarp/net/policy.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/str.hpp>\n\n#include <chrono>\n#include <cstdlib>\n#include <filesystem>\n#include <optional>\n#include <string>\n#include <unordered_set>\n#include <vector>\n\nnamespace llarp\n{\n    using SectionValues = llarp::ConfigParser::SectionValues;\n    using ConfigMap = llarp::ConfigParser::ConfigMap;\n\n    inline constexpr uint16_t DEFAULT_CLIENT_PORT{1091};\n    inline constexpr uint16_t DEFAULT_RELAY_PORT{1090};\n    inline const quic::Address DEFAULT_CLIENT_ADDR{\"0.0.0.0\", DEFAULT_CLIENT_PORT};\n    inline constexpr uint16_t DEFAULT_DNS_PORT{53};\n    inline constexpr int CLIENT_ROUTER_CONNECTIONS{4};\n\n    // TODO: don't use these maps. they're sloppy and difficult to follow\n    /// Small struct to gather all parameters needed for config generation to reduce the number of\n    /// parameters that need to be passed around.\n    struct ConfigGenParameters\n    {\n        ConfigGenParameters() = default;\n        virtual ~ConfigGenParameters() = default;\n\n        ConfigGenParameters(const ConfigGenParameters&) = delete;\n        ConfigGenParameters(ConfigGenParameters&&) = delete;\n\n        config::Type type;\n        std::filesystem::path default_data_dir;\n\n        /// get network platform (virtual for unit test mocks)\n        virtual const llarp::net::Platform* net_ptr();\n    };\n\n    struct RouterConfig\n    {\n        NetID net_id = NetID::MAINNET;\n\n        std::filesystem::path data_dir;\n\n        bool block_bogons = false;\n\n        int worker_threads = -1;\n        int net_threads = -1;\n\n        size_t job_que_size = 0;\n\n        std::optional<std::filesystem::path> rc_file;\n\n        bool is_relay = false;\n\n        std::optional<quic::Address> public_addr;\n\n        void define_config_options(ConfigDefinition& conf, const ConfigGenParameters& params);\n    };\n\n    /// config for path hop selection\n    struct PathConfig\n    {\n        int edge_connections{CLIENT_ROUTER_CONNECTIONS};\n\n        // If non-empty then *only* use these nodes for first hops.  (Except for single-hop outbound\n        // paths, which ignore this).\n        std::unordered_set<RouterID> strict_edges;\n\n        // Blacklist of relays to avoid using for edges or path hops\n        std::unordered_set<RouterID> snode_blacklist;\n\n        /// Number of paths to maintain for inbound reachability and network queries (such as\n        /// looking up client contacts).\n        int inbound_paths = 4;\n        int inbound_paths_extra = 0;\n\n        /// Number of times the same relay can be used as an inbound path pivot.  The default is 1,\n        /// which means every inbound path uses a distinct relay.\n        int inbound_pivot_reuse = 1;\n\n        /// Length of the \"inbound\" paths we use for inbound connections and network queries.\n        /// If unset, use client_hops.\n        std::optional<int> inbound_hops_;\n\n        // Retrieves the above, with built-in fallback to the client_hops value if not set.\n        int inbound_hops() const { return inbound_hops_.value_or(client_hops); }\n\n        /// Number of paths to maintain to *each* outgoing remote (relay or snode).\n        int outbound_paths = 2;\n\n        /// Number of hops when establishing a session to a relay (i.e. to a .snode, not *through* a\n        /// relay to reach a client).\n        std::optional<int> relay_hops_;\n\n        /// Retrieves the working value for relay-hops: the value if explicitly set, else one more\n        /// than the configured client hops.\n        int relay_hops() const { return relay_hops_.value_or(std::min(client_hops + 1, path::BUILD_LENGTH)); }\n\n        /// Number of hops when building an aligned path to a relay to reach a client on the other\n        /// side.\n        int client_hops = 3;\n\n        /// in our hops what netmask will we use for unique ips for hops\n        /// i.e. 32 for every hop unique ip, 24 unique /24 per hop, etc\n        uint8_t unique_hop_netmask{0};\n\n        // TODO: some day, if we ever support routers using IPv6, there would need to be a different\n        // ipv6 netmask value.\n\n        std::chrono::seconds min_expiry = 1min;\n        std::chrono::seconds acceptable_expiry = 5min;\n\n        std::chrono::milliseconds build_timeout{10s};\n        std::chrono::seconds ping_interval{5s};\n        int max_missed_pings{5};\n\n        // DEBUG ONLY: if set, this uses a repeatable RNG with the given seed for reproducible path\n        // selection.  This option only has an effect if lokinet is configured with\n        // -DLOKINET_DEBUG_PATH_SEED=ON (which is disabled by default).\n        std::optional<uint64_t> debug_path_seed;\n\n        void define_config_options(ConfigDefinition& conf, const ConfigGenParameters& params);\n    };\n\n    /** TODO:\n        - finalize supervenience of ExitConfig over deprecated config entries\n     */\n\n    /// Config options related to exit node services\n    struct ExitConfig\n    {\n        bool exit_enabled{false};\n\n        // Used by RemoteHandler to provide auth tokens for remote exits\n        std::unordered_map<NetworkAddress, std::string> auth_tokens;\n        std::unordered_map<std::string, std::string> sns_auth_tokens;\n\n        net::ExitPolicy exit_policy;\n\n        // Remote client ONS exit addresses mapped to local IP ranges pending ONS address resolution\n        // Reserved local IP ranges mapped to remote client ONS addresses (pending ONS resolution)\n        std::unordered_map<std::string, std::vector<std::variant<ipv4_range, ipv6_range>>> sns_ranges;\n\n        // Reserved local IP ranges mapped to remote client exit addresses\n        std::unordered_map<NetworkAddress, std::vector<std::variant<ipv4_range, ipv6_range>>> ranges;\n\n        void define_config_options(ConfigDefinition& conf, const ConfigGenParameters& params);\n    };\n\n    struct NetworkConfig\n    {\n        bool enable_profiling{false};\n        bool save_profiles{false};\n\n        std::optional<std::filesystem::path> keyfile;\n\n        bool enable_ipv6{false};\n        bool is_reachable{false};\n\n        /*   Auth specific config   */\n        auth::AuthType auth_type = auth::AuthType::NONE;\n        auth::AuthFileType auth_file_type = auth::AuthFileType::HASHES;\n\n        std::optional<std::string> auth_endpoint;\n        std::optional<std::string> auth_method;\n\n        std::unordered_set<NetworkAddress> auth_whitelist;\n\n        std::unordered_set<std::string> auth_static_tokens;\n\n        std::vector<std::filesystem::path> auth_files;\n\n        std::unordered_set<llarp::dns::SRVData> srv_records;\n\n        /* TESTNET: Under modification */\n\n        // Contents of this file are read directly into ::_reserved_local_addrs\n        std::optional<std::filesystem::path> addr_map_persist_file;\n\n        // the only member that refers to an actual interface\n        std::optional<std::string> _if_name;\n\n        std::optional<ipv4_net> _local_ip_net;    // [network]:ifaddr\n        std::optional<ipv6_net> _local_ipv6_net;  // [network]:ipv6\n\n        // Remote exit or hidden service addresses mapped to fixed local IP addresses\n        // TODO:\n        //  - load directly into TunEndpoint mapping\n        //      - when a session is created, check mapping when assigning IP's\n        std::unordered_map<NetworkAddress, ipv4> _reserved_local_ipv4;\n        std::unordered_map<NetworkAddress, ipv6> _reserved_local_ipv6;\n\n        // TESTNET: moved into ExitConfig!\n        bool allow_exit{false};\n        // Used by RemoteHandler to provide auth tokens for remote exits\n        std::unordered_map<NetworkAddress, std::string> exit_auths;\n        std::unordered_map<std::string, std::string> sns_exit_auths;\n        std::optional<net::ExitPolicy> traffic_policy;\n\n        // TESTNET: move into ExitConfig!\n        bool enable_route_poker{false};\n        bool blackhole_routes{false};\n\n        void define_config_options(ConfigDefinition& conf, const ConfigGenParameters& params);\n    };\n\n    struct DnsConfig\n    {\n        bool l3_intercept{false};\n\n        std::vector<std::filesystem::path> hostfiles;\n\n        /* TESTNET: Under modification */\n        std::vector<quic::Address> _upstream_dns;\n        quic::Address _default_dns{\"9.9.9.10\", DEFAULT_DNS_PORT};\n        std::optional<quic::Address> _query_bind;\n        std::vector<quic::Address> _bind_addrs;\n\n        // Deprecated\n        // std::vector<SockAddr_deprecated> upstream_dns;\n        // std::optional<SockAddr_deprecated> query_bind;\n        // std::vector<SockAddr_deprecated> bind_addr;\n        /*************************************/\n\n        std::unordered_multimap<std::string, std::string> extra_opts;\n\n        void define_config_options(ConfigDefinition& conf, const ConfigGenParameters& params);\n    };\n\n    struct LinksConfig\n    {\n        // DEPRECATED -- use [router]:public_addr/port instead\n        std::optional<quic::Address> public_addr;\n\n        std::optional<quic::Address> listen_addr;\n\n        void define_config_options(ConfigDefinition& conf, const ConfigGenParameters& params);\n    };\n\n    struct ApiConfig\n    {\n        bool enable_rpc_server = false;\n        std::vector<std::string> rpc_bind_addrs;\n\n        void define_config_options(ConfigDefinition& conf, const ConfigGenParameters& params);\n    };\n\n    struct LokidConfig\n    {\n        std::filesystem::path id_keyfile;\n        std::string rpc_addr;\n        bool disable_testing = false;\n\n        void define_config_options(ConfigDefinition& conf, const ConfigGenParameters& params);\n    };\n\n    struct BootstrapConfig\n    {\n        std::vector<std::filesystem::path> files;\n\n        void define_config_options(ConfigDefinition& conf, const ConfigGenParameters& params);\n    };\n\n    struct LoggingConfig\n    {\n        // Log type.  If nullopt then lokinet will not set up logging sinks at all (this is\n        // primarily aimed at embedded clients that have already set up logging).\n        std::optional<log::Type> type = log::Type::Print;\n\n        // levels can either be just a level (\"warn\"), or a list of cat levels such as:\n        // \"*=warning, cat1=debug, cat2*=trace\".  See oxen-logging for more details.  If empty then\n        // logging levels will not be set at all (and, again, is most useful for embedded clients).\n        std::string levels;\n\n        std::string file;\n\n        void define_config_options(ConfigDefinition& conf, const ConfigGenParameters& params);\n    };\n\n    struct Config\n    {\n        // Creates a config for the given lokinet instance type (relay, full client, or embedded\n        // client), loading configuration data from the given string, if given (all default config\n        // otherwise).  The default data directory (if not explicit set in the given config string)\n        // can optionally be provided.  If omitted (and not set in the string) it defaults to cwd.\n        Config(\n            config::Type type,\n            std::string config = \"\",\n            std::filesystem::path default_data_dir = std::filesystem::current_path());\n\n        // Creates a config for the given lokinet instance type (relay, full client, or embedded\n        // client), loading configuration data from an existing file.  The default data directory\n        // (if not set in the config itself) will be the directory containing the given config file.\n        Config(config::Type type, std::filesystem::path config_file);\n\n        Config(Config&&) = default;\n        Config(const Config&) = default;\n        Config& operator=(Config&&) = default;\n        Config& operator=(const Config&) = default;\n\n        virtual ~Config() = default;\n\n        /// create generation params (virtual for unit test mock)\n        virtual std::unique_ptr<ConfigGenParameters> make_gen_params() const;\n\n        RouterConfig router;\n        ExitConfig exit;\n        NetworkConfig network;\n        PathConfig paths;\n        DnsConfig dns;\n        LinksConfig links;\n        ApiConfig api;\n        LokidConfig lokid;\n        BootstrapConfig bootstrap;\n        LoggingConfig logging;\n\n        // Initialize config definition\n        void init_config(ConfigDefinition& conf, const ConfigGenParameters& params);\n\n        /// Insert config entries for backwards-compatibility (e.g. so that the config system will\n        /// tolerate old values that are no longer accepted)\n        ///\n        /// @param conf is the config to modify\n        void add_backcompat_opts(ConfigDefinition& conf);\n\n        std::string generate_config_base();\n\n        void save();\n\n        void override(std::string section, std::string key, std::string value);\n\n        void add_default(std::string section, std::string key, std::string value);\n\n        bool relay() const { return type == config::Type::Relay; }\n        bool embedded() const { return type == config::Type::EmbeddedClient; }\n        bool client() const { return !relay(); }\n\n      private:\n        void load_config_data(std::string ini, std::optional<std::filesystem::path> fname = std::nullopt);\n\n        void load_overrides(ConfigDefinition& conf) const;\n\n        std::vector<std::array<std::string, 3>> additional;\n        ConfigParser parser;\n        std::filesystem::path data_dir{std::filesystem::current_path()};\n        config::Type type;\n    };\n\n    // Ensures that a conf file exists, writing a default one if not present.  Only for full\n    // clients/routers (i.e. not embedded clients).\n    void ensure_config(\n        std::filesystem::path dataDir, std::filesystem::path confFile, bool overwrite, config::Type type);\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/config/definition.cpp",
    "content": "#include \"definition.hpp\"\n\n#include <llarp/util/formattable.hpp>\n#include <llarp/util/logging.hpp>\n\n#include <cassert>\n#include <iterator>\n#include <stdexcept>\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"config.def\");\n\n    static constexpr std::array true_values = {\"true\", \"TRUE\", \"T\", \"on\", \"ON\", \"1\", \"yes\", \"enable\", \"enabled\"};\n    static constexpr std::array false_values = {\"false\", \"FALSE\", \"F\", \"off\", \"OFF\", \"1\", \"no\", \"disable\", \"disabled\"};\n    std::optional<bool> parse_boolean(std::string_view input)\n    {\n        if (std::ranges::any_of(true_values, [&input](const auto& v) { return input == v; }))\n            return true;\n        if (std::ranges::any_of(false_values, [&input](const auto& v) { return input == v; }))\n            return false;\n        return std::nullopt;\n    }\n\n    template <>\n    bool OptionDefinition<bool>::from_string(const std::string& input)\n    {\n        if (auto b = parse_boolean(input))\n            return *b;\n        throw std::invalid_argument{\"{} is not a valid bool\"_format(input)};\n    }\n\n    ConfigDefinition& ConfigDefinition::define_option(std::unique_ptr<OptionDefinitionBase> def)\n    {\n        using namespace config;\n        // If explicitly deprecated or is a {client,relay} option in a {relay,client} config then\n        // add a dummy, warning option instead of this one.\n        bool bad = def->deprecated || (type == config::Type::Relay && def->client_only)\n            || (type != config::Type::Relay && def->relay_only)\n            || (type == config::Type::EmbeddedClient && def->no_embedded);\n        if (bad)\n            return define_option<std::string>(\n                def->section,\n                def->name,\n                MultiValue,\n                Hidden,\n                [deprecated = def->deprecated, type = type, opt = \"[{}]:{}\"_format(def->section, def->name)](\n                    std::string_view) {\n                    log::warning(\n                        logcat,\n                        \"*** WARNING: The config option {} is {} and has been ignored\",\n                        opt,\n                        (deprecated ? \"deprecated\" : \"invalid in {} configuration files\"_format(to_string(type))));\n                });\n\n        auto [sectionItr, newSect] = definitions.try_emplace(def->section);\n        if (newSect)\n            section_ordering.push_back(def->section);\n        auto& section = sectionItr->first;\n\n        auto [it, added] = definitions[section].try_emplace(std::string{def->name}, std::move(def));\n        if (!added)\n            throw std::invalid_argument{\"definition for [{}]:{} already exists\"_format(def->section, def->name)};\n\n        definition_ordering[section].push_back(it->first);\n\n        if (!it->second->comments.empty())\n            add_option_comments(section, it->first, std::move(it->second->comments));\n\n        return *this;\n    }\n\n    ConfigDefinition& ConfigDefinition::add_config_value(\n        std::string_view section, std::string_view name, std::string_view value)\n    {\n        // see if we have an undeclared handler to fall back to in case section or section:name is\n        // absent\n        auto undItr = undeclared_handlers.find(std::string(section));\n        bool haveUndeclaredHandler = (undItr != undeclared_handlers.end());\n\n        // get section, falling back to undeclared handler if needed\n        auto secItr = definitions.find(std::string(section));\n        if (secItr == definitions.end())\n        {\n            // fallback to undeclared handler if available\n            if (not haveUndeclaredHandler)\n                throw std::invalid_argument{\"unrecognized section [{}]\"_format(section)};\n            auto& handler = undItr->second;\n            handler(section, name, value);\n            return *this;\n        }\n\n        // section was valid, get definition by name\n        // fall back to undeclared handler if needed\n        auto& sectionDefinitions = secItr->second;\n        auto defItr = sectionDefinitions.find(std::string(name));\n        if (defItr != sectionDefinitions.end())\n        {\n            std::unique_ptr<OptionDefinitionBase>& definition = defItr->second;\n            definition->parse_value(std::string(value));\n            return *this;\n        }\n\n        if (not haveUndeclaredHandler)\n            throw std::invalid_argument{\"unrecognized option [{}]: {}\"_format(section, name)};\n\n        auto& handler = undItr->second;\n        handler(section, name, value);\n        return *this;\n    }\n\n    void ConfigDefinition::add_undeclared_handler(const std::string& section, UndeclaredValueHandler handler)\n    {\n        auto itr = undeclared_handlers.find(section);\n        if (itr != undeclared_handlers.end())\n            throw std::logic_error{\"section {} already has a handler\"_format(section)};\n\n        undeclared_handlers[section] = std::move(handler);\n    }\n\n    void ConfigDefinition::remove_undeclared_handler(const std::string& section)\n    {\n        auto itr = undeclared_handlers.find(section);\n        if (itr != undeclared_handlers.end())\n            undeclared_handlers.erase(itr);\n    }\n\n    void ConfigDefinition::add_options_validator(std::function<void()> validator)\n    {\n        options_validators.push_back(std::move(validator));\n    }\n\n    void ConfigDefinition::validate_required_fields()\n    {\n        visit_sections([&](const std::string& section, const DefinitionMap&) {\n            visit_definitions(section, [&](const std::string&, const std::unique_ptr<OptionDefinitionBase>& def) {\n                if (def->required and def->get_number_found() < 1)\n                {\n                    throw std::invalid_argument{\"[{}]:{} is required but missing\"_format(section, def->name)};\n                }\n\n                // should be handled earlier in OptionDefinition::parse_value()\n                assert(def->get_number_found() <= 1 or def->multi_valued);\n            });\n        });\n    }\n\n    void ConfigDefinition::accept_all_options()\n    {\n        visit_sections([this](const std::string& section, const DefinitionMap&) {\n            visit_definitions(section, [](const std::string&, const std::unique_ptr<OptionDefinitionBase>& def) {\n                def->try_accept();\n            });\n        });\n    }\n\n    void ConfigDefinition::validate_all_options()\n    {\n        for (auto& v : options_validators)\n            v();\n    }\n\n    void ConfigDefinition::add_section_comments(const std::string& section, std::vector<std::string> comments)\n    {\n        auto& sectionComments = section_comments[section];\n        for (auto& c : comments)\n            sectionComments.emplace_back(std::move(c));\n    }\n\n    void ConfigDefinition::add_option_comments(\n        const std::string& section, const std::string& name, std::vector<std::string> comments)\n    {\n        auto& defComments = definition_comments[section][name];\n        if (defComments.empty())\n            defComments = std::move(comments);\n        else\n            defComments.insert(\n                defComments.end(), std::make_move_iterator(comments.begin()), std::make_move_iterator(comments.end()));\n    }\n\n    std::string ConfigDefinition::generate_ini_config(bool useValues)\n    {\n        std::string ini;\n        auto ini_append = std::back_inserter(ini);\n\n        int sectionsVisited = 0;\n\n        visit_sections([&](const std::string& section, const DefinitionMap&) {\n            std::string sect_str;\n            auto sect_append = std::back_inserter(sect_str);\n\n            visit_definitions(section, [&](const std::string& name, const std::unique_ptr<OptionDefinitionBase>& def) {\n                bool has_comment = false;\n                // TODO: as above, this will create empty objects\n                // TODO: as above (but more important): this won't handle definitions with no\n                // entries\n                //       (i.e. those handled by UndeclaredValueHandler's)\n                for (const std::string& comment : definition_comments[section][name])\n                {\n                    fmt::format_to(sect_append, \"\\n# {}\", comment);\n                    has_comment = true;\n                }\n\n                if (useValues and def->get_number_found() > 0)\n                {\n                    for (const auto& val : def->values_as_string())\n                        fmt::format_to(sect_append, \"\\n{}={}\", name, val);\n                    *sect_append = '\\n';\n                }\n                else if (not def->hidden)\n                {\n                    if (auto defaults = def->default_values_as_string(); not defaults.empty())\n                        for (const auto& val : defaults)\n                            fmt::format_to(sect_append, \"\\n{}{}={}\", def->required ? \"\" : \"#\", name, val);\n                    else\n                        // We have no defaults so we append it as \"#opt-name=\" so that we show\n                        // the option name, and make it simple to uncomment and edit to the\n                        // desired value.\n                        fmt::format_to(sect_append, \"\\n#{}=\", name);\n                    *sect_append = '\\n';\n                }\n                else if (has_comment)\n                    *sect_append = '\\n';\n            });\n\n            if (sect_str.empty())\n                return;  // Skip sections with no options\n\n            if (sectionsVisited > 0)\n                ini += \"\\n\\n\";\n\n            fmt::format_to(ini_append, \"[{}]\\n\", section);\n\n            // TODO: this will create empty objects as a side effect of map's operator[]\n            // TODO: this also won't handle sections which have no definition\n            for (const std::string& comment : section_comments[section])\n            {\n                fmt::format_to(ini_append, \"# {}\\n\", comment);\n            }\n            ini += \"\\n\";\n            ini += sect_str;\n\n            sectionsVisited++;\n        });\n\n        return ini;\n    }\n\n    const std::unique_ptr<OptionDefinitionBase>& ConfigDefinition::lookup_definition_or_throw(\n        std::string_view section, std::string_view name) const\n    {\n        const auto sectionItr = definitions.find(std::string(section));\n        if (sectionItr == definitions.end())\n            throw std::invalid_argument{\"No config section [{}]\"_format(section)};\n\n        auto& sectionDefinitions = sectionItr->second;\n        const auto definitionItr = sectionDefinitions.find(std::string(name));\n        if (definitionItr == sectionDefinitions.end())\n            throw std::invalid_argument{\"No config item {} within section {}\"_format(name, section)};\n\n        return definitionItr->second;\n    }\n\n    std::unique_ptr<OptionDefinitionBase>& ConfigDefinition::lookup_definition_or_throw(\n        std::string_view section, std::string_view name)\n    {\n        return const_cast<std::unique_ptr<OptionDefinitionBase>&>(\n            const_cast<const ConfigDefinition*>(this)->lookup_definition_or_throw(section, name));\n    }\n\n    void ConfigDefinition::visit_sections(SectionVisitor visitor) const\n    {\n        for (const std::string& section : section_ordering)\n        {\n            const auto itr = definitions.find(section);\n            assert(itr != definitions.end());\n            visitor(section, itr->second);\n        }\n    };\n    void ConfigDefinition::visit_definitions(const std::string& section, DefVisitor visitor) const\n    {\n        const auto& defs = definitions.at(section);\n        const auto& defOrdering = definition_ordering.at(section);\n        for (const std::string& name : defOrdering)\n        {\n            const auto itr = defs.find(name);\n            assert(itr != defs.end());\n            visitor(name, itr->second);\n        }\n    };\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/config/definition.hpp",
    "content": "#pragma once\n\n#include <llarp/util/str.hpp>\n\n#include <fmt/chrono.h>\n#include <fmt/core.h>\n\n#include <cassert>\n#include <chrono>\n#include <concepts>\n#include <filesystem>\n#include <functional>\n#include <initializer_list>\n#include <memory>\n#include <optional>\n#include <sstream>\n#include <stdexcept>\n#include <type_traits>\n#include <unordered_map>\n#include <vector>\n\nnamespace llarp\n{\n    namespace config\n    {\n        enum class Type\n        {\n            Relay,\n            FullClient,\n            EmbeddedClient\n        };\n\n        inline constexpr std::string_view to_string(Type t)\n        {\n            switch (t)\n            {\n                case Type::Relay:\n                    return \"service node\";\n                case Type::FullClient:\n                    return \"client\";\n                case Type::EmbeddedClient:\n                    return \"embedded client\";\n            }\n            return \"unknown config type\";\n        }\n\n        namespace flag\n        {\n            // Base class for the following option flag types\n            struct opt\n            {};\n\n            struct REQUIRED : opt\n            {};\n            struct HIDDEN : opt\n            {};\n            struct MULTIVALUE : opt\n            {};\n            struct NOTEMBEDDED : opt\n            {};\n            struct RELAYONLY : NOTEMBEDDED\n            {};\n            struct CLIENTONLY : opt\n            {};\n            struct FULLCLIENTONLY : CLIENTONLY, NOTEMBEDDED\n            {};\n            struct DEPRECATED : opt\n            {};\n        }  // namespace flag\n\n        /// Value to pass for an OptionDefinition to indicate that the option is required\n        inline constexpr flag::REQUIRED Required{};\n        /// Value to pass for an OptionDefinition to indicate that the option should be hidden from\n        /// the generate config file if it is unset (and has no comment).  Typically for deprecated,\n        /// renamed options that still do something, and for internal dev options that aren't\n        /// usefully exposed. (For do-nothing deprecated options use Deprecated instead).\n        inline constexpr flag::HIDDEN Hidden{};\n        /// Value to pass for an OptionDefinition to indicate that the option takes multiple values\n        inline constexpr flag::MULTIVALUE MultiValue{};\n        /// Value to pass for an option that should only be set for relay configs. If found in a\n        /// client config it be ignored (but will produce a warning).\n        inline constexpr flag::RELAYONLY RelayOnly{};\n        /// Value to pass for an option that should only be set for client configs. If found in a\n        /// relay config it will be ignored (but will produce a warning).\n        inline constexpr flag::CLIENTONLY ClientOnly{};\n        /// Value to pass for an option that should only be set for full client configs but not\n        /// relay configs or embedded client configs. If found in either of this it will be ignored\n        /// (but will produce a warning).\n        inline constexpr flag::FULLCLIENTONLY FullClientOnly{};\n        /// Value to pass for an option that is not allowed in embedded configs.  This is implied by\n        /// RelayOnly and FullClientOnly, but can be specified separately for options supported in\n        /// both full clients and relays but not embedded configs.  If such an option is specified\n        /// for an embedded client config it will be ignored (but will produce a warning).\n        inline constexpr flag::NOTEMBEDDED NotEmbedded{};\n        /// Value to pass for an option that is deprecated and does nothing and should be ignored\n        /// (with a deprecation warning) if specified.  Note that Deprecated implies Hidden, and\n        /// that {client,relay}-only options in a {relay,client} config are also considered\n        /// Deprecated.\n        inline constexpr flag::DEPRECATED Deprecated{};\n\n        /// Wrapper to specify a default value to an OptionDefinition\n        template <typename T>\n        struct Default\n        {\n            T val;\n            constexpr explicit Default(T val) : val{std::move(val)} {}\n        };\n\n        /// Adds one or more comment lines to the option definition.\n        struct Comment\n        {\n            std::vector<std::string> comments;\n            explicit Comment(std::initializer_list<std::string> comments) : comments{std::move(comments)} {}\n        };\n\n        /// A convenience function that returns an acceptor which assigns to a reference.\n        ///\n        /// Note that this holds on to the reference; it must only be used when this is safe to do.\n        /// In particular, a reference to a local variable may be problematic.\n        template <typename T>\n        auto assignment_acceptor(T& ref)\n        {\n            return [&ref](T arg) { ref = std::move(arg); };\n        }\n\n        /// Returns a value acceptor that accepts any value within a range of values.\n        template <std::totally_ordered T>\n        auto bounded_assignment_acceptor(\n            T& ref, std::type_identity_t<T> min, std::type_identity_t<T> max, std::string setting_name)\n        {\n            return [&ref, min = std::move(min), max = std::move(max), name = std::move(setting_name)](T arg) {\n                if (arg < min || arg > max)\n                    throw std::invalid_argument{fmt::format(\"{} must be >= {} and <= {}\", name, min, max)};\n                ref = std::move(arg);\n            };\n        }\n        template <std::totally_ordered T>\n        auto lower_bounded_assignment_acceptor(T& ref, std::type_identity_t<T> min, std::string setting_name)\n        {\n            return [&ref, min = std::move(min), name = std::move(setting_name)](T arg) {\n                if (arg < min)\n                    throw std::invalid_argument{fmt::format(\"{} must be >= {}\", name, min)};\n                ref = std::move(arg);\n            };\n        }\n        template <std::totally_ordered T>\n        auto upper_bounded_assignment_acceptor(T& ref, std::type_identity_t<T> max, std::string setting_name)\n        {\n            return [&ref, max = std::move(max), name = std::move(setting_name)](T arg) {\n                if (arg > max)\n                    throw std::invalid_argument{fmt::format(\"{} must be <= {}\", name, max)};\n                ref = std::move(arg);\n            };\n        }\n        template <std::totally_ordered T>\n        auto bounded_assignment_acceptor(\n            std::optional<T>& ref, std::type_identity_t<T> min, std::type_identity_t<T> max, std::string setting_name)\n        {\n            return [&ref, min = std::move(min), max = std::move(max), name = std::move(setting_name)](T arg) {\n                if (arg < min || arg > max)\n                    throw std::invalid_argument{fmt::format(\"{} must be >= {} and <= {}\", name, min, max)};\n                ref = std::move(arg);\n            };\n        }\n\n        template <typename T>\n        constexpr bool is_default = false;\n        template <typename T>\n        constexpr bool is_default<Default<T>> = true;\n        template <typename U>\n        constexpr bool is_default<U&> = is_default<std::remove_cvref_t<U>>;\n\n        template <typename T>\n        constexpr bool is_default_array = false;\n        template <typename T, size_t N>\n        constexpr bool is_default_array<std::array<Default<T>, N>> = true;\n        template <typename U>\n        constexpr bool is_default_array<U&> = is_default_array<std::remove_cvref_t<U>>;\n\n        template <typename Option, typename T>\n        concept is_option = std::is_base_of_v<flag::opt, std::remove_cvref_t<Option>> or std::is_same_v<Comment, Option>\n            or is_default<Option> or is_default_array<Option> or std::is_invocable_v<std::remove_cvref_t<Option>, T>;\n    }  // namespace config\n\n    /// A base class for specifying config options and their constraints. The basic to/from string\n    /// type functions are provided pure-virtual. The type-aware implementations which implement\n    /// these functions are templated classes. One reason for providing a non-templated base class\n    /// is so that they can all be mixed into the same containers (albiet as pointers).\n    struct OptionDefinitionBase\n    {\n        template <typename... T>\n        OptionDefinitionBase(std::string section_, std::string name_, const T&...)\n            : section(std::move(section_)),\n              name(std::move(name_)),\n              required{(std::derived_from<T, config::flag::REQUIRED> || ...)},\n              multi_valued{(std::derived_from<T, config::flag::MULTIVALUE> || ...)},\n              deprecated{(std::derived_from<T, config::flag::DEPRECATED> || ...)},\n              hidden{deprecated || (std::derived_from<T, config::flag::HIDDEN> || ...)},\n              relay_only{(std::derived_from<T, config::flag::RELAYONLY> || ...)},\n              client_only{(std::derived_from<T, config::flag::CLIENTONLY> || ...)},\n              no_embedded{(std::derived_from<T, config::flag::NOTEMBEDDED> || ...)}\n        {}\n\n        virtual ~OptionDefinitionBase() = default;\n\n        /// Subclasses should provide their default value as a string\n        ///\n        /// @return the option's default value represented as a string\n        virtual std::vector<std::string> default_values_as_string() = 0;\n\n        /// Subclasses should parse and store the provided input\n        ///\n        /// @param input is the string input to interpret\n        virtual void parse_value(const std::string& input) = 0;\n\n        /// Subclasses should provide the number of values found.\n        ///\n        /// @return number of values found\n        virtual size_t get_number_found() const = 0;\n\n        /// Subclasess should write their parsed values as strings.\n        ///\n        /// @return the option's value(s) as strings\n        virtual std::vector<std::string> values_as_string() = 0;\n\n        /// Subclassess should call their acceptor, if present. See OptionDefinition for more\n        /// details.\n        ///\n        /// @throws if the acceptor throws or the option is required but missing\n        virtual void try_accept() const = 0;\n\n        std::string section;\n        std::string name;\n        bool required = false;\n        bool multi_valued = false;\n        bool deprecated = false;\n        bool hidden = false;\n        bool relay_only = false;\n        bool client_only = false;\n        bool no_embedded = false;\n        // Temporarily holds comments given during construction until the option is actually added\n        // to the owning ConfigDefinition.\n        std::vector<std::string> comments;\n    };\n\n    template <typename T>\n    constexpr bool is_chrono_option = false;\n    template <typename R, typename P>\n    constexpr bool is_chrono_option<std::chrono::duration<R, P>> = true;\n\n    /// The primary type-aware implementation of OptionDefinitionBase, this templated class allows\n    /// for implementations which can use fmt::format for conversion to string and\n    /// std::istringstream for input from string.  For chrono types of seconds or larger, we treat\n    /// raw integers as seconds, and otherwise allow an integer followed by a s/m/min/h/d suffix.\n    ///\n    /// Note that types (T) used as template parameters here must be used verbatim when calling\n    /// ConfigDefinition::getConfigValue(). Similar types such as uint32_t and int32_t cannot be\n    /// mixed.\n    template <typename T>\n    struct OptionDefinition : public OptionDefinitionBase\n    {\n        /// Constructor. Arguments are passed directly to OptionDefinitionBase.\n        ///\n        /// @param defaultValue_ is used in the following situations:\n        /// 1) as the return value for getValue() if there is no parsed value and required==false\n        /// 2) as the output in default_values_as_string(), used to generate config files\n        /// 3) as the output in valueAsString(), used to generate config files\n        ///\n        /// @param opts - 0 or more of config::Required, config::Hidden, config::Default{...}, etc.\n        /// tagged options or an invocable acceptor validate and internalize input (e.g. copy it for\n        /// runtime use). The acceptor should throw an exception with a useful message if it is not\n        /// acceptable.  Parameters may be passed in any order.\n        template <config::is_option<T>... Options>\n        OptionDefinition(std::string section_, std::string name_, Options&&... opts)\n            : OptionDefinitionBase(section_, name_, opts...)\n        {\n            constexpr bool has_default = ((config::is_default_array<Options> || config::is_default<Options>) || ...);\n            constexpr bool has_required = (std::is_same_v<std::remove_cvref_t<Options>, config::flag::REQUIRED> || ...);\n            constexpr bool has_hidden = (std::is_same_v<std::remove_cvref_t<Options>, config::flag::HIDDEN> || ...);\n            static_assert(not(has_default and has_required), \"Default{...} and Required are mutually exclusive\");\n            static_assert(not(has_hidden and has_required), \"Hidden and Required are mutually exclusive\");\n\n            (extract_default(std::forward<Options>(opts)), ...);\n            (extract_acceptor(std::forward<Options>(opts)), ...);\n            (extract_comments(std::forward<Options>(opts)), ...);\n        }\n\n        /// Extracts a default value from an config::Default<U> or an array of defaults (for\n        /// multi-valued options with multi-value default); ignores anything else.\n        template <typename U>\n        void extract_default(U&& defaultValue_)\n        {\n            if constexpr (config::is_default_array<U>)\n            {\n                if (!multi_valued)\n                    throw std::logic_error{\"Array config defaults require multiValue mode\"};\n\n                default_values.clear();\n                default_values.reserve(defaultValue_.size());\n                for (const auto& def : defaultValue_)\n                    default_values.push_back(def.val);\n            }\n            else if constexpr (config::is_default<U>)\n            {\n                static_assert(\n                    std::is_convertible_v<decltype(std::forward<U>(defaultValue_).val), T>,\n                    \"Cannot convert given llarp::config::Default to the required value type\");\n                default_values = {std::forward<U>(defaultValue_).val};\n            }\n        }\n\n        /// Extracts an acceptor (i.e. something callable with a `T`) from options; ignores anything\n        /// that isn't callable.\n        template <typename U>\n        void extract_acceptor(U&& acceptor_)\n        {\n            if constexpr (std::is_invocable_v<U, T>)\n                acceptor = std::forward<U>(acceptor_);\n        }\n\n        /// Extracts option Comments and forwards them addOptionComments.\n        template <typename U>\n        void extract_comments(U&& comment)\n        {\n            if constexpr (std::is_same_v<std::remove_cvref_t<U>, config::Comment>)\n                comments = std::forward<U>(comment).comments;\n        }\n\n        /// Returns the first parsed value, if available. Otherwise, provides the (first) default\n        /// value if the option is not required. Otherwise, returns an empty optional.\n        ///\n        /// @return an optional with the parsed value, the (first) default value, or no value.\n        std::optional<T> get_value() const\n        {\n            if (parsed_values.empty())\n            {\n                if (required || default_values.empty())\n                    return std::nullopt;\n                return default_values.front();\n            }\n            return parsed_values.front();\n        }\n\n        /// Returns the number of values found.\n        ///\n        /// @return number of values found\n        size_t get_number_found() const override { return parsed_values.size(); }\n\n        std::vector<std::string> default_values_as_string() override\n        {\n            if (default_values.empty())\n                return {};\n            if constexpr (std::is_same_v<std::filesystem::path, T>)\n                return {{default_values.front().string()}};\n            else\n            {\n                std::vector<std::string> def_strs;\n                def_strs.reserve(default_values.size());\n                for (const auto& v : default_values)\n                {\n                    if constexpr (std::is_same_v<bool, T>)\n                        def_strs.push_back(fmt::format(\"{}\", (bool)v));\n                    else\n                        def_strs.push_back(fmt::format(\"{}\", v));\n                }\n                return def_strs;\n            }\n        }\n\n        void parse_value(const std::string& input) override\n        {\n            if (not multi_valued and parsed_values.size() > 0)\n            {\n                throw std::invalid_argument{fmt::format(\"duplicate value for {}\", name)};\n            }\n\n            parsed_values.emplace_back(from_string(input));\n        }\n\n        T from_string(const std::string& input)\n        {\n            if constexpr (std::is_same_v<T, std::string>)\n            {\n                return input;\n            }\n            else if constexpr (is_chrono_option<T>)\n            {\n                using namespace std::literals;\n                using dseconds = std::chrono::duration<double>;\n                dseconds unit = 1s;\n                std::string_view in{input};\n                if (in.ends_with(\"h\"))\n                {\n                    unit = 1h;\n                    in.remove_suffix(1);\n                }\n                else if (bool min = in.ends_with(\"min\"); min || in.ends_with(\"m\"))\n                {\n                    unit = 1min;\n                    in.remove_suffix(min ? 3 : 1);\n                }\n                else if (in.ends_with(\"ms\"))\n                {\n                    unit = 1ms;\n                    in.remove_suffix(2);\n                }\n                else if (in.ends_with(\"us\"))\n                {\n                    unit = 1us;\n                    in.remove_suffix(2);\n                }\n                else if (in.ends_with(\"ns\"))\n                {\n                    unit = 1ns;\n                    in.remove_suffix(2);\n                }\n                else if (in.ends_with(\"s\"))\n                {\n                    in.remove_suffix(1);\n                }\n\n                if (int x; parse_int(in, x))\n                    return std::chrono::round<T>(x * unit);\n\n                throw std::invalid_argument{\"{} is not a valid duration; expected value such as 10s, 2000ms, 5min, 2h\"};\n            }\n            else\n            {\n                std::istringstream iss(input);\n                T t;\n                iss >> t;\n                if (iss.fail())\n                    throw std::invalid_argument{fmt::format(\"{} is not a valid {}\", input, typeid(T).name())};\n                return t;\n            }\n        }\n\n        std::vector<std::string> values_as_string() override\n        {\n            using namespace std::literals;\n            if (parsed_values.empty())\n                return {};\n            std::vector<std::string> result;\n            result.reserve(parsed_values.size());\n            for (const auto& v : parsed_values)\n            {\n                if constexpr (is_chrono_option<T>)\n                {\n                    if (v == 0s)\n                        result.push_back(fmt::format(\"{}s\", 0));\n                    else if (v >= 1h && v % 1h == 0s)\n                        result.push_back(fmt::format(\"{}h\", v / 1h));\n                    else if (v >= 1min && v % 1min == 0s)\n                        result.push_back(fmt::format(\"{}min\", v / 1min));\n                    else if (v >= 1s && v % 1s == 0s)\n                        result.push_back(fmt::format(\"{}s\", v / 1s));\n                    else if (v >= 1ms && v % 1ms == 0s)\n                        result.push_back(fmt::format(\"{}ms\", v / 1s));\n                    else if (v >= 1us && v % 1us == 0s)\n                        result.push_back(fmt::format(\"{}us\", v / 1us));\n                    else\n                        result.push_back(fmt::format(\"{}ns\", std::chrono::nanoseconds{v}.count()));\n                }\n                else\n                    result.push_back(fmt::format(\"{}\", v));\n            }\n            return result;\n        }\n\n        /// Attempts to call the acceptor function, if present. This function may throw if the value\n        /// is not acceptable. Additionally, try_accept should not be called if the option is\n        /// required and no value has been provided.\n        ///\n        /// @throws if required and no value present or if the acceptor throws\n        void try_accept() const override\n        {\n            if (required and parsed_values.empty())\n            {\n                throw std::runtime_error{fmt::format(\n                    \"cannot call try_accept() on [{}]:{} when required but no value available\", section, name)};\n            }\n\n            if (acceptor)\n            {\n                if (multi_valued)\n                {\n                    // add default value in multi value mode\n                    if (parsed_values.empty() and not default_values.empty())\n                        for (const auto& v : default_values)\n                            acceptor(v);\n\n                    for (auto value : parsed_values)\n                    {\n                        acceptor(value);\n                    }\n                }\n                else\n                {\n                    auto maybe = get_value();\n                    if (maybe)\n                        acceptor(*maybe);\n                }\n            }\n        }\n\n        std::vector<T> default_values;\n        std::vector<T> parsed_values;\n        std::function<void(T)> acceptor;\n    };\n\n    /// Specialization for bool types. We don't want to use stringstream parsing in this\n    /// case because we want to accept \"truthy\" and \"falsy\" string values (e.g. \"off\" == false)\n    template <>\n    bool OptionDefinition<bool>::from_string(const std::string& input);\n\n    // Returns true if the input string looks like a true value, false if it looks like a false\n    // value, and nullopt if neither.\n    std::optional<bool> parse_boolean(std::string_view input);\n\n    using UndeclaredValueHandler =\n        std::function<void(std::string_view section, std::string_view name, std::string_view value)>;\n\n    // map of k:v pairs\n    using DefinitionMap = std::unordered_map<std::string, std::unique_ptr<OptionDefinitionBase>>;\n\n    // map of section-name to map-of-definitions\n    using SectionMap = std::unordered_map<std::string, DefinitionMap>;\n\n    /// A ConfigDefinition holds an ordered set of OptionDefinitions defining the allowable values\n    /// and their constraints (specified through calls to defineOption()).\n    ///\n    /// The layout and grouping of the config options are modelled after the INI file format; each\n    /// option has a name and is grouped under a section. Duplicate option names are allowed only if\n    /// they exist in a different section. The ConfigDefinition can be serialized in the INI file\n    /// format using the generateINIConfig() function.\n    ///\n    /// Configured values (e.g. those encountered when parsing a file) can be provided through calls\n    /// to addConfigValue(). These take a std::string as a value, which is automatically parsed.\n    ///\n    /// The ConfigDefinition can be used to print out a full config string (or file), including\n    /// fields with defaults and optionally fields which have a specified value (values provided\n    /// through calls to addConfigValue()).\n    struct ConfigDefinition\n    {\n        explicit ConfigDefinition(config::Type type) : type{type} {}\n\n        /// Specify the parameters and type of a configuration option. The parameters are members of\n        /// OptionDefinitionBase; the type is inferred from OptionDefinition's template parameter T.\n        ///\n        /// This function should be called for every option that this Configuration supports, and\n        /// should be done before any other interactions involving that option.\n        ///\n        /// @param def should be a unique_ptr to a valid subclass of OptionDefinitionBase\n        /// @return `*this` for chaining calls\n        /// @throws std::invalid_argument if the option already exists\n        ConfigDefinition& define_option(std::unique_ptr<OptionDefinitionBase> def);\n\n        /// Convenience function which calls define_option with a OptionDefinition of the specified\n        /// type and with parameters passed through to OptionDefinition's constructor.\n        template <typename T, typename... Params>\n        ConfigDefinition& define_option(Params&&... args)\n        {\n            return define_option(std::make_unique<OptionDefinition<T>>(std::forward<Params>(args)...));\n        }\n\n        /// Specify a config value for the given section and name. The value should be a valid\n        /// string representing the type used by the option (e.g. the type provided when\n        /// defineOption() was called).\n        ///\n        /// If the specified option doesn't exist, an exception will be thrown. Otherwise, the\n        /// option's parse_value() will be invoked, and should throw an exception if the string\n        /// can't be parsed.\n        ///\n        /// @param section is the section this value resides in\n        /// @param name is the name of the value\n        /// @return `*this` for chaining calls\n        /// @throws if the option doesn't exist or the provided string isn't parseable\n        ConfigDefinition& add_config_value(std::string_view section, std::string_view name, std::string_view value);\n\n        /// Get a config value. If the value hasn't been provided but a default has, the default\n        /// will be returned. If no value and no default is provided, an empty optional will be\n        /// returned.\n        ///\n        /// The type T should exactly match that provided by the definition; it is not sufficient\n        /// for one type to be a valid substitution for the other.\n        ///\n        /// @param section is the section this value resides in\n        /// @param name is the name of the value\n        /// @return an optional providing the configured value, the default, or empty\n        /// @throws std::invalid_argument if there is no such config option or the wrong type T was\n        //          provided\n        template <typename T>\n        std::optional<T> get_config_value(std::string_view section, std::string_view name)\n        {\n            std::unique_ptr<OptionDefinitionBase>& definition = lookup_definition_or_throw(section, name);\n\n            auto derived = dynamic_cast<const OptionDefinition<T>*>(definition.get());\n            if (not derived)\n                throw std::invalid_argument{\n                    fmt::format(\"{} is the incorrect type for [{}]:{}\", typeid(T).name(), section, name)};\n\n            return derived->getValue();\n        }\n\n        /// Add an \"undeclared\" handler for the given section. This is a handler that will be called\n        /// whenever a k:v pair is found that doesn't match a provided definition.\n        ///\n        /// Any exception thrown by the handler will progagate back through the call to\n        /// addConfigValue().\n        ///\n        /// @param section is the section for which any undeclared values will invoke the provided\n        ///        handler\n        /// @param handler\n        /// @throws if there is already a handler for this section\n        void add_undeclared_handler(const std::string& section, UndeclaredValueHandler handler);\n\n        /// Removes an \"undeclared\" handler for the given section.\n        ///\n        /// @param section is the section which we want to remove the handler for\n        void remove_undeclared_handler(const std::string& section);\n\n        /// Validate that all required fields are present.\n        ///\n        /// @throws std::invalid_argument if configuration constraints are not met\n        void validate_required_fields();\n\n        /// Adds an options validator that runs after all options have been parsed and can be used\n        /// to check for conflicting or invalid option combinations or other checks that cannot be\n        /// performed when processing an individual item.\n        void add_options_validator(std::function<void()> validator);\n\n        /// Accept all options. This will call the acceptor (if present) on each option. Note that\n        /// this should only be called if all required fields are present (that is,\n        /// validateRequiredFields() has been or could be called without throwing).\n        ///\n        /// @throws if any option's acceptor throws\n        void accept_all_options();\n\n        /// Runs any options validators to check that accepted options are not conflicting.  For\n        /// example, if option A must be larger than B, this is where that check would be carried\n        /// out.\n        void validate_all_options();\n\n        /// validates and accept all parsed options\n        void process()\n        {\n            validate_required_fields();\n            accept_all_options();\n            validate_all_options();\n        }\n\n        /// Add comments for a given section. Comments are replayed in-order during config file\n        /// generation. A proper comment prefix will automatically be applied, and the entire\n        /// comment will otherwise be used verbatim (no automatic line separation, etc.).\n        ///\n        /// @param section\n        /// @param comment\n        void add_section_comments(const std::string& section, std::vector<std::string> comments);\n\n        /// Add comments for a given option. Similar to addSectionComment, but applies to a specific\n        /// [section]:name pair.\n        ///\n        /// @param section\n        /// @param name\n        /// @param comment\n        void add_option_comments(\n            const std::string& section, const std::string& name, std::vector<std::string> comments);\n\n        /// Generate a config string from the current config definition, optionally using overridden\n        /// values. The generated config will preserve insertion order of both sections and their\n        /// definitions.\n        ///\n        /// Definitions which are required or have an overriden value (and useValues == true) will\n        /// be written normally. Otherwise, they will be written commented-out in order to provide a\n        /// complete documentation of the configuration file.\n        ///\n        /// @param useValues specifies whether we use specified values (e.g. those from calls to\n        ///        addConfigValue()) or only definitions\n        /// @return a string containing the config in INI format\n        std::string generate_ini_config(bool useValues = false);\n\n      private:\n        // Config file; this defines where we skip client-only, relay-only, or non-embedded config\n        // items.\n        config::Type type;\n\n        std::unique_ptr<OptionDefinitionBase>& lookup_definition_or_throw(\n            std::string_view section, std::string_view name);\n        const std::unique_ptr<OptionDefinitionBase>& lookup_definition_or_throw(\n            std::string_view section, std::string_view name) const;\n\n        using SectionVisitor = std::function<void(const std::string&, const DefinitionMap&)>;\n        void visit_sections(SectionVisitor visitor) const;\n\n        using DefVisitor = std::function<void(const std::string&, const std::unique_ptr<OptionDefinitionBase>&)>;\n        void visit_definitions(const std::string& section, DefVisitor visitor) const;\n\n        SectionMap definitions;\n\n        std::unordered_map<std::string, UndeclaredValueHandler> undeclared_handlers;\n\n        // track insertion order. the vector<string>s are ordered list of section/option names.\n        std::vector<std::string> section_ordering;\n        std::unordered_map<std::string, std::vector<std::string>> definition_ordering;\n\n        // Post-parsing validators\n        std::vector<std::function<void()>> options_validators;\n\n        // comments for config file generation\n        using CommentList = std::vector<std::string>;\n        using CommentsMap = std::unordered_map<std::string, CommentList>;\n        CommentsMap section_comments;\n        std::unordered_map<std::string, CommentsMap> definition_comments;\n    };\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/config/ini.cpp",
    "content": "#include \"ini.hpp\"\n\n#include <llarp/util/formattable.hpp>\n#include <llarp/util/logging.hpp>\n\n#include <cctype>\n#include <fstream>\n#include <list>\n#include <stdexcept>\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"config.ini\");\n\n    void ConfigParser::load_file(const std::filesystem::path& fname)\n    {\n        _data = util::file_to_string(fname);\n        _filename = fname;\n        parse();\n    }\n\n    void ConfigParser::load_new_from_str(std::string str)\n    {\n        _data = str;\n        parse_all();\n    }\n\n    void ConfigParser::load_from_str(std::string str)\n    {\n        _data = str;\n        parse();\n    }\n\n    void ConfigParser::clear()\n    {\n        _overrides.clear();\n        _config.clear();\n        _data.clear();\n    }\n\n    static bool whitespace(char ch) { return std::isspace(static_cast<unsigned char>(ch)) != 0; }\n\n    /// Differs from parse() as parse_all() does NOT skip comments\n    /// parse_all() is only used by RPC endpoint 'config' for\n    /// reading new .ini files from string and writing them\n    void ConfigParser::parse_all()\n    {\n        std::list<std::string_view> lines;\n        {\n            auto itr = _data.begin();\n            // split into lines\n            while (itr != _data.end())\n            {\n                auto beg = itr;\n                while (itr != _data.end() && *itr != '\\n' && *itr != '\\r')\n                    ++itr;\n                lines.emplace_back(std::addressof(*beg), std::distance(beg, itr));\n                if (itr == _data.end())\n                    break;\n                ++itr;\n            }\n        }\n\n        std::string_view sectName;\n        size_t lineno = 0;\n        for (auto line : lines)\n        {\n            lineno++;\n            // Trim whitespace\n            while (!line.empty() && whitespace(line.front()))\n                line.remove_prefix(1);\n            while (!line.empty() && whitespace(line.back()))\n                line.remove_suffix(1);\n\n            // Skip blank lines but NOT comments\n            if (line.empty())\n                continue;\n\n            if (line.front() == '[' && line.back() == ']')\n            {\n                // section header\n                line.remove_prefix(1);\n                line.remove_suffix(1);\n                sectName = line;\n            }\n            else if (auto kvDelim = line.find('='); kvDelim != std::string_view::npos)\n            {\n                // key value pair\n                std::string_view k = line.substr(0, kvDelim);\n                std::string_view v = line.substr(kvDelim + 1);\n                // Trim inner whitespace\n                while (!k.empty() && whitespace(k.back()))\n                    k.remove_suffix(1);\n                while (!v.empty() && whitespace(v.front()))\n                    v.remove_prefix(1);\n\n                if (k.empty())\n                {\n                    throw std::runtime_error(fmt::format(\"{} invalid line ({}): '{}'\", _filename, lineno, line));\n                }\n\n                log::debug(logcat, \"{}:[{}]:{}={}\", _filename, sectName, k, v);\n                _config[std::string{sectName}].emplace(k, v);\n            }\n            else  // malformed?\n            {\n                throw std::runtime_error(fmt::format(\"{} invalid line ({}): '{}'\", _filename, lineno, line));\n            }\n        }\n    }\n\n    void ConfigParser::parse()\n    {\n        std::list<std::string_view> lines;\n        {\n            auto itr = _data.begin();\n            // split into lines\n            while (itr != _data.end())\n            {\n                auto beg = itr;\n                while (itr != _data.end() && *itr != '\\n' && *itr != '\\r')\n                    ++itr;\n                lines.emplace_back(std::addressof(*beg), std::distance(beg, itr));\n                if (itr == _data.end())\n                    break;\n                ++itr;\n            }\n        }\n\n        std::string_view sectName;\n        size_t lineno = 0;\n        for (auto line : lines)\n        {\n            lineno++;\n            // Trim whitespace\n            while (!line.empty() && whitespace(line.front()))\n                line.remove_prefix(1);\n            while (!line.empty() && whitespace(line.back()))\n                line.remove_suffix(1);\n\n            // Skip blank lines\n            if (line.empty() or line.front() == ';' or line.front() == '#')\n                continue;\n\n            if (line.front() == '[' && line.back() == ']')\n            {\n                // section header\n                line.remove_prefix(1);\n                line.remove_suffix(1);\n                sectName = line;\n            }\n            else if (auto kvDelim = line.find('='); kvDelim != std::string_view::npos)\n            {\n                // key value pair\n                std::string_view k = line.substr(0, kvDelim);\n                std::string_view v = line.substr(kvDelim + 1);\n                // Trim inner whitespace\n                while (!k.empty() && whitespace(k.back()))\n                    k.remove_suffix(1);\n                while (!v.empty() && whitespace(v.front()))\n                    v.remove_prefix(1);\n\n                if (k.empty())\n                {\n                    throw std::runtime_error(fmt::format(\"{} invalid line ({}): '{}'\", _filename, lineno, line));\n                }\n\n                log::debug(logcat, \"{}:[{}]:{}={}\", _filename, sectName, k, v);\n                _config[std::string{sectName}].emplace(k, v);\n            }\n            else  // malformed?\n            {\n                throw std::runtime_error(fmt::format(\"{} invalid line ({}): '{}'\", _filename, lineno, line));\n            }\n        }\n    }\n\n    void ConfigParser::iter_all_sections(std::function<void(std::string_view, const SectionValues&)> visit)\n    {\n        for (const auto& item : _config)\n            visit(item.first, item.second);\n    }\n\n    bool ConfigParser::visit_section(const char* name, std::function<bool(const SectionValues& sect)> visit) const\n    {\n        // m_Config is effectively:\n        // unordered_map< string, unordered_multimap< string, string  >>\n        // in human terms: a map of of sections\n        //                 where a section is a multimap of k:v pairs\n        auto itr = _config.find(name);\n        if (itr == _config.end())\n            return false;\n        return visit(itr->second);\n    }\n\n    void ConfigParser::add_override(\n        std::filesystem::path fpath, std::string section, std::string key, std::string value)\n    {\n        auto& data = _overrides[fpath];\n        data[section].emplace(key, value);\n    }\n\n    void ConfigParser::save()\n    {\n        // write overrides\n        for (const auto& [fname, overrides] : _overrides)\n        {\n            std::ofstream ofs(fname);\n            for (const auto& [section, values] : overrides)\n            {\n                ofs << std::endl << \"[\" << section << \"]\" << std::endl;\n                for (const auto& [key, value] : values)\n                {\n                    ofs << key << \"=\" << value << std::endl;\n                }\n            }\n        }\n        _overrides.clear();\n    }\n\n    void ConfigParser::save_new() const\n    {\n        if (not _overrides.empty())\n            throw std::invalid_argument(\"Override specified when attempting new .ini save\");\n        if (_config.empty())\n            throw std::invalid_argument(\"New config not loaded when attempting new .ini save\");\n        if (_filename.empty())\n            throw std::invalid_argument(\"New config cannot be saved with filepath specified\");\n\n        std::ofstream ofs(_filename);\n        for (const auto& [section, values] : _config)\n        {\n            ofs << std::endl << \"[\" << section << \"]\" << std::endl;\n            for (const auto& [key, value] : values)\n            {\n                ofs << key << \"=\" << value << std::endl;\n            }\n        }\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/config/ini.hpp",
    "content": "#pragma once\n\n#include <llarp/util/file.hpp>\n\n#include <functional>\n#include <string>\n#include <string_view>\n#include <unordered_map>\n\nnamespace llarp\n{\n    struct ConfigParser\n    {\n        using SectionValues = std::unordered_multimap<std::string, std::string>;\n        using ConfigMap = std::unordered_map<std::string, SectionValues>;\n        /// clear parser\n        void clear();\n\n        /// Load config file.  Throws on error.\n        void load_file(const std::filesystem::path& fname);\n\n        /// Load new .ini data from string (calls ParseAll() rather than Parse())\n        /// Throws on error.\n        void load_new_from_str(std::string str);\n\n        /// Load from string. Throws on error.\n        void load_from_str(std::string str);\n\n        /// iterate all sections and thier values\n        void iter_all_sections(std::function<void(std::string_view, const SectionValues&)> visit);\n\n        /// visit a section in config read only by name\n        /// return false if no section or value propagated from visitor\n        bool visit_section(const char* name, std::function<bool(const SectionValues&)> visit) const;\n\n        /// add a config option that is appended in another file\n        void add_override(std::filesystem::path file, std::string section, std::string key, std::string value);\n\n        /// save config overrides\n        void save();\n\n        /// save new .ini config file to path\n        void save_new() const;\n\n        void set_filename(const std::filesystem::path& f) { _filename = f; }\n\n      private:\n        void parse_all();\n\n        void parse();\n\n        std::string _data;\n        ConfigMap _config;\n        std::unordered_map<std::filesystem::path, ConfigMap, util::FileHash> _overrides;\n        std::filesystem::path _filename;\n    };\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/consensus/reachability_testing.cpp",
    "content": "#include \"reachability_testing.hpp\"\n\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/link/endpoint.hpp>\n#include <llarp/nodedb.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/rpc/oxend_rpc.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/random.hpp>\n\n#include <chrono>\n\nusing std::chrono::steady_clock;\n\nnamespace llarp::consensus\n{\n    static auto logcat = log::Cat(\"testing\");\n\n    reachability_testing::reachability_testing(Router& r) : router{r} {}\n\n    void reachability_testing::start()\n    {\n        if (router.config().lokid.disable_testing)\n            log::warning(logcat, \"Reachability testing DISABLED in config\");\n        else\n        {\n            log::debug(logcat, \"Starting reachability testing tickers\");\n            ticker = router.loop.call_every(TEST_INTERVAL, [this] { tick(); });\n            whine_ticker = router.loop.call_every(30s, [this] { check_incoming_tests(); });\n        }\n    }\n\n    void reachability_testing::stop()\n    {\n        ticker.reset();\n        whine_ticker.reset();\n    }\n\n    void reachability_testing::tick()\n    {\n        // Reachability testing is currently quite simple: when we want to test a node we\n        // connect to it using a *client ALPN* (so that the connection is not treated as a relay\n        // connection by either end), call the \"ping\" endpoint over it, wait for the pong, then\n        // disconnect.\n\n        // Don't run tests if we are not running or we are stopping, or aren't currently\n        // registered\n        if (not router.is_running() or not router.appears_registered())\n            return;\n\n        auto tests = get_failing();\n\n        if (auto maybe = next_random())\n            tests.emplace_back(*maybe, 0);\n\n        auto now = llarp::time_now_ms();\n\n        auto& node_db = router.node_db();\n\n        log::debug(logcat, \"{} service nodes for connectivity testing\", tests.size());\n\n        for (const auto& [rid, prev_fails] : tests)\n        {\n            if (not node_db.is_registered(rid))\n            {\n                log::debug(logcat, \"{} is no longer a registered service node; dropping from test list\", rid);\n                remove_node_from_failing(rid);\n                continue;\n            }\n\n            log::debug(\n                logcat, \"Establishing test connection to {} for reachability testing\", rid.to_network_address(true));\n\n            auto* rc = node_db.get_rc(rid);\n            if (!rc || rc->is_outdated(now))\n            {\n                log::debug(logcat, \"Test failed for {}: have no recent RC\", rid.to_network_address(true));\n                add_failing_node(rid, prev_fails);\n                continue;\n            }\n\n            auto [conn, btstr] = router.link_endpoint().testing_client_connect(*rc);\n            btstr->command(\n                \"ping\",\n                \"\",\n                TEST_REQUEST_TIMEOUT,\n                [this, weak_conn = std::weak_ptr{conn}, rid, prev_fails](quic::message m) mutable {\n                    auto conn = weak_conn.lock();\n                    if (conn)\n                        conn->close_connection();\n                    router.loop.call_soon([this, rid, prev_fails, m = std::move(m)] {\n                        if (m)\n                        {\n                            if (prev_fails)\n                                log::info(\n                                    logcat,\n                                    \"Successful SN reachability test to {} (after {} previous failures)\",\n                                    rid.to_network_address(true),\n                                    prev_fails);\n                            else\n                                log::info(\n                                    logcat, \"Successful SN reachability test to {}\", rid.to_network_address(true));\n                            remove_node_from_failing(rid);\n                        }\n                        else\n                        {\n                            log::info(\n                                logcat,\n                                \"Testing of {} failed: {}\",\n                                rid.to_network_address(true),\n                                m.timed_out ? \"request timed out\" : m.body());\n                            add_failing_node(rid, prev_fails);\n                        }\n                        router.oxend()->inform_connection(rid, (bool)m);\n                    });\n                });\n        }\n    }\n\n    void reachability_testing::check_incoming_tests(const time_point_t& now)\n    {\n        if (not router.appears_registered())\n        {\n            if (not last.was_unregistered and last.was_failing)\n                log::info(log_global, \"Disabling incoming ping warnings: service node is no longer registered\");\n            else\n                log::debug(logcat, \"Not checking incoming tests: not a registered relay\");\n            last.was_unregistered = true;\n            last.was_failing = false;\n            return;\n        }\n\n        const auto elapsed = now - std::max(startup, last.last_test);\n\n        // If we just became registered then use a longer threshold without pings before we flag it\n        // as a problem: it can take some time for nodes to get through their current queues and\n        // build a new queue that includes us.\n        bool failing = elapsed > (last.was_unregistered ? MAX_TIME_INITIAL : MAX_TIME_WITHOUT_PING);\n\n        bool whine = failing != last.was_failing || (failing && now - last.last_whine > WHINING_INTERVAL);\n\n        last.was_failing = failing;\n\n        if (whine)\n        {\n            last.last_whine = now;\n            if (!failing)\n                log::info(log_global, \"Received test ping; port is likely reachable!\");\n            else\n            {\n                if (last.last_test.time_since_epoch() == 0s)\n                    log::warning(log_global, \"Have NEVER received test pings!\");\n                else\n                    log::warning(log_global, \"Have not received pings in {:.1f} minutes\", fminutes{elapsed}.count());\n\n                log::warning(\n                    log_global, \"Check configured IP and port: Non-reachabibility may result in deregistration!\");\n            }\n        }\n    }\n\n    void reachability_testing::incoming_ping(const time_point_t& now)\n    {\n        last.last_test = now;\n\n        // If we had previous logged about a failure then log about the success immediately (rather\n        // than waiting for the next whine tick):\n        if (last.was_failing)\n            check_incoming_tests();\n    }\n\n    std::chrono::microseconds reachability_testing::retest_interval(int n_failures)\n    {\n        if (n_failures < 1)\n            n_failures = 1;\n        return std::clamp<std::chrono::microseconds>(\n            n_failures * RETEST_BACKOFF\n                + duration_cast<std::chrono::microseconds>(fseconds{RETEST_NOISE(llarp::csrng)}),\n            0s,\n            RETEST_MAX);\n    }\n\n    std::optional<RouterID> reachability_testing::next_random(bool _requeue)\n    {\n        std::optional<RouterID> sn;\n\n        // Pull the next element off the queue, but skip ourself, any that are no longer registered,\n        // and any that are currently known to be failing (those are queued for testing separately).\n        while (!testing_queue.empty())\n        {\n            auto& pk = testing_queue.back();\n\n            if (!failing.count(pk))\n                sn = pk;\n\n            testing_queue.pop_back();\n\n            if (sn)\n                return sn;\n        }\n\n        if (!_requeue)\n            // If we get here then we already tried rebuilding and still found nothing, so give up.\n            return std::nullopt;\n\n        // We exhausted the queue so repopulate it and try again from the top\n        testing_queue = router.node_db().get_registered_relays();\n        std::shuffle(testing_queue.begin(), testing_queue.end(), llarp::csrng);\n\n        // Recurse with the rebuilt list, but don't let it try rebuilding again if it exhausts the\n        // fresh list.\n        sn = next_random(/*_requeue=*/false);\n        return sn;\n    }\n\n    std::vector<std::pair<RouterID, int>> reachability_testing::get_failing(const time_point_t& now)\n    {\n        // Our failing_queue puts the oldest retest times at the top, so pop them off into our\n        // result until the top node should be retested sometime in the future (or we have enough).\n        std::vector<std::pair<RouterID, int>> result;\n        while (not failing_queue.empty() and result.size() < MAX_RETESTS_PER_TICK)\n        {\n            auto& [pk, retest_time, failures] = failing_queue.top();\n            if (retest_time > now)\n                break;\n            if (failing.count(pk))\n                result.emplace_back(pk, failures);\n            failing_queue.pop();\n        }\n        return result;\n    }\n\n    void reachability_testing::add_failing_node(const RouterID& pk, int previous_failures)\n    {\n        if (previous_failures < 0)\n            previous_failures = 0;\n        ++previous_failures;\n        failing.insert(pk);\n        failing_queue.emplace(\n            pk, std::chrono::steady_clock::now() + retest_interval(previous_failures), previous_failures);\n    }\n\n    void reachability_testing::remove_node_from_failing(const RouterID& pk) { failing.erase(pk); }\n\n}  // namespace llarp::consensus\n"
  },
  {
    "path": "llarp/consensus/reachability_testing.hpp",
    "content": "#pragma once\n\n#include <llarp/contact/router_id.hpp>\n#include <llarp/util/time.hpp>\n\n#include <chrono>\n#include <queue>\n#include <random>\n#include <unordered_set>\n#include <vector>\n\nnamespace llarp\n{\n    class Router;\n}\nnamespace oxen::quic\n{\n    class Ticker;\n}\n\nnamespace llarp::consensus\n{\n    namespace detail\n    {\n        using clock_t = std::chrono::steady_clock;\n        using time_point_t = std::chrono::time_point<clock_t>;\n\n        // Returns std::greater on the std::get<N>(v)th element value.\n        template <typename T, size_t N>\n        struct nth_greater\n        {\n            constexpr bool operator()(const T& lhs, const T& rhs) const\n            {\n                return std::greater<std::tuple_element_t<N, T>>{}(std::get<N>(lhs), std::get<N>(rhs));\n            }\n        };\n\n        struct incoming_test_state\n        {\n            time_point_t last_test{};\n            time_point_t last_whine{};\n            bool was_failing = false;\n            bool was_unregistered = false;\n        };\n\n    }  // namespace detail\n    using time_point_t = detail::time_point_t;\n    using clock_t = detail::clock_t;\n\n    using fseconds = std::chrono::duration<float, std::chrono::seconds::period>;\n    using fminutes = std::chrono::duration<float, std::chrono::minutes::period>;\n\n    class reachability_testing\n    {\n      public:\n        // How often we tick the timer to perform one new random test and check whether we need to\n        // do any re-tests.  The determines the overall testing rate (i.e. with 2 random tests per\n        // second, it would take 16.6 minutes to cycle through a full list of 2000 service nodes).\n        static constexpr auto TEST_INTERVAL = 333'333us;\n\n        // The maximum number of nodes that we will re-test at once (i.e. per\n        // TESTING_TIMING_INTERVAL); mainly intended to throttle ourselves if, for instance, our own\n        // connectivity loss makes us accumulate tons of nodes to test all at once.\n        static constexpr int MAX_RETESTS_PER_TICK = 3;\n\n        // The backoff after each consecutive test failure before we re-test.  Specifically we\n        // schedule the next re-test after a failure to occur in\n        //\n        //     (RETEST_BACKOFF*consecutive_failures) + RETEST_NOISE(rng)\n        //\n        // seconds (truncated to [0, RETEST_MAX]), where the randomness is to avoid clustering of\n        // retests.\n        static constexpr auto RETEST_BACKOFF = 10s;\n        std::normal_distribution<float> RETEST_NOISE{0, 3};\n        static constexpr auto RETEST_MAX = 2min;\n\n        // Returns a testing interval (as per above) for a node that has failed the last\n        // `n_failures` consecutive tests.\n        std::chrono::microseconds retest_interval(int n_failures);\n\n        // How long we allow the test request to take.  This time is the maximum allowed time for\n        // both establishing the connection and making the request once established.\n        static constexpr auto TEST_REQUEST_TIMEOUT = 4s;\n\n        // Maximum time without an incoming testing ping before we start whining about it (if we are\n        // registered), as that likely means that we are currently unreachable.\n        static constexpr auto MAX_TIME_WITHOUT_PING = 2min;\n\n        // When we transition from unregistered to register, hold off on whining about incoming\n        // pings for this long because we won't get added to other nodes' testing queues until they\n        // get through their current list and regenerate a new testing queue.\n        static constexpr auto MAX_TIME_INITIAL = 15min;\n\n        // Rate-limit for how often we whine in the logs about looking unreachable because we\n        // haven't received any recent pings.\n        static constexpr auto WHINING_INTERVAL = 5min;\n\n      private:\n        Router& router;\n\n        std::shared_ptr<oxen::quic::Ticker> ticker;\n        std::shared_ptr<oxen::quic::Ticker> whine_ticker;\n\n        // Queue of pubkeys of service nodes to test; we pop off the back of this until the queue\n        // empties then we refill it with a shuffled list of all pubkeys then pull off of it until\n        // it is empty again, etc.\n        std::vector<RouterID> testing_queue;\n\n        // When we started, so that we know not to hold off on whining about no pings for a while.\n        const time_point_t startup = clock_t::now();\n\n        // Pubkeys, next test times, and sequential failure counts of service nodes that are\n        // currently in \"failed\" status.\n        using FailingPK = std::tuple<RouterID, time_point_t, int>;\n        std::priority_queue<FailingPK, std::vector<FailingPK>, detail::nth_greater<FailingPK, 1>> failing_queue;\n        std::unordered_set<RouterID> failing;\n\n        // Track the last time *this node* was tested by other network nodes; used to detect and\n        // warn about possible network issues.\n        detail::incoming_test_state last;\n\n      public:\n        explicit reachability_testing(Router& r);\n\n        // Called by router when it is starting/stopping to start/stop our ticker.\n        void start();\n        void stop();\n\n        // Runs a tick iteration.\n        void tick();\n\n        // Returns the next random node to test from the random testing queue, skipping any nodes\n        // that are currently in the failed nodes queue.  If the random queue is empty, this will\n        // replenish it with a shuffled list of all known registered relays IDs.  If the we still\n        // can't find any relay after replenishing, this returns nullopt.\n        //\n        // `_requeue` is for internal use only and should not be given explicitly.\n        std::optional<RouterID> next_random(bool _requeue = true);\n\n        // Removes and returns up to MAX_RETESTS_PER_TICK nodes that are due to be tested (i.e.\n        // next-testing-time <= now).  Returns [snrecord, #previous-failures] for each.\n        std::vector<std::pair<RouterID, int>> get_failing(const time_point_t& now = clock_t::now());\n\n        // Adds a bad node pubkey to the failing list, to be re-tested soon (with a backoff\n        // depending on `failures`; see TESTING_BACKOFF).  `previous_failures` should be the number\n        // of previous failures *before* this one, i.e. 0 for a random general test; or the failure\n        // count returned by `get_failing` for repeated failures.\n        void add_failing_node(const RouterID& pk, int previous_failures = 0);\n\n        /// removes the public key from the failing set\n        void remove_node_from_failing(const RouterID& pk);\n\n        // Called when this router receives an incoming ping test request\n        void incoming_ping(const time_point_t& now = clock_t::now());\n\n        // Check whether we received incoming pings recently\n        void check_incoming_tests(const time_point_t& now = clock_t::now());\n    };\n\n}  // namespace llarp::consensus\n"
  },
  {
    "path": "llarp/constants/apple.hpp",
    "content": "#pragma once\n\n#include <cstdint>\n\nnamespace llarp::apple\n{\n    /// Localhost port on macOS where we proxy DNS requests *through* the tunnel, because without\n    /// calling into special snowflake Apple network APIs an extension's network connections all go\n    /// around the tunnel, even when the tunnel is (supposedly) the default route.\n    inline constexpr std::uint16_t dns_trampoline_port = 1053;\n\n    /// We query the above trampoline from unbound with this fixed source port (so that the\n    /// trampoline is simplified by not having to track different ports for different requests).\n    inline constexpr std::uint16_t dns_trampoline_source_port = 1054;\n}  // namespace llarp::apple\n"
  },
  {
    "path": "llarp/constants/files.hpp",
    "content": "#pragma once\n\n#include <filesystem>\n\n#ifndef _WIN32\n#include <pwd.h>\n#include <unistd.h>\n#endif\n\nnamespace llarp\n{\n    inline const std::filesystem::path our_rc_filename{\"self.signed\"};\n    inline const std::filesystem::path nodedb_dirname{\"nodedb\"};\n    inline const std::filesystem::path default_bootstrap{\"bootstrap.signed\"};\n    inline const std::filesystem::path default_config_filename{\"lokinet.ini\"};\n\n    inline std::filesystem::path GetDefaultDataDir()\n    {\n#ifndef _WIN32\n        std::filesystem::path datadir{\"/var/lib/lokinet\"};\n        if (auto uid = geteuid())\n        {\n            if (auto* pw = getpwuid(uid))\n            {\n                datadir = std::filesystem::path{pw->pw_dir} / \".lokinet\";\n            }\n        }\n        return datadir;\n#else\n        return std::filesystem::path{\"C:\\\\ProgramData\\\\Lokinet\"};\n#endif\n    }\n\n    inline std::filesystem::path GetDefaultConfigPath() { return GetDefaultDataDir() / default_config_filename; }\n\n    inline std::filesystem::path GetDefaultBootstrap() { return GetDefaultDataDir() / default_bootstrap; }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/constants/link_layer.hpp",
    "content": "#pragma once\n#include <llarp/util/time.hpp>\n\n#include <cstdlib>\n\nconstexpr size_t MAX_LINK_MSG_SIZE = 8192;\nstatic constexpr auto DefaultLinkSessionLifetime = 5min;\nconstexpr size_t MaxSendQueueSize = 1024 * 16;\nstatic constexpr auto LinkLayerConnectTimeout = 5s;\n\nnamespace llarp::constants\n{\n    static constexpr auto DefaultInboundIWPPort = uint16_t{1090};\n}\n"
  },
  {
    "path": "llarp/constants/net.hpp",
    "content": "#pragma once\n\nnamespace llarp::constants\n{\n    constexpr auto udp_header_bytes = 8;\n    constexpr auto ip_header_min_bytes = 20;\n}  // namespace llarp::constants\n"
  },
  {
    "path": "llarp/constants/path.hpp",
    "content": "#pragma once\n\n#include <llarp/util/time.hpp>\n\n#include <chrono>\n#include <cstddef>\n\nnamespace llarp::path\n{\n    /// pad messages to the nearest this many bytes\n    inline constexpr std::size_t PAD_SIZE{128};\n\n    /// Number of encryption \"frames\" inside path builds.  This implicitly defines the maximum\n    /// length of a path: shorter path builds still put data in all frames, but frame data beyond\n    /// the last hop are random unused data (so that the length of the path build message does not\n    /// reveal anything about the total number of hops for the path, and so that the final target\n    /// cannot tell how long the path was).\n    inline constexpr int BUILD_LENGTH = 8;\n\n    /// Length of each frame of a path build.\n    inline constexpr size_t BUILD_FRAME_SIZE = 169;\n\n    /// Max base lifetime of paths.  This is the lifetime of outbound paths, and is the maximum\n    /// target lifetime of inbound paths.  Inbound paths also have up some random fuzz added to\n    /// this, and so the actual maximum allowed by a relay is be slightly higher than this; see\n    /// next two variables.\n    inline constexpr std::chrono::seconds MAX_LIFETIME = 20min;\n\n    /// Maximum path expiry randomness: when building paths we add a random value up to this amount\n    /// to the path lifetime, and so relays will accept paths of up to MAX_LIFETIME plus this value.\n    inline constexpr std::chrono::seconds MAX_LIFETIME_FUZZ = 3min;\n\n    /// The maximum path life accepted by a relay: this is the maximum life plus the maximum amount\n    /// of random fuzz.\n    inline constexpr std::chrono::seconds MAX_LIFETIME_ACCEPTED = MAX_LIFETIME + MAX_LIFETIME_FUZZ;\n\n    /// The minimum expiry time slots for inbound paths.  See detailed comments in\n    /// SessionEndpoint::update_paths().\n    inline constexpr auto MAX_LIFETIME_SLOTS = 4;\n\n    static_assert(\n        std::chrono::seconds{MAX_LIFETIME} % MAX_LIFETIME_SLOTS == 0s,\n        \"MAX_LIFETIME_SLOTS must evenly divide MAX_LIFETIME seconds\");\n\n    /// How many locations a client contact gets published to.  The contact gets published to the\n    /// \"closest\" [this number] relays, using a metric based on the CC and relay IDs, for short term\n    /// redundancy for relays become unreachable or inactive via the Oxen chain.\n    ///\n    /// (Note that this value cannot be changed without upgrading relays and clients).\n    inline constexpr int CC_PUBLISH_LOCATIONS = 4;\n\n    /// after this many ms a path build times out\n    inline constexpr auto BUILD_TIMEOUT{10s};\n\n    inline constexpr auto MIN_PATH_BUILD_INTERVAL{500ms};\n\n    inline constexpr auto PATH_BUILD_RATE{100ms};\n\n    /// measure latency every this interval ms\n    inline constexpr std::chrono::milliseconds LATENCY_INTERVAL{20s};\n\n    /// if a path is inactive for this amount of time it's dead\n    inline constexpr std::chrono::milliseconds ALIVE_TIMEOUT{LATENCY_INTERVAL * 3 / 2};\n\n    /// how big transit hop traffic queues are\n    inline constexpr std::size_t TRANSIT_HOP_QUEUE_SIZE{256};\n\n}  // namespace llarp::path\n"
  },
  {
    "path": "llarp/constants/platform.hpp",
    "content": "#pragma once\n\n/// namespace for platform feature detection constexprs\nnamespace llarp::platform\n{\n    ///  are we linux  ?\n    inline constexpr bool is_linux =\n#ifdef __linux__\n        true\n#else\n        false\n#endif\n        ;\n\n    /// building with systemd enabled ?\n    inline constexpr bool with_systemd =\n#ifdef WITH_SYSTEMD\n        true\n#else\n        false\n#endif\n        ;\n\n    ///  are we freebsd ?\n    inline constexpr bool is_freebsd =\n#ifdef __FreeBSD__\n        true\n#else\n        false\n#endif\n        ;\n\n    /// are we windows ?\n    inline constexpr bool is_windows =\n#ifdef _WIN32\n        true\n#else\n        false\n#endif\n        ;\n\n    /// are we an apple platform ?\n    inline constexpr bool is_apple =\n#ifdef __APPLE__\n        true\n#else\n        false\n#endif\n        ;\n\n    /// are we building as an apple system extension\n    inline constexpr bool is_apple_sysex =\n#ifdef MACOS_SYSTEM_EXTENSION\n        true\n#else\n        false\n#endif\n        ;\n\n    /// are we an android platform ?\n    inline constexpr bool is_android =\n#ifdef ANDROID\n        true\n#else\n        false\n#endif\n        ;\n\n    /// are we an iphone ?\n    inline constexpr bool is_iphone =\n#ifdef IOS\n        true\n#else\n        false\n#endif\n        ;\n\n    /// are we running with pybind simulation mode enabled?\n    inline constexpr bool is_simulation =\n#ifdef LOKINET_HIVE\n        true\n#else\n        false\n#endif\n        ;\n    /// do we have systemd support ?\n    // on cross compiles sometimes weird permutations of target and host make this value not\n    // correct, this ensures it always is\n    inline constexpr bool has_systemd = is_linux and with_systemd and not(is_android or is_windows);\n\n    /// are we using macos ?\n    inline constexpr bool is_macos = is_apple and not is_iphone;\n\n    /// are we a mobile phone ?\n    inline constexpr bool is_mobile = is_android or is_iphone;\n\n    /// does this platform support native ipv6 ?\n    // TODO: make windows support ipv6\n    inline constexpr bool supports_ipv6 = not is_windows;\n}  // namespace llarp::platform\n"
  },
  {
    "path": "llarp/constants/proto.hpp",
    "content": "#pragma once\n\nnamespace llarp::constants\n{\n    /// current network wide protocol version\n    // TODO: enum class\n    constexpr auto proto_version = 0;\n\n}  // namespace llarp::constants\n"
  },
  {
    "path": "llarp/constants/version.cpp.in",
    "content": "#include <llarp/constants/version.hpp>\n#include <llarp/constants/proto.hpp>\n\nnamespace llarp\n{\n  // clang-format off\n  const std::array<uint8_t, 3> LOKINET_VERSION{{@lokinet_VERSION_MAJOR@, @lokinet_VERSION_MINOR@, @lokinet_VERSION_PATCH@}};\n  const char* const LOKINET_VERSION_TAG   = \"@VERSIONTAG@\";\n  const char* const LOKINET_VERSION_FULL  = \"lokinet-@lokinet_VERSION_MAJOR@.@lokinet_VERSION_MINOR@.@lokinet_VERSION_PATCH@-@VERSIONTAG@\";\n  // clang-format on\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/constants/version.hpp",
    "content": "#pragma once\n\n#include <array>\n#include <cstdint>\n\nnamespace llarp\n{\n    // Given a full lokinet version of: lokinet-1.2.3-abc these are:\n    extern const std::array<uint8_t, 3> LOKINET_VERSION;  // [1, 2, 3]\n    extern const char* const LOKINET_VERSION_TAG;         // \"abc\"\n    extern const char* const LOKINET_VERSION_FULL;        // \"lokinet-1.2.3-abc\"\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/contact/client_contact.cpp",
    "content": "#include \"client_contact.hpp\"\n\n#include <llarp/constants/path.hpp>\n#include <llarp/util/bspan.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/logging/buffer.hpp>\n\n#include <oxenc/bt_producer.h>\n#include <oxenc/bt_serialize.h>\n\n#include <algorithm>\n#include <type_traits>\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"client-intro\");\n\n    ClientContact::ClientContact(\n        PubKey pk,\n        std::unordered_set<dns::SRVData> srvs,\n        protocol_flag protocols,\n        std::optional<net::ExitPolicy> policy)\n        : _pubkey{std::move(pk)}, _srv{std::move(srvs)}, _protos{protocols}, _exit_policy{std::move(policy)}\n    {}\n\n    ClientContact::ClientContact(std::span<const std::byte> buf)\n    {\n        oxenc::bt_dict_consumer btdc{buf};\n\n        auto version = btdc.require<uint8_t>(\"\");\n\n        if (version != VERSION)\n            throw std::runtime_error{\n                \"Deserialized ClientContact with unsupported version {} (expected {})!\"_format(version, VERSION)};\n\n        _pubkey.assign(btdc.require_span<std::byte, PubKey::SIZE>(\"a\"));\n\n        if (btdc.skip_until(\"e\"))\n            _exit_policy.emplace().bt_decode(btdc.consume_dict_consumer());\n\n        for (auto sublist = btdc.require<oxenc::bt_list_consumer>(\"i\"); not sublist.is_finished();)\n            _intros.emplace_back(sublist.consume_dict_consumer());\n\n        if (!std::ranges::is_sorted(_intros, std::ranges::greater{}, &ClientIntro::expiry))\n            throw std::runtime_error{\"Invalid ClientContact: intros expiries are non-descending\"};\n\n        _protos = static_cast<protocol_flag>(btdc.require<std::underlying_type_t<protocol_flag>>(\"p\"));\n\n        if (auto sublist = btdc.maybe<oxenc::bt_list_consumer>(\"s\"))\n            while (not sublist->is_finished())\n                _srv.emplace(sublist->consume_dict_consumer());\n\n        btdc.finish();\n    }\n\n    void ClientContact::update_intros(std::vector<ClientIntro> iset)\n    {\n        if (iset.empty())\n            throw std::invalid_argument{\"Cannot update ClientContact with no ClientIntros!\"};\n        _intros = std::move(iset);\n        std::ranges::stable_sort(_intros, std::ranges::greater{}, &ClientIntro::expiry);\n        log::debug(logcat, \"ClientContact updated with {} ClientIntros\", _intros.size());\n    }\n\n#ifdef __cpp_lib_to_underlying\n    using std::to_underlying;\n#else\n    template <class Enum>\n    constexpr std::underlying_type_t<Enum> to_underlying(Enum e) noexcept\n    {\n        return static_cast<std::underlying_type_t<Enum>>(e);\n    }\n#endif\n\n    std::vector<std::byte> ClientContact::bt_encode() const\n    {\n        oxenc::bt_dict_producer btdp;\n        btdp.append<uint8_t>(\"\", VERSION);\n\n        btdp.append(\"a\", _pubkey.to_view());\n\n        if (_exit_policy)\n            _exit_policy->bt_encode(btdp.append_dict(\"e\"));\n\n        {\n            auto sublist = btdp.append_list(\"i\");\n            for (auto& i : _intros)\n                i.bt_encode(sublist.append_dict());\n        }\n\n        btdp.append(\"p\", to_underlying(_protos));\n\n        if (not _srv.empty())\n        {\n            auto sublist = btdp.append_list(\"s\");\n            for (auto& s : _srv)\n                s.bt_encode(sublist.append_dict());\n        }\n\n        auto encoded = btdp.view();\n        std::vector<std::byte> ret;\n        ret.resize(encoded.size());\n        std::memcpy(ret.data(), encoded.data(), encoded.size());\n        return ret;\n    }\n\n    bool ClientContact::is_expired(std::chrono::milliseconds now) const\n    {\n        // We only need to check the first one, because this is sorted newest-to-oldest and so if\n        // the first is expired they all are.\n        return _intros.empty() || _intros.front().is_expired(now);\n    }\n\n    EncryptedClientContact ClientContact::encrypt_and_sign(const Ed25519BlindedKey& blinded) const\n    {\n        EncryptedClientContact enc{};\n\n        try\n        {\n            enc.blinded_pubkey.assign(blinded.pubkey);\n            enc.encrypted = bt_encode();\n\n            crypto::xchacha20(enc.encrypted, SharedSecret{_pubkey}, enc.nonce);\n            enc.signed_at = llarp::time_now_ms();\n\n            auto btdp = enc.bt_encode_for_signing();\n            btdp.append_signature(\n                \"~\", [&blinded](std::span<const std::byte> to_sign) { return blinded.sign(to_sign); });\n\n            enc._bt_payload = std::move(btdp).str();\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"Exception encrypting and signing client contact: {}\", e.what());\n            throw;\n        }\n\n        return enc;\n    }\n\n    std::string ClientContact::to_string() const\n    {\n        return \"CC[{}{}, {}, {} intros]\"_format(\n            _pubkey.short_string(), _exit_policy ? \", exit\" : \"\", _intros.size(), llarp::to_string(_protos));\n    }\n\n    EncryptedClientContact::EncryptedClientContact(std::span<const std::byte> buf)\n        : EncryptedClientContact{std::string{reinterpret_cast<const char*>(buf.data()), buf.size()}}\n    {}\n    EncryptedClientContact::EncryptedClientContact(std::string buf) : _bt_payload{std::move(buf)}\n    {\n        bt_decode(oxenc::bt_dict_consumer{_bt_payload});\n    }\n\n    oxenc::bt_dict_producer EncryptedClientContact::bt_encode_for_signing() const\n    {\n        oxenc::bt_dict_producer btdp;\n        btdp.append(\"i\", blinded_pubkey.to_view());\n        btdp.append(\"n\", nonce.to_view());\n        btdp.append(\"t\", signed_at.count());\n        btdp.append(\"x\", std::span{encrypted});\n        return btdp;\n    }\n\n    /** EncryptedClientContact\n            \"i\" blinded pubkey\n            \"n\" nonce\n            \"t\" signing time\n            \"x\" encrypted payload\n            \"~\" signature\n    */\n    void EncryptedClientContact::bt_decode(oxenc::bt_dict_consumer&& btdc)\n    {\n        try\n        {\n            blinded_pubkey.assign(btdc.require_span<std::byte, PubKey::SIZE>(\"i\"));\n            nonce.assign(btdc.require_span<std::byte, SymmNonce::SIZE>(\"n\"));\n            signed_at = std::chrono::milliseconds{btdc.require<int64_t>(\"t\")};\n\n            auto enc = btdc.require_span<std::byte>(\"x\");\n            encrypted.assign(enc.begin(), enc.end());\n\n            btdc.require_signature(\"~\", [this](std::span<const std::byte> m, std::span<const std::byte> s) {\n                if (s.size() != 64)\n                    throw std::runtime_error{\"Invalid signature: not 64 bytes\"};\n\n                if (not crypto::verify(blinded_pubkey, m, s.first<64>()))\n                    throw std::runtime_error{\"EncryptedClientContact signature verification failed\"};\n            });\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"EncryptedClientContact deserialization failed: {}\", e.what());\n            log::trace(logcat, \"Failing Encrypted CC data: {}\", buffer_printer{_bt_payload});\n            throw;\n        }\n    }\n\n    std::optional<ClientContact> EncryptedClientContact::decrypt(const PubKey& root) const\n    {\n        std::optional<ClientContact> cc;\n        auto plaintext = encrypted;\n        crypto::xchacha20(plaintext, SharedSecret{root}, nonce);\n        try\n        {\n            cc.emplace(plaintext);\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"Client contact decryption failed for {}\", root);\n        }\n\n        return cc;\n    }\n\n    bool EncryptedClientContact::is_expired(std::chrono::milliseconds now) const\n    {\n        return now >= signed_at + path::MAX_LIFETIME_ACCEPTED;\n    }\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/contact/client_contact.hpp",
    "content": "#pragma once\n\n#include \"client_intro.hpp\"\n\n#include <llarp/constants/version.hpp>\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/dns/srv_data.hpp>\n#include <llarp/net/policy.hpp>\n#include <llarp/util/aligned.hpp>\n#include <llarp/util/buffer.hpp>\n#include <llarp/util/file.hpp>\n#include <llarp/util/time.hpp>\n\n#include <nlohmann/json_fwd.hpp>\n#include <oxenc/bt_producer.h>\n\n#include <unordered_set>\n#include <vector>\n\nnamespace llarp\n{\n    struct EncryptedClientContact;\n\n    // TESTNET:\n    inline static constexpr auto CC_PUBLISH_INTERVAL{5min};\n\n    /** ClientContact\n        On the wire we encode the data as a dict containing:\n            - \"\" : the CC format version, which must be == ClientContact::VERSION to be parsed successfully\n            - \"a\" : public key of the remote client instance\n            - \"e\" : (optional) exit policy containing sublists of accepted protocols and routed IP ranges\n            - \"i\" : list of client introductions corresponding to the different pivots through which paths can be built\n                    to the client instance\n            - \"p\" : supported protocols indicating the traffic accepted by the client instance; this indicates if the\n                    client is embedded and therefore requires a tunneled connection. Serialized as a bitwise flag of\n                    protocol_flag enums (llarp/net/policy.hpp)\n            - \"s\" : (optional) SRV records for lokinet DNS lookup\n    */\n    struct ClientContact\n    {\n        inline static constexpr uint8_t VERSION{0};\n\n        ClientContact() = default;\n\n        /// Constructs a ClientContact by parsing a serialized client contact value.  Throws if\n        /// invalid.\n        explicit ClientContact(std::span<const std::byte> buf);\n\n        /** Parameters:\n            - `private_data` : derived private subkey data\n            - `pubkey` : master identity key pubkey\n            - `srvs` : SRV records (optional, can be empty)\n            - `proto_flags` : client-supported protocols\n            - `policy` : exit-related traffic policy (optional)\n         */\n        ClientContact(\n            PubKey pk,\n            std::unordered_set<dns::SRVData> srvs,\n            protocol_flag protocols,\n            std::optional<net::ExitPolicy> policy = std::nullopt);\n\n        // Encrypts and signs the client contact with the given blinded keypair\n        EncryptedClientContact encrypt_and_sign(const Ed25519BlindedKey& blinded) const;\n\n        /// Replaces the client intros in the current introset with the given values.  It is not\n        /// necessary for the given values to be pre-sorted (i.e. this functions sorts them as\n        /// required).\n        void update_intros(std::vector<ClientIntro> intros);\n\n        const PubKey& pubkey() const { return _pubkey; }\n        // Returns the current intros; these will always be sorted in descending expiry order (i.e.\n        // last entry is the first to expire).\n        std::span<const ClientIntro> intros() const& { return _intros; }\n\n        const std::unordered_set<dns::SRVData>& SRVs() const { return _srv; }\n\n        protocol_flag protocols() const { return _protos; }\n\n        const std::optional<net::ExitPolicy>& exit_policy() const { return _exit_policy; }\n\n        bool is_expired(std::chrono::milliseconds now = llarp::time_now_ms()) const;\n\n      private:\n        PubKey _pubkey;\n\n        std::vector<ClientIntro> _intros;\n        std::unordered_set<dns::SRVData> _srv;\n\n        protocol_flag _protos;\n\n        // In exit mode, we advertise our policy for accepted traffic and the corresponding ranges\n        std::optional<net::ExitPolicy> _exit_policy;\n\n        std::vector<std::byte> bt_encode() const;\n\n      public:\n        bool operator==(const ClientContact& other) const\n        {\n            return std::tie(_pubkey, _intros, _srv, _protos, _exit_policy)\n                == std::tie(other._pubkey, other._intros, other._srv, other._protos, other._exit_policy);\n        }\n\n        std::string to_string() const;\n        static constexpr bool to_string_formattable = true;\n    };\n\n    // TODO: there should be a limit on how large an encCC we will store on relays, and we should\n    // check that when we generate & sign as well to make sure we don't exceed it.\n    //\n    /** EncryptedClientContact\n            \"i\" blinded local Ed25519 pubkey\n            \"n\" nonce\n            \"t\" signing time\n            \"x\" encrypted payload\n            \"~\" signature   (signed with blinded derived scalar `b`)\n    */\n    struct EncryptedClientContact\n    {\n        EncryptedClientContact() : nonce{SymmNonce::make_random()} {}\n\n        explicit EncryptedClientContact(std::span<const std::byte> buf);\n        explicit EncryptedClientContact(std::string buf);\n\n      private:\n        friend struct ClientContact;\n\n        PubKey blinded_pubkey;\n        SymmNonce nonce;\n        std::chrono::milliseconds signed_at{0s};\n        std::vector<std::byte> encrypted;\n\n        std::string _bt_payload;\n\n        // Returns a dict-in-progress containing everything except for the ~ signature.\n        [[nodiscard]] oxenc::bt_dict_producer bt_encode_for_signing() const;\n\n        void bt_decode(oxenc::bt_dict_consumer&& btdc);\n\n      public:\n        const PubKey& key() const { return blinded_pubkey; }\n\n        std::optional<ClientContact> decrypt(const PubKey& root) const;\n\n        std::string_view bt_payload() const { return _bt_payload; }\n\n        bool is_expired(std::chrono::milliseconds now = time_now_ms()) const;\n\n        bool newer_than(const EncryptedClientContact& that) const { return signed_at > that.signed_at; }\n    };\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/contact/client_intro.cpp",
    "content": "#include \"client_contact.hpp\"\n\n#include <llarp/util/logging.hpp>\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"client-intro\");\n\n    ClientIntro::ClientIntro(oxenc::bt_dict_consumer&& btdc)\n    {\n        expiry = std::chrono::sys_seconds{std::chrono::seconds{btdc.require<int64_t>(\"e\")}};\n        hop.assign(btdc.require_span<std::byte, HopID::SIZE>(\"h\"));\n        relay.assign(btdc.require_span<std::byte, RouterID::SIZE>(\"r\"));\n    }\n\n    ClientIntro::ClientIntro(std::string_view buf) : ClientIntro{oxenc::bt_dict_consumer{buf}} {}\n\n    void ClientIntro::bt_encode(oxenc::bt_dict_producer&& subdict) const\n    {\n        subdict.append(\"e\", expiry.time_since_epoch().count());\n        subdict.append(\"h\", hop.to_view());\n        subdict.append(\"r\", relay.to_view());\n    }\n\n    std::string ClientIntro::to_string() const\n    {\n        return \"Intro[{}, hop={}, exp={}]\"_format(\n            relay.short_string(), hop.short_string(), expiry.time_since_epoch().count());\n    }\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/contact/client_intro.hpp",
    "content": "#pragma once\n\n#include <llarp/contact/router_id.hpp>\n#include <llarp/crypto/types.hpp>\n#include <llarp/path/hopid.hpp>\n#include <llarp/util/time.hpp>\n\n#include <oxenc/bt.h>\n\nnamespace llarp\n{\n    struct ClientIntro\n    {\n        RouterID relay;\n        HopID hop;\n        std::chrono::sys_seconds expiry;\n\n        ClientIntro() = default;\n        ClientIntro(oxenc::bt_dict_consumer&&);\n        ClientIntro(std::string_view buf);\n\n        std::chrono::milliseconds expires_in(std::chrono::milliseconds now = llarp::time_now_ms()) const\n        {\n            return expiry.time_since_epoch() - now;\n        }\n\n        bool is_expired(std::chrono::milliseconds now = llarp::time_now_ms()) const { return expires_in(now) <= 0ms; }\n\n        void bt_encode(oxenc::bt_dict_producer&& subdict) const;\n\n        bool operator==(const ClientIntro& other) const = default;\n\n        std::string to_string() const;\n        static constexpr bool to_string_formattable = true;\n    };\n\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/contact/contactdb.cpp",
    "content": "#include \"contactdb.hpp\"\n\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/router/router.hpp>\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"contactdb\");\n\n    ContactDB::ContactDB(Router& r) : _router{r} {}\n\n    const EncryptedClientContact* ContactDB::get_encrypted_cc(const PubKey& blinded_key) const\n    {\n        if (auto it = _storage.find(blinded_key); it != _storage.end() && not it->second.is_expired())\n            return &it->second;\n        return nullptr;\n    }\n\n    size_t ContactDB::num_ccs() const { return _storage.size(); }\n\n    void ContactDB::start_tickers()\n    {\n        _purge_ticker = _router.loop.call_every(30s, [this]() { purge_ccs(); }, true);\n    }\n\n    void ContactDB::purge_ccs(std::chrono::milliseconds now)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        if (_router.is_stopping() || not _router.is_running())\n        {\n            log::debug(logcat, \"ContactDB unable to continue purge ticking -- router is stopped!\");\n            return;\n        }\n\n        size_t removed = std::erase_if(_storage, [&now](const auto& c) { return c.second.is_expired(now); });\n        if (removed)\n            log::debug(logcat, \"{} expired ClientContacts purged, {} remaining\", removed, _storage.size());\n        else\n            log::trace(logcat, \"No ClientContacts current expired (of {})\", _storage.size());\n    }\n\n    void ContactDB::put_cc(EncryptedClientContact enc)\n    {\n        auto& current = _storage[enc.key()];\n        if (enc.newer_than(current))\n            current = std::move(enc);\n    }\n\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/contact/contactdb.hpp",
    "content": "#pragma once\n\n#include \"client_contact.hpp\"\n\nnamespace oxen::quic\n{\n    struct Ticker;\n}\n\nnamespace llarp\n{\n    class Router;\n\n    /**\n        ContactDB TODO:\n        - Store nearest-furthest expiry, trim\n    */\n\n    /// This class mediates storage, retrieval, and functionality for ClientContacts\n    class ContactDB\n    {\n      private:\n        Router& _router;\n\n        std::unordered_map<PubKey, EncryptedClientContact, AlignedHasher> _storage;\n\n        std::shared_ptr<quic::Ticker> _purge_ticker;\n\n      public:\n        explicit ContactDB(Router& r);\n\n        const EncryptedClientContact* get_encrypted_cc(const PubKey& blinded_pk) const;\n\n        void put_cc(EncryptedClientContact enc);\n\n        void start_tickers();\n\n        size_t num_ccs() const;\n\n      private:\n        void purge_ccs(std::chrono::milliseconds now = llarp::time_now_ms());\n    };\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/contact/relay_contact.cpp",
    "content": "#include \"relay_contact.hpp\"\n\n#include <llarp/constants/version.hpp>\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/util/file.hpp>\n#include <llarp/util/formattable.hpp>\n#include <llarp/util/logging.hpp>\n\n#include <nlohmann/json.hpp>\n#include <oxenc/bt_producer.h>\n#include <oxenc/bt_serialize.h>\n\n#include <chrono>\n#include <unordered_set>\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"relay-contact\");\n\n    using namespace oxenc::literals;\n\n    void RelayContact::load(NetID netid, bool accept_expired)\n    {\n        oxenc::bt_dict_consumer btdc{_payload};\n\n        // The \"\" key containing the RC version key is optional: if omitted we assume version 0.  We\n        // still look for it, though, so that if we need to introduce backwards-incompatible RC\n        // changes for some reason we can do so in such a way that older versions will properly\n        // reject them.\n        if (uint8_t rc_ver = btdc.maybe<uint8_t>(\"\").value_or(0); rc_ver != RelayContact::VERSION)\n            throw std::runtime_error{\"Invalid RC: do not know how to parse v{} RCs\"_format(rc_ver)};\n\n        auto parsed_netid = static_cast<NetID>(btdc.maybe<int>(\"#\").value_or(static_cast<int>(NetID::MAINNET)));\n\n        if (netid != parsed_netid)\n            throw std::runtime_error{\n                \"Invalid RC netid: expected {}, got {}; this is an RC for a different network!\"_format(\n                    netid, parsed_netid)};\n        _netid = netid;\n\n        auto ipv4_port = btdc.require<std::string_view>(\"4\");\n\n        if (ipv4_port.size() != 6)\n            throw std::runtime_error{\n                \"Invalid RC address: expected 6-byte IPv4 IP/port, got {}\"_format(ipv4_port.size())};\n\n        sockaddr_in s4;\n        s4.sin_family = AF_INET;\n\n        std::memcpy(&s4.sin_addr.s_addr, ipv4_port.data(), 4);\n        std::memcpy(&s4.sin_port, ipv4_port.data() + 4, 2);\n\n        _addr = quic::Address{&s4};\n\n        if (!_addr.is_public() and BLOCK_BOGONS)\n            throw std::runtime_error{\"Invalid RC: IPv4 address is not a publicly routable IP\"};\n\n        if (auto ipv6_port = btdc.maybe<std::string_view>(\"6\"))\n        {\n            if (ipv6_port->size() != 18)\n                throw std::runtime_error{\n                    \"Invalid RC address: expected 18-byte IPv6 IP/port, got {}\"_format(ipv6_port->size())};\n\n            sockaddr_in6 s6{};\n            s6.sin6_family = AF_INET6;\n\n            std::memcpy(&s6.sin6_addr.s6_addr, ipv6_port->data(), 16);\n            std::memcpy(&s6.sin6_port, ipv6_port->data() + 16, 2);\n\n            _addr6.emplace(&s6);\n            if (!_addr6->is_public())\n                throw std::runtime_error{\"Invalid RC: IPv6 address is not a publicly routable IP\"};\n        }\n        else\n        {\n            _addr6.reset();\n        }\n\n        auto pubkey = btdc.require<std::string_view>(\"p\");\n        if (pubkey.size() != 32)\n            throw std::runtime_error{\"Invalid RC pubkey: expected 32 bytes, got {}\"_format(pubkey.size())};\n        std::memcpy(_router_id.data(), pubkey.data(), 32);\n\n        _timestamp = std::chrono::sys_seconds{std::chrono::seconds{btdc.require<uint64_t>(\"t\")}};\n\n        auto ver = btdc.require<std::span<const uint8_t>>(\"v\");\n\n        if (ver.size() != 3)\n            throw std::runtime_error{\"Invalid RC router version: received {} bytes, expected 3\"_format(ver.size())};\n\n        for (int i = 0; i < 3; i++)\n            _router_version[i] = ver[i];\n\n        btdc.require_signature(\n            \"~\", [this, accept_expired](std::span<const std::byte> msg, std::span<const std::byte> sig) {\n                if (sig.size() != 64)\n                    throw std::runtime_error{\"Invalid signature: not 64 bytes\"};\n\n                if (!accept_expired and is_expired(time_now_ms()))\n                    throw std::runtime_error{\"Rejecting expired relay contact!\"};\n\n                if (not crypto::verify(router_id(), msg, sig.first<64>()))\n                    throw std::runtime_error{\"Failed to verify relay contact signature\"};\n            });\n\n        if (not btdc.is_finished())\n            throw std::runtime_error{\"relay contact has invalid post-signature fields\"};\n\n        btdc.finish();\n    }\n\n    bool RelayContact::write(const std::filesystem::path& fname) const\n    {\n        try\n        {\n            util::buffer_to_file(fname, _payload);\n        }\n        catch (const std::exception& e)\n        {\n            log::error(logcat, \"Failed to write RC to {}: {}\", fname, e.what());\n            return false;\n        }\n        return true;\n    }\n\n    nlohmann::json RelayContact::extract_status() const\n    {\n        nlohmann::json obj{\n            {\"lastUpdated\", _timestamp.time_since_epoch().count()},\n            {\"publicRouter\", _addr.is_public()},\n            {\"identity\", _router_id.to_string()},\n            {\"address\", _addr.to_string()}};\n\n        return obj;\n    }\n\n    std::string RelayContact::to_string() const\n    {\n        return \"RCv{}[{} @ {}, t={}]\"_format(VERSION, _router_id, _addr, _timestamp.time_since_epoch().count());\n    }\n\n    bool RelayContact::has_ip_overlap(const RelayContact& other, uint8_t netmask) const\n    {\n        return (_addr.to_ipv4() / netmask).contains(other._addr.to_ipv4());\n    }\n\n    bool RelayContact::is_outdated(std::chrono::milliseconds now) const\n    {\n        return now >= _timestamp.time_since_epoch() + OUTDATED_AGE;\n    }\n\n    bool RelayContact::is_expired(std::chrono::milliseconds now) const\n    {\n        return now >= _timestamp.time_since_epoch() + LIFETIME;\n    }\n\n    std::chrono::milliseconds RelayContact::time_to_expiry(std::chrono::milliseconds now) const\n    {\n        const auto expiry = _timestamp.time_since_epoch() + LIFETIME;\n        return now < expiry ? expiry - now : 0s;\n    }\n\n    std::chrono::milliseconds RelayContact::age(std::chrono::milliseconds now) const\n    {\n        auto delta = now - _timestamp.time_since_epoch();\n        return delta > 0s ? delta : 0s;\n    }\n\n    bool RelayContact::expires_within_delta(std::chrono::milliseconds now, std::chrono::milliseconds dlt) const\n    {\n        return time_to_expiry(now) <= dlt;\n    }\n\n    static const std::unordered_set<std::string_view> obsolete_bootstraps{\n        // Currently none (since Lokinet network reboot invalidated all old ones anyway)\n        // \"7a16ac0b85290bcf69b2f3b52456d7e989ac8913b4afbb980614e249a3723218\"_hex,\n    };\n\n    bool RelayContact::is_obsolete() const { return obsolete_bootstraps.contains(_router_id.to_view()); }\n\n    bool RelayContact::address_changed(const RelayContact& other) const\n    {\n        return std::tie(_addr, _addr6) != std::tie(other._addr, other._addr6);\n    }\n\n    RelayContact::RelayContact(const Router& router)\n        : _router_id{router.id()},\n          _addr{router.public_addr()},\n          _timestamp{std::chrono::time_point_cast<std::chrono::seconds>(std::chrono::system_clock::now())},\n          _netid{router.netid()},\n          _router_version{llarp::LOKINET_VERSION}\n    {\n        oxenc::bt_dict_producer btdp;\n        if (VERSION != 0)\n            btdp.append(\"\", VERSION);\n\n        if (_netid != NetID::MAINNET)\n            btdp.append(\"#\", static_cast<int>(_netid));\n\n        std::array<unsigned char, 18> buf;\n\n        {\n            if (not _addr.is_ipv4())\n                throw std::runtime_error{\"Unable to encode RC: addr is not IPv4\"};\n\n            auto in4 = _addr.in4();\n\n            std::memcpy(buf.data(), &in4.sin_addr.s_addr, 4);\n            std::memcpy(buf.data() + 4, &in4.sin_port, 2);\n\n            btdp.append(\"4\", std::span<const uint8_t>{buf.data(), 6});\n        }\n\n        if (_addr6)\n        {\n            if (not _addr.is_ipv6())\n                throw std::runtime_error{\"Unable to encode RC: addr6 is set but is not IPv6\"};\n\n            auto in6 = _addr.in6();\n\n            std::memcpy(buf.data(), &in6.sin6_addr.s6_addr, 16);\n            std::memcpy(buf.data() + 16, &in6.sin6_port, 2);\n\n            btdp.append(\"6\", std::span<const uint8_t>{buf.data(), 18});\n        }\n\n        btdp.append(\"p\", _router_id.to_view());\n\n        btdp.append(\"t\", _timestamp.time_since_epoch().count());\n\n        static_assert(llarp::LOKINET_VERSION.size() == 3);\n        btdp.append(\"v\", std::span{_router_version});\n\n        btdp.append_signature(\"~\", [&router](std::span<const std::byte> to_sign) {\n            std::array<std::byte, SIGSIZE> sig;\n            router.secret_key().sign(sig, to_sign);\n            return sig;\n        });\n        _payload = std::move(btdp).str();\n\n        if (_payload.size() > MAX_RC_SIZE)\n            throw std::invalid_argument{\"Invalid RC: exceeds maximum size\"};\n    }\n\n    RelayContact::RelayContact(std::string_view data, NetID netid, bool accept_expired)\n    {\n        if (data.size() > MAX_RC_SIZE)\n            throw std::invalid_argument{\"Invalid RC: exceeds maximum size\"};\n        _payload = data;\n        load(netid, accept_expired);\n    }\n\n    template <>\n    RelayContact::RelayContact(const std::filesystem::path& fname, NetID netid, bool accept_expired)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n        _payload = util::file_to_string(fname);\n        load(netid, accept_expired);\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/contact/relay_contact.hpp",
    "content": "#pragma once\n\n#include \"router_id.hpp\"\n\n#include <llarp/net/id.hpp>\n#include <llarp/util/time.hpp>\n\n#include <nlohmann/json_fwd.hpp>\n#include <oxen/quic/address.hpp>\n#include <oxenc/bt_producer.h>\n\n#include <filesystem>\n\nnamespace llarp\n{\n    class Router;\n\n    namespace quic = oxen::quic;\n\n    /** RelayContact\n        On the wire we encode the data as a dict containing:\n        - \"\" : the RC format version, omitted when 0, and which must be == RelayContact::VERSION for\n               us to attempt to parse the reset of the fields.  (Future versions might have\n               backwards-compat support for lower versions).\n        - \"4\" : 6 byte packed IPv4 address & port: 4 bytes of IPv4 address followed by 2 bytes of\n                port, both encoded in network (i.e. big-endian) order.\n        - \"6\" : optional 18 byte IPv6 address & port: 16 byte raw IPv6 address followed by 2 bytes\n                of port in network order.\n        - \"i\" : optional network ID integer: 0 or omitted for mainnet, 1 for testnet.\n        - \"p\" : 32-byte router pubkey (Ed25519)\n        - \"t\" : timestamp when this RC record was created (which also implicitly determines when it\n                goes stale and when it expires).\n        - \"v\" : lokinet version of the router; this is a three-byte packed value of\n                MAJOR, MINOR, PATCH, e.g. \\x00\\x0a\\x03 for 0.10.3.\n        - \"~\" : signature of all of the previous serialized data, signed by \"p\", and *must* be the\n                last item in the dict.\n    */\n    struct RelayContact\n    {\n        /// The RC version.  Changing this means the RC will not be accepted by any previous\n        /// versions of Lokinet.\n        static constexpr uint8_t VERSION{0};\n\n        /// Unit tests disable this to allow private IP ranges in RCs, which normally get rejected.\n        inline static bool BLOCK_BOGONS{true};\n\n        /// Maximum permitted RC size.  This is considerably larger than needed to allow future\n        /// versions to add various fields without breaking the ability for existing lokinet\n        /// versions to handle the RC (for example: a ML-KEM-1024 PQC key is 1568 bytes).\n        static constexpr size_t MAX_RC_SIZE{2048};\n\n        /// How long (from its signing time) before an RC becomes \"outdated\".  Outdated records are\n        /// used (e.g. for path building) only if there are no newer records available, such as\n        /// might be the case when a client has been turned off for a while.\n        static constexpr auto OUTDATED_AGE{12h};\n\n        /// How long before an RC becomes invalid (and thus deleted).\n        static constexpr auto LIFETIME{30 * 24h};\n\n        /// Minimum age difference between an existing RC and a new, gossipped RC from the same\n        /// relay.  We ignore RCs that are not more than this amount older than the current one.\n        static constexpr auto MIN_GOSSIP_RC_AGE = 1min;\n\n        std::string_view view() const { return _payload; }\n\n        /// Getters for private attributes\n        const quic::Address& addr() const { return _addr; }\n\n        const std::optional<quic::Address>& addr6() const { return _addr6; }\n\n        const RouterID& router_id() const { return _router_id; }\n\n        const std::chrono::sys_seconds& timestamp() const { return _timestamp; }\n\n        NetID netid() const { return _netid; }\n\n      private:\n        // public signing public key\n        RouterID _router_id;\n\n        // advertised addresses\n        quic::Address _addr;\n        std::optional<quic::Address> _addr6;  // optional ipv6\n\n        std::chrono::sys_seconds _timestamp{};\n        NetID _netid = NetID::MAINNET;\n\n        // Lokinet version at the time the RC was produced\n        std::array<uint8_t, 3> _router_version;\n\n        // Contains the full bt-encoded payload of the RC.\n        std::string _payload;\n\n        // Loads data from the current `_payload` value.\n        void load(NetID netid, bool accept_expired = false);\n\n        auto compare_tuple() const { return std::tie(_router_id, _addr, _addr6, _timestamp, _router_version); }\n\n      public:\n        RelayContact() = default;\n        // Parses a serialized RC\n        RelayContact(std::string_view data, NetID netid, bool accept_expired = false);\n        // Reads a serialized RC from disk and parses it\n        template <std::same_as<std::filesystem::path> FSPath>\n        RelayContact(const FSPath& fname, NetID netid, bool accept_expired = false);\n\n        // Constructs a signed RC from the info in the given Router object.\n        explicit RelayContact(const Router& router);\n\n        nlohmann::json extract_status() const;\n\n        bool write(const std::filesystem::path& fname) const;\n\n        bool operator==(const RelayContact& other) const { return compare_tuple() == other.compare_tuple(); }\n\n        bool has_ip_overlap(const RelayContact& other, uint8_t netmask) const;\n\n        /// does this RC expire soon? default delta is 1 minute\n        bool expires_within_delta(std::chrono::milliseconds now, std::chrono::milliseconds dlt = 1min) const;\n\n        /// returns true if this RC is outdated and should be re-fetched\n        bool is_outdated(std::chrono::milliseconds now = llarp::time_now_ms()) const;\n\n        /// returns true if this RC is expired and should be removed\n        bool is_expired(std::chrono::milliseconds now) const;\n\n        /// returns time in ms until we expire or 0 if we have expired\n        std::chrono::milliseconds time_to_expiry(std::chrono::milliseconds now) const;\n\n        /// get the age of this RC in ms\n        std::chrono::milliseconds age(std::chrono::milliseconds now) const;\n\n        // Returns true if this RC is at least `at_least` newer than `other`.  (By default threshold\n        // is 1s, which is the minimum precision of RCs, and so this returns true if this is at all\n        // newer than other).\n        bool newer_than(const RelayContact& other, std::chrono::seconds at_least = 1s) const\n        {\n            return _timestamp - other._timestamp >= at_least;\n        }\n\n        // Returns true if this RC has a different contact address (IP/port) from `other`.  This is\n        // used when deciding how important an RC update is when deciding whether to gossip (minor\n        // updates are only gossipped if they change this contact info).\n        bool address_changed(const RelayContact& other) const;\n\n        // Returns true if this RC is on the hard-coded list of obsolete bootstrap nodes; this is\n        // only used when loading bootstraps to ensure known, no-longer-valid bootstraps are\n        // excluded even if in a stale bootstrap.signed file.\n        bool is_obsolete() const;\n\n        std::string to_string() const;\n        static constexpr bool to_string_formattable = true;\n    };\n\n}  // namespace llarp\n\ntemplate <>\nstruct std::hash<llarp::RelayContact>\n{\n    virtual size_t operator()(const llarp::RelayContact& r) const noexcept\n    {\n        return std::hash<llarp::PubKey>{}(r.router_id());\n    }\n};\n"
  },
  {
    "path": "llarp/contact/router_id.cpp",
    "content": "#include \"router_id.hpp\"\n\n#include <llarp/util/formattable.hpp>\n\n#include <nlohmann/json.hpp>\n#include <oxenc/base32z.h>\n\n#include <iterator>\n\nnamespace llarp\n{\n    namespace\n    {\n        constexpr auto RELAY_DOT_TLD = \".snode\"sv;\n        constexpr auto CLIENT_DOT_TLD = \".loki\"sv;\n        constexpr auto B32Z_ID_SIZE = oxenc::to_base32z_size(RouterID::SIZE);\n    }  // namespace\n\n    std::string RouterID::AddressPrinter::to_string() const\n    {\n        std::string r;\n        r.reserve(B32Z_ID_SIZE + (is_relay ? RELAY_DOT_TLD : CLIENT_DOT_TLD).size());\n        oxenc::to_base32z(rid.begin(), rid.end(), std::back_inserter(r));\n        r += is_relay ? RELAY_DOT_TLD : CLIENT_DOT_TLD;\n        return r;\n    }\n\n    std::string RouterID::to_string() const { return oxenc::to_base32z(begin(), end()); }\n\n    nlohmann::json RouterID::ExtractStatus() const { return {{\"snode\", to_string()}, {\"hex\", ToHex()}}; }\n\n    void RouterID::from_network_address(std::string_view str)\n    {\n        if (str.ends_with(RELAY_DOT_TLD))\n            str.remove_suffix(RELAY_DOT_TLD.size());\n        else if (str.ends_with(CLIENT_DOT_TLD))\n            str.remove_suffix(CLIENT_DOT_TLD.size());\n        else\n            throw std::invalid_argument{\n                \"Did not find expected .loki or .snode TLD in network address '{}'\"_format(str)};\n\n        if (str.size() != B32Z_ID_SIZE || !oxenc::is_base32z(str) || !(str.back() == 'o' || str.back() == 'y'))\n            throw std::invalid_argument{\"RouterID input is incorrect (input: {})\"_format(str)};\n\n        oxenc::from_base32z(str.begin(), str.end(), begin());\n    }\n\n    bool RouterID::from_relay_address(std::string_view str)\n    {\n        if (!str.ends_with(RELAY_DOT_TLD))\n            return false;\n        str.remove_suffix(RELAY_DOT_TLD.size());\n        if (str.size() != B32Z_ID_SIZE || !oxenc::is_base32z(str) || !(str.back() == 'o' || str.back() == 'y'))\n            return false;\n        oxenc::from_base32z(str.begin(), str.end(), begin());\n        return true;\n    }\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/contact/router_id.hpp",
    "content": "#pragma once\n\n#include <llarp/crypto/keys.hpp>\n#include <llarp/crypto/types.hpp>\n\n#include <nlohmann/json_fwd.hpp>\n\nnamespace llarp\n{\n    using namespace std::literals;\n\n    struct RouterID : public PubKey\n    {\n        using PubKey::PubKey;\n\n        nlohmann::json ExtractStatus() const;\n\n        std::string to_string() const;\n\n        // will throw on failure!\n        void from_network_address(std::string_view str);\n\n        bool from_relay_address(std::string_view str);\n\n        // Helper class that returns a fmt-printable address for a router ID on the fly.  This class\n        // should only be used ephemerally.\n        struct AddressPrinter\n        {\n            const RouterID& rid;\n            bool is_relay;\n            std::string to_string() const;\n            static constexpr bool to_string_formattable = true;\n        };\n        AddressPrinter to_network_address(bool is_relay = true) const { return {.rid = *this, .is_relay = is_relay}; }\n    };\n\n    inline bool operator==(const RouterID& lhs, const RouterID& rhs) { return lhs.as_array() == rhs.as_array(); }\n}  // namespace llarp\n\nnamespace std\n{\n    template <>\n    struct hash<llarp::RouterID> : hash<llarp::PubKey>\n    {};\n}  // namespace std\n"
  },
  {
    "path": "llarp/contact/sns.cpp",
    "content": "#include \"sns.hpp\"\n\n#include <llarp/address/address.hpp>\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/util/logging.hpp>\n\nnamespace llarp\n{\n    static auto logcat = llarp::log::Cat(\"ONSRecord\");\n\n    EncryptedSNSRecord EncryptedSNSRecord::deserialize(std::string_view bt) { return EncryptedSNSRecord{bt}; }\n\n    EncryptedSNSRecord::EncryptedSNSRecord(std::string_view bt) : _bt_payload{bt}\n    {\n        bt_decode(oxenc::bt_dict_consumer{_bt_payload});\n    }\n\n    void EncryptedSNSRecord::bt_decode(oxenc::bt_dict_consumer&& btdc)\n    {\n        try\n        {\n            ciphertext = btdc.require<std::string>(\"c\");\n            nonce.assign(btdc.require_span<std::byte, SymmNonce::SIZE>(\"n\"));\n        }\n        catch (...)\n        {\n            log::warning(logcat, \"EncryptedSNSRecord exception\");\n            throw;\n        }\n    }\n\n    std::string EncryptedSNSRecord::bt_encode() const\n    {\n        oxenc::bt_dict_producer btdp;\n\n        btdp.append(\"c\", ciphertext);\n        btdp.append(\"n\", nonce.to_view());\n\n        return std::move(btdp).str();\n    }\n\n    std::optional<NetworkAddress> EncryptedSNSRecord::decrypt(std::string_view sns_name) const\n    {\n        std::optional<NetworkAddress> ret;\n        if (ciphertext.empty())\n            return ret;\n\n        if (auto maybe = crypto::maybe_decrypt_name(ciphertext, nonce, sns_name))\n            ret.emplace(*maybe, /*is_client=*/true);\n\n        return ret;\n    }\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/contact/sns.hpp",
    "content": "#pragma once\n\n#include <llarp/crypto/types.hpp>\n#include <llarp/util/str.hpp>\n\n#include <oxenc/bt.h>\n\nnamespace llarp\n{\n    struct NetworkAddress;\n\n    /** Holds an entire SNS Record returned from a succfessful request to the `lookup_name` endpoint.\n        When transmitted over the wire back to the calling instance, it is bt-encoded and the SNS hash\n        ('ciphertext') is decrypted using the sns_name.\n\n        bt-encoded keys:\n            'c' : ciphertext\n            'n' : nonce\n    */\n    struct EncryptedSNSRecord\n    {\n      private:\n        explicit EncryptedSNSRecord(std::string_view bt);\n        void bt_decode(oxenc::bt_dict_consumer&& btdc);\n\n        std::string _bt_payload;\n\n      public:\n        SymmNonce nonce;\n        std::string ciphertext;\n\n        EncryptedSNSRecord() = default;\n\n        std::string_view bt_payload() const { return _bt_payload; }\n\n        static EncryptedSNSRecord deserialize(std::string_view bt);\n\n        std::string bt_encode() const;\n\n        std::optional<NetworkAddress> decrypt(std::string_view sns_name) const;\n    };\n\n    /// check if an sns name complies with the registration rules\n    inline bool is_valid_sns(std::string_view sns_name)\n    {\n        if (not sns_name.ends_with(\".loki\"))\n            return false;\n\n        // strip off .loki suffix\n        sns_name.remove_suffix(5);\n\n        // ensure chars are sane\n        for (const auto ch : sns_name)\n        {\n            if (ch == '-')\n                continue;\n            if (ch == '.')\n                continue;\n            if (ch >= 'a' and ch <= 'z')\n                continue;\n            if (ch >= '0' and ch <= '9')\n                continue;\n            return false;\n        }\n\n        // split into domain parts\n        const auto parts = split(sns_name, \".\");\n\n        // get root domain\n        const auto primaryName = parts[parts.size() - 1];\n        constexpr size_t MaxNameLen = 32;\n        constexpr size_t MaxPunycodeNameLen = 63;\n\n        // check against lns name blacklist\n        if (primaryName == \"localhost\")\n            return false;\n        if (primaryName == \"loki\")\n            return false;\n        if (primaryName == \"snode\")\n            return false;\n        // check for dashes\n        if (primaryName.find(\"-\") == std::string_view::npos)\n            return primaryName.size() <= MaxNameLen;\n        // check for dashes and end or beginning\n        if (*primaryName.begin() == '-' or *(primaryName.end() - 1) == '-')\n            return false;\n        // check for punycode name length\n        if (primaryName.size() > MaxPunycodeNameLen)\n            return false;\n        // check for xn--\n        return (primaryName[2] == '-' and primaryName[3] == '-') ? (primaryName[0] == 'x' and primaryName[1] == 'n')\n                                                                 : true;\n    }\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/context.cpp",
    "content": "#include <llarp.hpp>\n#include <llarp/config/config.hpp>\n#include <llarp/constants/version.hpp>\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/link/link_manager.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/service_manager.hpp>\n\n#include <oxen/quic/loop.hpp>\n\n#include <csignal>\n#include <memory>\n#include <stdexcept>\n\n#if (__FreeBSD__) || (__OpenBSD__) || (__NetBSD__)\n#include <pthread_np.h>\n#endif\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"context\");\n\n    // Defaulted here because the header doesn't have visibility of the unique_ptr destructors.\n    Context::~Context() = default;\n\n    bool Context::is_up() const { return router && router->is_running(); }\n\n    bool Context::is_waiting() const { return router && !router->is_running(); }\n\n    bool Context::looks_alive() const { return router && router->looks_alive(); }\n\n    void Context::start(Config conf, std::shared_ptr<oxen::quic::Loop> loop)\n    {\n        if (router)\n        {\n            log::error(logcat, \"Context::start called but Lokinet is already running\");\n            throw std::logic_error{\"Lokinet is already started\"};\n        }\n        if (loop)\n            log::debug(logcat, \"Re-using existing loop\");\n        else\n        {\n            log::debug(logcat, \"Initializing event loop...\");\n\n            loop = std::make_shared<quic::Loop>();\n            assert(loop->call_get([] { return 42; }) == 42);\n\n            log::debug(logcat, \"Event loop initialized!\");\n        }\n\n        std::promise<void> done_promise;\n        lifetime_waiter = done_promise.get_future();\n\n        std::shared_ptr<llarp::vpn::Platform> plat;\n#ifndef LOKINET_EMBEDDED_ONLY\n        if (!embedded)\n        {\n            log::debug(logcat, \"Initializing platform code...\");\n            plat = vpn::MakeNativePlatform(this);\n            if (!plat)\n                throw std::runtime_error{\"This platform is not currently supported!\"};\n        }\n#endif\n\n        log::debug(logcat, \"Starting main router...\");\n        try\n        {\n            router =\n                std::make_unique<Router>(std::move(conf), std::move(loop), std::move(plat), std::move(done_promise));\n        }\n        catch (const std::exception& e)\n        {\n            log::error(logcat, \"Failed to initialize router: {}\", e.what());\n            throw;\n        }\n    }\n\n    void Context::wait()\n    {\n        if (!router)\n            return;\n        lifetime_waiter.get();\n        router.reset();\n    }\n\n    void Context::stop()\n    {\n        if (!router)\n            return;\n        router->stop();\n    }\n\n    bool Context::is_stopping() const { return router && router->is_stopping(); }\n\n    void Context::signal(int sig)\n    {\n        if (router && (sig == SIGINT || sig == SIGTERM || sig == SIGKILL))\n        {\n            log::warning(\n                logcat,\n                \"Received signal SIG{}; stopping router...\",\n                sig == SIGINT        ? \"INT\"\n                    : sig == SIGTERM ? \"TERM\"\n                                     : \"KILL\");\n            stop();\n        }\n    }\n\n    Context::Context(bool embedded) : embedded{embedded}\n    {\n#ifndef LOKINET_EMBEDDED_ONLY\n        // service_manager is a global and context isnt\n        if (!embedded)\n            llarp::sys::service_manager->give_context(this);\n#endif\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/crypto/constants.hpp",
    "content": "#pragma once\n\n#include <cstddef>\n\ninline constexpr size_t NONCESIZE{24};\ninline constexpr size_t HMACSECSIZE{32};\ninline constexpr size_t HMACSIZE{32};\ninline constexpr size_t PUBKEYSIZE{32};\ninline constexpr size_t SHAREDKEYSIZE{32};\ninline constexpr size_t SHORTHASHSIZE{32};\ninline constexpr size_t SIGSIZE{64};\ninline constexpr size_t SECKEYSIZE{64};\n"
  },
  {
    "path": "llarp/crypto/crypto.cpp",
    "content": "#include \"crypto.hpp\"\n\n#include <llarp/crypto/keys.hpp>\n#include <llarp/util/bspan.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/random.hpp>\n\n#include <oxenc/endian.h>\n#include <sodium/core.h>\n#include <sodium/crypto_aead_xchacha20poly1305.h>\n#include <sodium/crypto_core_ed25519.h>\n#include <sodium/crypto_generichash.h>\n#include <sodium/crypto_scalarmult_curve25519.h>\n#include <sodium/crypto_scalarmult_ed25519.h>\n#include <sodium/crypto_sign.h>\n#include <sodium/crypto_stream_xchacha20.h>\n#include <sodium/utils.h>\n\n#include <cassert>\n#include <cstring>\n#include <stdexcept>\n#ifdef LOKINET_HAVE_CRYPT\n#include <crypt.h>\n#endif\n\nnamespace llarp::crypto\n{\n    static auto logcat = log::Cat(\"crypto\");\n\n    static_assert(MAC_SIZE == crypto_aead_xchacha20poly1305_ietf_ABYTES);\n    static_assert(SymmNonce::SIZE == crypto_stream_xchacha20_NONCEBYTES);\n    static_assert(SharedSecret::SIZE == crypto_stream_xchacha20_KEYBYTES);\n\n    static bool dh(\n        SharedSecret& out,\n        const PubKey& client_pk,\n        const PubKey& server_pk,\n        bool we_are_client,\n        const Ed25519SecretKey& local_keys,\n        const SymmNonce& nonce)\n    {\n        SharedSecret shared;\n\n        // Somewhat misnamed: actually gets the private scalar (which happens to be what you need\n        // for converting to X):\n        std::array<unsigned char, 32> a;\n        crypto_sign_ed25519_sk_to_curve25519(a.data(), local_keys.data());\n\n        if (crypto_scalarmult_ed25519(shared.data(), a.data(), (we_are_client ? server_pk : client_pk).data()))\n            return false;\n\n        crypto_generichash_blake2b_state h;\n        crypto_generichash_blake2b_init(&h, nonce.data(), nonce.size(), shared.size());\n        crypto_generichash_blake2b_update(&h, client_pk.data(), client_pk.size());\n        crypto_generichash_blake2b_update(&h, server_pk.data(), server_pk.size());\n        crypto_generichash_blake2b_update(&h, shared.data(), shared.size());\n        crypto_generichash_blake2b_final(&h, out.data(), out.size());\n        return true;\n    }\n\n    std::optional<RouterID> maybe_decrypt_name(std::string_view ciphertext, SymmNonce nonce, std::string_view namestr)\n    {\n        const auto payloadsize = ciphertext.size() - MAC_SIZE;\n        if (payloadsize != 32)\n            return std::nullopt;\n\n        auto bname = as_bspan(namestr);\n\n        // The (unkeyed) blake2b hash of the name is the (public) SNS storage key:\n        auto namehash = shorthash(bname);\n\n        // The value itself is encrypted with a symmetric key that is also a blake2b hash of the\n        // name, but using a hash key to make it unrelated to the public hash:\n        AlignedBuffer<32> derivedKey;\n        crypto_generichash_blake2b(\n            derivedKey.data(),\n            derivedKey.size(),\n            as_uspan(bname).data(),\n            bname.size(),\n            namehash.data(),\n            namehash.size());\n\n        auto result = std::make_optional<RouterID>();\n        if (crypto_aead_xchacha20poly1305_ietf_decrypt(\n                result->data(),\n                nullptr,\n                nullptr,\n                reinterpret_cast<const uint8_t*>(ciphertext.data()),\n                ciphertext.size(),\n                nullptr,\n                0,\n                nonce.data(),\n                derivedKey.data())\n            != 0)\n            return std::nullopt;\n\n        return result;\n    }\n\n    void xchacha20(std::span<std::byte> buf, const SharedSecret& secret, const SymmNonce& nonce)\n    {\n        auto* d = reinterpret_cast<unsigned char*>(buf.data());\n        crypto_stream_xchacha20_xor(d, d, buf.size(), nonce.data(), secret.data());\n    }\n\n    void xchacha20_poly1305_encrypt(std::span<std::byte> buf, const SharedSecret& secret, const SymmNonce& nonce)\n    {\n        if (buf.size() <= MAC_SIZE)\n        {\n            const auto err = fmt::format(\"Payload size {} is <= poly1305 AEAD size ({})!\", buf.size(), MAC_SIZE);\n            log::error(logcat, \"{}\", err);\n            throw std::invalid_argument{err};\n        }\n        auto payload_size = buf.size() - MAC_SIZE;\n        auto* buf_cptr = reinterpret_cast<unsigned char*>(buf.data());\n        crypto_aead_xchacha20poly1305_ietf_encrypt(\n            buf_cptr, nullptr, buf_cptr, payload_size, nullptr, 0, nullptr, nonce.data(), secret.data());\n    }\n\n    void xchacha20_poly1305_encrypt(std::string& buf, const SharedSecret& secret, const SymmNonce& nonce)\n    {\n        xchacha20_poly1305_encrypt(\n            std::span<std::byte>{reinterpret_cast<std::byte*>(buf.data()), buf.size()}, secret, nonce);\n    }\n\n    std::span<std::byte> xchacha20_poly1305_decrypt(\n        std::span<std::byte> buf, const SharedSecret& secret, const SymmNonce& nonce)\n    {\n        if (buf.size() <= MAC_SIZE)\n        {\n            log::warning(logcat, \"On decryption, payload size {} is < poly1305 AEAD size ({})!\", buf.size(), MAC_SIZE);\n            return {};\n        }\n        auto* buf_cptr = reinterpret_cast<unsigned char*>(buf.data());\n        unsigned long long payload_size{0};\n        if (crypto_aead_xchacha20poly1305_ietf_decrypt(\n                buf_cptr, &payload_size, nullptr, buf_cptr, buf.size(), nullptr, 0, nonce.data(), secret.data())\n            != 0)\n        {\n            log::warning(logcat, \"On decryption, payload failed authentication!\");\n            return {};\n        }\n        assert(payload_size == buf.size() - MAC_SIZE);\n        return buf.subspan(0, payload_size);\n    }\n\n    bool dh_client(SharedSecret& shared, const PubKey& pk, const Ed25519SecretKey& sk, const SymmNonce& n)\n    {\n        if (dh(shared, sk.to_pubkey(), pk, true, sk, n))\n            return true;\n\n        log::warning(logcat, \"dh_client - dh failed\");\n        return false;\n    }\n\n    std::tuple<SharedSecret, PubKey, SymmNonce> dh_client_gen(const PubKey& server_pk)\n    {\n        std::tuple<SharedSecret, PubKey, SymmNonce> result;\n        auto& [secret, eph_pk, nonce] = result;\n\n        auto eph_keys = generate_ed25519();\n        nonce = SymmNonce::make_random();\n        if (!dh_client(secret, server_pk, eph_keys, nonce))\n            throw std::invalid_argument{\"shared secret generation failed: remote pubkey is not a valid Ed25519 pubkey\"};\n        eph_pk.assign(eph_keys.pubkey_span());\n        return result;\n    }\n\n    /// path dh relay side\n    bool dh_server(SharedSecret& shared, const PubKey& pk, const Ed25519SecretKey& sk, const SymmNonce& n)\n    {\n        if (dh(shared, pk, sk.to_pubkey(), false, sk, n))\n            return true;\n\n        log::warning(logcat, \"dh_server - dh failed\");\n        return false;\n    }\n\n    void shorthash(std::span<std::byte, SHORTHASHSIZE> result, std::span<const std::byte> buf)\n    {\n        crypto_generichash_blake2b(\n            reinterpret_cast<unsigned char*>(result.data()),\n            result.size(),\n            reinterpret_cast<const unsigned char*>(buf.data()),\n            buf.size(),\n            nullptr,\n            0);\n    }\n    AlignedBuffer<SHORTHASHSIZE> shorthash(std::span<const std::byte> buf)\n    {\n        AlignedBuffer<SHORTHASHSIZE> result;\n        shorthash(result, buf);\n        return result;\n    }\n\n    bool verify(\n        std::span<const std::byte, PUBKEYSIZE> pub,\n        std::span<const std::byte> data,\n        std::span<const std::byte, SIGSIZE> sig)\n    {\n        return crypto_sign_verify_detached(\n                   as_uspan(sig).data(), as_uspan(data).data(), data.size(), as_uspan(pub).data())\n            != -1;\n    }\n\n    // FIXME: the following two functions are nearly identical, but different in stupid ways\n    void derive_encrypt_outer_wrapping(\n        const Ed25519SecretKey& shared_key,\n        SharedSecret& secret,\n        const SymmNonce& nonce,\n        const RouterID& remote,\n        std::span<std::byte> payload)\n    {\n        // derive shared key\n        if (!dh_client(secret, remote, shared_key, nonce))\n        {\n            auto err = \"DH client failed during shared key derivation!\"s;\n            log::warning(logcat, \"{}\", err);\n            throw std::runtime_error{\"err\"};\n        }\n\n        // encrypt hop_info (mutates in-place)\n        xchacha20(payload, secret, nonce);\n    }\n\n    void derive_decrypt_outer_wrapping(\n        const Ed25519SecretKey& local_sk,\n        SharedSecret& shared,\n        const PubKey& remote,\n        const SymmNonce& nonce,\n        std::span<std::byte> encrypted)\n    {\n        // derive shared secret using shared secret and our secret key (and nonce)\n        if (!dh_server(shared, remote, local_sk, nonce))\n        {\n            auto err = \"DH server failed during shared key derivation!\"s;\n            log::warning(logcat, \"{}\", err);\n            throw std::runtime_error{err};\n        }\n\n        // decrypt hop_info (mutates in-place)\n        xchacha20(encrypted, shared, nonce);\n\n        log::trace(logcat, \"Shared secret: {}\", shared.to_string());\n    }\n\n    std::array<unsigned char, 32> blinding_scalar(std::span<const std::byte, 32> pubkey, std::string_view blind_domain)\n    {\n        if (blind_domain.size() > crypto_generichash_KEYBYTES_MAX)\n            blind_domain = blind_domain.substr(0, crypto_generichash_KEYBYTES_MAX);\n\n        // n = H(pk, key=blind_domain)\n        std::array<unsigned char, 64> n;\n        crypto_generichash_blake2b(\n            n.data(),\n            n.size(),\n            reinterpret_cast<const unsigned char*>(pubkey.data()),\n            pubkey.size(),\n            reinterpret_cast<const unsigned char*>(blind_domain.data()),\n            blind_domain.size());\n\n        // out = scalar_reduce(n)\n        std::array<unsigned char, 32> out;\n        crypto_core_ed25519_scalar_reduce(out.data(), n.data());\n\n        return out;\n    }\n\n    bool blind(PubKey& blinded, const PubKey& root, std::string_view blind_domain)\n    {\n        return 0\n            == crypto_scalarmult_ed25519_noclamp(\n                   blinded.data(), blinding_scalar(root, blind_domain).data(), root.data());\n    }\n\n    Ed25519SecretKey generate_ed25519()\n    {\n        Ed25519SecretKey ret{};\n        PubKey pk;\n        [[maybe_unused]] int result = crypto_sign_ed25519_keypair(pk.data(), ret.data());\n        assert(result != -1);\n        return ret;\n    }\n\n    bool check_pubkey(const Ed25519SecretKey& keys)\n    {\n        PubKey pk;\n        Ed25519SecretKey sk;\n        crypto_sign_seed_keypair(pk.data(), sk.data(), keys.data());\n        return keys.to_pubkey() == pk;\n    }\n\n#ifdef LOKINET_HAVE_CRYPT\n    bool check_passwd_hash(std::string pwhash, std::string challenge)\n    {\n        bool ret = false;\n        auto pos = pwhash.find_last_of('$');\n        auto settings = pwhash.substr(0, pos);\n        crypt_data data{};\n        if (char* ptr = crypt_r(challenge.c_str(), settings.c_str(), &data))\n        {\n            ret = ptr == pwhash;\n        }\n        sodium_memzero(&data, sizeof(data));\n        return ret;\n    }\n#endif\n\n}  // namespace llarp::crypto\n\nnamespace llarp\n{\n    // Called during static initialization to initialize libsodium.  (The CSRNG return is\n    // not useful, but just here to get this called during static initialization of `csrng`).\n    static CSRNG _initialize_crypto()\n    {\n        if (sodium_init() == -1)\n        {\n            log::critical(crypto::logcat, \"sodium_init() failed, unable to continue!\");\n            std::abort();\n        }\n\n        return CSRNG{};\n    }\n\n    CSRNG csrng = _initialize_crypto();\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/crypto/crypto.hpp",
    "content": "#pragma once\n\n#include \"keys.hpp\"\n#include \"types.hpp\"\n\n#include <llarp/contact/router_id.hpp>\n\n#include <cstdint>\n\nnamespace llarp\n{\n    using namespace std::literals;\n}\n\nnamespace llarp::crypto\n{\n    /// decrypt cipherText given the key generated from name\n    std::optional<RouterID> maybe_decrypt_name(std::string_view ciphertext, SymmNonce nonce, std::string_view name);\n\n    /// xchacha symmetric cipher\n    void xchacha20(std::span<std::byte> buf, const SharedSecret&, const SymmNonce&);\n\n    static constexpr size_t MAC_SIZE = 16;\n\n    /// encrypts a buffer in-place, putting a MAC in the final bytes (which must be allocated\n    /// in the span but are not part of the data that is actually encrypted).\n    void xchacha20_poly1305_encrypt(std::span<std::byte> buf, const SharedSecret& secret, const SymmNonce& nonce);\n    void xchacha20_poly1305_encrypt(std::string& buf, const SharedSecret& secret, const SymmNonce& nonce);\n\n    /// decrypts a buffer in-place, validating it against the appended MAC\n    /// empty span means decryption failed; we don't allow encrypting an empty payload\n    std::span<std::byte> xchacha20_poly1305_decrypt(\n        std::span<std::byte> buf, const SharedSecret& secret, const SymmNonce& nonce);\n\n    /// path dh creator's side\n    ///\n    /// Note that the input \"nonce\" here is used domain separation in the shared secret generation,\n    /// but isn't used as an encryption nonce (i.e. the same nonce can be safely used for both\n    /// shared secret generation and an initial payload encryption).\n    bool dh_client(\n        SharedSecret& out, const PubKey& server_pk, const Ed25519SecretKey& client_seckey, const SymmNonce& nonce);\n\n    /// Generates an ephemeral keypair and random nonce, calls dh_client, then returns the resulting\n    /// shared secret, the ephemeral pubkey, and the nonce.  Throws std::invalid_argument if the\n    /// server pk is not valid.\n    std::tuple<SharedSecret, PubKey, SymmNonce> dh_client_gen(const PubKey& server_pk);\n\n    /// path dh relay side\n    bool dh_server(\n        SharedSecret& out, const PubKey& client_pk, const Ed25519SecretKey& server_seckey, const SymmNonce& nonce);\n    bool dh_server(uint8_t* shared_secret, const uint8_t* other_pk, const uint8_t* local_sk, const uint8_t* nonce);\n\n    /// blake2b 256 bit\n    void shorthash(std::span<std::byte, SHORTHASHSIZE> out, std::span<const std::byte> buf);\n    AlignedBuffer<SHORTHASHSIZE> shorthash(std::span<const std::byte> buf);\n\n    /// ed25519 verify\n    bool verify(\n        std::span<const std::byte, PUBKEYSIZE> pub,\n        std::span<const std::byte> data,\n        std::span<const std::byte, SIGSIZE> sig);\n\n    /// Used in path-build and session initiation messages. Derives a shared secret key for symmetric DH, encrypting\n    /// the given payload in-place. Will throw on failure of either the client DH derivation or the xchacha20\n    /// payload mutation\n    void derive_encrypt_outer_wrapping(\n        const Ed25519SecretKey& shared_key,\n        SharedSecret& secret,\n        const SymmNonce& nonce,\n        const RouterID& remote,\n        std::span<std::byte> payload);\n\n    /// Used in receiving path-build and session initiation messages. Derives a shared secret key using an ephemeral\n    /// pubkey and the provided nonce. The encrypted payload is mutated in-place. Will throw on failure of either\n    /// the server DH derivation or the xchacha20 payload mutation\n    void derive_decrypt_outer_wrapping(\n        const Ed25519SecretKey& local,\n        SharedSecret& shared,\n        const PubKey& remote,\n        const SymmNonce& nonce,\n        std::span<std::byte> encrypted);\n\n    /// Returns the Ed25519 scalar used for blinding of the given pubkey with the given\n    /// blind_domain.  See `blind`.\n    ///\n    /// This scalar can be used with either the root private scalar or\n    /// the root pubkey to produce the blinded private scalar or blinded pubkey, respectively.\n    ///\n    /// `pubkey` is usually simply passed via implicit conversion from a `PubKey` argument.\n    std::array<unsigned char, 32> blinding_scalar(std::span<const std::byte, 32> pubkey, std::string_view blind_domain);\n\n    /// Derive a blinded pubkey from a root pubkey and blinding domain and stores it in `derived`.\n    ///\n    /// blind_domain should be between 0 and 64 characters long (longer values will be truncated),\n    /// and generally should be one of the constants defined below.  A different blind_domain should\n    /// be used for each distinct blinding type, and will produce unrelated blinded keys.\n    ///\n    /// Returns true if successful, false if `root` is not a valid pubkey.\n    bool blind(PubKey& blinded, const PubKey& root, std::string_view blind_domain);\n\n    // Known values for the `blind_domain` argument of blind(), blinding_scalar(), and the\n    // Ed25519BlindedKey constructor.\n    namespace blinding\n    {\n        constexpr auto CLIENT_CONTACT = \"SessionRouterClientContact\"sv;\n    }\n\n    Ed25519SecretKey generate_ed25519();\n\n    // Verifies that the cached pubkey embedded in `keys` correctly corresponds with the seed value\n    // in `keys`; effectively this checks for corruption of the keys value, such as when loading the\n    // keypair from disk.\n    bool check_pubkey(const Ed25519SecretKey& keys);\n\n    bool check_passwd_hash(std::string pwhash, std::string challenge);\n\n}  // namespace llarp::crypto\n"
  },
  {
    "path": "llarp/crypto/key_manager.cpp",
    "content": "#include \"key_manager.hpp\"\n\n#include \"crypto.hpp\"\n#include \"keys.hpp\"\n\n#include <llarp/config/config.hpp>\n#include <llarp/util/file.hpp>\n#include <llarp/util/logging.hpp>\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"keymanager\");\n\n    void KeyManager::load_from_file(Ed25519SecretKey& key, const std::filesystem::path& fname)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        auto tmp = util::file_to_string(fname);\n        if ((tmp.size() == 128 or (tmp.size() == 129 and tmp.ends_with(\"\\n\"))\n             or (tmp.size() == 130 and tmp.ends_with(\"\\r\\n\")))\n            and oxenc::is_hex(tmp.begin(), tmp.begin() + 128))\n            oxenc::from_hex(tmp.begin(), tmp.begin() + 128, key.data());\n        else if (tmp.size() == 64)\n            std::memcpy(key.data(), tmp.data(), 64);\n        else if (tmp.starts_with('d') and tmp.ends_with('e'))\n        {\n            // Old Lokinet keys were bt-dicts with the key we care about in the 's' key:\n            oxenc::bt_dict_consumer old{tmp};\n            auto oldkey = old.require_span<unsigned char, 64>(\"s\");\n            std::memcpy(key.data(), oldkey.data(), 64);\n            old.finish();\n        }\n        else\n            throw std::invalid_argument{\n                \"Invalid key file {} ({}B): Expected 64 bytes, 128 hex, or legacy lokinet key file\"_format(\n                    fname, tmp.size())};\n\n        if (!key.check_pubkey())\n            throw std::invalid_argument{\"Invalid key file {}: Keypair seed and pubkey do not match\"};\n    }\n\n    bool KeyManager::write_to_file(const Ed25519SecretKey& key, const std::filesystem::path& fname, bool hex)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n        try\n        {\n            if (hex)\n            {\n                std::string out;\n                out.reserve(129);\n                oxenc::to_hex(key.begin(), key.end(), std::back_inserter(out));\n                out += '\\n';\n                util::buffer_to_file(fname, out);\n            }\n            else\n            {\n                util::buffer_to_file(fname, key.to_view());\n            }\n        }\n        catch (const std::exception& e)\n        {\n            log::error(logcat, \"Failed to write keypair to file: {}\", e.what());\n            return false;\n        }\n\n        return true;\n    }\n\n    KeyManager::KeyManager(const Config& config, bool is_relay)\n    {\n        if (not is_relay)\n        {\n            if (config.network.keyfile and std::filesystem::exists(*config.network.keyfile))\n            {\n                load_from_file(secret_key, *config.network.keyfile);\n                log::info(logcat, \"Successfully loaded persistent client key from config path\");\n            }\n            else\n            {\n                log::debug(logcat, \"Client generating secret key...\");\n                secret_key = crypto::generate_ed25519();\n\n                if (config.network.keyfile && !write_to_file(secret_key, *config.network.keyfile))\n                {\n                    log::critical(logcat, \"Failed to save persistent key to {}\", *config.network.keyfile);\n                    throw std::runtime_error{\"Failed to save configured persistent key file\"};\n                }\n            }\n\n            public_key.assign(secret_key.pubkey_span());\n\n            log::info(logcat, \"Client public key: {}\", public_key);\n        }\n        // else nothing to do: router's identity self.signed is always regenerated on the fly from\n        // the keys we get from oxend.\n    }\n\n    void KeyManager::update_idkey(Ed25519SecretKey&& newkey)\n    {\n        secret_key = std::move(newkey);\n        public_key.assign(secret_key.pubkey_span());\n        log::info(logcat, \"Relay key manager updated secret key; new public key: {}\", public_key);\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/crypto/key_manager.hpp",
    "content": "#pragma once\n\n#include <llarp/constants/files.hpp>\n#include <llarp/contact/router_id.hpp>\n\nnamespace llarp\n{\n    struct Config;\n\n    // KeyManager manages the cryptographic keys stored on disk for the local\n    // node. This includes private keys as well as the self-signed router contact\n    // file (e.g. \"self.signed\").\n    //\n    // Keys are either read from disk if they exist and are valid (see below) or\n    // are generated and written to disk.\n    struct KeyManager\n    {\n        friend class Router;\n\n      private:\n        KeyManager() = default;\n        KeyManager(const Config& config, bool is_relay);\n\n        Ed25519SecretKey secret_key;\n        RouterID public_key;\n\n        void update_idkey(Ed25519SecretKey&& newkey);\n\n      public:\n        const RouterID& router_id() const { return public_key; }\n\n        // Helper functions to load a key; these are used by KeyManager itself, but are exposed as\n        // they also have some uses for key loading outside KeyManager.  Loading accepts either 64\n        // raw bytes, or 128 hex (with optional trailing newline), and verifies that the loaded\n        // value contains matching pubkey and seed.  Writing writes either raw bytes, or hex with a\n        // trailing newline.\n        static void load_from_file(Ed25519SecretKey& key, const std::filesystem::path& fname);\n        static bool write_to_file(const Ed25519SecretKey& key, const std::filesystem::path& fname, bool hex = true);\n    };\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/crypto/keys.cpp",
    "content": "#include \"keys.hpp\"\n\n#include \"crypto.hpp\"\n\n#include <llarp/util/bspan.hpp>\n#include <llarp/util/logging.hpp>\n\n#include <sodium/crypto_core_ed25519.h>\n#include <sodium/crypto_generichash_blake2b.h>\n#include <sodium/crypto_scalarmult_ed25519.h>\n#include <sodium/crypto_sign.h>\n#include <sodium/utils.h>\n\nnamespace llarp\n{\n\n    static auto logcat = log::Cat(\"keys\");\n\n    bool PubKey::from_hex(const std::string& str)\n    {\n        if (str.size() != 2 * size())\n            return false;\n        oxenc::from_hex(str.begin(), str.end(), begin());\n        return true;\n    }\n\n    std::string PubKey::to_string() const { return oxenc::to_base32z(begin(), end()); }\n\n    PubKey& PubKey::operator=(const uint8_t* ptr)\n    {\n        std::copy(ptr, ptr + SIZE, begin());\n        return *this;\n    }\n\n    bool Ed25519SecretKey::check_pubkey() const\n    {\n        std::array<unsigned char, 32> pk;\n        std::array<unsigned char, 64> sk;\n        crypto_sign_seed_keypair(pk.data(), sk.data(), data());\n        return 0 == std::memcmp(pk.data(), pubkey_span().data(), 32);\n    }\n\n    void Ed25519SecretKey::recalculate()\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n        std::array<unsigned char, 32> pk;\n        std::array<unsigned char, 64> sk;\n        crypto_sign_seed_keypair(pk.data(), sk.data(), data());\n        std::memcpy(data() + 32, pk.data(), 32);\n    }\n\n    void Ed25519SecretKey::sign(std::span<std::byte, SIGSIZE> sig, std::span<const std::byte> buf) const\n    {\n        crypto_sign_detached(as_uspan(sig).data(), nullptr, as_uspan(buf).data(), buf.size(), data());\n    }\n\n    std::array<std::byte, SIGSIZE> Ed25519SecretKey::sign(std::span<const std::byte> buf) const\n    {\n        std::array<std::byte, SIGSIZE> sig;\n        sign(sig, buf);\n        return sig;\n    }\n\n    void Ed25519BlindedKey::sign(std::span<std::byte, SIGSIZE> sig, std::span<const std::byte> buf) const\n    {\n        auto ubuf = as_uspan(buf);\n\n        // r = H(s || M) where here s is pseudorandom bytes, generated under standard Ed25519 as\n        // part of hashing the seed (i.e. [a,s] = H(k), ignoring `a` clamping).  For blinded keys,\n        // however, we instead use `s` created at construction of the BlindedKey object (which is a\n        // domain-keyed blake2b hash of the root seed, rather than the second half of a SHA512 of\n        // the root key).\n        unsigned char nonce[64];\n        crypto_hash_sha512_state hs;\n        crypto_hash_sha512_init(&hs);\n        crypto_hash_sha512_update(&hs, hash_data.data(), hash_data.size());\n        crypto_hash_sha512_update(&hs, ubuf.data(), ubuf.size());\n        crypto_hash_sha512_final(&hs, nonce);\n        crypto_core_ed25519_scalar_reduce(nonce, nonce);\n\n        // Final signature consists of R || S:\n        auto R = as_uspan(sig.first<32>());\n        auto S = as_uspan(sig.last<32>());\n\n        // R = r * G, store directly into sig to make: sig = (R || uninitialized)\n        crypto_scalarmult_ed25519_base_noclamp(R.data(), nonce);\n\n        // hram = H(R || A || M)\n        unsigned char hram[64];\n        crypto_hash_sha512_init(&hs);\n        crypto_hash_sha512_update(&hs, R.data(), 32);\n        crypto_hash_sha512_update(&hs, pubkey.data(), 32);\n        crypto_hash_sha512_update(&hs, ubuf.data(), ubuf.size());\n        crypto_hash_sha512_final(&hs, hram);\n\n        // S = r + H(R || A || M) * a, and we store S directly into sig:\n        crypto_core_ed25519_scalar_reduce(hram, hram);\n        unsigned char mulres[32];\n        crypto_core_ed25519_scalar_mul(mulres, hram, scalar.data());\n        crypto_core_ed25519_scalar_add(S.data(), mulres, nonce);\n\n        sodium_memzero(nonce, sizeof nonce);\n    }\n\n    std::array<std::byte, SIGSIZE> Ed25519BlindedKey::sign(std::span<const std::byte> buf) const\n    {\n        std::array<std::byte, SIGSIZE> sig;\n        sign(sig, buf);\n        return sig;\n    }\n\n    Ed25519BlindedKey::Ed25519BlindedKey(std::span<const std::byte, 32> sc, std::span<const std::byte, 32> hd)\n        : scalar{sc}, hash_data{hd}\n    {\n        crypto_scalarmult_ed25519_base_noclamp(pubkey.data(), scalar.data());\n    }\n\n    Ed25519BlindedKey::Ed25519BlindedKey(const Ed25519SecretKey& root, std::string_view blind_domain)\n    {\n        // This function's name is a bit misleading: what it actually does is convert the seed to the\n        // Ed25519 private scalar, because that's the operation you do when converting Ed -> X (i.e.\n        // an X secret key *is* a private scalar, unlike Ed keys).  We don't care at all about X,\n        // but we do want the private scalar and this gives us exactly that.\n        //\n        // a = clamp(SHA512(seed)[0:32])\n        crypto_sign_ed25519_sk_to_curve25519(scalar.data(), root.data());\n\n        // f = blinding factor\n        auto bfactor = crypto::blinding_scalar(root.pubkey_span(), blind_domain);\n\n        // b = af\n        crypto_core_ed25519_scalar_mul(scalar.data(), scalar.data(), bfactor.data());\n\n        // Now compute the pubkey from the scalar b:\n        // B = bG\n        if (0 != crypto_scalarmult_ed25519_base_noclamp(pubkey.data(), scalar.data()))\n            throw std::runtime_error{\"Keypair blinding failed!\"};\n\n        // In regular Ed25519, the hash data (used during signing) is derived from the hash of seed\n        // (i.e. the second half of the SHA512 operation above).  It seems preferable, however, to\n        // not use identical hash data for a subkey, so instead we do our own keyed hash of the seed\n        // to get some equivalent (but different valued) hash data for the same purpose.\n        crypto_generichash_blake2b(\n            hash_data.data(),\n            hash_data.size(),\n            reinterpret_cast<const unsigned char*>(root.data()),\n            32,\n            reinterpret_cast<const unsigned char*>(blind_domain.data()),\n            blind_domain.size());\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/crypto/keys.hpp",
    "content": "#pragma once\n\n#include <llarp/crypto/constants.hpp>\n#include <llarp/util/aligned.hpp>\n#include <llarp/util/buffer.hpp>\n\nnamespace llarp\n{\n    struct PubKey : public AlignedBuffer<PUBKEYSIZE>\n    {\n        using AlignedBuffer<PUBKEYSIZE>::AlignedBuffer;\n\n        bool from_hex(const std::string& str);\n\n        std::string to_string() const;\n\n        // FIXME TODO revisit this\n        PubKey& operator=(const uint8_t* ptr);\n    };\n\n    struct PubKey;\n    struct Ed25519BlindedKey;\n\n    /// Stores a sodium \"secret key\" value, which is actually the Ed25519 seed\n    /// concatenated with the public key.  Note that the seed is *not* the private\n    /// key value itself, but rather the seed from which it can be calculated.\n    struct Ed25519SecretKey final : AlignedBuffer<SECKEYSIZE>\n    {\n        using AlignedBuffer<SECKEYSIZE>::AlignedBuffer;\n\n        // If constructed with just the seed, we recalculate the pubkey\n        explicit Ed25519SecretKey(const AlignedBuffer<32>& seed)\n        {\n            std::memcpy(data(), seed.data(), seed.size());\n            recalculate();\n        }\n\n        /// recalculate public component (last 32 bytes) from leading 32-byte seed component\n        void recalculate();\n\n        /// Verifies that the public component matches the seed component\n        bool check_pubkey() const;\n\n        std::span<const std::byte, 32> pubkey_span() const { return span().last<32>(); }\n        PubKey to_pubkey() const { return PubKey{pubkey_span()}; }\n\n        void sign(std::span<std::byte, SIGSIZE> out, std::span<const std::byte> buf) const;\n        std::array<std::byte, SIGSIZE> sign(std::span<const std::byte> buf) const;\n\n        static constexpr bool to_string_formattable{false};\n    };\n\n    /// Stores a private scalar, pubkey, and hash_data for a blinded key (typically) derived from a\n    /// Ed25519SecretKey.\n    struct Ed25519BlindedKey final\n    {\n        AlignedBuffer<32> scalar;\n        AlignedBuffer<32> hash_data;\n        PubKey pubkey;\n\n        // Constructs as blinded key as the result of blinding an Ed25519SecretKey\n        Ed25519BlindedKey(const Ed25519SecretKey& root, std::string_view blind_domain);\n\n        // Constructs a blinded key from a scalar and hash data.  The pubkey is derived during\n        // construction from the scalar.\n        Ed25519BlindedKey(std::span<const std::byte, 32> scalar, std::span<const std::byte, 32> hash_data);\n\n        // Produces an Ed25519 signature that validates with this blinded key's pubkey\n        void sign(std::span<std::byte, SIGSIZE> out, std::span<const std::byte> buf) const;\n        std::array<std::byte, SIGSIZE> sign(std::span<const std::byte> buf) const;\n\n        static constexpr bool to_string_formattable{false};\n    };\n\n}  // namespace llarp\n\nnamespace std\n{\n    template <>\n    struct hash<llarp::PubKey> : public hash<llarp::AlignedBuffer<PUBKEYSIZE>>\n    {};\n}  //  namespace std\n"
  },
  {
    "path": "llarp/crypto/types.cpp",
    "content": "#include \"types.hpp\"\n\n#include <llarp/util/logging.hpp>\n\n#include <sodium/randombytes.h>\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"cryptoutils\");\n\n    SymmNonce SymmNonce::make_random()\n    {\n        SymmNonce n;\n        randombytes_buf(n.data(), n.size());\n        return n;\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/crypto/types.hpp",
    "content": "#pragma once\n\n#include \"constants.hpp\"\n\n#include <llarp/util/aligned.hpp>\n\n#include <algorithm>\n\nnamespace llarp\n{\n    struct SharedSecret final : AlignedBuffer<SHAREDKEYSIZE>\n    {};\n\n    struct Signature final : AlignedBuffer<SIGSIZE>\n    {};\n\n    struct SymmNonce final : AlignedBuffer<NONCESIZE>\n    {\n        using AlignedBuffer<NONCESIZE>::AlignedBuffer;\n\n        SymmNonce operator^(const SymmNonce& other) const\n        {\n            SymmNonce ret;\n            std::transform(begin(), end(), other.begin(), ret.begin(), std::bit_xor<>());\n            return ret;\n        }\n\n        static SymmNonce make_random();\n    };\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/dns/dns.hpp",
    "content": "#pragma once\n\n#include <llarp/util/logging.hpp>\n\n#include <cstdint>\n\nnamespace llarp::dns\n{\n    constexpr uint16_t qTypeSRV = 33;\n    constexpr uint16_t qTypeAAAA = 28;\n    constexpr uint16_t qTypeTXT = 16;\n    constexpr uint16_t qTypeMX = 15;\n    constexpr uint16_t qTypePTR = 12;\n    constexpr uint16_t qTypeCNAME = 5;\n    constexpr uint16_t qTypeNS = 2;\n    constexpr uint16_t qTypeA = 1;\n\n    constexpr uint16_t qClassIN = 1;\n\n    constexpr uint16_t flags_QR = (1 << 15);\n    constexpr uint16_t flags_AA = (1 << 10);\n    constexpr uint16_t flags_TC = (1 << 9);\n    constexpr uint16_t flags_RD = (1 << 8);\n    constexpr uint16_t flags_RA = (1 << 7);\n    constexpr uint16_t flags_RCODENameError = (3);\n    constexpr uint16_t flags_RCODEServFail = (2);\n    constexpr uint16_t flags_RCODENoError = (0);\n\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/message.cpp",
    "content": "#include \"message.hpp\"\n\n#include \"dns.hpp\"\n#include \"name.hpp\"\n#include \"srv_data.hpp\"\n\n#include <llarp/net/ip_packet.hpp>\n#include <llarp/util/buffer.hpp>\n\n#include <nlohmann/json.hpp>\n#include <oxenc/endian.h>\n\n#include <array>\n\nnamespace llarp::dns\n{\n    static auto logcat = log::Cat(\"dns\");\n\n    bool MessageHeader::Encode(llarp_buffer_t* buf) const\n    {\n        if (!buf->put_uint16(_id))\n            return false;\n        if (!buf->put_uint16(_fields))\n            return false;\n        if (!buf->put_uint16(_qd_count))\n            return false;\n        if (!buf->put_uint16(_an_count))\n            return false;\n        if (!buf->put_uint16(_ns_count))\n            return false;\n        return buf->put_uint16(_ar_count);\n    }\n\n    bool MessageHeader::Decode(llarp_buffer_t* buf)\n    {\n        if (!buf->read_uint16(_id))\n            return false;\n        if (!buf->read_uint16(_fields))\n            return false;\n        if (!buf->read_uint16(_qd_count))\n            return false;\n        if (!buf->read_uint16(_an_count))\n            return false;\n        if (!buf->read_uint16(_ns_count))\n            return false;\n        if (!buf->read_uint16(_ar_count))\n            return false;\n        return true;\n    }\n\n    nlohmann::json MessageHeader::ToJSON() const { return nlohmann::json{}; }\n\n    Message::Message(Message&& other)\n        : hdr_id(std::move(other.hdr_id)),\n          hdr_fields(std::move(other.hdr_fields)),\n          questions(std::move(other.questions)),\n          answers(std::move(other.answers)),\n          authorities(std::move(other.authorities)),\n          additional(std::move(other.additional))\n    {}\n\n    Message::Message(const Message& other)\n        : hdr_id(other.hdr_id),\n          hdr_fields(other.hdr_fields),\n          questions(other.questions),\n          answers(other.answers),\n          authorities(other.authorities),\n          additional(other.additional)\n    {}\n\n    Message::Message(const MessageHeader& hdr) : hdr_id(hdr._id), hdr_fields(hdr._fields)\n    {\n        questions.resize(size_t(hdr._qd_count));\n        answers.resize(size_t(hdr._an_count));\n        authorities.resize(size_t(hdr._ns_count));\n        additional.resize(size_t(hdr._ar_count));\n    }\n\n    Message::Message(const Question& question) : hdr_id{0}, hdr_fields{} { questions.emplace_back(question); }\n\n    bool Message::Encode(llarp_buffer_t* buf) const\n    {\n        MessageHeader hdr;\n        hdr._id = hdr_id;\n        hdr._fields = hdr_fields;\n        hdr._qd_count = questions.size();\n        hdr._an_count = answers.size();\n        hdr._ns_count = 0;\n        hdr._ar_count = 0;\n\n        if (!hdr.Encode(buf))\n            return false;\n\n        for (const auto& question : questions)\n            if (!question.Encode(buf))\n                return false;\n\n        for (const auto& answer : answers)\n            if (!answer.Encode(buf))\n                return false;\n\n        return true;\n    }\n\n    bool Message::Decode(llarp_buffer_t* buf)\n    {\n        for (auto& qd : questions)\n        {\n            if (!qd.Decode(buf))\n            {\n                log::error(logcat, \"failed to decode question\");\n                return false;\n            }\n            log::debug(logcat, \"question: {}\", qd);\n        }\n        for (auto& an : answers)\n        {\n            if (not an.Decode(buf))\n            {\n                log::debug(logcat, \"failed to decode answer\");\n                return false;\n            }\n        }\n        return true;\n    }\n\n    nlohmann::json Message::ToJSON() const\n    {\n        std::vector<nlohmann::json> ques;\n        std::vector<nlohmann::json> ans;\n        for (const auto& q : questions)\n        {\n            ques.push_back(q.ToJSON());\n        }\n        for (const auto& a : answers)\n        {\n            ans.push_back(a.ToJSON());\n        }\n        return nlohmann::json{{\"questions\", ques}, {\"answers\", ans}};\n    }\n\n    std::vector<std::byte> Message::to_buffer() const\n    {\n        std::vector<std::byte> tmp;\n        tmp.resize(1500);\n        llarp_buffer_t buf{tmp};\n        if (not Encode(&buf))\n            throw std::runtime_error(\"cannot encode dns message\");\n        tmp.resize(buf.cur - buf.base);\n        return tmp;\n    }\n\n    void Message::add_serv_fail(RR_TTL_t)\n    {\n        if (questions.size())\n        {\n            hdr_fields |= flags_RCODEServFail;\n            // authorative response with recursion available\n            hdr_fields |= flags_QR | flags_AA | flags_RA;\n            // don't allow recursion on this request\n            hdr_fields &= ~flags_RD;\n        }\n    }\n\n    static constexpr uint16_t reply_flags(uint16_t setbits) { return setbits | flags_QR | flags_AA | flags_RA; }\n\n    void Message::add_IN_reply(uint32_t addr, RR_TTL_t ttl)\n    {\n        // TODO: IPv6 support\n        if (questions.size())\n        {\n            hdr_fields = reply_flags(hdr_fields);\n            auto& rec = answers.emplace_back();\n            rec.rr_name = questions[0].qname;\n            rec.rr_class = qClassIN;\n            rec.ttl = ttl;\n            rec.rr_type = qTypeA;\n            rec.rData.resize(4);\n            oxenc::write_host_as_big(addr, rec.rData.data());\n        }\n    }\n\n    void Message::add_reply(std::string name, RR_TTL_t ttl)\n    {\n        if (questions.size())\n        {\n            hdr_fields = reply_flags(hdr_fields);\n\n            const auto& question = questions[0];\n            answers.emplace_back();\n            auto& rec = answers.back();\n            rec.rr_name = question.qname;\n            rec.rr_type = question.qtype;\n            rec.rr_class = qClassIN;\n            rec.ttl = ttl;\n            std::array<uint8_t, 512> tmp = {{0}};\n            llarp_buffer_t buf(tmp);\n            if (EncodeNameTo(&buf, name))\n            {\n                buf.sz = buf.cur - buf.base;\n                rec.rData.resize(buf.sz);\n                memcpy(rec.rData.data(), buf.base, buf.sz);\n            }\n        }\n    }\n\n    void Message::add_ns_reply(std::string name, RR_TTL_t ttl)\n    {\n        if (not questions.empty())\n        {\n            hdr_fields = reply_flags(hdr_fields);\n\n            const auto& question = questions[0];\n            answers.emplace_back();\n            auto& rec = answers.back();\n            rec.rr_name = question.qname;\n            rec.rr_type = qTypeNS;\n            rec.rr_class = qClassIN;\n            rec.ttl = ttl;\n            std::array<uint8_t, 512> tmp = {{0}};\n            llarp_buffer_t buf(tmp);\n            if (EncodeNameTo(&buf, name))\n            {\n                buf.sz = buf.cur - buf.base;\n                rec.rData.resize(buf.sz);\n                memcpy(rec.rData.data(), buf.base, buf.sz);\n            }\n        }\n    }\n\n    void Message::add_CNAME_reply(std::string name, RR_TTL_t ttl)\n    {\n        if (questions.size())\n        {\n            hdr_fields = reply_flags(hdr_fields);\n\n            const auto& question = questions[0];\n            answers.emplace_back();\n            auto& rec = answers.back();\n            rec.rr_name = question.qname;\n            rec.rr_type = qTypeCNAME;\n            rec.rr_class = qClassIN;\n            rec.ttl = ttl;\n            std::array<uint8_t, 512> tmp = {{0}};\n            llarp_buffer_t buf(tmp);\n            if (EncodeNameTo(&buf, name))\n            {\n                buf.sz = buf.cur - buf.base;\n                rec.rData.resize(buf.sz);\n                memcpy(rec.rData.data(), buf.base, buf.sz);\n            }\n        }\n    }\n\n    void Message::add_mx_reply(std::string name, uint16_t priority, RR_TTL_t ttl)\n    {\n        if (questions.size())\n        {\n            hdr_fields = reply_flags(hdr_fields);\n\n            const auto& question = questions[0];\n            answers.emplace_back();\n            auto& rec = answers.back();\n            rec.rr_name = question.qname;\n            rec.rr_type = qTypeMX;\n            rec.rr_class = qClassIN;\n            rec.ttl = ttl;\n            std::array<uint8_t, 512> tmp = {{0}};\n            llarp_buffer_t buf(tmp);\n            buf.put_uint16(priority);\n            if (EncodeNameTo(&buf, name))\n            {\n                buf.sz = buf.cur - buf.base;\n                rec.rData.resize(buf.sz);\n                memcpy(rec.rData.data(), buf.base, buf.sz);\n            }\n        }\n    }\n\n    void Message::add_srv_reply(std::vector<SRVData> records, RR_TTL_t ttl)\n    {\n        hdr_fields = reply_flags(hdr_fields);\n\n        const auto& question = questions[0];\n\n        for (const auto& srv : records)\n        {\n            if (not srv.is_valid())\n            {\n                add_nx_reply();\n                return;\n            }\n\n            answers.emplace_back();\n            auto& rec = answers.back();\n            rec.rr_name = question.qname;\n            rec.rr_type = qTypeSRV;\n            rec.rr_class = qClassIN;\n            rec.ttl = ttl;\n\n            std::array<uint8_t, 512> tmp = {{0}};\n            llarp_buffer_t buf(tmp);\n\n            buf.put_uint16(srv.priority);\n            buf.put_uint16(srv.weight);\n            buf.put_uint16(srv.port);\n\n            std::string target;\n            if (srv.target == \"\")\n            {\n                // get location of second dot (after service.proto) in qname\n                size_t pos = question.qname.find(\".\");\n                pos = question.qname.find(\".\", pos + 1);\n\n                target = question.qname.substr(pos + 1);\n            }\n            else\n            {\n                target = srv.target;\n            }\n\n            if (not EncodeNameTo(&buf, target))\n            {\n                add_nx_reply();\n                return;\n            }\n\n            buf.sz = buf.cur - buf.base;\n            rec.rData.resize(buf.sz);\n            memcpy(rec.rData.data(), buf.base, buf.sz);\n        }\n    }\n\n    void Message::add_txt_reply(std::string str, RR_TTL_t ttl)\n    {\n        auto& rec = answers.emplace_back();\n        rec.rr_name = questions[0].qname;\n        rec.rr_class = qClassIN;\n        rec.rr_type = qTypeTXT;\n        rec.ttl = ttl;\n        std::array<uint8_t, 1024> tmp{};\n        llarp_buffer_t buf(tmp);\n        while (not str.empty())\n        {\n            const auto left = std::min(str.size(), size_t{256});\n            const auto sub = str.substr(0, left);\n            uint8_t byte = left;\n            *buf.cur = byte;\n            buf.cur++;\n            if (not buf.write(sub.begin(), sub.end()))\n                throw std::length_error(\"text record too big\");\n            str = str.substr(left);\n        }\n        buf.sz = buf.cur - buf.base;\n        rec.rData.resize(buf.sz);\n        std::copy_n(buf.base, buf.sz, rec.rData.data());\n    }\n\n    void Message::add_nx_reply(RR_TTL_t)\n    {\n        if (questions.size())\n        {\n            answers.clear();\n            authorities.clear();\n            additional.clear();\n\n            // authorative response with recursion available\n            hdr_fields = reply_flags(hdr_fields);\n            // don't allow recursion on this request\n            hdr_fields &= ~flags_RD;\n            hdr_fields |= flags_RCODENameError;\n        }\n    }\n\n    std::string Message::to_string() const\n    {\n        return fmt::format(\n            \"[DNSMessage id={:x} fields={:x} questions={{{}}} answers={{{}}} authorities={{{}}} \"\n            \"additional={{{}}}]\",\n            hdr_id,\n            hdr_fields,\n            fmt::join(questions, \",\"),\n            fmt::join(answers, \",\"),\n            fmt::join(authorities, \",\"),\n            fmt::join(additional, \",\"));\n    }\n\n    std::optional<Message> maybe_parse_dns_msg(std::span<const std::byte> b)\n    {\n        MessageHeader hdr{};\n        llarp_buffer_t buf{b};\n\n        if (not hdr.Decode(&buf))\n            return std::nullopt;\n\n        auto msg = std::make_optional<Message>(hdr);\n        if (not msg->Decode(&buf))\n            msg.reset();\n\n        return msg;\n    }\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/message.hpp",
    "content": "#pragma once\n\n#include \"question.hpp\"\n#include \"rr.hpp\"\n#include \"serialize.hpp\"\n\n#include <optional>\n\nnamespace llarp\n{\n    struct IPPacket;\n\n    namespace dns\n    {\n        struct SRVData;\n\n        struct MessageHeader : public Serialize\n        {\n          public:\n            static constexpr size_t Size = 12;\n\n            MessageHeader() = default;\n\n            uint16_t _id;\n            uint16_t _fields;\n            uint16_t _qd_count;\n            uint16_t _an_count;\n            uint16_t _ns_count;\n            uint16_t _ar_count;\n\n            bool Encode(llarp_buffer_t* buf) const override;\n\n            bool Decode(llarp_buffer_t* buf) override;\n\n            nlohmann::json ToJSON() const override;\n\n            bool operator==(const MessageHeader& h) const\n            {\n                return std::tie(_id, _fields, _qd_count, _an_count, _ns_count, _ar_count)\n                    == std::tie(h._id, h._fields, h._qd_count, h._an_count, h._ns_count, h._ar_count);\n            }\n        };\n\n        struct Message : public Serialize\n        {\n            explicit Message(const MessageHeader& hdr);\n            explicit Message(const Question& question);\n\n            Message(Message&& other);\n            Message(const Message& other);\n\n            nlohmann::json ToJSON() const override;\n\n            void add_nx_reply(RR_TTL_t ttl = 1);\n\n            void add_serv_fail(RR_TTL_t ttl = 30);\n\n            void add_mx_reply(std::string name, uint16_t priority, RR_TTL_t ttl = 1);\n\n            void add_CNAME_reply(std::string name, RR_TTL_t ttl = 1);\n\n            void add_IN_reply(uint32_t addr, RR_TTL_t ttl = 1);\n\n            void add_reply(std::string name, RR_TTL_t ttl = 1);\n\n            void add_srv_reply(std::vector<SRVData> records, RR_TTL_t ttl = 1);\n\n            void add_ns_reply(std::string name, RR_TTL_t ttl = 1);\n\n            void add_txt_reply(std::string value, RR_TTL_t ttl = 1);\n\n            bool Encode(llarp_buffer_t* buf) const override;\n\n            bool Decode(llarp_buffer_t* buf) override;\n\n            // Wrapper around Encode that encodes into a new buffer and returns it\n            std::vector<std::byte> to_buffer() const;\n\n            std::string to_string() const;\n\n            uint16_t hdr_id;\n            uint16_t hdr_fields;\n            std::vector<Question> questions;\n            std::vector<ResourceRecord> answers;\n            std::vector<ResourceRecord> authorities;\n            std::vector<ResourceRecord> additional;\n        };\n\n        std::optional<Message> maybe_parse_dns_msg(std::span<const std::byte> buf);\n    }  // namespace dns\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/dns/name.cpp",
    "content": "#include \"name.hpp\"\n\n#include <llarp/util/str.hpp>\n\n#include <oxenc/hex.h>\n\nnamespace llarp::dns\n{\n    std::optional<std::string> DecodeName(llarp_buffer_t* buf, bool trimTrailingDot)\n    {\n        if (buf->size_left() < 1)\n            return std::nullopt;\n        auto result = std::make_optional<std::string>();\n        auto& name = *result;\n        size_t l;\n        do\n        {\n            l = *buf->cur;\n            buf->cur++;\n            if (l)\n            {\n                if (buf->size_left() < l)\n                    return std::nullopt;\n\n                name.append((const char*)buf->cur, l);\n                name += '.';\n            }\n            buf->cur = buf->cur + l;\n        } while (l);\n        /// trim off last dot\n        if (trimTrailingDot)\n            name.pop_back();\n        return result;\n    }\n\n    bool EncodeNameTo(llarp_buffer_t* buf, std::string_view name)\n    {\n        if (name.size() && name.back() == '.')\n            name.remove_suffix(1);\n\n        for (auto part : llarp::split(name, \".\"))\n        {\n            size_t l = part.length();\n            if (l > 63)\n                return false;\n            *(buf->cur) = l;\n            buf->cur++;\n            if (buf->size_left() < l)\n                return false;\n            if (l)\n            {\n                std::memcpy(buf->cur, part.data(), l);\n                buf->cur += l;\n            }\n            else\n                break;\n        }\n        *buf->cur = 0;\n        buf->cur++;\n        return true;\n    }\n\n    std::optional<std::variant<ipv4, ipv6>> DecodePTR(std::string_view name)\n    {\n        bool isV6 = false;\n        auto pos = name.find(\".in-addr.arpa\");\n\n        if (pos == std::string::npos)\n        {\n            pos = name.find(\".ip6.arpa\");\n            isV6 = true;\n        }\n\n        if (pos == std::string::npos)\n            return std::nullopt;\n\n        name = name.substr(0, pos + 1);\n        const auto numdots = std::count(name.begin(), name.end(), '.');\n\n        if (numdots == 4 && !isV6)\n        {\n            std::array<uint8_t, 4> q;\n\n            for (int i = 3; i >= 0; i--)\n            {\n                pos = name.find('.');\n                if (!llarp::parse_int(name.substr(0, pos), q[i]))\n                    return std::nullopt;\n                name.remove_prefix(pos + 1);\n            }\n\n            return ipv4(q[0], q[1], q[2], q[3]);\n        }\n        if (numdots == 32 && name.size() == 64 && isV6)\n        {\n            // We're going to convert from nybbles a.b.c.d.e.f.0.1.2.3.[...] into hex string\n            // \"badcfe1032...\", then decode the hex string to bytes.\n            std::array<char, 32> in;\n            auto in_pos = in.data();\n\n            for (size_t i = 0; i < 64; i += 4)\n            {\n                if (not(oxenc::is_hex_digit(name[i]) and name[i + 1] == '.' and oxenc::is_hex_digit(name[i + 2])\n                        and name[i + 3] == '.'))\n                    return std::nullopt;\n\n                // Flip the nybbles because the smallest one is first\n                *in_pos++ = name[i + 2];\n                *in_pos++ = name[i];\n            }\n\n            assert(in_pos == in.data() + in.size());\n\n            // our string right now is the little endian representation, so load it as such on\n            // little endian, or in reverse on big endian.\n\n            std::string arg;\n\n            if constexpr (oxenc::little_endian)\n                arg = oxenc::from_hex(in.begin(), in.end());\n            else\n                arg = std::string{in.data(), in.size()};\n\n            return ipv6{arg};\n        }\n        return std::nullopt;\n    }\n\n    bool NameIsReserved(std::string_view name)\n    {\n        const std::vector<std::string_view> reserved_names = {\n            \".snode.loki\"sv, \".loki.loki\"sv, \".snode.loki.\"sv, \".loki.loki.\"sv};\n        for (const auto& reserved : reserved_names)\n        {\n            if (ends_with(name, reserved))  // subdomain foo.loki.loki\n                return true;\n            if (name == reserved.substr(1))  // loki.loki itself\n                return true;\n        }\n        return false;\n    }\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/name.hpp",
    "content": "#pragma once\n\n#include <llarp/address/types.hpp>\n#include <llarp/util/buffer.hpp>\n\n#include <optional>\n#include <string>\n\nnamespace llarp::dns\n{\n    /// decode name from buffer; return nullopt on failure\n    std::optional<std::string> DecodeName(llarp_buffer_t* buf, bool trimTrailingDot = false);\n\n    /// encode name to buffer\n    bool EncodeNameTo(llarp_buffer_t* buf, std::string_view name);\n\n    std::optional<std::variant<ipv4, ipv6>> DecodePTR(std::string_view name);\n\n    bool NameIsReserved(std::string_view name);\n\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/nm_platform.cpp",
    "content": "#include \"nm_platform.hpp\"\n#ifdef WITH_SYSTEMD\n\nextern \"C\"\n{\n#include <net/if.h>\n}\n\n#include <llarp/linux/dbus.hpp>\n\nnamespace llarp::dns::nm\n{\n    void Platform::set_resolver(unsigned int, quic::Address, bool)\n    {\n        // todo: implement me eventually\n    }\n}  // namespace llarp::dns::nm\n#endif\n"
  },
  {
    "path": "llarp/dns/nm_platform.hpp",
    "content": "#pragma once\n#include \"platform.hpp\"\n\n#include <llarp/constants/platform.hpp>\n\n#include <type_traits>\n#include <unordered_map>\n\nnamespace llarp::dns\n{\n    namespace nm\n    {\n        // a dns platform that sets dns via network manager\n        class Platform : public I_Platform\n        {\n          public:\n            ~Platform() override = default;\n\n            void set_resolver(unsigned int index, quic::Address dns, bool global) override;\n        };\n    };  // namespace nm\n    using NM_Platform_t = std::conditional_t<false, nm::Platform, Null_Platform>;\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/platform.cpp",
    "content": "#include \"platform.hpp\"\n\nnamespace llarp::dns\n{\n    void Multi_Platform::add_impl(std::unique_ptr<I_Platform> impl) { m_Impls.emplace_back(std::move(impl)); }\n\n    void Multi_Platform::set_resolver(unsigned int index, quic::Address dns, bool global)\n    {\n        if (m_Impls.empty())\n            return;\n        size_t fails{0};\n        for (const auto& ptr : m_Impls)\n        {\n            try\n            {\n                ptr->set_resolver(index, dns, global);\n            }\n            catch (std::exception& ex)\n            {\n                log::warning(log::Cat(\"dns\"), \"{}\", ex.what());\n                fails++;\n            }\n        }\n        if (fails == m_Impls.size())\n            throw std::runtime_error{\"tried all ways to set resolver and failed\"};\n    }\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/platform.hpp",
    "content": "#pragma once\n\n#include <llarp/address/types.hpp>\n#include <llarp/util/logging.hpp>\n\n#include <oxen/quic/address.hpp>\n\n#include <memory>\n\n#ifndef _WIN32\n#include <net/if.h>\n#endif\n\nnamespace llarp::dns\n{\n    /// sets dns settings in a platform dependant way\n    class I_Platform\n    {\n      public:\n        virtual ~I_Platform() = default;\n\n        /// Attempts to set lokinet as the DNS server.\n        /// throws if unsupported or fails.\n        ///\n        ///\n        /// \\param if_index -- the interface index to which we add the DNS servers, this can be\n        /// gotten from the interface name e.g. lokitun0 (Typically tun_endpoint.GetIfName().) and\n        /// then put through if_nametoindex(). \\param dns -- the listening address of the lokinet\n        /// DNS server \\param global -- whether to set up lokinet for all DNS queries (true) or just\n        /// .loki & .snode addresses (false).\n        virtual void set_resolver(unsigned int if_index, quic::Address dns, bool global) = 0;\n    };\n\n    /// a dns platform does silently does nothing, successfully\n    class Null_Platform : public I_Platform\n    {\n      public:\n        ~Null_Platform() override = default;\n        void set_resolver(unsigned int, quic::Address, bool) override {}\n    };\n\n    /// a collection of dns platforms that are tried in order when setting dns\n    class Multi_Platform : public I_Platform\n    {\n        std::vector<std::unique_ptr<I_Platform>> m_Impls;\n\n      public:\n        ~Multi_Platform() override = default;\n        /// add a platform to be owned\n        void add_impl(std::unique_ptr<I_Platform> impl);\n\n        /// try all owned platforms to set the resolver, throws if none of them work\n        void set_resolver(unsigned int if_index, quic::Address dns, bool global) override;\n    };\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/question.cpp",
    "content": "#include \"question.hpp\"\n\n#include \"dns.hpp\"\n#include \"name.hpp\"\n\n#include <llarp/util/str.hpp>\n\n#include <nlohmann/json.hpp>\n\nnamespace llarp::dns\n{\n    static auto logcat = log::Cat(\"dns\");\n\n    Question::Question(Question&& other)\n        : qname(std::move(other.qname)), qtype(std::move(other.qtype)), qclass(std::move(other.qclass))\n    {}\n    Question::Question(const Question& other) : qname(other.qname), qtype(other.qtype), qclass(other.qclass) {}\n\n    Question::Question(std::string name, QType_t type) : qname{std::move(name)}, qtype{type}, qclass{qClassIN}\n    {\n        if (qname.empty())\n            throw std::invalid_argument{\"qname cannot be empty\"};\n    }\n\n    bool Question::Encode(llarp_buffer_t* buf) const\n    {\n        if (!EncodeNameTo(buf, qname))\n            return false;\n        if (!buf->put_uint16(qtype))\n            return false;\n        return buf->put_uint16(qclass);\n    }\n\n    bool Question::Decode(llarp_buffer_t* buf)\n    {\n        if (auto name = DecodeName(buf))\n            qname = *std::move(name);\n        else\n        {\n            log::error(logcat, \"failed to decode name\");\n            return false;\n        }\n        if (!buf->read_uint16(qtype))\n        {\n            log::error(logcat, \"failed to decode type\");\n            return false;\n        }\n        if (!buf->read_uint16(qclass))\n        {\n            log::error(logcat, \"failed to decode class\");\n            return false;\n        }\n        return true;\n    }\n\n    nlohmann::json Question::ToJSON() const\n    {\n        return nlohmann::json{{\"qname\", qname}, {\"qtype\", qtype}, {\"qclass\", qclass}};\n    }\n\n    bool Question::IsName(const std::string& other) const\n    {\n        // does other have a . at the end?\n        if (other.find_last_of('.') == (other.size() - 1))\n            return other == qname;\n        // no, add it and retry\n        return IsName(other + \".\");\n    }\n\n    bool Question::IsLocalhost() const\n    {\n        return (qname == \"localhost.loki.\" or llarp::ends_with(qname, \".localhost.loki.\"));\n    }\n\n    bool Question::HasSubdomains() const\n    {\n        const auto parts = split(qname, \".\", true);\n        return parts.size() >= 3;\n    }\n\n    std::string Question::Subdomains() const\n    {\n        if (qname.size() < 2)\n            return \"\";\n\n        size_t pos;\n\n        pos = qname.rfind('.', qname.size() - 2);\n        if (pos == std::string::npos or pos == 0)\n            return \"\";\n\n        pos = qname.rfind('.', pos - 1);\n        if (pos == std::string::npos or pos == 0)\n            return \"\";\n\n        return qname.substr(0, pos);\n    }\n\n    std::string Question::Name() const { return qname.substr(0, qname.find_last_of('.')); }\n\n    bool Question::HasTLD(const std::string& tld) const\n    {\n        return qname.find(tld) != std::string::npos && qname.rfind(tld) == (qname.size() - tld.size()) - 1;\n    }\n\n    std::string Question::to_string() const\n    {\n        return \"DNSQuestion:[ qname:{} | qtype:{} | qclass:{} ]\"_format(qname, qtype, qclass);\n    }\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/question.hpp",
    "content": "#pragma once\n\n#include \"serialize.hpp\"\n\nnamespace llarp::dns\n{\n    using QType_t = uint16_t;\n    using QClass_t = uint16_t;\n\n    struct Question : public Serialize\n    {\n        Question() = default;\n\n        explicit Question(std::string name, QType_t type);\n\n        Question(Question&& other);\n        Question(const Question& other);\n\n        bool Encode(llarp_buffer_t* buf) const override;\n\n        bool Decode(llarp_buffer_t* buf) override;\n\n        std::string to_string() const;\n\n        bool operator==(const Question& other) const\n        {\n            return qname == other.qname && qtype == other.qtype && qclass == other.qclass;\n        }\n\n        std::string qname;\n        QType_t qtype;\n        QClass_t qclass;\n\n        /// determine if we match a name\n        bool IsName(const std::string& other) const;\n\n        /// is the name [something.]localhost.loki. ?\n        bool IsLocalhost() const;\n\n        /// return true if we have subdomains in ths question\n        bool HasSubdomains() const;\n\n        /// get subdomain(s), if any, from qname\n        std::string Subdomains() const;\n\n        /// return qname with no trailing .\n        std::string Name() const;\n\n        /// determine if we are using this TLD\n        bool HasTLD(const std::string& tld) const;\n\n        nlohmann::json ToJSON() const override;\n    };\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/rr.cpp",
    "content": "#include \"rr.hpp\"\n\n#include \"dns.hpp\"\n#include \"name.hpp\"\n\n#include <nlohmann/json.hpp>\n\nnamespace llarp::dns\n{\n    static auto logcat = log::Cat(\"dns\");\n\n    ResourceRecord::ResourceRecord(const ResourceRecord& other)\n        : rr_name(other.rr_name), rr_type(other.rr_type), rr_class(other.rr_class), ttl(other.ttl), rData(other.rData)\n    {}\n\n    ResourceRecord::ResourceRecord(ResourceRecord&& other)\n        : rr_name(std::move(other.rr_name)),\n          rr_type(std::move(other.rr_type)),\n          rr_class(std::move(other.rr_class)),\n          ttl(std::move(other.ttl)),\n          rData(std::move(other.rData))\n    {}\n\n    ResourceRecord::ResourceRecord(std::string name, RRType_t type, RR_RData_t data)\n        : rr_name{std::move(name)}, rr_type{type}, rr_class{qClassIN}, ttl{1}, rData{std::move(data)}\n    {}\n\n    bool ResourceRecord::Encode(llarp_buffer_t* buf) const\n    {\n        if (not EncodeNameTo(buf, rr_name))\n            return false;\n        if (!buf->put_uint16(rr_type))\n        {\n            return false;\n        }\n        if (!buf->put_uint16(rr_class))\n        {\n            return false;\n        }\n        if (!buf->put_uint32(ttl))\n        {\n            return false;\n        }\n        if (!EncodeRData(buf, rData))\n        {\n            return false;\n        }\n        return true;\n    }\n\n    bool ResourceRecord::Decode(llarp_buffer_t* buf)\n    {\n        uint16_t discard;\n        if (!buf->read_uint16(discard))\n            return false;\n        if (!buf->read_uint16(rr_type))\n        {\n            log::debug(logcat, \"failed to decode rr type\");\n            return false;\n        }\n        if (!buf->read_uint16(rr_class))\n        {\n            log::debug(logcat, \"failed to decode rr class\");\n            return false;\n        }\n        if (!buf->read_uint32(ttl))\n        {\n            log::debug(logcat, \"failed to decode ttl\");\n            return false;\n        }\n        if (!DecodeRData(buf, rData))\n        {\n            log::debug(logcat, \"failed to decode rr rdata {}\", *this);\n            return false;\n        }\n        return true;\n    }\n\n    nlohmann::json ResourceRecord::ToJSON() const\n    {\n        return nlohmann::json{\n            {\"name\", rr_name},\n            {\"type\", rr_type},\n            {\"class\", rr_class},\n            {\"ttl\", ttl},\n            {\"rdata\", std::string{reinterpret_cast<const char*>(rData.data()), rData.size()}}};\n    }\n\n    std::string ResourceRecord::to_string() const\n    {\n        return \"RR:[ name:{} | type:{} | class:{} | ttl:{} | rdata-size:{} ]\"_format(\n            rr_name, rr_type, rr_class, ttl, rData.size());\n    }\n\n    bool ResourceRecord::HasCNameForTLD(const std::string& tld) const\n    {\n        if (rr_type != qTypeCNAME)\n            return false;\n        llarp_buffer_t buf(rData);\n        if (auto name = DecodeName(&buf))\n            return name->rfind(tld) == name->size() - tld.size() - 1;\n        return false;\n    }\n\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/rr.hpp",
    "content": "#pragma once\n\n#include \"serialize.hpp\"\n\n#include <vector>\n\nnamespace llarp::dns\n{\n    using RRClass_t = uint16_t;\n    using RRType_t = uint16_t;\n    using RR_RData_t = std::vector<uint8_t>;\n    using RR_TTL_t = uint32_t;\n\n    struct ResourceRecord : public Serialize\n    {\n        ResourceRecord() = default;\n        ResourceRecord(const ResourceRecord& other);\n        ResourceRecord(ResourceRecord&& other);\n\n        explicit ResourceRecord(std::string name, RRType_t type, RR_RData_t rdata);\n\n        bool Encode(llarp_buffer_t* buf) const override;\n\n        bool Decode(llarp_buffer_t* buf) override;\n\n        nlohmann::json ToJSON() const override;\n\n        std::string to_string() const;\n\n        bool HasCNameForTLD(const std::string& tld) const;\n\n        std::string rr_name;\n        RRType_t rr_type;\n        RRClass_t rr_class;\n        RR_TTL_t ttl;\n        RR_RData_t rData;\n\n        static constexpr bool to_string_formattable = true;\n    };\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/sd_platform.cpp",
    "content": "#ifdef WITH_SYSTEMD\n#include \"sd_platform.hpp\"\n\nextern \"C\"\n{\n#include <net/if.h>\n}\n\n#include <llarp/linux/dbus.hpp>\n\nnamespace llarp::dns::sd\n{\n    void Platform::set_resolver(unsigned int if_ndx, quic::Address dns, bool global)\n    {\n        linux::DBUS _dbus{\"org.freedesktop.resolve1\", \"/org/freedesktop/resolve1\", \"org.freedesktop.resolve1.Manager\"};\n        // This passing address by bytes and using two separate calls for ipv4/ipv6 is gross, but\n        // the alternative is to build up a bunch of crap with va_args, which is slightly more\n        // gross.\n        const bool isStandardDNSPort = dns.port() == 53;\n        if (dns.is_ipv6())\n        {\n            ipv6 ipv6{dns.in6().sin6_addr};\n            static_assert(sizeof(ipv6) == 16);\n\n            auto* a = reinterpret_cast<const uint8_t*>(&ipv6);\n            if (isStandardDNSPort)\n            {\n                _dbus(\n                    \"SetLinkDNS\",\n                    \"ia(iay)\",\n                    (int32_t)if_ndx,\n                    (int)1,             // number of \"iayqs\"s we are passing\n                    (int32_t)AF_INET6,  // network address type\n                    (int)16,            // network addr byte size\n                                        // clang-format off\n              a[0], a[1], a[2],  a[3],  a[4],  a[5],  a[6],  a[7],\n              a[8], a[9], a[10], a[11], a[12], a[13], a[14], a[15] // yuck\n                                        // clang-format on\n                );\n            }\n            else\n            {\n                _dbus(\n                    \"SetLinkDNSEx\",\n                    \"ia(iayqs)\",\n                    (int32_t)if_ndx,\n                    (int)1,             // number of \"iayqs\"s we are passing\n                    (int32_t)AF_INET6,  // network address type\n                    (int)16,            // network addr byte size\n                    // clang-format off\n              a[0], a[1], a[2],  a[3],  a[4],  a[5],  a[6],  a[7],\n              a[8], a[9], a[10], a[11], a[12], a[13], a[14], a[15], // yuck\n                            // clang-format on\n                    dns.port(),\n                    nullptr  // dns server name (for TLS SNI which we don't care about)\n                );\n            }\n        }\n        else\n        {\n            ipv4 ipv4{oxenc::big_to_host<uint32_t>(dns.in4().sin_addr.s_addr)};\n\n            auto* a = reinterpret_cast<const uint8_t*>(&ipv4);\n\n            if (isStandardDNSPort)\n            {\n                _dbus(\n                    \"SetLinkDNS\",\n                    \"ia(iay)\",\n                    (int32_t)if_ndx,\n                    (int)1,            // number of \"iayqs\"s we are passing\n                    (int32_t)AF_INET,  // network address type\n                    (int)4,            // network addr byte size\n                                       // clang-format off\n              a[0], a[1], a[2], a[3] // yuck\n                                       // clang-format on\n                );\n            }\n            else\n            {\n                _dbus(\n                    \"SetLinkDNSEx\",\n                    \"ia(iayqs)\",\n                    (int32_t)if_ndx,\n                    (int)1,            // number of \"iayqs\"s we are passing\n                    (int32_t)AF_INET,  // network address type\n                    (int)4,            // network addr byte size\n                    // clang-format off\n              a[0], a[1], a[2], a[3], // yuck\n                           // clang-format on\n                    dns.port(),\n                    nullptr  // dns server name (for TLS SNI which we don't care about)\n                );\n            }\n        }\n\n        if (global)\n            // Setting \".\" as a routing domain gives this DNS server higher priority in resolution\n            // compared to dns servers that are set without a domain (e.g. the default for a\n            // DHCP-configured DNS server)\n            _dbus(\n                \"SetLinkDomains\",\n                \"ia(sb)\",\n                (int32_t)if_ndx,\n                (int)1,  // array size\n                \".\"      // global DNS root\n            );\n        else\n            // Only resolve .loki and .snode through lokinet (so you keep using your local DNS\n            // server for everything else, which is nicer than forcing everything though lokinet's\n            // upstream DNS).\n            _dbus(\n                \"SetLinkDomains\",\n                \"ia(sb)\",\n                (int32_t)if_ndx,\n                (int)2,   // array size\n                \"loki\",   // domain\n                (int)1,   // routing domain = true\n                \"snode\",  // domain\n                (int)1    // routing domain = true\n            );\n    }\n}  // namespace llarp::dns::sd\n#endif\n"
  },
  {
    "path": "llarp/dns/sd_platform.hpp",
    "content": "#pragma once\n#include \"platform.hpp\"\n\n#include <llarp/constants/platform.hpp>\n\n#include <type_traits>\n\nnamespace llarp::dns\n{\n    namespace sd\n    {\n        /// a dns platform that sets dns via systemd resolved\n        class Platform : public I_Platform\n        {\n          public:\n            ~Platform() override = default;\n\n            void set_resolver(unsigned int if_index, quic::Address dns, bool global) override;\n        };\n    }  // namespace sd\n    using SD_Platform_t = std::conditional_t<llarp::platform::has_systemd, sd::Platform, Null_Platform>;\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/serialize.cpp",
    "content": "#include \"serialize.hpp\"\n\nnamespace llarp::dns\n{\n    Serialize::~Serialize() = default;\n\n    bool EncodeRData(llarp_buffer_t* buf, const std::vector<uint8_t>& v)\n    {\n        if (v.size() > 65536)\n            return false;\n        uint16_t len = v.size();\n        if (!buf->put_uint16(len))\n            return false;\n        if (buf->size_left() < len)\n            return false;\n        memcpy(buf->cur, v.data(), len);\n        buf->cur += len;\n        return true;\n    }\n\n    bool DecodeRData(llarp_buffer_t* buf, std::vector<uint8_t>& v)\n    {\n        uint16_t len;\n        if (!buf->read_uint16(len))\n            return false;\n        size_t left = buf->size_left();\n        if (left < len)\n            return false;\n        v.resize(size_t(len));\n        if (len)\n        {\n            memcpy(v.data(), buf->cur, len);\n            buf->cur += len;\n        }\n        return true;\n    }\n\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/serialize.hpp",
    "content": "#pragma once\n\n#include <llarp/util/buffer.hpp>\n\n#include <nlohmann/json_fwd.hpp>\n\n#include <vector>\n\nnamespace llarp::dns\n{\n    /// base type for serializable dns entities\n    struct Serialize\n    {\n        virtual ~Serialize() = 0;\n\n        /// encode entity to buffer\n        virtual bool Encode(llarp_buffer_t* buf) const = 0;\n\n        /// decode entity from buffer\n        virtual bool Decode(llarp_buffer_t* buf) = 0;\n\n        /// convert this whatever into json\n        virtual nlohmann::json ToJSON() const = 0;\n\n        static constexpr bool to_string_formattable = true;\n    };\n\n    bool EncodeRData(llarp_buffer_t* buf, const std::vector<uint8_t>& rdata);\n\n    bool DecodeRData(llarp_buffer_t* buf, std::vector<uint8_t>& rdata);\n\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/server.cpp",
    "content": "#include \"server.hpp\"\n\n#include \"message.hpp\"\n#include \"nm_platform.hpp\"\n#include \"sd_platform.hpp\"\n\n#include <llarp/constants/apple.hpp>\n#include <llarp/constants/platform.hpp>\n\n#include <oxen/log.hpp>\n#include <oxen/quic/udp.hpp>\n#include <unbound.h>\n\n#include <memory>\n#include <optional>\n#include <stdexcept>\n#include <utility>\n\nnamespace llarp::dns\n{\n    static auto logcat = log::Cat(\"dns\");\n\n    void QueryJob_Base::cancel()\n    {\n        Message reply{_query};\n        reply.add_serv_fail();\n        send_reply(reply.to_buffer());\n    }\n\n    /// sucks up udp packets from a bound socket and feeds it to a server\n    class UDPReader : public PacketSource, public std::enable_shared_from_this<UDPReader>\n    {\n        Server& _dns;\n        std::unique_ptr<quic::UDPSocket> _udp;\n        quic::Address _local_addr;\n\n      public:\n        explicit UDPReader(Server& dns, quic::Loop& loop, quic::Address bind) : _dns{dns}\n        {\n            _udp = std::make_unique<quic::UDPSocket>(\n                loop.get_event_base(), bind, /*gso=*/false, [this](quic::Packet&& pkt) {\n                    auto& src = pkt.path.remote;  // \"remote\" address is packet source, we (\"local\") are destination\n                    if (src == _local_addr)\n                    {\n                        log::debug(logcat, \"DNS packet received, not handling because we're the packet source\", src);\n                        return;\n                    }\n\n                    if (not _dns.maybe_handle_payload(shared_from_this(), _local_addr, src, pkt.data()))\n                    {\n                        log::warning(logcat, \"did not handle dns packet from {} to {}\", src, _local_addr);\n                    }\n                    log::trace(logcat, \"Handled DNS packet from {} to {}\", src, _local_addr);\n                });\n\n            if (auto maybe_addr = bound_on())\n            {\n                _local_addr = *maybe_addr;\n                log::debug(logcat, \"lokinet DNS server bound on {}\", _local_addr);\n            }\n            else\n                throw std::runtime_error{\"cannot find which address our dns socket is bound on\"};\n        }\n\n        std::optional<quic::Address> bound_on() const override { return _udp->address(); }\n\n        bool would_loop(const quic::Address& to, const quic::Address& /*from*/) const override\n        {\n            return to != _local_addr;\n        }\n\n        void send_udp(const quic::Address& to, const quic::Address&, std::span<const std::byte> data) const override\n        {\n            const size_t bufsize = data.size();\n            size_t n_pkts = 1;\n            auto [ior, sent] = _udp->send(quic::Path{_local_addr, to}, data.data(), &bufsize, 0, n_pkts);\n\n            log::trace(\n                logcat,\n                \"dns server {} UDP packet to {} (ec={})\",\n                ior.success() ? \"sent\" : \"failed to send\",\n                to,\n                ior.error_code);\n        }\n    };\n\n    namespace libunbound\n    {\n        class Resolver;\n\n        class Query : public QueryJob_Base, public std::enable_shared_from_this<Query>\n        {\n            std::shared_ptr<PacketSource> src;\n            quic::Address resolverAddr;\n            quic::Address askerAddr;\n\n          public:\n            explicit Query(\n                std::weak_ptr<Resolver> parent_,\n                Message query,\n                std::shared_ptr<PacketSource> pktsrc,\n                quic::Address toaddr,\n                quic::Address fromaddr)\n                : QueryJob_Base{std::move(query)},\n                  src{std::move(pktsrc)},\n                  resolverAddr{std::move(toaddr)},\n                  askerAddr{std::move(fromaddr)},\n                  parent{parent_}\n            {}\n            std::weak_ptr<Resolver> parent;\n            int id{};\n\n            void send_reply(std::vector<std::byte> buf) override;\n        };\n\n        /// Resolver_Base that uses libunbound\n        class Resolver final : public Resolver_Base, public std::enable_shared_from_this<Resolver>\n        {\n            ub_ctx* m_ctx = nullptr;\n            quic::Loop& _loop;\n#ifdef _WIN32\n            // windows is dumb so we do ub mainloop in a thread\n            std::thread runner;\n            std::atomic<bool> running;\n#else\n            // std::shared_ptr<uvw::PollHandle> _poller;\n#endif\n\n            std::optional<quic::Address> _local_addr;\n            std::unordered_set<std::shared_ptr<Query>> _pending;\n\n            struct ub_result_deleter\n            {\n                void operator()(ub_result* ptr) { ::ub_resolve_free(ptr); }\n            };\n\n            const net::Platform* net_ptr() const { return llarp::net::Platform::Default_ptr(); }\n\n            static void callback(void* data, int err, ub_result* _result)\n            {\n                log::debug(logcat, \"got dns response from libunbound\");\n                // take ownership of ub_result\n                std::unique_ptr<ub_result, ub_result_deleter> result{_result};\n                // borrow query\n                auto* query = static_cast<Query*>(data);\n                if (err)\n                {\n                    // some kind of error from upstream\n                    log::warning(logcat, \"Upstream DNS failure: {}\", ub_strerror(err));\n                    query->cancel();\n                    return;\n                }\n\n                log::trace(logcat, \"queueing dns response from libunbound to userland\");\n\n                std::vector<std::byte> payload;\n                payload.resize(result->answer_len);\n                std::memcpy(payload.data(), result->answer_packet, result->answer_len);\n                llarp_buffer_t buf{payload};\n                MessageHeader hdr;\n                hdr.Decode(&buf);\n                hdr._id = query->underlying().hdr_id;\n                buf.cur = buf.base;\n                hdr.Encode(&buf);\n\n                // send reply\n                query->send_reply(std::move(payload));\n            }\n\n            void add_upstream_resolver(const quic::Address& dns)\n            {\n                auto str = \"{}@{}\"_format(dns.host(), dns.port());\n\n                if (auto err = ub_ctx_set_fwd(m_ctx, str.c_str()))\n                {\n                    throw std::runtime_error{fmt::format(\"cannot use {} as upstream dns: {}\", str, ub_strerror(err))};\n                }\n            }\n\n            bool configure_apple_trampoline(const quic::Address& dns)\n            {\n                // On Apple, when we turn on exit mode, we tear down and then reestablish the\n                // unbound resolver: in exit mode, we set use upstream to a localhost trampoline\n                // that redirects packets through the tunnel.  In non-exit mode, we directly use the\n                // upstream, so we look here for a reconfiguration to use the trampoline port to\n                // check which state we're in.\n                //\n                // We have to do all this crap because we can't directly connect to upstream from\n                // here: within the network extension, macOS ignores the tunnel we are managing and\n                // so, if we didn't do this, all our DNS queries would leak out around the tunnel.\n                // Instead we have to bounce things through the objective C trampoline code (which\n                // is what actually handles the upstream querying) so that it can call into Apple's\n                // special snowflake API to set up a socket that has the magic Apple snowflake sauce\n                // added on top so that it actually routes through the tunnel instead of around it.\n                //\n                // But the trampoline *always* tries to send the packet through the tunnel, and that\n                // will only work in exit mode.\n                //\n                // All of this macos behaviour is all carefully and explicitly documented by Apple\n                // with plenty of examples and other exposition, of course, just like all of their\n                // wonderful new APIs to reinvent standard unix interfaces with half-baked\n                // replacements.\n\n                if constexpr (platform::is_apple)\n                {\n                    if (dns.host() == \"127.0.0.1\" and dns.port() == apple::dns_trampoline_port)\n                    {\n                        // macOS is stupid: the default (0.0.0.0) fails with \"send failed: Can't\n                        // assign requested address\" when unbound tries to connect to the localhost\n                        // address using a source address of 0.0.0.0.  Yay apple.\n                        set_opt(\"outgoing-interface:\", \"127.0.0.1\");\n\n                        // The trampoline expects just a single source port (and sends everything\n                        // back to it).\n                        set_opt(\"outgoing-range:\", \"1\");\n                        set_opt(\"outgoing-port-avoid:\", \"0-65535\");\n                        set_opt(\"outgoing-port-permit:\", \"{}\"_format(apple::dns_trampoline_source_port));\n                        return true;\n                    }\n                }\n                return false;\n            }\n\n            void configure_upstream(const llarp::DnsConfig& conf)\n            {\n                bool is_apple_tramp = false;\n\n                // set up forward dns\n                for (const auto& dns : conf._upstream_dns)\n                {\n                    add_upstream_resolver(dns);\n                    is_apple_tramp = is_apple_tramp or configure_apple_trampoline(dns);\n                }\n\n                if (auto maybe_addr = conf._query_bind; maybe_addr and not is_apple_tramp)\n                {\n                    quic::Address addr{*maybe_addr};\n                    auto host = addr.host();\n\n                    if (addr.port() == 0)\n                    {\n                        // unbound manages their own sockets because of COURSE it does. so we find\n                        // an open port on our system and use it so we KNOW what it is before giving\n                        // it to unbound to explicitly bind to JUST that port.\n\n                        auto fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);\n#ifdef _WIN32\n                        if (fd == INVALID_SOCKET)\n#else\n                        if (fd == -1)\n#endif\n                        {\n                            throw std::invalid_argument{\n                                fmt::format(\"Failed to create UDP socket for unbound: {}\", strerror(errno))};\n                        }\n\n#ifdef _WIN32\n#define CLOSE closesocket\n#else\n#define CLOSE close\n#endif\n                        if (0 != bind(fd, static_cast<const sockaddr*>(addr), addr.socklen()))\n                        {\n                            CLOSE(fd);\n                            throw std::invalid_argument{\n                                fmt::format(\"Failed to bind UDP socket for unbound: {}\", strerror(errno))};\n                        }\n                        struct sockaddr_storage sas;\n                        auto* sa = reinterpret_cast<struct sockaddr*>(&sas);\n                        socklen_t sa_len = sizeof(sas);\n                        int rc = getsockname(fd, sa, &sa_len);\n                        CLOSE(fd);\n#undef CLOSE\n                        if (rc != 0)\n                        {\n                            throw std::invalid_argument{\n                                fmt::format(\"Failed to query UDP port for unbound: {}\", strerror(errno))};\n                        }\n\n                        addr = quic::Address{sa, sizeof(sockaddr)};\n                    }\n                    _local_addr = addr;\n\n                    log::debug(logcat, \"sending dns queries from {}\", addr.to_string());\n                    // set up query bind port if needed\n                    set_opt(\"outgoing-interface:\", host);\n                    set_opt(\"outgoing-range:\", \"1\");\n                    set_opt(\"outgoing-port-avoid:\", \"0-65535\");\n                    set_opt(\"outgoing-port-permit:\", \"{}\"_format(addr.port()));\n                }\n            }\n\n            void set_opt(const std::string& key, const std::string& val)\n            {\n                ub_ctx_set_option(m_ctx, key.c_str(), val.c_str());\n            }\n\n            // Copy of the DNS config (a copy because on some platforms, like Apple, we change the\n            // applied upstream DNS settings when turning on/off exit mode).\n            llarp::DnsConfig m_conf;\n\n          public:\n            explicit Resolver(quic::Loop& loop, llarp::DnsConfig conf) : _loop{loop}, m_conf{std::move(conf)}\n            {\n                up(m_conf);\n            }\n\n            ~Resolver() override { down(); }\n\n            std::string_view resolver_name() const override { return \"unbound\"; }\n\n            std::optional<quic::Address> get_local_addr() const override { return _local_addr; }\n\n            void remove_pending(const std::shared_ptr<Query>& query) { _pending.erase(query); }\n\n            void up(const llarp::DnsConfig& conf)\n            {\n                if (m_ctx)\n                    throw std::logic_error{\"Internal error: attempt to Up() dns server multiple times\"};\n\n                m_ctx = ::ub_ctx_create();\n                // set libunbound settings\n\n                set_opt(\"do-tcp:\", \"no\");\n\n                for (const auto& [k, v] : conf.extra_opts)\n                    set_opt(k, v);\n\n                // add host files\n                for (const auto& file : conf.hostfiles)\n                {\n                    const auto str = file.string();\n                    if (auto ret = ub_ctx_hosts(m_ctx, str.c_str()))\n                    {\n                        throw std::runtime_error{fmt::format(\"Failed to add host file {}: {}\", file, ub_strerror(ret))};\n                    }\n                }\n\n                configure_upstream(conf);\n\n                // set async\n                ub_ctx_async(m_ctx, 1);\n                // setup mainloop\n#ifdef _WIN32\n                running = true;\n                runner = std::thread{[this]() {\n                    while (running)\n                    {\n                        // poll and process callbacks it this thread\n                        if (ub_poll(m_ctx))\n                        {\n                            ub_process(m_ctx);\n                        }\n                        else  // nothing to do, sleep.\n                            std::this_thread::sleep_for(10ms);\n                    }\n                }};\n#else\n                // TODO: replace uvw shim shit with new libev stuff\n                // if (auto loop_ptr = loop->MaybeGetUVWLoop())\n                // {\n                //     _poller = loop_ptr->resource<uvw::PollHandle>(ub_fd(m_ctx));\n                //     _poller->on<uvw::PollEvent>([this](auto&, auto&) { ub_process(m_ctx); });\n                //     _poller->start(uvw::PollHandle::Event::READABLE);\n                //     return;\n                // }\n#endif\n            }\n\n            void down() override\n            {\n#ifdef _WIN32\n                if (running.exchange(false))\n                {\n                    log::debug(logcat, \"shutting down win32 dns thread\");\n                    runner.join();\n                }\n#else\n                // if (_poller)\n                //     _poller->close();\n#endif\n                if (m_ctx)\n                {\n                    ::ub_ctx_delete(m_ctx);\n                    m_ctx = nullptr;\n\n                    // destroy any outstanding queries that unbound hasn't fired yet\n                    if (not _pending.empty())\n                    {\n                        log::debug(logcat, \"cancelling {} pending queries\", _pending.size());\n                        // We must copy because Cancel does a loop call to remove itself, but since\n                        // we are already in the main loop it happens immediately, which would\n                        // invalidate our iterator if we were looping through m_Pending at the time.\n                        auto copy = _pending;\n                        for (const auto& query : copy)\n                            query->cancel();\n                    }\n                }\n            }\n\n            int rank() const override { return 10; }\n\n            void reset_resolver(std::optional<std::vector<quic::Address>> replace_upstream) override\n            {\n                down();\n                if (replace_upstream)\n                    m_conf._upstream_dns = std::move(*replace_upstream);\n                up(m_conf);\n            }\n\n            template <typename Callable>\n            void call(Callable&& f)\n            {\n                _loop.call(std::forward<Callable>(f));\n            }\n\n            bool maybe_hook_dns(\n                const std::shared_ptr<PacketSource>& source,\n                const Message& query,\n                const quic::Address& to,\n                const quic::Address& from) override\n            {\n                log::trace(logcat, \"maybe_hook_dns called\");\n                auto tmp = std::make_shared<Query>(weak_from_this(), query, source, to, from);\n                // no questions, send fail\n                if (query.questions.empty())\n                {\n                    log::debug(logcat, \"dns from {} to {} has empty query questions, sending failure reply\", from, to);\n                    tmp->cancel();\n                    return true;\n                }\n\n                for (const auto& q : query.questions)\n                {\n                    // dont process .loki or .snode\n                    if (q.HasTLD(\".loki\") or q.HasTLD(\".snode\"))\n                    {\n                        log::warning(\n                            logcat,\n                            \"dns from {} to {} is for .loki or .snode but got to the unbound \"\n                            \"resolver, sending \"\n                            \"failure reply\",\n                            from,\n                            to);\n                        tmp->cancel();\n                        return true;\n                    }\n                }\n                if (not m_ctx)\n                {\n                    // we are down\n                    log::debug(\n                        logcat,\n                        \"dns from {} to {} got to the unbound resolver, but the resolver isn't set \"\n                        \"up, \"\n                        \"sending failure reply\",\n                        from,\n                        to);\n                    tmp->cancel();\n                    return true;\n                }\n\n#ifdef _WIN32\n                if (not running)\n                {\n                    // we are stopping the win32 thread\n                    log::debug(\n                        logcat,\n                        \"dns from {} to {} got to the unbound resolver, but the resolver isn't \"\n                        \"running, \"\n                        \"sending failure reply\",\n                        from,\n                        to);\n                    tmp->Cancel();\n                    return true;\n                }\n#endif\n                const auto& q = query.questions[0];\n                if (auto err = ub_resolve_async(\n                        m_ctx, q.Name().c_str(), q.qtype, q.qclass, tmp.get(), &Resolver::callback, nullptr))\n                {\n                    log::warning(logcat, \"failed to send upstream query with libunbound: {}\", ub_strerror(err));\n                    tmp->cancel();\n                }\n                else\n                {\n                    log::trace(logcat, \"dns from {} to {} processing via libunbound\", from, to);\n                    _pending.insert(std::move(tmp));\n                }\n\n                return true;\n            }\n        };\n\n        void Query::send_reply(std::vector<std::byte> data)\n        {\n            log::trace(logcat, \"Query::send_reply called\");\n            if (_done.test_and_set())\n                return;\n\n            auto parent_ptr = parent.lock();\n\n            if (parent_ptr)\n            {\n                parent_ptr->call(\n                    [self = shared_from_this(), parent_ptr = std::move(parent_ptr), data = std::move(data)] {\n                        log::trace(\n                            logcat,\n                            \"forwarding dns response from libunbound to userland (resolverAddr: {}, \"\n                            \"askerAddr: {})\",\n                            self->resolverAddr,\n                            self->askerAddr);\n                        self->src->send_udp(self->askerAddr, self->resolverAddr, data);\n                        // remove query\n                        parent_ptr->remove_pending(self);\n                    });\n            }\n            else\n                log::error(logcat, \"no parent\");\n        }\n    }  // namespace libunbound\n\n    Server::Server(quic::Loop& loop, llarp::DnsConfig conf, unsigned int netif)\n        : _loop{loop}, _conf{std::move(conf)}, _platform{create_platform()}, m_NetIfIndex{std::move(netif)}\n    {}\n\n    std::vector<std::weak_ptr<Resolver_Base>> Server::get_all_resolvers() const\n    {\n        return {_resolvers.begin(), _resolvers.end()};\n    }\n\n    void Server::start()\n    {\n        // set up udp sockets\n        for (const auto& addr : _conf._bind_addrs)\n        {\n            if (auto ptr = make_packet_source_on(addr, _conf))\n                add_packet_source(std::move(ptr));\n        }\n\n        // add default resolver as needed\n        if (auto ptr = make_default_resolver())\n            add_resolver(ptr);\n    }\n\n    std::shared_ptr<I_Platform> Server::create_platform() const\n    {\n        auto plat = std::make_shared<Multi_Platform>();\n        if constexpr (llarp::platform::has_systemd)\n        {\n            plat->add_impl(std::make_unique<SD_Platform_t>());\n            plat->add_impl(std::make_unique<NM_Platform_t>());\n        }\n        return plat;\n    }\n\n    std::shared_ptr<PacketSource> Server::make_packet_source_on(const quic::Address& addr, const llarp::DnsConfig&)\n    {\n        return std::make_shared<UDPReader>(*this, _loop, addr);\n    }\n\n    std::shared_ptr<Resolver_Base> Server::make_default_resolver()\n    {\n        if (_conf._upstream_dns.empty())\n        {\n            log::debug(\n                logcat,\n                \"explicitly no upstream dns providers specified, we will not resolve anything but \"\n                \".loki \"\n                \"and .snode\");\n            return nullptr;\n        }\n\n        return std::make_shared<libunbound::Resolver>(_loop, _conf);\n    }\n\n    std::vector<quic::Address> Server::bound_packet_source_addrs() const\n    {\n        std::vector<quic::Address> addrs;\n\n        for (const auto& src : _packet_sources)\n        {\n            if (auto ptr = src.lock())\n                if (auto maybe_addr = ptr->bound_on())\n                    addrs.emplace_back(*maybe_addr);\n        }\n        return addrs;\n    }\n\n    std::optional<quic::Address> Server::first_bound_packet_source_addr() const\n    {\n        for (const auto& src : _packet_sources)\n        {\n            if (auto ptr = src.lock())\n                if (auto bound = ptr->bound_on())\n                    return bound;\n        }\n        return std::nullopt;\n    }\n\n    void Server::add_resolver(std::weak_ptr<Resolver_Base> resolver) { _resolvers.insert(resolver); }\n\n    void Server::add_resolver(std::shared_ptr<Resolver_Base> resolver)\n    {\n        _owned_resolvers.insert(resolver);\n        add_resolver(std::weak_ptr<Resolver_Base>{resolver});\n    }\n\n    void Server::add_packet_source(std::weak_ptr<PacketSource> pkt) { _packet_sources.push_back(pkt); }\n\n    void Server::add_packet_source(std::shared_ptr<PacketSource> pkt)\n    {\n        add_packet_source(std::weak_ptr<PacketSource>{pkt});\n        _owned_packet_sources.push_back(std::move(pkt));\n    }\n\n    void Server::stop()\n    {\n        for (const auto& resolver : _resolvers)\n        {\n            if (auto ptr = resolver.lock())\n                ptr->down();\n        }\n    }\n\n    void Server::reset()\n    {\n        for (const auto& resolver : _resolvers)\n        {\n            if (auto ptr = resolver.lock())\n                ptr->reset_resolver();\n        }\n    }\n\n    void Server::set_dns_mode(bool all_queries)\n    {\n        if (auto maybe_addr = first_bound_packet_source_addr())\n            _platform->set_resolver(m_NetIfIndex, *maybe_addr, all_queries);\n    }\n\n    bool Server::maybe_handle_payload(\n        const std::shared_ptr<PacketSource>& ptr,\n        const quic::Address& to,\n        const quic::Address& from,\n        std::span<const std::byte> payload)\n    {\n        // dont process to prevent feedback loop\n        if (ptr->would_loop(to, from))\n        {\n            log::warning(logcat, \"preventing dns packet replay to={} from={}\", to, from);\n            return false;\n        }\n\n        auto maybe = maybe_parse_dns_msg(payload);\n        if (not maybe)\n        {\n            log::warning(logcat, \"invalid dns message format from {} to dns listener on {}\", from, to);\n            return false;\n        }\n\n        auto& msg = *maybe;\n        // we don't provide a DoH resolver because it requires verified TLS\n        // TLS needs X509/ASN.1-DER and opting into the Root CA Cabal\n        // thankfully mozilla added a backdoor that allows ISPs to turn it off\n        // so we disable DoH for firefox using mozilla's ISP backdoor\n        // see: https://github.com/oxen-io/lokinet/issues/832\n        for (const auto& q : msg.questions)\n        {\n            // is this firefox looking for their backdoor record?\n            if (q.IsName(\"use-application-dns.net\"))\n            {\n                // yea it is, let's turn off DoH because god is dead.\n                msg.add_nx_reply();\n                // press F to pay respects and send it back where it came from\n                ptr->send_udp(from, to, msg.to_buffer());\n                return true;\n            }\n        }\n\n        if (_resolvers.empty())\n        {\n            log::warning(logcat, \"Trying to resolve DNS query, but we no resolver set up.\");\n            return false;\n        }\n        for (const auto& resolver : _resolvers)\n        {\n            if (auto res_ptr = resolver.lock())\n            {\n                log::trace(logcat, \"check resolver {} for dns from {} to {}\", res_ptr->resolver_name(), from, to);\n                if (res_ptr->maybe_hook_dns(ptr, msg, to, from))\n                {\n                    log::trace(logcat, \"resolver {} handling dns from {} to {}\", res_ptr->resolver_name(), from, to);\n                    return true;\n                }\n            }\n        }\n        return false;\n    }\n\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/server.hpp",
    "content": "#pragma once\n\n#include \"message.hpp\"\n#include \"platform.hpp\"\n\n#include <llarp/config/config.hpp>\n#include <llarp/net/ip_packet.hpp>\n#include <llarp/util/compare_ptr.hpp>\n\n#include <oxen/quic/address.hpp>\n#include <oxen/quic/loop.hpp>\n\n#include <set>\n#include <utility>\n\nnamespace llarp::dns\n{\n    /// a job handling 1 dns query\n    class QueryJob_Base\n    {\n      protected:\n        /// the original dns query\n        Message _query;\n\n        /// True if we've sent a reply (including via a call to cancel)\n        std::atomic_flag _done = ATOMIC_FLAG_INIT;\n\n      public:\n        explicit QueryJob_Base(Message query) : _query{std::move(query)} {}\n\n        virtual ~QueryJob_Base() = default;\n\n        Message& underlying() { return _query; }\n\n        const Message& underlying() const { return _query; }\n\n        /// cancel this operation and inform anyone who cares\n        void cancel();\n\n        /// send a raw buffer back to the querier\n        virtual void send_reply(std::vector<std::byte> buf) = 0;\n    };\n\n    class PacketSource\n    {\n      public:\n        /// stop reading packets and end operation\n        virtual ~PacketSource() = default;\n\n        /// return true if traffic with source and dest addresses would cause a\n        /// loop in resolution and thus should not be sent to query handlers\n        virtual bool would_loop(const quic::Address& to, const quic::Address& from) const = 0;\n\n        /// send UDP payload with src and dst address containing buf on this packet source\n        virtual void send_udp(\n            const quic::Address& to, const quic::Address& from, std::span<const std::byte> payload) const = 0;\n\n        /// returns the sockaddr we are bound on if applicable\n        virtual std::optional<quic::Address> bound_on() const = 0;\n    };\n\n    /// non complex implementation of QueryJob_Base for use in things that\n    /// only ever called on the mainloop thread\n    class QueryJob : public QueryJob_Base, std::enable_shared_from_this<QueryJob>\n    {\n        std::shared_ptr<PacketSource> src;\n        const quic::Address resolver;\n        const quic::Address asker;\n\n      public:\n        explicit QueryJob(\n            std::shared_ptr<PacketSource> source,\n            const Message& query,\n            const quic::Address& to_,\n            const quic::Address& from_)\n            : QueryJob_Base{query}, src{std::move(source)}, resolver{to_}, asker{from_}\n        {}\n\n        void send_reply(std::vector<std::byte> buf) override { src->send_udp(asker, resolver, buf); }\n    };\n\n    /// handler of dns query hooking\n    /// intercepts dns for internal processing\n    class Resolver_Base\n    {\n      protected:\n        /// return the sorting order for this resolver\n        /// lower means it will be tried first\n        virtual int rank() const = 0;\n\n      public:\n        virtual ~Resolver_Base() = default;\n\n        /// less than via rank\n        bool operator<(const Resolver_Base& other) const { return rank() < other.rank(); }\n\n        /// greater than via rank\n        bool operator>(const Resolver_Base& other) const { return rank() > other.rank(); }\n\n        /// get local socket address that queries are sent from\n        virtual std::optional<quic::Address> get_local_addr() const { return std::nullopt; }\n\n        /// get printable name\n        virtual std::string_view resolver_name() const = 0;\n\n        /// reset the resolver state, optionally replace upstream info with new info.  The default\n        /// base implementation does nothing.\n        virtual void reset_resolver(std::optional<std::vector<quic::Address>> = std::nullopt) {}\n\n        /// cancel all pending requests and cease further operation.  Default operation is a no-op.\n        virtual void down() {}\n\n        /// attempt to handle a dns message\n        /// returns true if we consumed this query and it should not be processed again\n        virtual bool maybe_hook_dns(\n            const std::shared_ptr<PacketSource>& source,\n            const Message& query,\n            const quic::Address& to,\n            const quic::Address& from) = 0;\n    };\n\n    // Base class for DNS proxy\n    class Server\n    {\n      protected:\n        /// add a packet source to this server, does share ownership\n        void add_packet_source(std::shared_ptr<PacketSource> resolver);\n        /// add a resolver to this packet handler, does share ownership\n        void add_resolver(std::shared_ptr<Resolver_Base> resolver);\n\n        /// create the platform dependant dns stuff\n        virtual std::shared_ptr<I_Platform> create_platform() const;\n\n      public:\n        virtual ~Server() = default;\n\n        explicit Server(quic::Loop& loop, llarp::DnsConfig conf, unsigned int netif_index);\n\n        /// returns all sockaddr we have from all of our PacketSources\n        std::vector<quic::Address> bound_packet_source_addrs() const;\n\n        /// returns the first sockaddr we have on our packet sources if we have one\n        std::optional<quic::Address> first_bound_packet_source_addr() const;\n\n        /// add a resolver to this packet handler, does not share ownership\n        void add_resolver(std::weak_ptr<Resolver_Base> resolver);\n\n        /// add a packet source to this server, does not share ownership\n        void add_packet_source(std::weak_ptr<PacketSource> resolver);\n\n        /// create a packet source bound on bindaddr but does not add it\n        virtual std::shared_ptr<PacketSource> make_packet_source_on(\n            const quic::Address& bindaddr, const llarp::DnsConfig& conf);\n\n        /// sets up all internal binds and such and begins operation\n        virtual void start();\n\n        /// stops all operation\n        virtual void stop();\n\n        /// reset the internal state\n        virtual void reset();\n\n        /// create the default resolver for out config\n        virtual std::shared_ptr<Resolver_Base> make_default_resolver();\n\n        std::vector<std::weak_ptr<Resolver_Base>> get_all_resolvers() const;\n\n        /// feed a packet buffer from a packet source.\n        /// returns true if we decided to process the packet and consumed it\n        /// returns false if we dont want to process the packet\n        bool maybe_handle_payload(\n            const std::shared_ptr<PacketSource>& pktsource,\n            const quic::Address& resolver,\n            const quic::Address& from,\n            std::span<const std::byte> buf);\n\n        /// set which dns mode we are in.\n        /// true for intercepting all queries. false for just .loki and .snode\n        void set_dns_mode(bool all_queries);\n\n      protected:\n        quic::Loop& _loop;\n        llarp::DnsConfig _conf;\n        std::shared_ptr<I_Platform> _platform;\n\n      private:\n        const unsigned int m_NetIfIndex;\n        // TODO FIXME: this ownership model is cursed.\n        std::set<std::shared_ptr<Resolver_Base>, ComparePtr<std::shared_ptr<Resolver_Base>>> _owned_resolvers;\n        std::set<std::weak_ptr<Resolver_Base>, CompareWeakPtr<Resolver_Base>> _resolvers;\n\n        std::vector<std::shared_ptr<PacketSource>> _owned_packet_sources;\n        std::vector<std::weak_ptr<PacketSource>> _packet_sources;\n    };\n\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/srv_data.cpp",
    "content": "#include \"srv_data.hpp\"\n\n#include <llarp/util/formattable.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/str.hpp>\n\n#include <nlohmann/json.hpp>\n#include <oxenc/bt_serialize.h>\n\nnamespace llarp::dns\n{\n    static auto logcat = log::Cat(\"SRVData\");\n\n    SRVData::SRVData(std::string _proto, uint16_t _priority, uint16_t _weight, uint16_t _port, std::string _target)\n        : service_proto{std::move(_proto)},\n          priority{_priority},\n          weight{_weight},\n          port{_port},\n          target{std::move(_target)}\n    {\n        if (not is_valid())\n            throw std::invalid_argument{\"Invalid SRVData!\"};\n    }\n\n    SRVData::SRVData(oxenc::bt_dict_consumer&& btdc)\n    {\n        try\n        {\n            bt_decode(std::move(btdc));\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"SRVData parsing exception: {}\", e.what());\n        }\n    }\n\n    bool SRVData::is_valid() const\n    {\n        // if target is of first two forms outlined above\n        if (target == \".\" or target.size() == 0)\n        {\n            return true;\n        }\n\n        // check target size is not absurd\n        if (target.size() > TARGET_MAX_SIZE)\n        {\n            log::warning(logcat, \"SRVData target larger than max size ({})\", TARGET_MAX_SIZE);\n            return false;\n        }\n\n        // does target end in .loki?\n        size_t pos = target.find(\".loki\");\n        if (pos != std::string::npos && pos == (target.size() - 5))\n        {\n            return true;\n        }\n\n        // does target end in .snode?\n        pos = target.find(\".snode\");\n        if (pos != std::string::npos && pos == (target.size() - 6))\n        {\n            return true;\n        }\n\n        // if we're here, target is invalid\n        log::warning(logcat, \"SRVData invalid\");\n        return false;\n    }\n\n    bool SRVData::from_string(std::string_view srvString)\n    {\n        log::debug(logcat, \"SRVData::fromString(\\\"{}\\\")\", srvString);\n\n        // split on spaces, discard trailing empty strings\n        auto splits = split(srvString, \" \", false);\n\n        if (splits.size() != 5 && splits.size() != 4)\n        {\n            log::warning(logcat, \"SRV record should have either 4 or 5 space-separated parts\");\n            return false;\n        }\n\n        service_proto = splits[0];\n\n        if (not parse_int(splits[1], priority))\n        {\n            log::warning(logcat, \"SRV record failed to parse \\\"{}\\\" as uint16_t (priority)\", splits[1]);\n            return false;\n        }\n\n        if (not parse_int(splits[2], weight))\n        {\n            log::warning(logcat, \"SRV record failed to parse \\\"{}\\\" as uint16_t (weight)\", splits[2]);\n            return false;\n        }\n\n        if (not parse_int(splits[3], port))\n        {\n            log::warning(logcat, \"SRV record failed to parse \\\"{}\\\" as uint16_t (port)\", splits[3]);\n            return false;\n        }\n\n        if (splits.size() == 5)\n            target = splits[4];\n        else\n            target = \"\";\n\n        return is_valid();\n    }\n\n    void SRVData::bt_encode(oxenc::bt_dict_producer&& btdp) const\n    {\n        btdp.append(\"p\", port);\n        btdp.append(\"s\", service_proto);\n        btdp.append(\"t\", target);\n        btdp.append(\"u\", priority);\n        btdp.append(\"w\", weight);\n    }\n\n    std::string SRVData::bt_encode() const\n    {\n        oxenc::bt_dict_producer btdp;\n        bt_encode(std::move(btdp));\n        return std::move(btdp).str();\n    }\n\n    bool SRVData::bt_decode(std::string buf)\n    {\n        try\n        {\n            return bt_decode(oxenc::bt_dict_consumer{buf});\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"SRVData parsing exception: {}\", e.what());\n            return false;\n        }\n    }\n\n    bool SRVData::bt_decode(oxenc::bt_dict_consumer&& btdc)\n    {\n        try\n        {\n            port = btdc.require<uint16_t>(\"p\");\n            service_proto = btdc.require<std::string>(\"s\");\n            target = btdc.require<std::string>(\"t\");\n            priority = btdc.require<uint16_t>(\"u\");\n            weight = btdc.require<uint16_t>(\"w\");\n\n            return is_valid();\n        }\n        catch (const std::exception& e)\n        {\n            auto err = \"SRVData parsing exception: {}\"_format(e.what());\n            log::warning(logcat, \"{}\", err);\n            throw std::runtime_error{err};\n        }\n    }\n\n    std::optional<SRVData> SRVData::from_srv_string(std::string buf)\n    {\n        if (SRVData ret; ret.from_string(std::move(buf)))\n            return ret;\n\n        return std::nullopt;\n    }\n\n    nlohmann::json SRVData::ExtractStatus() const\n    {\n        return nlohmann::json{\n            {\"proto\", service_proto}, {\"priority\", priority}, {\"weight\", weight}, {\"port\", port}, {\"target\", target}};\n    }\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/dns/srv_data.hpp",
    "content": "#pragma once\n\n#include <nlohmann/json_fwd.hpp>\n#include <oxenc/bt_producer.h>\n#include <oxenc/bt_serialize.h>\n\n#include <string_view>\n#include <tuple>\n\nnamespace llarp::dns\n{\n    inline constexpr size_t TARGET_MAX_SIZE{200};\n\n    using SRVTuple = std::tuple<std::string, uint16_t, uint16_t, uint16_t, std::string>;\n\n    /** SRVData\n\n        bt-encoded keys:\n            'p' : port\n            's' : service protocol\n            't' : target\n            'u' : priority\n            'w' : weight\n    */\n    struct SRVData\n    {\n        SRVData() = default;\n        // SRVData constructor expecting a bt-encoded dictionary\n        SRVData(oxenc::bt_dict_consumer&& btdc);\n        SRVData(std::string _proto, uint16_t _priority, uint16_t _weight, uint16_t _port, std::string _target);\n\n        /* bind-like formatted string for SRV records in config file\n         *\n         * format:\n         *   srv=service.proto priority weight port target\n         *\n         * exactly one space character between parts.\n         *\n         * target can be empty, in which case the space after port should\n         * be omitted.  if this is the case, the target is\n         * interpreted as the .loki or .snode of the current context.\n         *\n         * if target is not empty, it must be either\n         *  - simply a full stop (dot/period) OR\n         *  - a name within the .loki or .snode subdomains. a target\n         *    specified in this manner must not end with a full stop.\n         */\n        static std::optional<SRVData> from_srv_string(std::string buf);\n\n        std::string service_proto;  // service and protocol may as well be together\n\n        uint16_t priority;\n        uint16_t weight;\n        uint16_t port;\n\n        // target string for the SRV record to point to\n        // options:\n        //   empty                     - refer to query name\n        //   dot                       - authoritative \"no such service available\"\n        //   any other .loki or .snode - target is that .loki or .snode\n        std::string target;\n\n        // do some basic validation on the target string\n        // note: this is not a conclusive, regex solution,\n        // but rather some sanity/safety checks\n        bool is_valid() const;\n\n        bool operator==(const SRVData& other) const\n        {\n            return std::tie(service_proto, priority, weight, port, target)\n                == std::tie(other.service_proto, other.priority, other.weight, other.port, other.target);\n        }\n\n        void bt_encode(oxenc::bt_dict_producer&& btdp) const;\n\n        // TESTNET: TODO: remove this after refactoring IntroSet -> ClientContact\n        std::string bt_encode() const;\n\n        bool bt_decode(std::string buf);\n\n        nlohmann::json ExtractStatus() const;\n\n      private:\n        bool bt_decode(oxenc::bt_dict_consumer&& btdc);\n        bool from_string(std::string_view srvString);\n    };\n\n}  // namespace llarp::dns\n\nnamespace std\n{\n    template <>\n    struct hash<llarp::dns::SRVData>\n    {\n        size_t operator()(const llarp::dns::SRVData& data) const noexcept\n        {\n            const std::hash<std::string> h_str{};\n            const std::hash<uint16_t> h_port{};\n            return h_str(data.service_proto) ^ (h_str(data.target) << 3) ^ (h_port(data.priority) << 5)\n                ^ (h_port(data.weight) << 7) ^ (h_port(data.port) << 9);\n        }\n    };\n}  // namespace std\n"
  },
  {
    "path": "llarp/dns/string.hpp",
    "content": "#pragma once\n\n#include <string>\n\nstruct llarp_buffer_t;\n\nnamespace llarp::dns\n{\n    using name_t = std::string;\n\n    /// decode name from buffer\n    bool decode_name(llarp_buffer_t* buf, name_t& name);\n\n    /// encode name to buffer\n    bool encode_name(llarp_buffer_t* buf, const name_t& name);\n\n}  // namespace llarp::dns\n"
  },
  {
    "path": "llarp/ev/fd_poller.cpp",
    "content": "#include \"fd_poller.hpp\"\n\n#include <llarp/util/logging.hpp>\n\n#include <event2/event.h>\n\nnamespace llarp::ev\n{\n\n    static auto logcat = log::Cat(\"ev.fd\");\n\n    FDPoller::FDPoller(oxen::quic::Loop& loop, int fd, std::function<void()> on_readable)\n        : _fd{fd}, _on_readable{std::move(on_readable)}\n    {\n        if (!_on_readable)\n            throw std::invalid_argument{\"FDPoller requires non-null on_readable function\"};\n        _ev.reset(event_new(\n            loop.get_event_base(),\n            _fd,\n            EV_READ | EV_PERSIST,\n            [](evutil_socket_t, short, void* s) {\n                assert(s);\n                auto* self = static_cast<FDPoller*>(s);\n                try\n                {\n                    self->_on_readable();\n                }\n                catch (const std::exception& e)\n                {\n                    log::error(logcat, \"FDPoller callback raised uncaught exception: {}\", e.what());\n                }\n            },\n            this));\n        if (!_ev || 0 != event_add(_ev.get(), nullptr))\n            throw std::invalid_argument{\"Failed to create libevent event!\"};\n\n        log::debug(logcat, \"FD poller watching fd {}\", _fd);\n    }\n}  // namespace llarp::ev\n"
  },
  {
    "path": "llarp/ev/fd_poller.hpp",
    "content": "#pragma once\n#include <oxen/quic/loop.hpp>\n\nnamespace llarp::ev\n{\n    /** FDPoller\n     *\n     * This poller watches a file descriptor for readability, triggering when the fd becomes\n     * readable.\n     */\n    class FDPoller\n    {\n      public:\n        // No move/copy/etc\n        FDPoller() = delete;\n        FDPoller(const FDPoller&) = delete;\n        FDPoller(FDPoller&&) = delete;\n        FDPoller& operator=(const FDPoller&) = delete;\n        FDPoller& operator=(FDPoller&&) = delete;\n\n        // Starts a file decriptor poller that watches the given file descriptor via the given event\n        // loop.  Caller must ensure that the loop remains valid for the lifetime of the constructed\n        // FDPoller.\n        FDPoller(oxen::quic::Loop& loop, int fd, std::function<void()> on_readable);\n\n      private:\n        int _fd;\n        oxen::quic::event_ptr _ev;\n        std::function<void()> _on_readable;\n    };\n\n}  // namespace llarp::ev\n"
  },
  {
    "path": "llarp/ev/tcp.cpp",
    "content": "#include \"tcp.hpp\"\n\n#include <llarp/net/ip_packet.hpp>\n#include <llarp/util/logging/buffer.hpp>\n\nnamespace llarp\n{\n    static auto logcat = oxen::log::Cat(\"ev-tcp\");\n\n    constexpr auto evconnlistener_deleter = [](::evconnlistener *e) {\n        log::trace(logcat, \"Invoking evconnlistener deleter!\");\n        if (e)\n            evconnlistener_free(e);\n    };\n\n    /// Checks rv for being -1 and, if so, raises a system_error from errno.  Otherwise returns it.\n    static int check_rv(int rv)\n    {\n#ifdef _WIN32\n        if (rv == SOCKET_ERROR)\n            throw std::system_error{WSAGetLastError(), std::system_category()};\n#else\n        if (rv == -1)\n            throw std::system_error{errno, std::system_category()};\n#endif\n        return rv;\n    }\n\n    static void tcp_read_cb(bufferevent *bev, void *user_arg)\n    {\n        std::vector<uint8_t> buf{};\n        buf.resize(2048);\n\n        // Load data from input buffer to local buffer\n        // FIXME: handle nwrite == 0\n        auto nwrite = bufferevent_read(bev, buf.data(), buf.size());\n        buf.resize(nwrite);\n\n        log::trace(logcat, \"TCP socket received {}B: {}\", nwrite, buffer_printer{buf});\n\n        auto *conn = reinterpret_cast<TCPConnection *>(user_arg);\n        assert(conn);\n\n        conn->stream->send(std::move(buf));\n    };\n\n    static void tcp_write_cb([[maybe_unused]] bufferevent *bev, void *user_arg)\n    {\n        auto *conn = reinterpret_cast<TCPConnection *>(user_arg);\n        conn->on_write_available();\n    }\n\n    void TCPConnection::on_write_available()\n    {\n        log::debug(logcat, \"TCP Tunnel connection, write to local conn was blocked but is now available.\");\n        stream->resume();\n    }\n\n    static void tcp_event_cb(bufferevent *bev, short what, void *user_arg)\n    {\n        (void)bev;\n        (void)user_arg;\n        auto *conn = reinterpret_cast<TCPConnection *>(user_arg);\n        assert(conn);\n\n        log::log(\n            logcat,\n            what & BEV_EVENT_ERROR ? log::Level::err : log::Level::debug,\n            \"TCP Connection {} event: {}\",\n            what & BEV_EVENT_READING ? \"READ\" : \"WRITE\",\n            what & BEV_EVENT_EOF             ? \"EOF\"\n                : what & BEV_EVENT_ERROR     ? \"ERROR\"\n                : what & BEV_EVENT_TIMEOUT   ? \"TIMEOUT\"\n                : what & BEV_EVENT_CONNECTED ? \"CONNECTED\"\n                                             : \"IMPOSSIBLE\");\n\n        // this is where the InboundSession confirms it established a TCP connection to the backend app\n        if (what & BEV_EVENT_CONNECTED)\n        {\n            log::info(logcat, \"TCP connect operation finished!\");\n            conn->stream->resume();\n        }\n        if (what & BEV_EVENT_ERROR)\n        {\n            log::critical(logcat, \"TCP Connection encountered error from bufferevent\");\n        }\n        if (what & (BEV_EVENT_EOF | BEV_EVENT_ERROR))\n        {\n            log::debug(logcat, \"TCP Connection closing tunneled QUIC stream\");\n\n            conn->stream->close();\n        }\n    };\n\n    static void tcp_listen_cb(\n        struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *src, int socklen, void *user_arg)\n    {\n        quic::Address source{src, static_cast<socklen_t>(socklen)};\n        log::debug(logcat, \"TCP RECEIVED -- SRC:{}\", source);\n\n        auto *b = evconnlistener_get_base(listener);\n        auto *bevent = bufferevent_socket_new(b, fd, BEV_OPT_CLOSE_ON_FREE | BEV_OPT_THREADSAFE);\n\n        auto *handle = reinterpret_cast<TCPHandle *>(user_arg);\n        assert(handle);\n\n        // make TCPConnection here!\n        auto *conn = handle->_conn_maker(bevent, fd);\n\n        bufferevent_setcb(bevent, tcp_read_cb, tcp_write_cb, tcp_event_cb, conn);\n        bufferevent_enable(bevent, EV_READ | EV_WRITE);\n    };\n\n    static void tcp_err_cb(struct evconnlistener * /* e */, void *user_arg)\n    {\n        int ec = EVUTIL_SOCKET_ERROR();\n        log::critical(logcat, \"TCP LISTENER RECEIVED ERROR CODE {}:{}\", ec, evutil_socket_error_to_string(ec));\n\n        auto *handle = reinterpret_cast<TCPHandle *>(user_arg);\n        assert(handle);\n        (void)handle;\n\n        // DISCUSS: close everything here?\n    };\n\n    TCPConnection::TCPConnection(bufferevent *_bev, evutil_socket_t _fd, std::shared_ptr<quic::Stream> s)\n        : bev{_bev}, fd{_fd}, stream{std::move(s)}\n    {\n        stream->set_data_callback([this, _bev](quic::Stream &stream, std::span<const std::byte> data) mutable {\n            // libquic FIXME: would be convenient to be able to ask the stream if it's inbound or outbound\n            // here since this callback is used for both and that would make logging more clear.\n            if (stream.is_paused())\n            {\n                // FIXME: C++23 makes this syntax nicer\n                pending_buffer.insert(pending_buffer.end(), data.begin(), data.end());\n                return;\n            }\n            std::byte *cur = pending_buffer.data();\n            size_t written = 0;\n            while (written < pending_buffer.size())\n            {\n                constexpr size_t chunk_size = 1500;  // FIXME: this, obviously; ass number\n                size_t s = std::min(chunk_size, pending_buffer.size() - written);\n                if (bufferevent_write(_bev, cur, s) != 0)\n                {\n                    // FIXME: if hypothetically quic/lokinet stream is finished sending,\n                    // so this is the last call of this callback, but socket is blocked, not\n                    // letting us write the last chunk(s) buffered, what to do?\n                    break;\n                }\n                written += s;\n                cur += s;\n            }\n            if (written < pending_buffer.size())\n            {\n                log::debug(logcat, \"TCP Tunnel stream unpaused, but we failed to write all queued data.\");\n                size_t new_size = pending_buffer.size() - written;\n                std::memmove(pending_buffer.data(), pending_buffer.data() + written, new_size);\n                pending_buffer.resize(new_size);\n                pending_buffer.insert(pending_buffer.end(), data.begin(), data.end());\n            }\n            if (written == pending_buffer.size())\n                pending_buffer.clear();\n            else\n            {  // we got unpaused, but clogged the (kernel?) buffer again, pause again\n                stream.pause();\n                pending_buffer.insert(pending_buffer.end(), data.begin(), data.end());\n                return;\n            }\n\n            if (auto rv = bufferevent_write(_bev, data.data(), data.size()); rv != 0)\n            {\n                log::debug(logcat, \"TCP Tunnel refused write, pausing quic stream and buffering.\");\n                pending_buffer.resize(data.size());\n                std::memcpy(pending_buffer.data(), data.data(), data.size());\n                return;\n            }\n\n            log::debug(logcat, \"Stream (id:{}) wrote {}B to TCP buffer\", stream.stream_id(), data.size());\n        });\n    }\n\n    void TCPConnection::stop_reading() { bufferevent_disable(bev, EV_READ); }\n\n    void TCPConnection::resume_reading() { bufferevent_enable(bev, EV_READ); }\n\n    TCPConnection::~TCPConnection()\n    {\n        bufferevent_free(bev);\n        log::debug(logcat, \"TCPSocket shut down!\");\n    }\n\n    void TCPConnection::close(uint64_t ec)\n    {\n        log::info(logcat, \"TCP connection closing with application error code: {}\", ec);\n    }\n\n    std::shared_ptr<TCPHandle> TCPHandle::make_server(quic::Loop &ev, tcpconn_hook cb, uint16_t port)\n    {\n        std::shared_ptr<TCPHandle> h{new TCPHandle(ev, std::move(cb), port)};\n        return h;\n    }\n\n    std::shared_ptr<TCPHandle> TCPHandle::make_client(quic::Loop &ev, quic::Address connect)\n    {\n        std::shared_ptr<TCPHandle> h{new TCPHandle{ev, std::move(connect)}};\n        return h;\n    }\n\n    TCPHandle::TCPHandle(quic::Loop &ev_loop, quic::Address connect) : _ev{ev_loop}, _connect{std::move(connect)} {}\n\n    TCPHandle::TCPHandle(quic::Loop &ev_loop, tcpconn_hook cb, uint16_t p) : _ev{ev_loop}, _conn_maker{std::move(cb)}\n    {\n        if (!_conn_maker)\n            throw std::logic_error{\"TCPSocket construction requires a non-empty receive callback\"};\n\n        _init_server(p);\n    }\n\n    std::shared_ptr<TCPConnection> TCPHandle::connect(\n        event_base *_ev, quic::Address src, std::shared_ptr<quic::Stream> s, uint16_t port)\n    {\n        sockaddr_in _addr = src.in4();\n        _addr.sin_port = htonl(port);\n\n        // NB: BEV_OPT_THREADSAFE not used because this should only ever be touched\n        // by a single thread.\n        bufferevent *_bev = bufferevent_socket_new(_ev, -1, BEV_OPT_CLOSE_ON_FREE);\n\n        if (bufferevent_socket_connect(_bev, (struct sockaddr *)&_addr, sizeof(_addr)) < 0)\n        {\n            log::warning(logcat, \"Failed to make bufferevent-based TCP connection!\");\n            return nullptr;\n        }\n\n        auto tcp_conn = std::make_shared<TCPConnection>(_bev, -1, std::move(s));\n\n        // only set after a call to bufferevent_socket_connect\n        tcp_conn->fd = bufferevent_getfd(_bev);\n\n        return tcp_conn;\n    }\n\n    void TCPHandle::_init_client() {}\n\n    void TCPHandle::_init_server(uint16_t port)\n    {\n        sockaddr_in _tcp{};\n        _tcp.sin_family = AF_INET;\n        _tcp.sin_addr.s_addr = INADDR_ANY;\n        _tcp.sin_port = htonl(port);\n\n        _tcp_listener = _ev.template shared_ptr<struct evconnlistener>(\n            evconnlistener_new_bind(\n                _ev.get_event_base(),\n                tcp_listen_cb,\n                this,\n                LEV_OPT_CLOSE_ON_FREE | LEV_OPT_THREADSAFE | LEV_OPT_REUSEABLE,\n                -1,\n                reinterpret_cast<sockaddr *>(&_tcp),\n                sizeof(sockaddr)),\n            evconnlistener_deleter);\n\n        if (not _tcp_listener)\n        {\n            throw std::runtime_error{\n                \"TCP listener construction failed: {}\"_format(evutil_socket_error_to_string(EVUTIL_SOCKET_ERROR()))};\n        }\n\n        _sock = evconnlistener_get_fd(_tcp_listener.get());\n        check_rv(getsockname(_sock, _bound, _bound.socklen_ptr()));\n        log::debug(logcat, \"tcp listener, bound to {}\", _bound);\n        evconnlistener_set_error_cb(_tcp_listener.get(), tcp_err_cb);\n    }\n\n    TCPHandle::~TCPHandle()\n    {\n        _tcp_listener.reset();\n        log::debug(logcat, \"TCPHandle shut down!\");\n    }\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/ev/tcp.hpp",
    "content": "#pragma once\n\n#include <llarp/util/buffer.hpp>\n#include <llarp/util/logging.hpp>\n\n#include <oxen/quic/loop.hpp>\n#include <oxen/quic/stream.hpp>\n\nextern \"C\"\n{\n#include <arpa/inet.h>\n#include <event2/buffer.h>\n#include <event2/bufferevent.h>\n#include <event2/listener.h>\n}\n\nnamespace llarp\n{\n    namespace quic = oxen::quic;\n\n    class QUICTunnel;\n\n    struct TCPConnection\n    {\n        TCPConnection(bufferevent* _bev, evutil_socket_t _fd, std::shared_ptr<quic::Stream> _s);\n\n        TCPConnection() = delete;\n\n        /// Non-copyable and non-moveable\n        TCPConnection(const TCPConnection& s) = delete;\n        TCPConnection& operator=(const TCPConnection& s) = delete;\n        TCPConnection(TCPConnection&& s) = delete;\n        TCPConnection& operator=(TCPConnection&& s) = delete;\n\n        ~TCPConnection();\n\n        bufferevent* bev;\n        evutil_socket_t fd;\n\n        std::shared_ptr<quic::Stream> stream;\n\n        std::vector<std::byte> pending_buffer;\n\n        void on_stream_data(quic::Stream& stream, std::span<const std::byte> data);\n\n        void close(uint64_t ec = 0);\n\n        void on_write_available();\n\n        void stop_reading();\n        void resume_reading();\n    };\n\n    using tcpconn_hook = std::function<TCPConnection*(bufferevent*, evutil_socket_t)>;\n\n    class TCPHandle\n    {\n        using socket_t =\n#ifndef _WIN32\n            int\n#else\n            SOCKET\n#endif\n            ;\n\n        quic::Loop& _ev;\n        std::shared_ptr<::evconnlistener> _tcp_listener;\n\n        // The OutboundSession will set up an evconnlistener and set the listening socket address inside ::_bound\n        quic::Address _bound{};\n\n        // The InboundSession will set this address to the lokinet-primary-ip to connect to\n        std::optional<quic::Address> _connect = std::nullopt;\n\n        socket_t _sock;\n\n        explicit TCPHandle(quic::Loop& ev, tcpconn_hook cb, uint16_t p);\n\n        explicit TCPHandle(quic::Loop& ev, quic::Address connect);\n\n      public:\n        TCPHandle() = delete;\n\n        tcpconn_hook _conn_maker;\n\n        // The OutboundSession object will hold a server listening on some localhost:port, returning that port to the\n        // application for it to make a TCP connection\n        static std::shared_ptr<TCPHandle> make_server(quic::Loop& ev, tcpconn_hook cb, uint16_t port = 0);\n\n        // The InboundSession object will hold a client that connects to some application configured\n        // lokinet-primary-ip:port every time the OutboundSession opens a new stream over the tunneled connection\n        static std::shared_ptr<TCPHandle> make_client(quic::Loop& ev, quic::Address connect);\n\n        ~TCPHandle();\n\n        uint16_t port() const { return _bound.port(); }\n\n        const quic::Address& bind_address() const { return _bound; }\n\n        static std::shared_ptr<TCPConnection> connect(\n            event_base* _ev, quic::Address src, std::shared_ptr<quic::Stream> s, uint16_t port);\n\n      private:\n        void _init_client();\n\n        void _init_server(uint16_t port);\n    };\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/ev/udp.cpp",
    "content": "#include \"udp.hpp\"\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"ev-udp\");\n\n    inline constexpr size_t MAX_BATCH =\n#if defined(OXEN_LIBQUIC_UDP_SENDMMSG) || defined(OXEN_LIBQUIC_UDP_GSO)\n        24;\n#else\n        1;\n#endif\n\n    UDPHandle::UDPHandle(const std::shared_ptr<quic::Loop>& ev, const quic::Address& bind, net_pkt_hook cb) : _loop{ev}\n    {\n        socket = std::make_unique<UDPSocket>(ev->get_event_base(), bind, std::move(cb));\n        _local = socket->address();\n    }\n\n    UDPHandle::~UDPHandle() { socket.reset(); }\n\n    io_result UDPHandle::_send_impl(const quic::Path& path, std::byte* buf, size_t size, uint8_t ecn, size_t& n_pkts)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        auto* bufsize = &size;\n\n        if (!socket)\n        {\n            log::warning(logcat, \"Cannot send packets on closed socket ({})\", path);\n            return io_result{EBADF};\n        }\n\n        assert(n_pkts >= 1 && n_pkts <= MAX_BATCH);\n\n        log::trace(logcat, \"Sending {} UDP packet(s) {}...\", n_pkts, path);\n\n        auto [ret, sent] = socket->send(path, buf, bufsize, ecn, n_pkts);\n\n        if (ret.failure() && !ret.blocked())\n        {\n            log::error(logcat, \"Error sending packets {}: {}\", path, ret.str_error());\n            n_pkts = 0;  // Drop any packets, as we had a serious error\n            return ret;\n        }\n\n        if (sent < n_pkts)\n        {\n            if (sent == 0)  // Didn't send *any* packets, i.e. we got entirely blocked\n                log::debug(logcat, \"UDP sent none of {}\", n_pkts);\n\n            else\n            {\n                // We sent some but not all, so shift the unsent packets back to the beginning of buf/bufsize\n                log::debug(logcat, \"UDP undersent {}/{}\", sent, n_pkts);\n                size_t offset = std::accumulate(bufsize, bufsize + sent, size_t{0});\n                size_t len = std::accumulate(bufsize + sent, bufsize + n_pkts, size_t{0});\n                std::memmove(buf, buf + offset, len);\n                std::copy(bufsize + sent, bufsize + n_pkts, bufsize);\n                n_pkts -= sent;\n            }\n\n            // We always return EAGAIN (so that .blocked() is true) if we failed to send all, even\n            // if that isn't strictly what we got back as the return value (sendmmsg gives back a\n            // non-error on *partial* success).\n            return io_result{EAGAIN};\n        }\n\n        n_pkts = 0;\n\n        return ret;\n    }\n\n    void UDPHandle::_send_or_queue(\n        const quic::Path& path, std::vector<std::byte> buf, uint8_t ecn, std::function<void(io_result)> callback)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        if (!socket)\n        {\n            log::warning(logcat, \"Cannot sent to dead socket for path {}\", path);\n            if (callback)\n                callback(io_result{EBADF});\n            return;\n        }\n\n        size_t n_pkts = 1;\n        // size_t bufsize = buf.size();\n        auto res = _send_impl(path, buf.data(), buf.size(), ecn, n_pkts);\n\n        if (res.blocked())\n        {\n            socket->when_writeable([this, path, buf = std::move(buf), ecn, cb = std::move(callback)]() mutable {\n                _send_or_queue(path, std::move(buf), ecn, std::move(cb));\n            });\n        }\n        else if (callback)\n            callback({});\n    }\n\n    io_result UDPHandle::send(const quic::Address& dest, std::span<const std::byte> data)\n    {\n        return _send_impl(quic::Path{_local, dest}, data.data(), data.size(), 0);\n    }\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/ev/udp.hpp",
    "content": "#pragma once\n\n#include <llarp/net/ip_packet.hpp>\n#include <llarp/util/buffer.hpp>\n#include <llarp/util/logging.hpp>\n\n#include <oxen/quic/address.hpp>\n#include <oxen/quic/loop.hpp>\n#include <oxen/quic/udp.hpp>\n\nnamespace llarp\n{\n    using UDPSocket = quic::UDPSocket;\n\n    using io_result = quic::io_result;\n\n    class UDPHandle\n    {\n      public:\n        UDPHandle() = delete;\n        explicit UDPHandle(const std::shared_ptr<quic::Loop>& ev, const quic::Address& bind, net_pkt_hook cb);\n        ~UDPHandle();\n\n      private:\n        std::shared_ptr<quic::Loop> _loop;\n        std::unique_ptr<UDPSocket> socket;\n        quic::Address _local;\n\n        void _send_or_queue(\n            const quic::Path& path,\n            std::vector<std::byte> buf,\n            uint8_t ecn,\n            std::function<void(io_result)> callback = nullptr);\n\n      public:\n        io_result send(const quic::Address& dest, std::span<const std::byte> data);\n\n        quic::Address bind() { return _local; }\n    };\n\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/handlers/session.cpp",
    "content": "#include \"session.hpp\"\n\n#include <llarp/constants/path.hpp>\n#include <llarp/contact/contactdb.hpp>\n#include <llarp/contact/relay_contact.hpp>\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/link/endpoint.hpp>\n#include <llarp/messages/dht.hpp>\n#include <llarp/messages/fetch.hpp>\n#include <llarp/messages/path.hpp>\n#include <llarp/messages/session.hpp>\n#include <llarp/nodedb.hpp>\n#include <llarp/path/path.hpp>\n#include <llarp/path/transit_hop.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/session/session.hpp>\n#include <llarp/util/bspan.hpp>\n#include <llarp/util/random.hpp>\n#include <llarp/util/time.hpp>\n\n#include <oxenc/base32z.h>\n\n#include <memory>\n#include <random>\n\nnamespace llarp::handlers\n{\n    static auto logcat = log::Cat(\"session_ep\");\n\n    SessionEndpoint::SessionEndpoint(Router& r)\n        : path::\n              PathHandler{r, r.config().paths.inbound_paths + r.config().paths.inbound_paths_extra, r.config().paths.inbound_hops()},\n          cc_blind_keys{r.secret_key(), crypto::blinding::CLIENT_CONTACT}\n    {\n        const auto& netconf = router.config().network;\n\n        _auth_tokens = netconf.exit_auths;\n\n        // *All* clients currently support speaking via QUIC tunnel:\n        protocols = protocol_flag::QUIC_TUNNEL;\n        if (!router.embedded())\n        {\n            // raw IPv4/IPv6/exit traffic all require a full tun interface.\n\n            protocols = protocol_flag::IPV4;\n            if (netconf.enable_ipv6)\n                protocols |= protocol_flag::IPV6;\n            if (router.is_exit_node())\n                protocols |= protocol_flag::EXIT;\n        }\n\n        client_contact =\n            ClientContact{router.key_manager.router_id(), netconf.srv_records, protocols, netconf.traffic_policy};\n    }\n\n    std::array<int, 5> SessionEndpoint::session_stats() const\n    {\n        std::array<int, 5> stats{0};\n        auto& [in, out_r, out_c, out_r_pending, out_c_pending] = stats;\n\n        for (const auto& s : std::views::values(_sessions))\n        {\n            if (s->is_closed())\n                continue;\n            if (s->is_outbound)\n            {\n                if (s->is_relay_session)\n                {\n                    out_r++;\n                    if (!s->is_established())\n                        out_r_pending++;\n                }\n                else\n                {\n                    out_c++;\n                    if (!s->is_established())\n                        out_c_pending++;\n                }\n            }\n            else\n                in++;\n        }\n\n        return stats;\n    }\n\n    std::array<int, 3> SessionEndpoint::path_stats(std::chrono::milliseconds now) const\n    {\n        std::array<int, 3> stats{0};\n        auto& [in, out_r, out_c] = stats;\n        in = num_paths();\n\n        for (const auto& s : std::views::values(_sessions))\n            if (!s->is_closed() && s->is_outbound)\n            {\n                auto& os = static_cast<const session::OutboundSession&>(*s);\n                if (os.is_relay_session)\n                    out_r += os.num_paths(now);\n                else\n                    out_c += os.num_paths(now);\n            }\n\n        return stats;\n    }\n\n    void SessionEndpoint::close_session(std::shared_ptr<session::Session>& s, bool send_close)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        if (!s)\n            return;\n\n        s->close(send_close);\n\n        const auto& remote = s->remote();\n        if (auto& tun = router.tun_endpoint())\n            tun->unmap(remote);\n\n        if (auto it = _sessions.find(remote); it != _sessions.end())\n        {\n            if (auto& s = it->second)\n                _session_tags.erase(s->inbound_tag());\n            _sessions.erase(it);\n        }\n    }\n\n    bool SessionEndpoint::close_session(NetworkAddress remote, bool send_close)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        if (auto it = _sessions.find(remote); it != _sessions.end())\n        {\n            close_session(it->second, send_close);\n            return true;\n        }\n\n        log::warning(logcat, \"Could not find session (remote:{}) to close!\", remote);\n        return false;\n    }\n\n    bool SessionEndpoint::close_session(session_tag t, bool send_close)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        if (auto it = _session_tags.find(t); it != _session_tags.end())\n        {\n            close_session(it->second, send_close);\n            return true;\n        }\n\n        log::warning(logcat, \"Could not find session (tag:{}) to close!\", t);\n        return false;\n    }\n\n    void SessionEndpoint::tick(std::chrono::milliseconds now)\n    {\n        log::trace(logcat, \"SessionEndpoint ticking sessions...\");\n        for (const auto& [addr, session] : _sessions)\n            session->tick(now);\n\n        path::PathHandler::tick(now);\n    }\n\n    void SessionEndpoint::stop(bool send_close)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        _running = false;\n\n        if (_path_rotater)\n        {\n            _path_rotater.reset();\n            log::trace(logcat, \"Path rotation ticker stopped!\");\n        }\n\n        // Do a best-effort close; if send_close is true these close(true) calls should queue a\n        // path_close on the active stream, even though we immediately drop the streams below, which\n        // should still typically arrive at the other side.\n        for (auto& s : std::views::values(_sessions))\n            s->close(send_close);\n\n        _sessions.clear();\n        _session_tags.clear();\n\n        path::PathHandler::stop();\n    }\n\n    void SessionEndpoint::cleanup_old_fuzz(int oldest_slot)\n    {\n        if (auto it = _slot_fuzz.lower_bound(oldest_slot); it != _slot_fuzz.end() && it != _slot_fuzz.begin())\n            _slot_fuzz.erase(_slot_fuzz.begin(), it);\n    }\n\n    std::chrono::seconds SessionEndpoint::inbound_path_fuzz(int slot)\n    {\n        auto it = _slot_fuzz.lower_bound(slot);\n        if (it != _slot_fuzz.end() && it->first == slot)\n            return it->second;\n\n        // Note that this fuzz must not be negative!  If we allowed negative fuzz then a path could\n        // expire *before* its slot expired, and as a result we would try to build a new very short\n        // path to make up for the expired slot.\n        std::normal_distribution<float> dist{0, path::MAX_LIFETIME_FUZZ.count() / 2.575829f};\n        std::chrono::seconds fuzz;\n        do\n        {\n            fuzz = std::chrono::seconds{static_cast<int>(dist(csrng))};\n            if (fuzz < 0s)\n                fuzz = -fuzz;\n        } while (fuzz > path::MAX_LIFETIME_FUZZ);\n\n        _slot_fuzz.emplace_hint(it, slot, fuzz);\n\n        return fuzz;\n    }\n\n    void SessionEndpoint::update_paths(std::chrono::milliseconds now)\n    {\n        int have = num_paths(now);\n        // If you ask for more than 10 inbound paths (which is only possible via an undocumented\n        // option) and more than the number of RCs we know about then silently cut off at the number\n        // of RCs we know about (or a multiple of those, if pivot reuse is allowed) to avoid seeing\n        // a warning about not being able to select new pivots every 250ms.\n        int max_paths = router.node_db().num_rcs() * router.config().paths.inbound_pivot_reuse;\n        int needed = (_target_paths > 10 && _target_paths > max_paths ? max_paths : _target_paths) - have;\n        if (needed <= 0)\n        {\n            log::trace(\n                logcat,\n                \"SessionEndpoint doesn't need more paths right now (have {} >= target {})\",\n                have,\n                _target_paths);\n            return;\n        }\n\n        if (cooldown())\n        {\n            log::debug(\n                logcat,\n                \"SessionEndpoint needs {} more paths (to reach target {}), but path builds are currently in cooldown \"\n                \"because the last {} path builds failed\",\n                needed,\n                _target_paths,\n                _consecutive_failures);\n            return;\n        }\n\n        log::debug(\n            logcat,\n            \"SessionEndpoint building {} additional paths to random remotes to reach target of {} paths\",\n            needed,\n            _target_paths);\n\n        // If we *don't* have distinct IP ranges from our current edges (e.g. by random chance, or\n        // with only a single edge, or just because that's how the pinned edges pan out) then we\n        // want to exclude the random terminus that we choose to exclude that singleton range from\n        // being the terminus: because otherwise we can end up in a situation where it is impossible\n        // to respect the distinct-ip-range setting because once we have selected a pivot, and feed\n        // it into PathHandler::select_hops_to_remote, it has no choice but to use that pivot *and*\n        // the edge, and those conflict.\n        //\n        // (If we have multiple ranges for edges then it's not an issue because whichever pivot we\n        // select will have at least one available edge not in its range).\n        //\n        // So, if we're in that only-one-edge-ip-range case, we apply the edge exclusion back here at\n        // terminus selection so that our selection here doesn't force select_hops_to_remote into\n        // that situation.\n\n        auto unique_edge_range = router.link_endpoint().unique_edge_range();\n\n        auto filter = [this, &unique_edge_range](const RelayContact& rc) {\n            if (unique_edge_range and unique_edge_range->contains(rc.addr().to_ipv4()))\n                return false;\n\n            // Exclude any inbound pivots we are already using (or using more than\n            // inbound_pivot_reuse times, if that option is higher than 1) so that we diversify:\n            int count = 0;\n            const auto& rid = rc.router_id();\n            for (const auto& p : paths())\n                if (p.terminal_rid() == rid)\n                    if (++count >= router.config().paths.inbound_pivot_reuse)\n                        return false;\n\n            return not router.router_profiling().is_bad_for_path(rid, 1);\n        };\n\n        // Path lifetime selection\n        // -----------------------\n        //\n        // We want our CC path expiries to be spread out temporally because if we have them\n        // clustered together, we can end up in a situation where all our inbound paths expire at\n        // the same time, and so someone connected to us cannot stay connected, because they have\n        // no other paths to rotate to.\n        //\n        // To solve this, we try to ensure that paths are always spread out across expiries.  For\n        // example, with 4 target inbound paths, we ideally want paths that expire at\n        //\n        // [P1: t₀+5min, P2: t₀+10min, P3: t₀+15min, P4: t₀+20min]\n        //\n        // and when we reach t₁ = t₀+5min, the first expires, and we build a new one for t₁+20min\n        // (== t₀ + 25min), thus ending up with:\n        //\n        // [P2: t₁+5min, P3: t₁+10min, P4: t₁+15min, P5: t₁+20min]\n        //\n        // and so on over time.\n        //\n        // The problem, however, is that paths can die.  Suppose, for instance, that P4 dies at\n        // t₂ = t₁+1min.  That means just before processing the path death we have:\n        //\n        // [P2: t₂+4min, P3: t₂+9min, P4: t₂+14min, P5: t₂+19min]\n        //\n        // and then after processing we have:\n        //\n        // [P2: t₂+4min, P3: t₂+9min, P5: t₂+19min]\n        //\n        // If we construct a new, full-lifetime path at this point we'll have:\n        //\n        // [P2: t₂+4min, P3: t₂+9min, P5: t₂+19min, P6: t₂+20min]\n        //\n        // which is okay for now, but will lead to us having a perpetual 1min gap between P5 and P6\n        // (and then also between the P9 and P10 replacements when P5/6 expire), and a 9min gap\n        // between P4/P5 (and between its replacements).\n        //\n        // Worse, if all your paths die at once (for instance, because your local internet died\n        // temporarily) you would rebuild all 4 paths at once, and could end up with all expiring at\n        // the same time.\n        //\n        // So it isn't enough to just spread them out initially: we have to create new paths with a\n        // lifetime that slots them into the expiry time the path they are replacing would have had.\n        // E.g. rather than the above lumpy distribution, we want the paths with P6 to look like:\n        //\n        // [P2: t₂+4min, P3: t₂+9min, P6: t₂+14min, P5: t₂+19min]\n        //\n        // so that \"re-slotting\" the path with the earlier timestamp keeps the distribution nice and\n        // spread out, preventing unwanted clustering of expiries.\n        //\n        // For 2 or 3 paths, we space things out proportionally, e.g.:\n        //\n        // 2: [P1: t+10m, P2: t+20m]\n        // 3: [P1: t+6m40s, P2: t+13m20s, P3: t+20m]\n        //\n        // Beyond 4, we use the same 5m spacing as with 4, but double up some slots.  For example,\n        // with 7 paths:\n        //\n        // 7: [P1: t+5m, P2&P5: t+10m, P3&P6: t+15m, P4&P7: t+20m]\n        //\n        // This is somewhat lopsided for non-multiples of 4, but there's still lots of spread in\n        // there so that even with multiple paths expiring at the same time, there are still lots of\n        // alternatives for remotes to switch to.\n        //\n        // Note that all of the above ignores \"fuzz\", i.e. each path has a small random amount of\n        // lifetime (well less than 5min) added to it to reduce the fingerprintability of path build\n        // expiries.  All of the above still holds with respect to slots, it's just that where we\n        // write \"+Nm\" it's actually \"+Nm+fuzz[0,3m]\".\n\n        std::vector<std::chrono::seconds> expiries;\n        expiries.reserve(needed);\n        {\n            const int slots = std::min(_target_paths, path::MAX_LIFETIME_SLOTS);\n\n            // Our expiry slot size.  Generally 5min, but longer if you use fewer than 4 paths:\n            const std::chrono::seconds slot_size = path::MAX_LIFETIME / slots;\n            assert(path::MAX_LIFETIME % slots == 0s);\n\n            std::array<int, path::MAX_LIFETIME_SLOTS> slot_count = {0};\n\n            // The base slot, measured in multiples of `slot_size` relative to our fixed basis: we\n            // consider other path expiries relative to this base slot.\n            //\n            // The +1 here is because (now-basis)/slot_size (i.e. without the +1) is going to give\n            // us a slot index that translates to a slot start time in the past (i.e. 0-5min ago),\n            // but we don't build for that slot: instead we build for slots at +5m, +10m, +15m, +20m\n            // from that now-or-earlier point.  Thus +1 brings us up to the first slot position\n            // within the next [0-5min], and that is our \"slot0\" value, i.e. the index 0 slot of all\n            // slots we consider building for.\n            //\n            // There is an argument to be made to not build new paths that would only have a\n            // duration of 0-5 min, but for now it's much simpler and cleaner to just build those\n            // paths anyway (if no path in that slot).\n            int slot0 =\n                static_cast<int>((std::chrono::floor<std::chrono::seconds>(now) - path_expiry_basis) / slot_size + 1);\n\n            // First count up all the slots we are already using with existing paths:\n            int path_count = 0;\n            for (auto& path : paths())\n            {\n                path_count++;\n                // Path expiries will be up to +MAX_LIFETIME_FUZZ of their slot target expiry time, so we need\n                // to be sure that the maximum fuzz is less then the smallest possible slot size so\n                // that it is guaranteed to be counted in the same slot:\n                static_assert(\n                    path::MAX_LIFETIME_FUZZ < path::MAX_LIFETIME / path::MAX_LIFETIME_SLOTS,\n                    \"The slot calculation below requires path max fuzz be strictly smaller than the smallest allowed \"\n                    \"path slot size!\");\n                auto slot = (path.expiry() - path_expiry_basis) / slot_size;\n                if (slot < slot0)\n                {\n                    log::debug(logcat, \"Ignoring expired/expiring path slot {}\", slot);\n                    continue;  // Path is expired/expiring, so ignore it.\n                }\n                slot -= slot0;\n                if (slot >= slots)\n                {\n                    log::warning(logcat, \"Found inbound path with unexpected future expiry, this should not happen!\");\n                    continue;\n                }\n                slot_count[slot]++;\n            }\n\n            log::trace(\n                logcat, \"Current {} path expiry slots (oldest-newest): {}\", path_count, fmt::join(slot_count, \"-\"));\n\n            // We want all paths built in a given slot to expire at the same time so that we publish\n            // CCs on average once every 5 minutes, even if we are using many paths, and so we reuse\n            // the same fuzz value for any paths built in the same slot (whether in this build or a\n            // previous one that we are rebuilding for here).\n            cleanup_old_fuzz(slot0);\n\n            // Now we select new ones by looking for the slot with the fewest paths in it, preferring\n            // later slots (i.e. longer expiries) in case of a tie, and keep repeating this for\n            // however many paths we need:\n            for (int i = 0; i < needed; i++)\n            {\n                int best = 0;\n                for (int j = 1; j < slots; j++)\n                    if (slot_count[j] <= slot_count[best])\n                        best = j;\n                const auto slot = slot0 + best;\n                expiries.emplace_back(path_expiry_basis + slot * slot_size + inbound_path_fuzz(slot));\n                slot_count[best]++;\n            }\n\n            log::trace(logcat, \"Select new path expiries: {}\", fmt::join(expiries, \", \"));\n        }\n\n        auto next_expiry = expiries.begin();\n\n        if (num_hops() == 1)\n        {\n            // In single-hop mode the edge and pivot are the same thing, and so we need to select\n            // the edge (according to various configured edge selection rules), because if we\n            // selected a random pivot first we'd bypass all the edge rules (pinned edges,\n            // relay-connections, and so on).\n            for (; needed > 0; needed--)\n            {\n                auto only_hop = select_first_hop();\n                if (!only_hop)\n                {\n                    log::warning(logcat, \"Unable to build a new inbound single-hop path: no eligible edges\");\n                    return;\n                }\n                build(std::span{&*only_hop, 1}, *next_expiry++);\n            }\n        }\n        else\n        {\n            // We can potentially multi-pass through this because it can only return at most # of\n            // RCs, but pivot reuse might mean that we need to build paths using some RCs more than\n            // once.  (This is probably mostly a testnet concern).\n            int built = 0;\n            do\n            {\n                auto new_pivots = router.node_db().get_n_random_rcs(needed, true, filter);\n                if (new_pivots.empty())\n                    break;\n                needed -= static_cast<int>(new_pivots.size());\n                for (const llarp::RelayContact* rc : new_pivots)\n                {\n                    log::debug(logcat, \"Selected new inbound path terminus {}\", rc->router_id().short_string());\n                    auto hops = select_hops_to_remote(rc->router_id());\n                    if (!hops)\n                        continue;  // No need to warn: the call above should already if it fails\n\n                    build(*hops, *next_expiry++);\n                    built++;\n                }\n            } while (needed > 0);\n\n            if (needed > 0)\n                log::warning(\n                    logcat,\n                    \"Failed to build {} of {} new inbound paths: ran out of available unused/acceptable pivots\",\n                    needed,\n                    needed + built);\n        }\n    }\n\n    void SessionEndpoint::on_path_build_success(int64_t /*build_id*/, path::Path& p)\n    {\n        log::debug(logcat, \"Successfully built path {}\", p);\n\n        if (not router.config().network.is_reachable)\n            return;\n\n        // else we publish an introset, so check if we need to publish it now.\n\n        // We just built a new path: if that path brings our active paths to the target number of\n        // paths then we want to publish the introset.  *Typically* this will just end up on a\n        // regular timer (i.e. paths expire naturally, we rebuild them, and once all expired ones\n        // are rebuilt we end up here to republish), but in case of premature path death we might\n        // also end up here (and also need to republish so that clients in the wild don't try\n        // aligning to the dead path).\n\n        if (num_active_paths() < _target_paths)\n            return;  // We haven't met our target yet (or they are still building), so wait for them\n                     // to finish rebuilding before we publish.\n\n        log::info(logcat, \"Inbound active paths changed; re-publishing client contact\");\n        update_and_publish_localcc();\n    }\n\n    void SessionEndpoint::on_path_build_failure(int64_t /*build_id*/, path::Path* path, bool timeout)\n    {\n        if (path)\n            log::warning(logcat, \"Path build for {} {}\", *path, timeout ? \"timed out\" : \"failed\");\n        else\n            log::warning(logcat, \"Path build failed: cannot construct a new path right now\");\n    }\n\n    void SessionEndpoint::resolve_sns_mappings()\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n        auto& sns_ranges = router.config().exit.sns_ranges;\n\n        if (not sns_ranges.empty())\n        {\n            log::debug(logcat, \"SessionEndpoint resolving {} SNS addresses mapped to IP ranges\", sns_ranges.size());\n\n            for (const auto& [name, ip_range] : sns_ranges)\n            {\n                resolve_sns(name, [this, ip_range](std::optional<NetworkAddress> maybe_addr) {\n                    if (maybe_addr)\n                    {\n                        log::critical(\n                            logcat,\n                            \"UNIMPLEMENTED: Successfully resolved SNS lookup for {} mapped to IPRange:{}\",\n                            *maybe_addr,\n                            ip_range);\n                        // TODO FIXME: we need to sort out how these addresses get actually\n                        // mapped.\n                        //_range_map.insert_or_assign(std::move(ip_range), std::move(*maybe_addr));\n                    }\n                    // we don't need to print a fail message, as it is logged prior to invoking with std::nullopt\n                });\n            }\n        }\n\n        auto& sns_auths = router.config().network.sns_exit_auths;\n\n        if (auto n_sns_auths = sns_auths.size(); n_sns_auths > 0)\n        {\n            log::debug(logcat, \"SessionEndpoint resolving {} ONS addresses mapped to auth tokens\", n_sns_auths);\n\n            for (const auto& [name, auth_token] : sns_auths)\n            {\n                resolve_sns(name, [this, auth_token](std::optional<NetworkAddress> maybe_addr) {\n                    if (maybe_addr)\n                    {\n                        log::debug(\n                            logcat, \"Successfully resolved SNS lookup for {} mapped to static auth token\", *maybe_addr);\n                        _auth_tokens.emplace(std::move(*maybe_addr), std::move(auth_token));\n                    }\n                    // we don't need to print a fail message, as it is logged prior to invoking with std::nullopt\n                });\n            }\n        }\n    }\n\n    void SessionEndpoint::resolve_sns(std::string sns, std::function<void(std::optional<NetworkAddress>)> func)\n    {\n        Lock_t l{paths_mutex};\n        if (not is_valid_sns(sns))\n        {\n            log::debug(logcat, \"Invalid SNS name ({}) queried for lookup\", sns);\n            return func(std::nullopt);\n        }\n\n        log::debug(logcat, \"Looking up SNS name {}\", sns);\n\n        auto remaining = std::make_shared<int>(0);\n        auto response_handler = [sns, remaining, func = std::move(func)](auto resp) {\n            int rem = --*remaining;\n            if (rem < 0)\n                return;  // Some other request beat us to it\n\n            std::optional<NetworkAddress> client_addr;\n\n            if (resp.ok())\n            {\n                try\n                {\n                    log::debug(logcat, \"Call to ResolveSNS succeeded!\");\n\n                    auto enc = ResolveSNS::deserialize_response(oxenc::bt_dict_consumer{resp.body});\n\n                    client_addr = enc.decrypt(sns);\n                    if (client_addr)\n                    {\n                        log::debug(\n                            logcat, \"Successfully decrypted SNS record (name: {}, address: {})\", sns, *client_addr);\n                    }\n                    else\n                        log::warning(logcat, \"Failed to decrypt SNS record (name: {})\", sns);\n                }\n                catch (const std::exception& e)\n                {\n                    log::warning(logcat, \"Exception during SNS response handling: {}\", e.what());\n                }\n            }\n\n            if (client_addr)\n            {\n                *remaining = 0;\n                func(std::move(client_addr));\n            }\n            else if (rem == 0)\n            {\n                // If this is the last outstanding response, and still didn't succeed, then signal\n                // the lookup failure to the callback:\n                func(std::nullopt);\n            }\n        };\n\n        auto name_hash = crypto::shorthash(as_bspan(sns));\n\n        // TODO FIXME: this should not be fired down *every* path.\n        for (auto& path : paths())\n        {\n            ++*remaining;\n            log::debug(\n                logcat, \"Querying pivot:{} for name lookup (target: {})\", path.terminal_rid().short_string(), sns);\n            path.resolve_sns(name_hash, response_handler);\n        }\n\n        if (*remaining == 0)\n        {\n            log::warning(logcat, \"Unable to resolve Lokinet SNS {}: we have no active paths\", sns);\n            func(std::nullopt);\n        }\n    }\n\n    void SessionEndpoint::lookup_relay_contact(RouterID remote, std::function<void(std::optional<RelayContact>)> func)\n    {\n        if (auto* maybe_rc = router.node_db().get_rc(remote))\n        {\n            log::debug(logcat, \"RelayContact for remote (rid: {}) found locally!\", remote);\n            return func(*maybe_rc);\n        }\n\n        log::debug(logcat, \"Looking up RelayContact for remote (rid:{})\", remote.to_network_address(true));\n\n        auto remaining = std::make_shared<int>(0);\n\n        auto response_handler = [this, remote, func = std::move(func), remaining](auto resp) {\n            int rem = --*remaining;\n            if (rem < 0)\n            {  // Some other path handler already replied\n                log::trace(logcat, \"Dropping duplicate `fetch_rc` response (success: {})\", resp.ok());\n                return;\n            }\n\n            std::optional<RelayContact> rc;\n            try\n            {\n                if (resp.ok())\n                {\n                    log::info(logcat, \"Call to FetchRC succeeded!\");\n                    auto rcs = FetchRC::deserialize_response(router.netid(), oxenc::bt_dict_consumer{resp.body});\n\n                    if (rcs.empty())\n                        log::warning(logcat, \"Received empty response from `fetch_rc` request!\");\n                    else if (rcs.size() > 1)\n                        log::warning(\n                            logcat, \"Received more RC's than expected (n:{}) from `fetch_rc` request!\", rcs.size());\n                    else\n                    {\n                        log::debug(logcat, \"Storing RelayContact for remote rid:{}\", remote);\n                        router.node_db().put_rc(rcs.front());\n                        rc = std::move(rcs.front());\n                    }\n                }\n                else\n                {\n                    std::optional<std::string> status = std::nullopt;\n                    oxenc::bt_dict_consumer btdc{resp.body};\n\n                    if (auto s = btdc.maybe<std::string>(messages::STATUS_KEY))\n                        status = s;\n\n                    log::warning(logcat, \"Call to FetchRCs FAILED; reason: {}\", status.value_or(\"<none given>\"));\n                }\n            }\n            catch (const std::exception& e)\n            {\n                log::warning(logcat, \"An error occured processing fetched rc response: {}\", e.what());\n            }\n\n            if (rc)\n            {\n                *remaining = 0;\n                func(std::move(rc));\n            }\n            else if (rem == 0)\n            {\n                // We are the last path response and there have been no successes, so signal failure\n                func(std::nullopt);\n            }\n        };\n\n        Lock_t l{paths_mutex};\n\n        for (const auto& [_, p] : _paths)\n        {\n            if (not p or not p->is_active())\n                continue;\n\n            ++*remaining;\n            log::debug(\n                logcat,\n                \"Querying pivot (rid:{}) for RelayContact lookup target (rid:{})\",\n                p->terminal_rid().short_string(),\n                remote);\n\n            p->fetch_relay_contact(remote, response_handler);\n        }\n\n        if (*remaining == 0)\n        {\n            log::warning(logcat, \"RC lookup failed: no usable paths!\");\n            func(std::nullopt);\n        }\n    }\n\n    void SessionEndpoint::lookup_client_intro(RouterID remote, std::function<void(std::optional<ClientContact>)> func)\n    {\n        PubKey remote_key;\n        if (!crypto::blind(remote_key, remote, crypto::blinding::CLIENT_CONTACT))\n        {\n            log::error(\n                logcat,\n                \"Failed to blind remote address {}: this is most likely not a valid address\",\n                remote.to_network_address(false));\n            func(std::nullopt);\n            return;\n        }\n\n        log::debug(\n            logcat,\n            \"Looking up ClientContact (key: {}) for remote (rid:{})\",\n            remote_key,\n            remote.to_network_address(false));\n\n        auto remaining = std::make_shared<int>(0);\n\n        auto response_handler = [remote, func = std::move(func), remaining](auto resp) {\n            int rem = --*remaining;\n            if (rem < 0)\n            {\n                // Another path response already returned it\n                log::trace(logcat, \"Dropping duplicate `find_cc` response (success: {})\", resp.ok());\n                return;\n            }\n\n            std::optional<ClientContact> cc;\n            try\n            {\n                if (resp.ok())\n                {\n                    log::info(logcat, \"Call to FindClientContact succeeded!\");\n                    auto enc = FindClientContact::deserialize_response(oxenc::bt_dict_consumer{resp.body});\n\n                    if (auto intro = enc.decrypt(remote))\n                    {\n                        log::debug(logcat, \"Storing ClientContact for remote rid:{}\", remote);\n                        cc = std::move(intro);\n                    }\n                    else\n                        log::warning(logcat, \"Failed to decrypt returned EncryptedClientContact!\");\n                }\n                else\n                {\n                    std::optional<std::string> status = std::nullopt;\n                    oxenc::bt_dict_consumer btdc{resp.body};\n\n                    if (auto s = btdc.maybe<std::string>(messages::STATUS_KEY))\n                        status = s;\n\n                    log::warning(\n                        logcat, \"Call to FindClientContact FAILED; reason: {}\", status.value_or(\"<none given>\"));\n                }\n            }\n            catch (const std::exception& e)\n            {\n                log::warning(logcat, \"Exception: {}\", e.what());\n            }\n\n            if (cc)\n            {\n                *remaining = 0;\n                func(std::move(cc));\n            }\n            else if (rem == 0)\n            {\n                // Last chance and all failed, so trigger failure\n                func(std::nullopt);\n            }\n        };\n\n        Lock_t l{paths_mutex};\n\n        for (const auto& [_, p] : _paths)\n        {\n            if (not p or not p->is_active())\n                continue;\n\n            ++*remaining;\n            log::debug(\n                logcat,\n                \"Querying pivot (rid:{}) for ClientContact lookup target (rid:{})\",\n                p->terminal_rid().short_string(),\n                remote);\n\n            p->find_client_contact(remote_key, response_handler);\n        }\n\n        if (*remaining == 0)\n        {\n            log::warning(logcat, \"CC lookup failed: no usable paths!\");\n            func(std::nullopt);\n        }\n    }\n\n    void SessionEndpoint::update_and_publish_localcc()\n    {\n        if (!router.config().network.is_reachable)\n        {\n            log::debug(logcat, \"Not publishing CC: publishing is disabled by config\");\n            return;\n        }\n\n        log::debug(logcat, \"Updating and publishing ClientContact...\");\n\n        auto now = llarp::time_now_ms();\n        std::vector<ClientIntro> intros;\n        for (const auto& [hopid, p] : _paths)\n            if (p and p->is_active(now))\n                intros.push_back(p->make_intro());\n\n        client_contact.update_intros(std::move(intros));\n\n        log::debug(logcat, \"New ClientContact: {}\", client_contact);\n#ifndef NDEBUG\n        log::trace(logcat, \"ClientContact details:\");\n        log::trace(logcat, \"Pubkey: {}\", client_contact.pubkey());\n        log::trace(logcat, \"Intros ({}):\", client_contact.intros().size());\n        for (const auto& ci : client_contact.intros())\n            log::trace(\n                logcat,\n                \"    • {}, hopid: {}, expiry: {}\",\n                ci.relay.to_network_address(),\n                ci.hop,\n                std::chrono::floor<std::chrono::seconds>(ci.expires_in(now)));\n#endif\n\n        try\n        {\n            publish_client_contact(client_contact.encrypt_and_sign(cc_blind_keys));\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"ClientContact encryption/signing exception: {}\", e.what());\n        }\n    }\n\n    bool SessionEndpoint::validate(const NetworkAddress& remote, std::optional<std::string> maybe_auth)\n    {\n        auto& netconf = router.config().network;\n        auto& tokens = netconf.auth_static_tokens;\n        auto& whitelist = netconf.auth_whitelist;\n        if (tokens.empty() && whitelist.empty())\n            return true;  // No auth required\n\n        if (maybe_auth && tokens.contains(*maybe_auth))\n            return true;  // valid auth token\n\n        if (whitelist.contains(remote))\n            return true;  // valid address\n\n        return false;\n    }\n\n    std::optional<std::variant<ipv4, ipv6>> SessionEndpoint::map_session(const session::Session& s)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        if (const auto& tun = router.tun_endpoint())\n        {\n            log::debug(logcat, \"Successfully mapped inbound session; mapping session to local TUN IP\");\n\n            if (auto maybe_ipv4 = tun->map(s.remote()))\n            {\n                log::info(\n                    logcat,\n                    \"TUN device successfully mapped session (remote: {}) to local ip: {}\",\n                    s.remote(),\n                    *maybe_ipv4);\n                return maybe_ipv4;\n            }\n            // TODO: ipv6\n\n            // TODO: if this fails, we should close the session\n            log::warning(logcat, \"TUN device failed to map session (remote: {}) to local ip\", s.remote());\n            return std::nullopt;\n        }\n\n        // TODO: if we're not tun-based -- currently not allowing inbound sessions for non-tun\n\n        return std::nullopt;\n    }\n\n    void SessionEndpoint::handle_session_init(std::vector<std::byte>&& payload, std::shared_ptr<path::Path> path)\n    {\n        std::shared_ptr<session::InboundSession> new_session{};\n        try\n        {\n            new_session = std::make_shared<session::InboundClientSession>(*this, std::move(path), std::move(payload));\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"Inbound session rejected: {}\", e.what());\n            return;\n        }\n        session_post_init(std::move(new_session));\n    }\n\n    void SessionEndpoint::handle_session_init(std::vector<std::byte>&& payload, std::shared_ptr<path::TransitHop> thop)\n    {\n        log::warning(logcat, \"SessionEndpoint::handle_session_init (relay)\");\n        std::shared_ptr<session::InboundSession> new_session{};\n        try\n        {\n            new_session = std::make_shared<session::InboundRelaySession>(*this, std::move(thop), std::move(payload));\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"Inbound session rejected: {}\", e.what());\n            return;\n        }\n        log::warning(logcat, \"SessionEndpoint::handle_session_init (relay) calling post_init\");\n        session_post_init(std::move(new_session));\n    }\n\n    void SessionEndpoint::session_post_init(std::shared_ptr<session::InboundSession> new_session)\n    {\n        // FIXME: for now only tun clients can have inbound sessions, but eventually that will\n        //        not be the case and we'll need to \"if tun\" this.\n        if (!map_session(*new_session))\n        {\n            log::warning(\n                logcat,\n                \"Unable to map session to tun IP (or not allowing inbound sessions); dropping inbound session from {}\",\n                new_session->remote());\n            return;\n        }\n\n        // TODO FIXME: this is racy, e.g. if two clients establish a session to each other at the\n        // same time, then they can drop different ones.  We should instead use a decision metric\n        // for dropping that decides the same way on both sides (e.g. prefer session initiated by\n        // the side with the smaller pubkey).\n        // FIXME: If the initiator does not get our response in time, they will try again\n        // to establish a session; in that case we should replace what we have.\n        auto& s = _sessions[new_session->remote()];\n        auto* sptr = new_session.get();\n        if (!s)\n        {\n            s = std::move(new_session);\n            _session_tags[s->inbound_tag()] = s;\n            // TODO: response with our inbound tag\n        }\n        log::warning(logcat, \"sending session_init_accept\");\n        sptr->session_init_accept();\n    }\n\n    void SessionEndpoint::publish_client_contact(const EncryptedClientContact& ecc)\n    {\n        auto now = std::chrono::steady_clock::now();\n        ++cc_count;\n        // Send our CC down each inbound session so that everyone who is already connected to us\n        // gets it pushed to them without having to always poll the network for updates.\n        for (const auto& [addr, session] : _sessions)\n        {\n            // don't publish client contact to other end of outbound session\n            if (session->is_outbound)\n                return;\n            log::debug(\n                logcat,\n                \"Publishing ClientContact#{} to remote on inbound session (remote:{})\",\n                cc_count,\n                session->remote());\n\n            session->publish_client_contact(ecc);\n        }\n\n        // Pick four random inbound paths to publish on, and then on each one we send along a 0-3\n        // location indicating where we want it to forward it (e.g. 0 means DHT-closest, 1 means 2nd\n        // closest, etc.).\n        std::vector<path::Path*> paths;\n        paths.resize(path::CC_PUBLISH_LOCATIONS);\n        auto end = std::ranges::sample(\n            active_paths() | std::views::transform([](auto& p) { return &p; }),\n            paths.begin(),\n            path::CC_PUBLISH_LOCATIONS,\n            llarp::csrng);\n        paths.resize(std::distance(paths.begin(), end));\n        if (paths.empty())\n        {\n            // This should be impossible: we should only have triggered a publish once we reached\n            // our target number of active paths, but somehow found no active paths!\n            log::error(logcat, \"Internal error: attempt to publish CC with no active paths!\");\n            assert(false);\n            return;\n        }\n        std::shuffle(paths.begin(), paths.end(), llarp::csrng);\n\n        // Tracks number of successes and number of outstanding requests so that we can log success\n        // (or error) when the last response comes back:\n        auto remaining_success = std::make_shared<std::pair<int, int>>(path::CC_PUBLISH_LOCATIONS, 0);\n\n        for (int location = 0; location < path::CC_PUBLISH_LOCATIONS; location++)\n        {\n            // % because we might have fewer than path::CC_PUBLISH_LOCATIONS, and if that happens we\n            // just use some paths for multiple locations:\n            auto& p = *paths[location % paths.size()];\n            log::debug(logcat, \"Publishing ClientContact to location {} via {}\", location, p);\n            p.publish_client_contact(\n                ecc,\n                location,\n                [started = now, remaining_success, via = p.terminal_rid(), location, cc_num = cc_count](auto resp) {\n                    auto elapsed =\n                        std::chrono::round<std::chrono::milliseconds>(std::chrono::steady_clock::now() - started);\n\n                    log::debug(\n                        logcat,\n                        \"{} CC#{} publish[{}] via relay {} in {}\",\n                        resp.ok()            ? \"Successful\"\n                            : resp.timed_out ? \"Timeout during\"\n                                             : \"Error during\",\n                        cc_num,\n                        location,\n                        via,\n                        elapsed);\n                    if (!resp.ok())\n                        log::debug(logcat, \"CC publish error response: {}\", buffer_printer(resp.body));\n\n                    auto& [remaining, success] = *remaining_success;\n                    remaining--;\n                    if (resp.ok())\n                        success++;\n\n                    if (not remaining)\n                    {  // This is the last response\n                        log::log(\n                            logcat,\n                            not success                                    ? log::Level::err\n                                : success < path::CC_PUBLISH_LOCATIONS / 2 ? log::Level::warn\n                                                                           : log::Level::info,\n                            \"CC#{} publish success to {}/{} publish locations in {}\",\n                            cc_num,\n                            success,\n                            path::CC_PUBLISH_LOCATIONS,\n                            elapsed);\n                    }\n                });\n        }\n    }\n\n    std::optional<std::string_view> SessionEndpoint::fetch_auth_token(const NetworkAddress& remote) const\n    {\n        std::optional<std::string_view> ret = std::nullopt;\n\n        if (auto itr = _auth_tokens.find(remote); itr != _auth_tokens.end())\n            ret = itr->second;\n\n        return ret;\n    }\n\n    std::shared_ptr<session::Session> SessionEndpoint::initiate_remote_session(\n        const NetworkAddress& remote,\n        std::function<void(session::Session& session)> on_attempted,\n        std::optional<std::chrono::milliseconds> timeout)\n    {\n        return router.loop.call_get([this, &remote, &on_attempted, &timeout] {\n            auto& s = _sessions[remote];\n            if (s && !s->is_closed())\n            {\n                if (on_attempted)\n                {\n                    if (s->is_established())\n                        on_attempted(*s);\n                    else\n                    {\n                        assert(s->is_outbound);  // Inbound sessions are always established\n                        // We have an already-in-progress but not-yet-established session, so just\n                        // hook the callback up to it to be fired when it finishes establishing:\n                        static_cast<session::OutboundSession*>(s.get())->on_established(\n                            std::move(on_attempted), timeout);\n                    }\n                }\n            }\n            else\n            {\n                auto tag = next_tag();\n                if (remote.client())\n                    s = router.loop.make_shared<session::OutboundClientSession>(\n                        remote, *this, tag, std::move(on_attempted), timeout);\n                else\n                    s = router.loop.make_shared<session::OutboundRelaySession>(\n                        remote, *this, tag, std::move(on_attempted), timeout);\n                _session_tags.emplace(tag, s);\n            }\n\n            return s;\n        });\n    }\n\n    session_tag SessionEndpoint::next_tag()\n    {\n        // zero tag used to represent a session init for convenience\n        while (_session_tags.contains(last_tag) || last_tag == 0)\n            last_tag++;\n        return last_tag;\n    }\n}  //  namespace llarp::handlers\n"
  },
  {
    "path": "llarp/handlers/session.hpp",
    "content": "#pragma once\n\n#include <llarp/address/address.hpp>\n#include <llarp/config/config.hpp>\n#include <llarp/contact/client_contact.hpp>\n#include <llarp/path/path_handler.hpp>\n#include <llarp/path/transit_hop.hpp>\n#include <llarp/session/session.hpp>\n#include <llarp/util/random.hpp>\n#include <llarp/util/time.hpp>\n\n#include <chrono>\n#include <concepts>\n#include <memory>\n\nnamespace llarp\n{\n    namespace rpc\n    {\n        class RPCServer;\n    }\n\n    namespace handlers\n    {\n        using session::session_tag;\n\n        class SessionEndpoint final : public path::PathHandler\n        {\n            friend class rpc::RPCServer;\n            friend class session::Session;\n\n            std::unordered_set<dns::SRVData> _srv_records;\n\n            // Inbound path lifetimes within a slot are always determined relative to this base\n            // value, so that if we need a path in the (15,20] minute range, we will always pick the\n            // same value in that slot by using this basis value.\n            const std::chrono::seconds path_expiry_basis =\n                std::chrono::floor<std::chrono::seconds>(llarp::time_now_ms());\n\n            std::unordered_map<NetworkAddress, std::shared_ptr<session::Session>> _sessions;\n            std::unordered_map<session_tag, std::shared_ptr<session::Session>> _session_tags;\n\n            session_tag last_tag = llarp::csrng();\n\n            // this could probably map to a pair of vectors, or pending packets could\n            // be wrapped in callbacks, but for now this works\n            // std::unordered_map<NetworkAddress, std::vector<IPPacket>> pending_sessions;\n            // std::unordered_map<NetworkAddress, std::vector<std::function<void(bool)>>> pending_session_hooks;\n\n            ClientContact client_contact;\n            Ed25519BlindedKey cc_blind_keys;\n            int cc_count = -1;\n            protocol_flag protocols;\n\n            // Used for logging connected/disconnected status:\n            bool connected = false;\n\n            // auth tokens for making outbound sessions; some of these are copied at construction,\n            // some (with ONS names) get looked up and populated later.\n            std::unordered_map<NetworkAddress, std::string> _auth_tokens;\n\n            std::optional<std::string_view> fetch_auth_token(const NetworkAddress& remote) const;\n\n            void close_session(std::shared_ptr<session::Session>& s, bool send_close);\n\n            void on_path_build_failure(int64_t build_id, path::Path* path, bool timeout) override;\n            void on_path_build_success(int64_t build_id, path::Path& p) override;\n\n            void session_post_init(std::shared_ptr<session::InboundSession> new_session);\n\n            // Returns a random amount of path \"fuzz\" to add to the path build time to make it\n            // harder to correlate repeated path builds over time from the same client.\n            //\n            // The value is cached so that repeated calls with the same slot return the same value.\n            //\n            // This currently returns a truncated normal distribution with truncation points at 0\n            // and MAX_LIFETIME_FUZZ such that the truncation point eliminates values above the\n            // 0.5th percentile of the distribution.\n            std::chrono::seconds inbound_path_fuzz(int slot);\n\n            // The cache of slot fuzz values returned by inbound_path_fuzz.\n            std::map<int, std::chrono::seconds> _slot_fuzz;\n\n            // Called to clean up expired slot values out of _slot_fuzz\n            void cleanup_old_fuzz(int oldest_slot);\n\n          public:\n            SessionEndpoint(Router& r);\n\n            void stop(bool send_close);\n\n            // Checks if we need more inbound paths and, if so, starts building them.\n            void update_paths(std::chrono::milliseconds now) override;\n\n            // bool build_path_to_random(bool exclude_current_termini)\n\n            /// Returns array of:\n            /// - inbound sessions (i.e. from remote clients)\n            /// - outbound relay sessions (pending or established)\n            /// - outbound client sessions (pending or established)\n            /// - pending outbound relay sessions\n            /// - pending outbound client sessions\n            ///\n            /// For relays, all but the first value will be 0 (relays do not establish outbound\n            /// sessions).\n            std::array<int, 5> session_stats() const;\n\n            /// Returns array of path counts:\n            /// - inbound/utility paths (used for inbound sessions and network queries)\n            /// - paths for outbound relay sessions\n            /// - paths for outbound client sessions\n            std::array<int, 3> path_stats(std::chrono::milliseconds now = llarp::time_now_ms()) const;\n\n            // quic::Address local_address() const { return _local_addr; }\n\n            // get copy of all srv records\n            std::unordered_set<dns::SRVData> srv_records() const { return _srv_records; }\n\n            template <std::derived_from<session::Session> S = session::Session>\n            S* get_session(const session_tag& tag) const\n            {\n                auto it = _session_tags.find(tag);\n                if (it == _session_tags.end())\n                    return nullptr;\n                return dynamic_cast<S*>(it->second.get());\n            }\n\n            template <std::derived_from<session::Session> S = session::Session>\n            S* get_session(const NetworkAddress& remote) const\n            {\n                auto it = _sessions.find(remote);\n                if (it == _sessions.end())\n                    return nullptr;\n                return dynamic_cast<S*>(it->second.get());\n            }\n\n            bool close_session(NetworkAddress remote, bool send_close = false);\n\n            bool close_session(session_tag t, bool send_close = false);\n\n            /// Called to perform CC publishing.  This is called upon inbound path build completion\n            /// if that completion results in a full set of target paths, so that we effectively\n            /// republish whenever inbound paths change.\n            void update_and_publish_localcc();\n\n            void publish_client_contact(const EncryptedClientContact& ecc);\n\n            // SessionEndpoint can use either a whitelist or a static auth token list to  validate incomininbg requests\n            // to initiate a session\n            bool validate(const NetworkAddress& remote, std::optional<std::string> maybe_auth = std::nullopt);\n\n            // FIXME: should SessionEndpoint have these mappings at all?\n            std::optional<std::variant<ipv4, ipv6>> map_session(const session::Session& s);\n            void map_remote_to_local_addr(NetworkAddress remote, quic::Address local);\n            void unmap_local_addr_by_remote(const NetworkAddress& remote);\n            void unmap_remote_by_name(const std::string& name);\n\n            void handle_session_init(std::vector<std::byte>&& payload, std::shared_ptr<path::Path> path);\n            void handle_session_init(std::vector<std::byte>&& payload, std::shared_ptr<path::TransitHop> thop);\n\n            // Called on a client when we receive a session_init from another client to create an\n            // InboundClientSession.  Returns nullopt if the session cannot be created, otherwise\n            // returns the random session tag we have associated with the inbound session.\n            std::optional<session_tag> create_inbound_session(\n                const NetworkAddress& initiator,\n                const HopID& remote_pivot_txid,\n                std::shared_ptr<path::Path> path,\n                const SharedSecret& session_key);\n\n            // Called on a relay when we receive a session_init from a client to create an\n            // InboundRelaySession.  Returns nullopt if the session cannot be created, otherwise\n            // returns the random session tag we have associated with the inbound session.\n            std::optional<session_tag> create_inbound_session(\n                const NetworkAddress& initiator,\n                const HopID& remote_pivot_txid,\n                std::shared_ptr<path::TransitHop> path,\n                const SharedSecret& session_key);\n\n            // lookup SNS address to return \"{pubkey}.loki\" hidden service or exit node operated on a remote client\n            void resolve_sns(std::string name, std::function<void(std::optional<NetworkAddress>)> func);\n\n            void lookup_remote_srv(\n                std::string name, std::string service, std::function<void(std::vector<dns::SRVData>)> handler);\n\n            void lookup_relay_contact(RouterID remote, std::function<void(std::optional<RelayContact>)> func);\n\n            void lookup_client_intro(RouterID remote, std::function<void(std::optional<ClientContact>)> func);\n\n            // resolves any config mappings that parsed ONS addresses to their pubkey network address\n            void resolve_sns_mappings();\n\n            // Initiates a session to the given remote client or snode address.  Calls\n            // `on_attempted` when the connection is either established (immediately, if a session\n            // to the target is already established) or when the connection attempt times out (the\n            // caller can check `session.is_established()` to figure out which one occured).\n            //\n            // The timeout, if omitted/nullopt, defaults to the [paths]build-timeout config option.\n            //\n            // Note that this resulting session could be outbound or inbound: i.e. if the target is\n            // a client (.loki) that has already established a session to this lokinet instance then\n            // that existing session is used rather than building a new outbound one.\n            //\n            // NB: this method can be safely called from outside the event loop (e.g. in embedded\n            // usage).\n            std::shared_ptr<session::Session> initiate_remote_session(\n                const NetworkAddress& remote,\n                std::function<void(session::Session& session)> on_established,\n                std::optional<std::chrono::milliseconds> timeout = std::nullopt);\n\n            void tick(std::chrono::milliseconds now) override;\n\n            void queue_session_packet(const NetworkAddress& remote, IPPacket pkt);\n\n            session_tag next_tag();\n        };\n\n    }  // namespace handlers\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/handlers/tun.cpp",
    "content": "#include \"tun.hpp\"\n\n#include <variant>\n#ifndef _WIN32\n#include <sys/socket.h>\n#endif\n\n#include <llarp/auth/auth.hpp>\n#include <llarp/constants/platform.hpp>\n#include <llarp/contact/sns.hpp>\n#include <llarp/dns/dns.hpp>\n#include <llarp/dns/name.hpp>\n#include <llarp/nodedb.hpp>\n#include <llarp/router/route_poker.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/util/logging/buffer.hpp>\n#include <llarp/util/str.hpp>\n\n#include <nlohmann/json.hpp>\n\nnamespace llarp::handlers\n{\n    static auto logcat = log::Cat(\"tun\");\n\n    bool TunEndpoint::maybe_hook_dns(\n        const std::shared_ptr<dns::PacketSource>& source,\n        const dns::Message& query,\n        const quic::Address& to,\n        const quic::Address& from)\n    {\n        if (not should_hook_dns_message(query))\n            return false;\n\n        auto job = std::make_shared<dns::QueryJob>(source, query, to, from);\n        if (!handle_hooked_dns_message(query, [job](auto msg) { job->send_reply(msg.to_buffer()); }))\n            job->cancel();\n        return true;\n    }\n\n    /// Intercepts DNS IP packets on platforms where binding to a low port isn't viable.\n    /// (windows/macos/ios/android ... aka everything that is not linux... funny that)\n    class DnsInterceptor : public dns::PacketSource\n    {\n        ip_pkt_hook _hook;\n        quic::Address _our_ip;  // maybe should be an IP type...?\n        llarp::DnsConfig _config;\n\n      public:\n        explicit DnsInterceptor(ip_pkt_hook reply, quic::Address our_ip, llarp::DnsConfig conf)\n            : _hook{std::move(reply)}, _our_ip{std::move(our_ip)}, _config{std::move(conf)}\n        {}\n\n        ~DnsInterceptor() override = default;\n\n        void send_udp(\n            const quic::Address& to, const quic::Address& from, std::span<const std::byte> payload) const override\n        {\n            log::critical(logcat, \"DNS interceptor FIXME!\");\n            if (payload.empty())\n                return;\n            // FIXME: this\n            (void)to;\n            (void)from;\n            (void)payload;\n            // _hook(data.make_udp(to, from));\n        }\n\n        std::optional<quic::Address> bound_on() const override { return std::nullopt; }\n\n        bool would_loop(const quic::Address& to, const quic::Address& from) const override\n        {\n            if constexpr (platform::is_apple)\n            {\n                // DNS on Apple is a bit weird because in order for the NetworkExtension itself to\n                // send data through the tunnel we have to proxy DNS requests through Apple APIs\n                // (and so our actual upstream DNS won't be set in our resolvers, which is why the\n                // vanilla WouldLoop won't work for us).  However when active the mac also only\n                // queries the main tunnel IP for DNS, so we consider anything else to be\n                // upstream-bound DNS to let it through the tunnel.\n                return to != _our_ip;\n            }\n            else if (auto maybe_addr = _config._query_bind)\n            {\n                const auto& addr = *maybe_addr;\n                // omit traffic to and from our dns socket\n                return addr == to or addr == from;\n            }\n            return false;\n        }\n    };\n\n    class TunDNS : public dns::Server\n    {\n        const TunEndpoint* _tun;\n        std::optional<quic::Address> _query_bind;\n        quic::Address _our_ip;\n\n      public:\n        std::shared_ptr<dns::PacketSource> pkt_source;\n\n        ~TunDNS() override = default;\n\n        explicit TunDNS(TunEndpoint* ep, const llarp::DnsConfig& conf)\n            : dns::Server{ep->router().loop, conf, 0},\n              _tun{ep},\n              _query_bind{conf._query_bind},\n              _our_ip{ep->get_ipv4()}  // FIXME: What about IPv6?\n        {\n            if (_query_bind)\n                _our_ip.set_port(_query_bind->port());\n        }\n\n        std::shared_ptr<dns::PacketSource> make_packet_source_on(\n            const quic::Address&, const llarp::DnsConfig& conf) override\n        {\n            (void)_tun;\n            auto ptr = std::make_shared<DnsInterceptor>(\n                [](IPPacket pkt) {\n                    (void)pkt;\n                    // ep->handle_write_ip_packet(pkt.ConstBuffer(), pkt.srcv6(), pkt.dstv6(), 0);\n                },\n                _our_ip,\n                conf);\n            pkt_source = ptr;\n            return ptr;\n        }\n    };\n\n    // NB: It looks like this could/should be called during the constructor,\n    // but as it passes weak_from_this to the dns server, it has to be after.\n    void TunEndpoint::setup_dns()\n    {\n        log::debug(logcat, \"{} setting up DNS...\", name());\n\n        auto& dns_config = _router.config().dns;\n        const auto& info = get_vpn_interface()->interface_info();\n\n        if (dns_config.l3_intercept)\n        {\n            // FIXME: this entire if block is so broken...\n            _dns = std::make_unique<TunDNS>(this, dns_config);\n            auto* dns = static_cast<TunDNS*>(_dns.get());\n\n            uint16_t p = 53;\n\n            while (p < 100)\n            {\n                try\n                {\n                    _packet_router->add_udp_handler(p, [this, dns](IPPacket pkt) {\n                        // TODO FIXME\n                        log::critical(logcat, \"TODO FIXME: L3 udp interceptor!\");\n                        // if (dns->maybe_handle_payload(dns->pkt_source, pkt.destination(), pkt.source(),\n                        // pkt.udp_data()))\n                        //     return;\n\n                        handle_outbound_packet(std::move(pkt));\n                    });\n                }\n                catch (const std::exception& e)\n                {\n                    if (p += 1; p >= 100)\n                        throw std::runtime_error{\"Failed to port map udp handler: {}\"_format(e.what())};\n                }\n            }\n        }\n        else\n            _dns = std::make_unique<dns::Server>(_router.loop, dns_config, info.index);\n\n        _dns->add_resolver(weak_from_this());\n        _dns->start();\n\n        if (dns_config.l3_intercept)\n        {\n            if (auto vpn = _router.vpn_platform())\n            {\n                // get the first local address we know of\n                std::optional<quic::Address> localaddr;\n\n                for (auto res : _dns->get_all_resolvers())\n                {\n                    if (auto ptr = res.lock())\n                    {\n                        localaddr = ptr->get_local_addr();\n\n                        if (localaddr)\n                            break;\n                    }\n                }\n                if (platform::is_windows)\n                {\n                    // auto dns_io = vpn->create_packet_io(0, localaddr);\n                    // router().loop()->add_ticker([dns_io, handler = m_PacketRouter]() {\n                    //   net::IPPacket pkt = dns_io->ReadNextPacket();\n                    //   while (not pkt.empty())\n                    //   {\n                    //     handler->HandleIPPacket(std::move(pkt));\n                    //     pkt = dns_io->ReadNextPacket();\n                    //   }\n                    // });\n                    // m_RawDNS = dns_io;\n                }\n\n                (void)vpn;\n            }\n\n            if (_raw_DNS)\n                _raw_DNS->Start();\n        }\n    }\n\n    nlohmann::json TunEndpoint::ExtractStatus() const\n    {\n        // auto obj = service::Endpoint::ExtractStatus();\n        // obj[\"ifaddr\"] = m_OurRange.to_string();\n        // obj[\"ifname\"] = m_IfName;\n\n        // std::vector<std::string> upstreamRes;\n        // for (const auto& ent : m_DnsConfig.upstream_dns)\n        //   upstreamRes.emplace_back(ent.to_string());\n        // obj[\"ustreamResolvers\"] = upstreamRes;\n\n        // std::vector<std::string> localRes;\n        // for (const auto& ent : m_DnsConfig.bind_addr)\n        //   localRes.emplace_back(ent.to_string());\n        // obj[\"localResolvers\"] = localRes;\n\n        // // for backwards compat\n        // if (not m_DnsConfig.bind_addr.empty())\n        //   obj[\"localResolver\"] = localRes[0];\n\n        // nlohmann::json ips{};\n        // for (const auto& item : m_IPActivity)\n        // {\n        //   nlohmann::json ipObj{{\"lastActive\", to_json(item.second)}};\n        //   std::string remoteStr;\n        //   AlignedBuffer<32> addr = m_IPToAddr.at(item.first);\n        //   if (m_SNodes.at(addr))\n        //     remoteStr = RouterID(addr.as_array()).to_string();\n        //   else\n        //     remoteStr = service::Address(addr.as_array()).to_string();\n        //   ipObj[\"remote\"] = remoteStr;\n        //   std::string ipaddr = item.first.to_string();\n        //   ips[ipaddr] = ipObj;\n        // }\n        // obj[\"addrs\"] = ips;\n        // obj[\"ourIP\"] = m_OurIP.to_string();\n        // obj[\"nextIP\"] = m_NextIP.to_string();\n        // obj[\"maxIP\"] = m_MaxIP.to_string();\n        // return obj;\n        return {};\n    }\n\n    void TunEndpoint::reconfigure_dns(std::vector<quic::Address> servers)\n    {\n        if (_dns)\n        {\n            for (auto weak : _dns->get_all_resolvers())\n            {\n                if (auto ptr = weak.lock())\n                    ptr->reset_resolver(servers);\n            }\n        }\n    }\n\n    TunEndpoint::TunEndpoint(Router& r) : _router{r}\n    {\n        _packet_router =\n            std::make_shared<vpn::PacketRouter>([this](IPPacket pkt) { handle_outbound_packet(std::move(pkt)); });\n\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        auto& net_conf = _router.config().network;\n\n        _exit_policy = net_conf.traffic_policy;\n\n        ipv6_enabled = net_conf.enable_ipv6;\n        if (ipv6_enabled)\n        {\n            _local_ipv6_net = net_conf._local_ipv6_net;\n            if (_local_ipv6_net)\n                _local_ipv6_range_iterator.emplace(*_local_ipv6_net);\n        }\n\n        _if_name = net_conf._if_name.value_or(\"\");\n        if (net_conf._local_ip_net)\n            _local_net = *net_conf._local_ip_net;\n\n        if (net_conf.addr_map_persist_file)\n        {\n            _persisting_addr_file = net_conf.addr_map_persist_file;\n            persist_addrs = true;\n        }\n\n        for (auto& [remote, local] : net_conf._reserved_local_ipv4)\n            _local_ipv4_mapping.insert_or_assign(local, remote);\n        for (auto& [remote, local] : net_conf._reserved_local_ipv6)\n            _local_ipv6_mapping.insert_or_assign(local, remote);\n\n        log::debug(logcat, \"Tun constructing IPRange iterator on local network: {}\", _local_net);\n        _local_range_iterator = IPRangeIterator{_local_net};\n\n        _local_netaddr = NetworkAddress{_router.id(), !_router.is_service_node};\n        _local_ipv4_mapping.insert_or_assign(_local_net.ip, std::move(_local_netaddr));\n\n        vpn::InterfaceInfo info;\n        info.ifname = _if_name;\n        info.addrs.emplace_back(_local_net);\n\n        if (ipv6_enabled and _local_ipv6_net)\n        {\n            log::info(logcat, \"{} using ipv6 range:{}\", name(), *_local_ipv6_net);\n            info.addrs.emplace_back(*_local_ipv6_net);\n        }\n\n        log::debug(logcat, \"{} setting up network...\", name());\n\n        log::info(logcat, \"{} using IPv4 address range {}\", name(), _local_net);\n        if (ipv6_enabled && _local_ipv6_net)\n            log::info(logcat, \"{} using IPv6 address range {}\", name(), _local_ipv6_net);\n\n        _net_if = router().vpn_platform()->create_interface(std::move(info), &_router);\n        _if_name = _net_if->interface_info().ifname;\n\n        log::info(logcat, \"{} got network interface:{}\", name(), _if_name);\n    }\n\n    static bool is_random_snode(const dns::Message& msg) { return msg.questions[0].IsName(\"random.snode\"); }\n\n    static bool is_localhost_loki(const dns::Message& msg) { return msg.questions[0].IsLocalhost(); }\n\n    static dns::Message& clear_dns_message(dns::Message& msg)\n    {\n        msg.authorities.resize(0);\n        msg.additional.resize(0);\n        msg.answers.resize(0);\n        msg.hdr_fields &= ~dns::flags_RCODENameError;\n        return msg;\n    }\n\n    template <typename T, typename... Args>\n    static std::optional<T> try_making(Args&&... args)\n    {\n        try\n        {\n            return std::make_optional<T>(std::forward<Args>(args)...);\n        }\n        catch (...)\n        {\n            return std::nullopt;\n        }\n    }\n\n    bool TunEndpoint::handle_hooked_dns_message(dns::Message msg, std::function<void(dns::Message)> reply)\n    {\n        log::trace(logcat, \"handle_hooked_dns_message\");\n        (void)msg;\n        (void)reply;\n        // auto ReplyToSNodeDNSWhenReady = [this, reply](RouterID snode, auto msg, bool isV6) ->\n        // bool {\n        //   return EnsurePathToSNode(\n        //       snode,\n        //       [this, snode, msg, reply, isV6](\n        //           const RouterID&,\n        //           std::shared_ptr<session::BaseSession> s,\n        //           [[maybe_unused]] session_tag tag) {\n        //         SendDNSReply(snode, s, msg, reply, isV6);\n        //       });\n        // };\n        // auto ReplyToLokiDNSWhenReady = [this, reply, timeout = PathAlignmentTimeout()](\n        //                                    service::Address addr, auto msg, bool isV6) -> bool {\n        //   using service::Address;\n        //   using service::OutboundContext;\n        //   if (HasInboundConvo(addr))\n        //   {\n        //     // if we have an inbound convo to this address don't mark as outbound so we don't\n        //     have a\n        //     // state race this codepath is hit when an application verifies that reverse and\n        //     forward\n        //     // dns records match for an inbound session\n        //     SendDNSReply(addr, this, msg, reply, isV6);\n        //     return true;\n        //   }\n        //   MarkAddressOutbound(addr);\n        //   return EnsurePathToService(\n        //       addr,\n        //       [this, addr, msg, reply, isV6](const Address&, OutboundContext* ctx) {\n        //         SendDNSReply(addr, ctx, msg, reply, isV6);\n        //       },\n        //       timeout);\n        // };\n\n        // auto ReplyToDNSWhenReady = [ReplyToLokiDNSWhenReady, ReplyToSNodeDNSWhenReady](\n        //                                std::string name, auto msg, bool isV6) {\n        //   if (auto saddr = service::Address(); saddr.FromString(name))\n        //     ReplyToLokiDNSWhenReady(saddr, msg, isV6);\n\n        //   if (auto rid = RouterID(); rid.from_snode_address(name))\n        //     ReplyToSNodeDNSWhenReady(rid, msg, isV6);\n        // };\n\n        // auto ReplyToLokiSRVWhenReady = [this, reply, timeout = PathAlignmentTimeout()](\n        //                                    service::Address addr, auto msg) -> bool {\n        //   using service::Address;\n        //   using service::OutboundContext;\n        //   // TODO: how do we handle SRV record lookups for inbound sessions?\n        //   MarkAddressOutbound(addr);\n        //   return EnsurePathToService(\n        //       addr,\n        //       [msg, addr, reply](const Address&, OutboundContext* ctx) {\n        //         if (ctx == nullptr)\n        //           return;\n\n        //         const auto& introset = ctx->GetCurrentIntroSet();\n        //         msg->AddSRVReply(introset.GetMatchingSRVRecords(addr.subdomain));\n        //         reply(*msg);\n        //       },\n        //       timeout);\n        // };\n\n        /*\n        if (msg.answers.size() > 0)\n        {\n          const auto& answer = msg.answers[0];\n          if (answer.HasCNameForTLD(\".snode\"))\n          {\n            llarp_buffer_t buf(answer.rData);\n            auto qname = dns::DecodeName(&buf, true);\n            if (not qname)\n              return false;\n            RouterID addr;\n            if (not addr.from_snode_address(*qname))\n              return false;\n            auto replyMsg = std::make_shared<dns::Message>(clear_dns_message(msg));\n            return ReplyToSNodeDNSWhenReady(addr, std::move(replyMsg), false);\n          }\n          else if (answer.HasCNameForTLD(\".loki\"))\n          {\n            llarp_buffer_t buf(answer.rData);\n            auto qname = dns::DecodeName(&buf, true);\n            if (not qname)\n              return false;\n\n            service::Address addr;\n            if (not addr.FromString(*qname))\n              return false;\n\n            auto replyMsg = std::make_shared<dns::Message>(clear_dns_message(msg));\n            return ReplyToLokiDNSWhenReady(addr, replyMsg, false);\n          }\n        }\n        */\n\n        if (msg.questions.size() != 1)\n        {\n            log::debug(logcat, \"bad number of dns questions: {}\", msg.questions.size());\n            return false;\n        }\n\n        std::string our_name = _router.id().to_network_address(_router.is_service_node).to_string();\n\n        std::string qname = msg.questions[0].Name();\n        const auto nameparts = split(qname, \".\");\n        std::string hostname, tld;\n        if (nameparts.size() >= 2)\n        {\n            hostname = nameparts[nameparts.size() - 2];\n            tld = nameparts[nameparts.size() - 1];\n        }\n        else\n        {\n            log::debug(logcat, \"bad DNS request, no TLD or hostname: {}\", qname);\n            return false;\n        }\n        bool is_localhost = hostname == \"localhost\"s && tld == \"loki\"s;\n        std::string sns_name;\n        if (nameparts.size() >= 2 and ends_with(qname, \".loki\"))\n        {\n            sns_name = hostname;\n            sns_name += \".loki\"sv;\n        }\n        /*\n        if (msg.questions[0].qtype == dns::qTypeTXT)\n        {\n          RouterID snode;\n          if (snode.from_snode_address(qname))\n          {\n            if (auto rc = router().node_db().get_rc(snode))\n              msg.AddTXTReply(std::string{rc->view()});\n            else\n              msg.AddNXReply();\n            reply(msg);\n\n            return true;\n          }\n\n          if (msg.questions[0].IsLocalhost() and msg.questions[0].HasSubdomains())\n          {\n            const auto subdomain = msg.questions[0].Subdomains();\n            if (subdomain == \"exit\")\n            {\n              if (HasExit())\n              {\n                std::string s;\n                _exit_map.ForEachEntry([&s](const auto& range, const auto& exit) {\n                  fmt::format_to(std::back_inserter(s), \"{}={}; \", range, exit);\n                });\n                msg.AddTXTReply(std::move(s));\n              }\n              else\n              {\n                msg.AddNXReply();\n              }\n            }\n            else if (subdomain == \"netid\")\n            {\n              msg.AddTXTReply(fmt::format(\"netid={};\", RelayContact::ACTIVE_NETID));\n            }\n            else\n            {\n              msg.AddNXReply();\n            }\n          }\n          else\n          {\n            msg.AddNXReply();\n          }\n\n          reply(msg);\n        }\n        else if (msg.questions[0].qtype == dns::qTypeMX)\n        {\n          // mx record\n          service::Address addr;\n          if (addr.FromString(qname, \".loki\") || addr.FromString(qname, \".snode\")\n              || is_random_snode(msg) || is_localhost_loki(msg))\n          {\n            msg.AddMXReply(qname, 1);\n          }\n          else if (service::is_valid_name(sns_name))\n          {\n            lookup_name(\n                sns_name, [msg, sns_name, reply](std::string name_result, bool success) mutable {\n                  if (success)\n                  {\n                    msg.AddMXReply(name_result, 1);\n                  }\n                  else\n                    msg.AddNXReply();\n\n                  reply(msg);\n                });\n\n            return true;\n          }\n          else\n            msg.AddNXReply();\n          reply(msg);\n        }\n        else if (msg.questions[0].qtype == dns::qTypeCNAME)\n        {\n          if (is_random_snode(msg))\n          {\n            if (auto random = router().GetRandomGoodRouter())\n            {\n              msg.AddCNAMEReply(random->to_string(), 1);\n            }\n            else\n              msg.AddNXReply();\n          }\n          else if (msg.questions[0].IsLocalhost() and msg.questions[0].HasSubdomains())\n          {\n            const auto subdomain = msg.questions[0].Subdomains();\n            if (subdomain == \"exit\" and HasExit())\n            {\n              _exit_map.ForEachEntry(\n                  [&msg](const auto&, const auto& exit) { msg.AddCNAMEReply(exit.to_string(), 1);\n                  });\n            }\n            else\n            {\n              msg.AddNXReply();\n            }\n          }\n          else if (is_localhost_loki(msg))\n          {\n            size_t counter = 0;\n            context->ForEachService(\n                [&](const std::string&, const std::shared_ptr<service::Endpoint>& service) ->\n                bool {\n                  const service::Address addr = service->GetIdentity().pub.Addr();\n                  msg.AddCNAMEReply(addr.to_string(), 1);\n                  ++counter;\n                  return true;\n                });\n            if (counter == 0)\n              msg.AddNXReply();\n          }\n          else\n            msg.AddNXReply();\n          reply(msg);\n        }\n        */\n        /*else*/ if (msg.questions[0].qtype == dns::qTypeA || msg.questions[0].qtype == dns::qTypeAAAA)\n        {\n            auto reply_with_mapped_address = [reply, msg](const std::optional<ipv4>& maybe_ip) mutable {\n                if (maybe_ip)\n                {\n                    msg.add_IN_reply(maybe_ip->addr);\n                    reply(msg);\n                    return;\n                }\n                msg.add_nx_reply();\n                reply(msg);\n            };\n\n            const bool isV6 = msg.questions[0].qtype == dns::qTypeAAAA;\n            const bool isV4 = msg.questions[0].qtype == dns::qTypeA;\n            (void)isV6;\n            (void)isV4;\n            /*\n            if (isV6 && !ipv6_enabled)\n            {  // empty reply but not a NXDOMAIN so that client can retry IPv4\n              msg.AddNSReply(\"localhost.loki.\");\n            }\n            // on MacOS this is a typeA query\n            else if (is_random_snode(msg))\n            {\n              if (auto random = router().GetRandomGoodRouter())\n              {\n                msg.AddCNAMEReply(random->to_string(), 1);\n                return ReplyToSNodeDNSWhenReady(*random, std::make_shared<dns::Message>(msg),\n                isV6);\n              }\n\n              msg.AddNXReply();\n            }\n            */\n            /*else*/ if (is_localhost)\n            {\n                // FIXME: the code below checks about if we have a tun bound, and\n                // if we're operating as an exit (if that was requested), and those\n                // concepts need to be revived\n                /*\n                const bool lookingForExit = msg.questions[0].Subdomains() == \"exit\";\n                huint128_t ip = GetIfAddr();\n                if (ip.h)\n                {\n                  if (lookingForExit)\n                  {\n                    if (HasExit())\n                    {\n                      _exit_map.ForEachEntry(\n                          [&msg](const auto&, const auto& exit) { msg.AddCNAMEReply(exit.to_string());\n                          });\n                      msg.AddINReply(ip, isV6);\n                    }\n                    else\n                    {\n                      msg.AddNXReply();\n                    }\n                  }\n                  else\n                  {\n                    msg.AddCNAMEReply(our_name, 1);\n                    msg.AddINReply(ip, isV6);\n                  }\n                }\n                else\n                {\n                  msg.AddNXReply();\n                }\n                */\n\n                msg.add_CNAME_reply(our_name, 1);\n                msg.add_IN_reply(_local_net.ip.addr);\n                reply(msg);\n            }\n            else if (auto maybe_netaddr = try_making<NetworkAddress>(\"{}.{}\"_format(hostname, tld)))\n            {\n                // DNS lookup implies we want a session, so make one (NOP if we have one)\n                // This also means if we don't use that session the IP mapping will release when\n                // it expires, which it wouldn't otherwise without a tedious periodic check.\n                _router.session_endpoint().initiate_remote_session(*maybe_netaddr, nullptr);\n                reply_with_mapped_address(map(*maybe_netaddr));\n                return true;\n            }\n            else if (tld == \"loki\"sv)\n            {\n                _router.session_endpoint().resolve_sns(\n                    \"{}.loki\"_format(hostname),\n                    [this, reply, reply_with_mapped_address, msg](std::optional<NetworkAddress> maybe_netaddr) mutable {\n                        if (maybe_netaddr)\n                        {\n                            reply_with_mapped_address(map(*maybe_netaddr));\n                            return;\n                        }\n                        msg.add_nx_reply();\n                        reply(msg);\n                    });\n            }\n            /*\n            else if (addr.FromString(qname, \".loki\"))\n            {\n              if (isV4 && ipv6_enabled)\n              {\n                msg.hdr_fields |= dns::flags_QR | dns::flags_AA | dns::flags_RA;\n              }\n              else\n              {\n                return ReplyToLokiDNSWhenReady(addr, std::make_shared<dns::Message>(msg), isV6);\n              }\n            }\n            else if (addr.FromString(qname, \".snode\"))\n            {\n              if (isV4 && ipv6_enabled)\n              {\n                msg.hdr_fields |= dns::flags_QR | dns::flags_AA | dns::flags_RA;\n              }\n              else\n              {\n                return ReplyToSNodeDNSWhenReady(\n                    addr.as_array(), std::make_shared<dns::Message>(msg), isV6);\n              }\n            }\n            else if (service::is_valid_name(sns_name))\n            {\n              lookup_name(\n                  sns_name,\n                  [msg = std::make_shared<dns::Message>(msg),\n                   name = Name(),\n                   sns_name,\n                   isV6,\n                   reply,\n                   ReplyToDNSWhenReady](std::string name_result, bool success) mutable {\n                    if (not success)\n                    {\n                      log::warning(logcat, \"{} (ONS name: {}) not resolved\", name, sns_name);\n                      msg->AddNXReply();\n                      reply(*msg);\n                    }\n\n                    ReplyToDNSWhenReady(name_result, msg, isV6);\n                  });\n              return true;\n            }\n            else\n              msg.AddNXReply();\n\n            reply(msg);\n          }\n          else if (msg.questions[0].qtype == dns::qTypePTR)\n          {\n            // reverse dns\n            if (auto ip = dns::DecodePTR(msg.questions[0].qname))\n            {\n              if (auto maybe = ObtainAddrForIP(*ip))\n              {\n                var::visit([&msg](auto&& result) { msg.AddAReply(result.to_string()); }, *maybe);\n                reply(msg);\n                return true;\n              }\n            }\n\n            msg.AddNXReply();\n            reply(msg);\n            return true;\n          }\n          else if (msg.questions[0].qtype == dns::qTypeSRV)\n          {\n            auto srv_for = msg.questions[0].Subdomains();\n            auto name = msg.questions[0].qname;\n            if (is_localhost_loki(msg))\n            {\n              msg.AddSRVReply(intro_set().GetMatchingSRVRecords(srv_for));\n              reply(msg);\n              return true;\n            }\n            LookupServiceAsync(\n                name,\n                srv_for,\n                [reply, msg = std::make_shared<dns::Message>(std::move(msg))](auto records) {\n                  if (records.empty())\n                  {\n                    msg->AddNXReply();\n                  }\n                  else\n                  {\n                    msg->AddSRVReply(records);\n                  }\n                  reply(*msg);\n                });\n            return true;\n          }\n          else\n          {\n            msg.AddNXReply();\n            reply(msg);\n          }\n          return true;\n          */\n        }\n        return true;\n    }\n\n    bool TunEndpoint::supports_ipv6() const { return ipv6_enabled; }\n\n    // FIXME: pass in which question it should be addressing\n    bool TunEndpoint::should_hook_dns_message(const dns::Message& msg) const\n    {\n        // llarp::service::Address addr;\n        if (msg.questions.size() == 1)\n        {\n            /// hook every .loki\n            if (msg.questions[0].HasTLD(\".loki\"))\n                return true;\n            /// hook every .snode\n            if (msg.questions[0].HasTLD(\".snode\"))\n                return true;\n            // hook any ranges we own\n            if (msg.questions[0].qtype == llarp::dns::qTypePTR)\n            {\n                if (auto ip = dns::DecodePTR(msg.questions[0].qname))\n                {\n                    if (auto* v4 = std::get_if<ipv4>(&*ip))\n                        return _local_net.contains(*v4);\n                    if (_local_ipv6_net)\n                        return _local_ipv6_net->contains(std::get<ipv6>(*ip));\n                }\n                return false;\n            }\n        }\n        for (const auto& answer : msg.answers)\n        {\n            if (answer.HasCNameForTLD(\".loki\"))\n                return true;\n            if (answer.HasCNameForTLD(\".snode\"))\n                return true;\n        }\n        return false;\n    }\n\n    std::string TunEndpoint::get_if_name() const { return _if_name; }\n\n    const ipv4& TunEndpoint::get_ipv4() const { return _local_net.ip; }\n    const ipv6* TunEndpoint::get_ipv6() const { return _local_ipv6_net ? &_local_ipv6_net->ip : nullptr; }\n\n    const ipv4_net& TunEndpoint::get_ipv4_network() const { return _local_net; }\n\n    bool TunEndpoint::is_service_node() const { return _router.is_service_node; }\n\n    bool TunEndpoint::is_exit_node() const { return _router.is_exit_node(); }\n\n    bool TunEndpoint::stop()\n    {\n        // stop vpn tunnel\n        if (_net_if)\n            _net_if->Stop();\n        if (_raw_DNS)\n            _raw_DNS->Stop();\n\n        // save address map if applicable\n        if (_persisting_addr_file and not platform::is_android)\n        {\n            const auto& file = *_persisting_addr_file;\n            log::debug(logcat, \"{} saving address map to {}\", name(), file);\n            // if (auto maybe = util::OpenFileStream<std::filesystem::ofstream>(file, std::ios_base::binary))\n            // {\n            //   std::map<std::string, std::string> addrmap;\n            //   for (const auto& [ip, addr] : m_IPToAddr)\n            //   {\n            //     if (not m_SNodes.at(addr))\n            //     {\n            //       const service::Address a{addr.as_array()};\n            //       if (HasInboundConvo(a))\n            //         addrmap[ip.to_string()] = a.to_string();\n            //     }\n            //   }\n            //   const auto data = oxenc::bt_serialize(addrmap);\n            //   maybe->write(data.data(), data.size());\n            // }\n        }\n\n        if (_dns)\n            _dns->stop();\n\n        return true;\n    }\n\n    template <typename RangeIterator>\n    static std::optional<typename RangeIterator::ip_t> get_next_local_ipvX(\n        RangeIterator& rit,\n        const typename RangeIterator::ip_net_t& local_net,\n        address_map<typename RangeIterator::ip_t>& local_mapping)\n    {\n        // if our IP range is exhausted, we loop back around to see if any have been unmapped from terminated\n        // sessions; we only want to reset the iterator and loop back through once though\n        bool has_reset = false;\n\n        do\n        {\n            // this will be std::nullopt if IP range is exhausted OR the IP incrementing overflowed (basically\n            // equal)\n            if (auto maybe_next_ip = rit.next_ip())\n            {\n                if (not local_mapping.has_local(*maybe_next_ip))\n                    return maybe_next_ip;\n                // local IP is already assigned; try again\n                continue;\n            }\n\n            if (has_reset)\n                break;\n\n            log::debug(logcat, \"Resetting IP range iterator for range: {}...\", local_net);\n            rit.reset();\n            has_reset = true;\n        } while (true);\n\n        return std::nullopt;\n    }\n\n    std::optional<ipv4> TunEndpoint::get_next_local_ipv4()\n    {\n        return get_next_local_ipvX(_local_range_iterator, _local_net, _local_ipv4_mapping);\n    }\n\n    std::optional<ipv6> TunEndpoint::get_next_local_ipv6()\n    {\n        if (!_local_ipv6_range_iterator || !_local_ipv6_net)\n            return std::nullopt;\n\n        return get_next_local_ipvX(*_local_ipv6_range_iterator, *_local_ipv6_net, _local_ipv6_mapping);\n    }\n\n    std::optional<ipv4> TunEndpoint::map(const NetworkAddress& remote)\n    {\n        std::optional<ipv4> ret = std::nullopt;\n\n        // first: check if we have a config value for this remote\n        if (auto maybe_ip = _local_ipv4_mapping.get_local(remote))\n        {\n            ret = maybe_ip;\n            log::debug(logcat, \"Local IP for session to remote ({}) pre-loaded from config: {}\", remote, *maybe_ip);\n        }\n        // Otherwise go find a new local IP for it\n        else if (auto maybe_next_ip = get_next_local_ipv4())\n        {\n            log::debug(logcat, \"Local IP for session to remote ({}) assigned: {}\", remote, *maybe_next_ip);\n            ret = maybe_next_ip;\n            _local_ipv4_mapping.insert_or_assign(*maybe_next_ip, remote);\n        }\n        else\n            log::error(logcat, \"TUN device failed to assign local private IP for remote: {}\", remote);\n\n        return ret;\n    }\n\n    void TunEndpoint::unmap(const NetworkAddress& remote)\n    {\n        if (_local_ipv4_mapping.has_remote(remote))\n        {\n            _local_ipv4_mapping.unmap(remote);\n            log::debug(logcat, \"TUN device unmapped session to remote: {}\", remote);\n        }\n        else\n            log::warning(logcat, \"TUN device could not unmap session (remote: {})\", remote);\n    }\n\n    // handles an outbound packet going OUT from user -> network\n    void TunEndpoint::handle_outbound_packet(IPPacket pkt)\n    {\n        ipv4 src, dest;\n        if (!pkt.is_ipv4())\n        {\n            if (pkt.is_ipv6())\n            {\n                log::debug(logcat, \"Dropping IPv6 packet: not yet supported\");\n                return;\n            }\n            log::debug(logcat, \"Dropping non-IP packet\");\n            return;\n        }\n\n        log::trace(logcat, \"outbound packet: {}: {}\", pkt.info_line(), buffer_printer{pkt.span()});\n\n        src = *pkt.source_ipv4();\n        dest = *pkt.dest_ipv4();\n\n        log::trace(logcat, \"src:{}, dest:{}\", src, dest);\n\n        if constexpr (llarp::platform::is_apple)\n        {\n            if (dest == _local_net.ip)\n            {\n                rewrite_and_send_packet(std::move(pkt), std::move(src), std::move(dest));\n                return;\n            }\n        }\n\n        // we pass `dest` because that is our local private IP on the outgoing IPPacket\n        if (auto maybe_remote = _local_ipv4_mapping.get_remote(dest))\n        {\n            auto& remote = *maybe_remote;\n            pkt.clear_addresses();\n\n            if (auto session = _router.session_endpoint().get_session(remote))\n            {\n                log::trace(\n                    logcat,\n                    \"Dispatching outbound {}B packet for session (remote: {}): {}\",\n                    pkt.size(),\n                    remote,\n                    pkt.info_line());\n                session->send_session_data_message(pkt.span(), pkt.protocol());\n            }\n            else\n            {\n                log::debug(logcat, \"No session for remote: {} for outbound packet, attempting to create one!\", remote);\n\n                // TODO FIXME: this lookup is not right to initiate a new lookup on each packet:\n                // rather, since we don't have a session, we need to initiate one, and let *it* do\n                // the lookup.\n                if (remote.client())\n                {\n                    _router.session_endpoint().lookup_client_intro(\n                        remote.router_id(),\n                        [this, remote, pkt = std::move(pkt)](std::optional<llarp::ClientContact> cc) mutable {\n                            if (cc)\n                            {\n                                log::debug(logcat, \"client intro for {} found: {}\", remote, *cc);\n                                auto s = _router.session_endpoint().initiate_remote_session(remote, nullptr);\n                                s->send_session_data_message(pkt.span(), pkt.protocol());\n                                return;\n                            }\n                            log::debug(logcat, \"It appears {} has no contact information available.\", remote);\n                            if (auto icmp = pkt.make_icmp_unreachable())\n                                send_packet_to_net_if(std::move(*icmp));\n                        });\n                }\n                else\n                {\n                    _router.session_endpoint().lookup_relay_contact(\n                        remote.router_id(),\n                        [this, remote, pkt = std::move(pkt)](std::optional<llarp::RelayContact> rc) mutable {\n                            if (rc)\n                            {\n                                log::debug(logcat, \"Relay contact for {} found: {}\", remote, *rc);\n                                auto s = _router.session_endpoint().initiate_remote_session(remote, nullptr);\n                                s->send_session_data_message(pkt.span(), pkt.protocol());\n                                return;\n                            }\n                            log::debug(logcat, \"It appears {} has no contact information available.\", remote);\n                            if (auto icmp = pkt.make_icmp_unreachable())\n                                send_packet_to_net_if(std::move(*icmp));\n                        });\n                }\n            }\n        }\n        else\n        {\n            log::trace(logcat, \"Could not find remote for route {}\", pkt.info_line());\n\n            // make ICMP unreachable\n            if (auto icmp = pkt.make_icmp_unreachable())\n                send_packet_to_net_if(std::move(*icmp));\n        }\n    }\n\n    std::optional<ipv4> TunEndpoint::obtain_src_for_ipv4_remote(const NetworkAddress& remote)\n    {\n        if (auto maybe_src = _local_ipv4_mapping.get_local(remote))\n            return maybe_src;\n\n        log::warning(logcat, \"Unable to find mapped IPv4 for inbound packet from remote {}\", remote);\n        return std::nullopt;\n    }\n    std::optional<ipv6> TunEndpoint::obtain_src_for_ipv6_remote(const NetworkAddress& remote)\n    {\n        if (auto maybe_src = _local_ipv6_mapping.get_local(remote))\n            return maybe_src;\n\n        log::warning(logcat, \"Unable to find mapped IPv6 for inbound packet from remote {}\", remote);\n        return std::nullopt;\n    }\n\n    void TunEndpoint::send_packet_to_net_if(IPPacket pkt)\n    {\n        _router.loop.call([this, pkt = std::move(pkt)]() mutable { _net_if->write_packet(std::move(pkt)); });\n    }\n\n    void TunEndpoint::rewrite_and_send_packet(IPPacket&& pkt, const ipv4& src, const ipv4& dest)\n    {\n        pkt.update_ipv4_address(src, dest);\n        send_packet_to_net_if(std::move(pkt));\n    }\n    void TunEndpoint::rewrite_and_send_packet(IPPacket&& pkt, const ipv6& src, const ipv6& dest)\n    {\n        pkt.update_ipv6_address(src, dest);\n        send_packet_to_net_if(std::move(pkt));\n    }\n\n    // FIXME: replace session_tag with packet type flag (uint8_t), because session_tag is definitely\n    // not the right thing.\n    // FIXME 2: we need separate flags for to-exit and from-exit\n    void TunEndpoint::handle_inbound_packet(IPPacket pkt, uint8_t type, NetworkAddress remote)\n    {\n        (void)type;              // TODO FIXME use this\n        bool to_exit = false;    // TODO FIXME\n        bool from_exit = false;  // TODO FIXME\n\n        if (to_exit)  // traffic exiting through this node\n        {\n            log::trace(logcat, \"inbound exit pkt for exit node: {}\", pkt.info_line());\n            if (not is_allowing_traffic(pkt))\n            {\n                log::warning(logcat, \"Dropping inbound exit packet: denied by local traffic policy\");\n                return;\n            }\n\n            if (pkt.is_ipv4())\n            {\n                if (auto src = obtain_src_for_ipv4_remote(remote))\n                    return rewrite_and_send_packet(std::move(pkt), *src, *pkt.dest_ipv4());\n            }\n            else\n            {\n                if (auto src = obtain_src_for_ipv6_remote(remote))\n                    return rewrite_and_send_packet(std::move(pkt), *src, *pkt.dest_ipv6());\n            }\n            return;\n        }\n\n        if (from_exit)  // return traffic coming back from an exit\n        {\n            log::trace(logcat, \"inbound return exit pkt: {}\", pkt.info_line());\n            if (pkt.is_ipv4())\n                return rewrite_and_send_packet(std::move(pkt), *pkt.source_ipv4(), _local_net.ip);\n            if (_local_ipv6_net)\n                return rewrite_and_send_packet(std::move(pkt), *pkt.source_ipv6(), _local_ipv6_net->ip);\n            return;\n        }\n\n        log::trace(logcat, \"inbound pkt to host: {}\", pkt.info_line());\n        if (pkt.is_ipv4())\n        {\n            if (auto src = obtain_src_for_ipv4_remote(remote))\n                return rewrite_and_send_packet(std::move(pkt), *src, _local_net.ip);\n        }\n        else if (_local_ipv6_net)\n        {\n            if (auto src = obtain_src_for_ipv6_remote(remote))\n                return rewrite_and_send_packet(std::move(pkt), *src, _local_ipv6_net->ip);\n        }\n    }\n\n    void TunEndpoint::start_poller()\n    {\n        _poller = std::make_unique<ev::FDPoller>(_router.loop, _net_if->PollFD(), [this] {\n            for (auto pkt = _net_if->read_next_packet(); not pkt.empty(); pkt = _net_if->read_next_packet())\n            {\n                log::trace(logcat, \"packet router receiving {}\", pkt.info_line());\n                _packet_router->handle_ip_packet(std::move(pkt));\n            }\n        });\n        log::debug(logcat, \"TUN successfully started FD poller!\");\n    }\n\n    bool TunEndpoint::is_allowing_traffic(const IPPacket& pkt) const\n    {\n        return _exit_policy ? _exit_policy->allow_ip_traffic(pkt) : true;\n    }\n\n    std::pair<std::optional<ipv4>, std::optional<ipv6>> TunEndpoint::get_mapped_ip(const NetworkAddress& addr)\n    {\n        return {_local_ipv4_mapping.get_local(addr), _local_ipv6_mapping.get_local(addr)};\n    }\n\n    TunEndpoint::~TunEndpoint() { log::trace(logcat, \"TunEndpoint::~TunEndpoint()\"); }\n\n}  // namespace llarp::handlers\n"
  },
  {
    "path": "llarp/handlers/tun.hpp",
    "content": "#pragma once\n\n#include \"tun_base.hpp\"\n\n#include <llarp/address/map.hpp>\n#include <llarp/dns/server.hpp>\n#include <llarp/ev/fd_poller.hpp>\n#include <llarp/net/ip_packet.hpp>\n#include <llarp/util/thread/threading.hpp>\n#include <llarp/vpn/packet_router.hpp>\n#include <llarp/vpn/platform.hpp>\n\nnamespace llarp::traffic_type\n{\n    constexpr uint8_t UDP = 0;\n    constexpr uint8_t TCP = 1;\n    constexpr uint8_t RAW = 2;\n    constexpr uint8_t TUNNELED_QUIC = 3;\n\n    inline constexpr bool is_valid(uint8_t t) { return t >= UDP && t <= TUNNELED_QUIC; }\n}  // namespace llarp::traffic_type\n\nnamespace llarp::handlers\n{\n    inline constexpr auto TUN = \"tun\"sv;\n    inline constexpr auto LOKI_RESOLVER = \"lokinet\"sv;\n\n    class TunEndpoint : public TunEPBase, public dns::Resolver_Base, public std::enable_shared_from_this<TunEndpoint>\n    {\n      public:\n        TunEndpoint(Router& r);\n        ~TunEndpoint() override;\n\n      private:\n        Router& _router;\n\n        /// dns subsystem for this endpoint\n        std::unique_ptr<dns::Server> _dns;\n\n        /// our local ip network\n        ipv4_net _local_net;\n        IPv4RangeIterator _local_range_iterator{_local_net};\n\n        std::optional<ipv6_net> _local_ipv6_net;\n        std::optional<IPv6RangeIterator> _local_ipv6_range_iterator;\n\n        /// Our local Network Address holding our network pubkey\n        NetworkAddress _local_netaddr;\n\n        /// list of strict connect addresses for hooks\n        // std::vector<IpAddress> _strict_connect_addrs;\n\n        /// use v6?\n        bool ipv6_enabled = false;\n\n        std::string _if_name;\n\n        std::shared_ptr<vpn::NetworkInterface> _net_if;\n        std::unique_ptr<ev::FDPoller> _poller;\n\n        std::shared_ptr<vpn::PacketRouter> _packet_router;\n\n        std::optional<net::ExitPolicy> _exit_policy = std::nullopt;\n\n        /// a file to load / store the ephemeral address map to\n        std::optional<std::filesystem::path> _persisting_addr_file = std::nullopt;\n        bool persist_addrs{false};\n\n        /// for raw packet dns\n        std::shared_ptr<vpn::PacketIO> _raw_DNS;\n\n      public:\n        vpn::NetworkInterface* get_vpn_interface() { return _net_if.get(); }\n\n        std::string_view name() const { return TUN; }\n\n        int rank() const override { return 0; }\n\n        std::string_view resolver_name() const override { return LOKI_RESOLVER; }\n\n        bool maybe_hook_dns(\n            const std::shared_ptr<dns::PacketSource>& source,\n            const dns::Message& query,\n            const quic::Address& to,\n            const quic::Address& from) override;\n\n        // Reconfigures DNS servers and restarts libunbound with the new servers.\n        void reconfigure_dns(std::vector<quic::Address> servers);\n\n        void configure();\n\n        std::string get_if_name() const;\n\n        // Returns the lokinet tun IPv4 address\n        const ipv4& get_ipv4() const;\n        // Returns the lokinet tun IPv6 address by pointer, or nullptr if ipv6 is not configured.\n        const ipv6* get_ipv6() const;\n\n        // Returns the lokinet tun IPv4 network; the address is set to this tun device's local\n        // address (i.e. the .1 address).\n        const ipv4_net& get_ipv4_network() const;\n\n        nlohmann::json ExtractStatus() const;\n\n        bool supports_ipv6() const;\n\n        bool should_hook_dns_message(const dns::Message& msg) const;\n\n        bool handle_hooked_dns_message(dns::Message query, std::function<void(dns::Message)> sendreply);\n\n        void tick_tun(std::chrono::milliseconds now);\n\n        bool stop();\n\n        bool is_service_node() const;\n\n        bool is_exit_node() const;\n\n        void setup_dns();\n\n        // INPROGRESS: new API\n        // Handles an outbound packet going OUT to the network\n        void handle_outbound_packet(IPPacket pkt);\n\n        void rewrite_and_send_packet(IPPacket&& pkt, const ipv4& src, const ipv4& dest);\n        void rewrite_and_send_packet(IPPacket&& pkt, const ipv6& src, const ipv6& dest);\n\n        // TESTNET: TODO: new inbound packet handling logic\n        void handle_inbound_packet(IPPacket pkt, uint8_t type, NetworkAddress remote) override;\n\n        // Handles an inbound packet coming IN from the network\n        // bool handle_inbound_packet(IPPacket pkt, NetworkAddress remote, bool is_exit_session, bool\n        // is_outbound_session);\n\n        // Obtains an available IPv4 address from the tun device and associates the given lokinet\n        // remote address with it.  If the mapping already exists, this returns the existing IP,\n        // otherwise it assigns a new one.  The association persists until unmapped.  Returns the\n        // mapped ipv4 address, or nullptr if one could not be assigned.\n        std::optional<ipv4> map(const NetworkAddress& remote) override;\n        // TODO:\n        // std::optional<ipv6> map_address_to_local_ipv6(const NetworkAddress& remote);\n\n        // Removes any mapped IP for the given remote from the tun IP map.\n        void unmap(const NetworkAddress& remote) override;\n\n        std::optional<net::ExitPolicy> get_exit_policy() const { return _exit_policy; }\n\n        /// ip packet against any exit policies we have\n        /// returns false if this traffic is disallowed by any of those policies\n        /// returns true otherwise\n        bool is_allowing_traffic(const IPPacket& pkt) const;\n\n        std::pair<std::optional<ipv4>, std::optional<ipv6>> get_mapped_ip(const NetworkAddress& addr);\n\n        const Router& router() const { return _router; }\n\n        Router& router() { return _router; }\n\n        void start_poller() override;\n\n        // Stores assigned IP's for each session in/out of this lokinet instance\n        //  - Reserved local addresses are directly pre-loaded from config\n        //  - Persisting address map is directly pre-loaded from config\n        address_map<ipv4> _local_ipv4_mapping;\n        address_map<ipv6> _local_ipv6_mapping;\n\n      private:\n        std::optional<ipv4> get_next_local_ipv4();\n        std::optional<ipv6> get_next_local_ipv6();\n\n        std::optional<ipv4> obtain_src_for_ipv4_remote(const NetworkAddress& remote);\n        std::optional<ipv6> obtain_src_for_ipv6_remote(const NetworkAddress& remote);\n\n        void send_packet_to_net_if(IPPacket pkt);\n    };\n\n}  // namespace llarp::handlers\n"
  },
  {
    "path": "llarp/handlers/tun_base.hpp",
    "content": "#pragma once\n\n#include <llarp/address/address.hpp>\n#include <llarp/address/types.hpp>\n#include <llarp/net/ip_packet.hpp>\n\nnamespace llarp::handlers\n{\n\n    // Abstract class for TUN handling.  This base interface exists so that embedded clients can be\n    // built without needing to compile any tun code at all.\n    class TunEPBase\n    {\n      public:\n        virtual ~TunEPBase() = default;\n\n        virtual void start_poller() = 0;\n\n        virtual std::optional<ipv4> map(const NetworkAddress& remote) = 0;\n        virtual void unmap(const NetworkAddress& remote) = 0;\n\n        virtual void handle_inbound_packet(IPPacket pkt, uint8_t type, NetworkAddress remote) = 0;\n    };\n\n}  // namespace llarp::handlers\n"
  },
  {
    "path": "llarp/link/connection.cpp",
    "content": "#include \"connection.hpp\"\n\n#include <llarp/util/logging.hpp>\n\nnamespace llarp::link\n{\n    static auto logcat = log::Cat(\"link_conn\");\n\n    Connection::Connection(std::shared_ptr<quic::Connection> c, std::shared_ptr<quic::BTRequestStream> s)\n        : conn{std::move(c)}, datagrams{conn->datagrams()}, control_stream{std::move(s)}\n    {}\n\n    void Connection::close_quietly()\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n        conn->set_close_quietly();\n        conn->close_connection();\n    }\n}  // namespace llarp::link\n"
  },
  {
    "path": "llarp/link/connection.hpp",
    "content": "#pragma once\n\n#include <oxen/quic/btstream.hpp>\n#include <oxen/quic/connection.hpp>\n#include <oxen/quic/datagram.hpp>\n\nnamespace llarp\n{\n    namespace quic = oxen::quic;\n\n    using quic::connection_closed_callback;\n    using quic::connection_established_callback;\n}  // namespace llarp\n\nnamespace llarp::link\n{\n    struct Connection\n    {\n        Connection(std::shared_ptr<quic::Connection> c, std::shared_ptr<quic::BTRequestStream> s);\n\n        std::shared_ptr<quic::Connection> conn;\n        std::shared_ptr<quic::Datagrams> datagrams;\n        std::shared_ptr<quic::BTRequestStream> control_stream;\n\n        bool is_inbound() const { return conn->is_inbound(); }\n\n        void close_quietly();\n    };\n}  // namespace llarp::link\n"
  },
  {
    "path": "llarp/link/endpoint.cpp",
    "content": "#include \"endpoint.hpp\"\n\n#include \"link_manager.hpp\"\n\n#include <llarp/nodedb.hpp>\n#include <llarp/util/time.hpp>\n\n#include <oxen/quic/btstream.hpp>\n#include <oxen/quic/connection_ids.hpp>\n#include <oxen/quic/context.hpp>  // TODO FIXME: can't construct an Endpoint without this!\n#include <oxen/quic/opt.hpp>\n#include <sodium/crypto_generichash_blake2b.h>\n\nnamespace llarp::link\n{\n    static auto logcat = log::Cat(\"link.endpoint\");\n\n    void relay_conn::set_conn(std::shared_ptr<link::Connection> c, bool is_inbound)\n    {\n        auto& ptr = is_inbound ? inbound : outbound;\n        if (ptr)\n            ptr->close_quietly();\n        ptr = std::move(c);\n\n        if (is_inbound ? not outbound or inbound_wins : not inbound or not inbound_wins)\n            conn = ptr.get();\n    }\n\n    void relay_conn::close_quietly(bool direction_inbound)\n    {\n        auto& to_close = direction_inbound ? inbound : outbound;\n        if (not to_close)\n            return;\n\n        to_close->close_quietly();\n        to_close.reset();\n\n        // Switch preferred conn to the other direction (will be nullptr if the other direction\n        // isn't established):\n        conn = (direction_inbound ? outbound : inbound).get();\n    }\n\n    void relay_conn::close_all_quietly()\n    {\n        if (inbound)\n            inbound->close_quietly();\n        if (outbound)\n            outbound->close_quietly();\n        inbound = outbound = nullptr;\n        conn = nullptr;\n    }\n\n    void relay_conn::close_redundant() { close_quietly(not inbound_wins); }\n\n    static std::vector<uint8_t> make_static_secret(\n        const Ed25519SecretKey& sk, std::string_view static_secret_key = \"Lokinet static shared secret key\"sv)\n    {\n        std::vector<uint8_t> secret;\n        secret.resize(32);\n\n        crypto_generichash_blake2b_state st;\n        crypto_generichash_blake2b_init(\n            &st, reinterpret_cast<const uint8_t*>(static_secret_key.data()), static_secret_key.size(), secret.size());\n        crypto_generichash_blake2b_update(&st, sk.data(), sk.size());\n        crypto_generichash_blake2b_final(&st, secret.data(), secret.size());\n\n        return secret;\n    }\n\n    Endpoint::Endpoint(Manager& lm)\n        : manager{lm},\n          router{lm.router},\n          loop{std::make_unique<quic::Loop>()},\n          tls_creds{\n              router.is_service_node ? quic::GNUTLSCreds::make_from_ed_keys(\n                                           {reinterpret_cast<const char*>(router.secret_key().data()), 32},\n                                           {reinterpret_cast<const char*>(router.id().data()), 32})\n                                     : quic::GNUTLSCreds::make_unauthenticated()}\n    {\n        std::optional<quic::opt::inbound_alpns> inbound_alpn;\n        if (router.is_service_node)\n            inbound_alpn.emplace({RELAY_ALPN, CLIENT_ALPN, BOOTSTRAP_ALPN});\n\n        endpoint = quic::Endpoint::endpoint(\n            *loop,\n            router.listen_addr(),\n            quic::opt::static_secret{make_static_secret(router.secret_key())},\n            [this](quic::Connection& conn) { on_conn_established(conn); },\n            [this](quic::Connection& conn, uint64_t ec) { on_conn_closed(conn, ec); },\n            [this](quic::datagram dgram) {\n                // Transfer handling to the router loop:\n                router.loop.call([this, msg = std::move(dgram).extract()]() mutable {\n                    manager.handle_session_message(std::move(msg));\n                });\n            },\n            inbound_alpn,\n            quic::opt::outbound_alpns{{router.is_service_node ? RELAY_ALPN : CLIENT_ALPN}},\n            quic::opt::enable_datagrams{quic::Splitting::ACTIVE}.queue_limit(2'000'000));\n\n        tls_creds->enable_outbound_0rtt(\n            [this](\n                const quic::RemoteAddress& remote, std::vector<unsigned char> data, std::chrono::sys_seconds expiry) {\n                RouterID rid;\n                if (remote.view_remote_key().size() != RouterID::SIZE)\n                {\n                    log::warning(\n                        logcat,\n                        \"Not storing 0RTT ticker: unexpected remote pubkey size {}\",\n                        remote.view_remote_key().size());\n                    return;\n                }\n                rid.assign(remote.view_remote_key().first<RouterID::SIZE>());\n                router.node_db().store_0rtt(rid, std::move(data), expiry);\n            },\n            [this](const quic::RemoteAddress& remote) {\n                std::optional<std::vector<unsigned char>> ret;\n                if (remote.view_remote_key().size() != RouterID::SIZE)\n                    return ret;\n                RouterID rid{remote.view_remote_key().first<RouterID::SIZE>()};\n                ret = router.node_db().extract_0rtt(rid);\n                return ret;\n            });\n\n        if (router.is_service_node)\n        {\n            tls_creds->enable_inbound_0rtt(0s, 48h);\n\n            tls_creds->request_client_keys([this](const std::span<const uint8_t> key, const std::string_view alpn) {\n                // NB: this code *must not* call_get into the router event loop, because there are\n                // lots of places that router call-get's into endloop.loop, and so any attempt to\n                // call-get the other direction is a recipe for deadlock.\n\n                if (alpn != RELAY_ALPN)\n                {\n                    // For non-relay (i.e. client or bootstrap) conns we don't need a key, but if\n                    // they provided one, it has to at least be of the correct size for an Ed25519\n                    // pubkey.\n                    if (key.empty() || key.size() == PubKey::SIZE)\n                        return true;\n\n                    log::warning(\n                        logcat,\n                        \"Rejecting incoming {} connection with invalid optional pubkey ({} bytes, expected {})\",\n                        alpn,\n                        key.size(),\n                        RouterID::SIZE);\n                    return false;\n                }\n\n                // Otherwise the incoming conn is from a relay: we only request client keys, but if\n                // the incoming connection is a relay (using the relay ALPN) then it must provide\n                // one:\n                if (key.size() != RouterID::SIZE)\n                {\n                    log::warning(\n                        logcat,\n                        \"Rejecting incoming {} connection with missing or invalid pubkey ({} bytes, expected {})\",\n                        alpn,\n                        key.size(),\n                        RouterID::SIZE);\n                    return false;\n                }\n                RouterID other{key.first<RouterID::SIZE>()};\n\n                if (other == router.id())\n                {\n                    log::error(\n                        logcat,\n                        \"Rejecting incoming relay connection from relay with our own key ({})\",\n                        other.to_network_address());\n                    return false;\n                }\n\n                if (router.node_db().is_registered(other))\n                    return true;\n\n                log::warning(logcat, \"Rejecting incoming relay connection from unregistered RID {}\", other);\n                return false;\n            });\n\n            endpoint->listen(\n                tls_creds,\n                quic::opt::idle_timeout{router.is_service_node ? RELAY_INBOUND_IDLE_TIMEOUT : CLIENT_IDLE_TIMEOUT});\n        }\n    }\n\n    void Endpoint::start_tickers()\n    {\n        if (router.is_service_node)\n            redundancy_ticker = router.loop.call_every(REDUNDANT_LINGER, [this] { close_redundant(); });\n    }\n\n    link::Connection* Endpoint::get_relay_conn(const RouterID& relay) const\n    {\n        return router.loop.call_get([this, relay]() -> link::Connection* {\n            if (router.is_service_node)\n            {\n                if (auto it = relay_conns.find(relay); it != relay_conns.end())\n                    return it->second.conn;\n            }\n            else if (auto it = client_conns.find(relay); it != client_conns.end())\n                return it->second.get();\n\n            return nullptr;\n        });\n    }\n\n    void Endpoint::close_redundant(std::chrono::milliseconds now)\n    {\n        for (auto it = relay_bidir.begin(); it != relay_bidir.end();)\n        {\n            auto& [rid, since] = *it;\n            if (now >= since + REDUNDANT_LINGER)\n            {\n                auto rcit = relay_conns.find(rid);\n                // If this assertion fails then something else in here isn't cleaning up properly:\n                assert(rcit != relay_conns.end() and rcit->second.inbound and rcit->second.outbound);\n                rcit->second.close_redundant();\n                it = relay_bidir.erase(it);\n            }\n            else\n                ++it;\n        }\n    }\n\n    link::Connection* Endpoint::get_client_conn(const RouterID& remote) const\n    {\n        if (router.is_service_node)\n            return nullptr;\n        return router.loop.call_get([this, remote]() -> link::Connection* {\n            if (auto itr = client_conns.find(remote); itr != client_conns.end())\n                return itr->second.get();\n            return nullptr;\n        });\n    }\n\n    void Endpoint::for_each_relay_conn(std::function<void(const RouterID&, link::Connection&)> func) const\n    {\n        assert(router.loop.inside());\n\n        if (manager.is_stopping)\n            return;\n\n        for (const auto& [rid, relay] : relay_conns)\n        {\n            assert(relay.conn);\n            func(rid, *relay.conn);\n        }\n    }\n\n    std::unordered_set<RouterID> Endpoint::get_current_relays(bool include_pending) const\n    {\n        std::unordered_set<RouterID> ret;\n\n        if (router.is_service_node)\n        {\n            for (auto& [rid, conn] : relay_conns)\n            {\n                assert(conn.conn);  // nullptr means something left a bad conn in here instead of clearing it out\n                ret.insert(rid);\n            }\n        }\n        else\n        {\n            for (auto& [rid, conn] : client_conns)\n            {\n                assert(conn);\n                ret.insert(rid);\n            }\n        }\n        if (include_pending)\n        {\n            for (auto& [rid, c] : pending_outbound)\n            {\n                assert(c);\n                ret.insert(rid);\n            }\n        }\n\n        return ret;\n    }\n\n    std::optional<quic::ipv4_net> Endpoint::unique_edge_range() const\n    {\n        std::optional<quic::ipv4_net> network;\n\n        auto mask = router.config().paths.unique_hop_netmask;\n        if (not mask or router.is_service_node)\n            return network;\n\n        for (auto& [rid, conn] : client_conns)\n        {\n            auto ip = conn->conn->remote().to_ipv4();\n            if (not network)\n                network = ip % mask;\n            else if (not network->contains(ip))\n            {\n                // There are at least two different networks\n                network.reset();\n                break;\n            }\n        }\n        return network;\n    }\n\n    bool Endpoint::connected_to_relay(const RouterID& relay, bool include_pending) const\n    {\n        if (router.is_service_node ? relay_conns.contains(relay) : client_conns.contains(relay))\n            return true;\n        if (include_pending and pending_outbound.contains(relay))\n            return true;\n        return false;\n    }\n\n    void Endpoint::shutdown()\n    {\n        log::debug(logcat, \"Closing all connections\");\n        for (auto& [rid, conn] : relay_conns)\n            conn.close_all_quietly();\n        relay_conns.clear();\n        relay_bidir.clear();\n\n        for (auto& conn : pending_outbound | std::views::values)\n            conn->close_quietly();\n        pending_outbound.clear();\n\n        for (auto& conn : client_conns | std::views::values)\n            conn->close_quietly();\n        client_conns.clear();\n\n        for (auto& conn : inbound_clients | std::views::values)\n            conn->close_quietly();\n        inbound_clients.clear();\n\n        log::debug(logcat, \"Closing quic endpoint\");\n        endpoint.reset();\n\n        log::info(logcat, \"Stopping network endpoint event loop\");\n        loop.reset();\n    }\n\n    std::array<int, 5> Endpoint::relay_connection_counts() const\n    {\n        if (not router.is_service_node)\n            return {0};\n        return router.loop.call_get([this] {\n            std::array<int, 5> result{0};\n            auto& [relays, out, in, pending, clients] = result;\n\n            relays = static_cast<int>(relay_conns.size());\n            clients = static_cast<int>(inbound_clients.size());\n            pending = static_cast<int>(pending_outbound.size());\n            for (const auto& c : std::views::values(relay_conns))\n            {\n                if (c.inbound)\n                    ++in;\n                if (c.outbound)\n                    ++out;\n            }\n\n            return result;\n        });\n    }\n\n    std::array<int, 2> Endpoint::client_connection_counts() const\n    {\n        return router.loop.call_get([this] {\n            return std::array{static_cast<int>(client_conns.size()), static_cast<int>(pending_outbound.size())};\n        });\n    }\n\n    int Endpoint::num_relay_conns(bool include_pending) const\n    {\n        return router.loop.call_get([this, &include_pending] {\n            int c;\n            if (router.is_service_node)\n            {\n                c = static_cast<int>(relay_conns.size());\n                if (include_pending)\n                    // Check each pending conn instead of just adding pending_outbound.count() because\n                    // we don't want to double count a relay if we have a pending outbound connection\n                    // to a relay with which we have already established an inbound connection:\n                    for (auto& [rid, conn] : pending_outbound)\n                        if (!relay_conns.contains(rid))\n                            c++;\n            }\n            else\n            {\n                // We are a client, and so *all* connections are outbound to a relay that we\n                // initiated: Unlike the above, we would never initiate to an already pending or\n                // already connected node, so don't have to worry about duplicates.\n                c = static_cast<int>(client_conns.size() + (include_pending ? pending_outbound.size() : 0));\n            }\n            return c;\n        });\n    }\n\n    std::pair<bool, quic::BTRequestStream*> Endpoint::ctrl_stream_impl(const RelayContact& rc)\n    {\n        assert(router.loop.inside());\n        std::pair<bool, quic::BTRequestStream*> result;\n        auto& [res_est, res_str] = result;\n\n        auto rid = rc.router_id();\n\n        if (auto* c = get_relay_conn(rid))\n        {\n            res_est = true;\n            res_str = c->control_stream.get();\n            return result;\n        }\n\n        res_est = false;\n        auto& pending = pending_outbound[rid];\n        if (!pending)\n        {\n            log::debug(logcat, \"Initiating new connection to send to {}\", rid.short_string());\n            auto conn = endpoint->connect(\n                quic::RemoteAddress{rid.to_view(), rc.addr()},\n                tls_creds,\n                quic::opt::keep_alive{router.is_service_node ? RELAY_OUTBOUND_KEEP_ALIVE : CLIENT_KEEP_ALIVE},\n                quic::opt::idle_timeout{router.is_service_node ? RELAY_OUTBOUND_IDLE_TIMEOUT : CLIENT_IDLE_TIMEOUT});\n\n            auto control_stream = make_control(*conn, rid, router.is_service_node ? RELAY_ALPN : CLIENT_ALPN);\n\n            pending = std::make_shared<Connection>(std::move(conn), std::move(control_stream));\n        }\n        res_str = pending->control_stream.get();\n\n        return result;\n    }\n\n    bool Endpoint::ensure_connection(const RelayContact& rc)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n        return ctrl_stream_impl(rc).first;\n    }\n\n    quic::BTRequestStream& Endpoint::control_stream_for(const RelayContact& rc)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n        return *ctrl_stream_impl(rc).second;\n    }\n\n    void Endpoint::send_command(\n        const RelayContact& rc,\n        std::string endpoint,\n        std::vector<std::byte> body,\n        std::function<void(quic::message)> response_handler)\n    {\n        if (manager.is_stopping)\n            return;\n\n        if (response_handler)\n            // Wrap the handler to transfer to the router loop for execution:\n            response_handler = [f = std::move(response_handler), &rloop = router.loop](quic::message m) mutable {\n                rloop.call([f = std::move(f), m = std::move(m)]() mutable { f(std::move(m)); });\n            };\n\n        control_stream_for(rc).command(std::move(endpoint), std::move(body), std::move(response_handler));\n    }\n\n    bool Endpoint::send_command(\n        const RouterID& rid,\n        std::string endpoint,\n        std::vector<std::byte> body,\n        std::function<void(quic::message)> response_handler)\n    {\n        if (manager.is_stopping)\n            return false;\n\n        auto* rc = router.node_db().get_rc(rid);\n        if (!rc)\n        {\n            log::error(\n                logcat, \"Unable to send {} control command to {}: RC not found\", endpoint, rid.to_network_address());\n            return false;\n        }\n\n        send_command(*rc, std::move(endpoint), std::move(body), std::move(response_handler));\n        return true;\n    }\n\n    bool Endpoint::send_command(\n        const quic::ConnectionID& client_cid,\n        std::string endpoint,\n        std::vector<std::byte> body,\n        std::function<void(quic::message)> response_handler)\n    {\n        assert(router.is_service_node);\n        if (manager.is_stopping)\n            return false;\n\n        auto it = inbound_clients.find(client_cid);\n        if (it == inbound_clients.end())\n        {\n            log::debug(\n                logcat, \"Unable to send {} control command to client[{}]: connection not found\", endpoint, client_cid);\n            return false;\n        }\n\n        auto& conn = it->second;\n        if (!conn || conn->conn->is_closing())\n        {\n            log::debug(\n                logcat, \"Unable to send {} control command to client[{}]: connection is closing\", endpoint, client_cid);\n            return false;\n        }\n\n        if (response_handler)\n            // Wrap the handler to transfer to the router loop for execution:\n            response_handler = [f = std::move(response_handler), &rloop = router.loop](quic::message m) mutable {\n                rloop.call([f = std::move(f), m = std::move(m)]() mutable { f(std::move(m)); });\n            };\n\n        conn->control_stream->command(std::move(endpoint), std::move(body), std::move(response_handler));\n        return true;\n    }\n\n    bool Endpoint::send_datagram(const RouterID& relay, std::vector<std::byte> body)\n    {\n        if (manager.is_stopping)\n            return false;\n\n        if (auto* conn = get_relay_conn(relay))\n        {\n            conn->datagrams->send(std::move(body));\n            return true;\n        }\n\n        // Unlike send_command, if we don't have an established connection yet then we simply\n        // drop this rather than trying to store it in a queue.\n        return false;\n    }\n\n    bool Endpoint::send_datagram(const quic::ConnectionID& cid, std::vector<std::byte> body)\n    {\n        if (manager.is_stopping)\n            return false;\n\n        auto it = inbound_clients.find(cid);\n        if (it == inbound_clients.end())\n            return false;\n\n        auto& conn = *it->second;\n        conn.datagrams->send(std::move(body));\n        return true;\n    }\n\n    std::shared_ptr<quic::BTRequestStream> Endpoint::make_control(\n        quic::Connection& conn, std::span<const unsigned char> remote_key, std::string_view alpn)\n    {\n        std::shared_ptr<quic::BTRequestStream> control_stream;\n\n        std::variant<RouterID, quic::ConnectionID> remote{conn.reference_id()};\n        if (alpn == RELAY_ALPN)\n        {\n            assert(remote_key.size() == RouterID::SIZE);\n            remote.emplace<RouterID>(remote_key.first<RouterID::SIZE>());\n        }\n\n        if (conn.is_inbound())\n        {\n            assert(router.is_service_node);\n            control_stream =\n                conn.template queue_incoming_stream<quic::BTRequestStream>([](quic::Stream&, uint64_t error_code) {\n                    log::warning(logcat, \"BTRequestStream closed unexpectedly (ec:{})\", error_code);\n                });\n\n            log::trace(logcat, \"Queued BTStream to be opened (ID:{})\", control_stream->stream_id());\n            assert(control_stream->stream_id() == 0);\n\n            if (alpn == BOOTSTRAP_ALPN)\n            {\n                assert(router.is_service_node);  // A client should never be receiving an *inbound*\n                                                 // bootstrap ALPN connection.\n                manager.register_bootstrap_commands(*control_stream);\n            }\n        }\n        else\n        {\n            control_stream = conn.open_stream<quic::BTRequestStream>(\n                [](quic::Stream&, uint64_t error_code) {\n                    log::warning(logcat, \"BTRequestStream closed unexpectedly (ec:{})\", error_code);\n                },\n                quic::opt::stream_notify);\n\n            log::trace(logcat, \"Opened BTStream (ID:{})\", control_stream->stream_id());\n        }\n\n        if (alpn != BOOTSTRAP_ALPN)\n            manager.register_commands(*control_stream, remote);\n\n        return control_stream;\n    }\n\n    static auto log_bs = log::Cat(\"bootstrap\");\n    void Endpoint::on_inbound_conn(\n        std::shared_ptr<quic::Connection> qconn, std::shared_ptr<quic::BTRequestStream> control)\n    {\n        assert(router.is_service_node);\n\n        std::optional<RouterID> rid;\n        if (not qconn->remote_key().empty())\n        {\n            assert(\n                qconn->remote_key().size() == RouterID::SIZE);  // Should have been checked in the key verify callback\n            rid.emplace(qconn->remote_key().first<RouterID::SIZE>());\n        }\n\n        auto alpn = qconn->selected_alpn();\n        if (alpn == BOOTSTRAP_ALPN)\n        {\n            log::debug(log_bs, \"New incoming bootstrap connection from {}\", qconn->remote());\n            return;\n        }\n\n        auto conn = std::make_shared<link::Connection>(std::move(qconn), std::move(control));\n        if (alpn == RELAY_ALPN)\n        {\n            assert(rid);  // key verification should have enforced this\n            auto [it, ins] = relay_conns.emplace(*rid, *rid < router.id());\n            auto& relcon = it->second;\n            assert(ins ? !relcon.conn : !!relcon.conn);\n            bool already_had_inbound{relcon.inbound};\n            relcon.set_conn(std::move(conn), true);\n            if (relcon.outbound)\n                relay_bidir[*rid] = llarp::time_now_ms();\n\n            log::debug(\n                logcat,\n                \"{} incoming relay connection from {} ({} outbound connection)\",\n                already_had_inbound ? \"Replaced existing\" : \"New\",\n                rid->to_network_address(true),\n                !relcon.outbound          ? \"no current\"\n                    : relcon.inbound_wins ? \"have redundant\"\n                                          : \"have more-preferred\");\n        }\n        else\n        {\n            // We are a service node, and so we use this container to holds client connections to us:\n            log::debug(logcat, \"New incoming client connection from {}\", conn->conn->remote());\n            auto& cid = conn->conn->reference_id();\n            inbound_clients.emplace(cid, std::move(conn));\n        }\n    }\n\n    void Endpoint::on_outbound_conn(std::shared_ptr<quic::Connection> qconn)\n    {\n        assert(qconn->remote_key().size() == RouterID::SIZE);\n        RouterID rid{qconn->remote_key().first<RouterID::SIZE>()};\n\n        assert(router.is_service_node == (qconn->selected_alpn() != CLIENT_ALPN));\n\n        auto pit = pending_outbound.find(rid);\n        if (pit == pending_outbound.end())\n        {\n            log::error(logcat, \"Internal error: outbound connection completed without a pending connection\");\n            return;\n        }\n\n        if (router.is_service_node)\n        {\n            auto [it, ins] = relay_conns.emplace(rid, rid < router.id());\n            auto& relcon = it->second;\n            assert(ins ? !relcon.conn : !!relcon.conn);\n            bool already_had_outbound{relcon.outbound};\n            if (already_had_outbound)\n                log::error(\n                    logcat, \"Internal error: duplicate outbound connection established, but that shouldn't happen\");\n\n            relcon.set_conn(std::move(pit->second), false);\n            if (relcon.inbound)\n                relay_bidir[rid] = llarp::time_now_ms();\n\n            log::debug(\n                logcat,\n                \"{} outbound connection to {} ({} inbound connection)\",\n                already_had_outbound ? \"Replaced existing\" : \"Established\",\n                rid.to_network_address(true),\n                !relcon.inbound           ? \"no current\"\n                    : relcon.inbound_wins ? \"have more-preferred\"\n                                          : \"have redundant\");\n        }\n        else\n        {\n            // we are the client; this container holds our client connections (to relays)\n            auto& cc = client_conns[rid];\n            bool replaced{cc};\n            if (replaced)\n            {\n                log::error(\n                    logcat, \"Internal error: duplicate outbound connection established, but that shouldn't happen\");\n                cc->close_quietly();\n            }\n            cc = std::move(pit->second);\n            log::debug(\n                logcat,\n                \"{} connection to {}\",\n                replaced ? \"Replaced existing\" : \"Established\",\n                rid.to_network_address(true));\n        }\n\n        pending_outbound.erase(pit);\n\n        if (not router.is_service_node)\n            router.on_edge_conn_change();\n    }\n\n    void Endpoint::on_conn_established(quic::Connection& conn)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        std::shared_ptr<quic::BTRequestStream> inbound_cstream;\n        if (conn.is_inbound())\n        {\n            // We have to set up the control stream here, before the router loop transfer below,\n            // because the stream must be queued before stream data gets processed (which could\n            // happen immediately after this method call returns) so that we don't accidentally end\n            // up with a plain Stream for the stream id rather than a BTRequestStream.\n            inbound_cstream = make_control(conn, conn.remote_key(), conn.selected_alpn());\n        }\n\n        router.loop.call([this, weak = conn.weak_from_this(), inbound_cstream = std::move(inbound_cstream)]() mutable {\n            auto conn = weak.lock();\n            if (not conn)\n            {\n                log::warning(logcat, \"Connection died before connection open callback execution!\");\n                return;\n            }\n\n            if (conn->is_inbound())\n                on_inbound_conn(std::move(conn), std::move(inbound_cstream));\n            else\n                on_outbound_conn(std::move(conn));\n        });\n    }\n\n    void Endpoint::on_conn_closed(quic::Connection& conn, uint64_t ec)\n    {\n        auto alpn = conn.selected_alpn();\n        if (alpn == BOOTSTRAP_ALPN)\n        {\n            assert(conn.is_inbound());  // Outbound bs conns should not use this callback\n\n            // These are untracked, so don't need to enter the below cleanup code.\n            log::debug(logcat, \"bootstrap connection closed, ec={}\", ec);\n            return;\n        }\n\n        if (alpn == RELAY_ALPN && conn.remote_key().size() != RouterID::SIZE)\n        {\n            log::debug(logcat, \"Rejected inbound connection (ec={}), nothing to do\", ec);\n            return;\n        }\n\n        router.loop.call([this, connptr = conn.shared_from_this(), ec] {\n            auto& conn = *connptr;\n            auto alpn = conn.selected_alpn();\n\n            std::optional<RouterID> rid;\n            if (conn.remote_key().size() == RouterID::SIZE)\n                rid.emplace(conn.remote_key().first<RouterID::SIZE>());\n\n            bool found = false;\n\n            if (alpn == RELAY_ALPN)\n            {\n                assert(router.is_service_node);\n                assert(rid);\n                if (auto it = relay_conns.find(*rid); it != relay_conns.end())\n                {\n                    assert(router.is_service_node);\n                    auto& relcon = it->second;\n                    if (relcon.inbound && connptr == relcon.inbound->conn)\n                    {\n                        relcon.close_quietly(true);\n                        found = true;\n                        log::debug(\n                            logcat, \"Inbound connection from {} closed (ec={})\", rid->to_network_address(true), ec);\n                    }\n                    if (relcon.outbound && connptr == relcon.outbound->conn)\n                    {\n                        relcon.close_quietly(false);\n                        found = true;\n                        log::debug(\n                            logcat, \"Outbound connection to {} closed (ec={})\", rid->to_network_address(true), ec);\n                    }\n                    if (!relcon.conn)\n                    {\n                        log::debug(logcat, \"No remaining relay connection with {}!\", rid->to_network_address(true));\n                        relay_conns.erase(it);\n                    }\n                    if (found)\n                    {\n                        relay_bidir.erase(*rid);\n                        return;\n                    }\n                }\n\n                if (auto it = pending_outbound.find(*rid); it != pending_outbound.end())\n                {\n                    log::debug(\n                        logcat,\n                        \"Pending relay connection to {} closed before establishing (ec={})\",\n                        rid->to_network_address(true),\n                        ec);\n                    pending_outbound.erase(it);\n                    found = true;\n                }\n            }\n            else if (alpn == CLIENT_ALPN)\n            {\n                if (router.is_service_node)\n                {\n                    assert(conn.is_inbound());  // Relays do make outbound client conns for testing,\n                                                // but they do not use this close callback.\n                    if (auto it = inbound_clients.find(conn.reference_id()); it != inbound_clients.end())\n                    {\n                        log::debug(logcat, \"Client connection from {} closed (ec={})\", conn.remote(), ec);\n                        it->second->close_quietly();\n                        inbound_clients.erase(it);\n                        found = true;\n                    }\n                }\n                else\n                {\n                    assert(conn.is_outbound());\n\n                    if (auto it = client_conns.find(*rid); it != client_conns.end() and connptr == it->second->conn)\n                    {\n                        log::debug(\n                            logcat,\n                            \"Closed connection to {} (ec={})\",\n                            rid->to_network_address(!router.is_service_node),\n                            ec);\n                        client_conns.erase(it);\n                        found = true;\n                    }\n                }\n            }\n            else if (conn.is_outbound())\n            {\n                // Unknown or empty ALPN -- this is an outbound conn that didn't establish (and thus\n                // didn't negotiate the ALPN):\n                assert(rid);  // Outbound conns start out with the target pubkey known\n                if (auto it = pending_outbound.find(*rid); it != pending_outbound.end() and connptr == it->second->conn)\n                {\n                    pending_outbound.erase(it);\n                    found = true;\n                }\n            }\n            else\n                assert(false);  // Somehow we have an *inbound* conn without an ALPN, but that should\n                                // never have been established in the first place.\n\n            if (!found)\n                log::warning(\n                    logcat,\n                    \"Closed untracked connection {} {} @ {} (cid={}, ec={})\",\n                    conn.is_inbound() ? \"from\" : \"to\",\n                    rid ? rid->to_network_address(true /* don't know! */).to_string() : \"\",\n                    conn.remote(),\n                    conn.reference_id(),\n                    ec);\n\n            if (not router.is_service_node)\n                router.on_edge_conn_change();\n        });\n    }\n\n    // Establish a no-creds, no-0rtt, bootstrap or relay-testing (client ALPN) connection.\n    static auto testcat = log::Cat(\"testing\");\n    std::pair<std::shared_ptr<quic::Connection>, std::shared_ptr<quic::BTRequestStream>> special_connect_impl(\n        quic::Endpoint& endpoint, const RelayContact& rc, std::string_view alpn)\n    {\n        std::pair<std::shared_ptr<quic::Connection>, std::shared_ptr<quic::BTRequestStream>> ret;\n        auto& [conn, control] = ret;\n\n        bool bs = alpn == BOOTSTRAP_ALPN;\n\n        log::debug(\n            logcat,\n            \"Initiating new {} connection to {} @ {}\",\n            alpn,\n            rc.router_id().to_network_address(true),\n            rc.addr());\n\n        conn = endpoint.connect(\n            quic::RemoteAddress{rc.router_id().to_view(), rc.addr()},\n            quic::opt::idle_timeout{BOOTSTRAP_IDLE_TIMEOUT},\n            quic::opt::outbound_alpn(alpn),\n            [bs](quic::Connection& conn) {\n                log::debug(\n                    bs ? logcat : testcat,\n                    \"Successfully connected to {} {} @ {}\",\n                    bs ? \"bootstrap\" : \"testee\",\n                    RouterID{conn.remote_key().first<32>()}.to_network_address(true),\n                    conn.remote());\n            },\n            [bs](quic::Connection& conn, uint64_t ec) {\n                if (ec)\n                    log::log(\n                        bs ? logcat : testcat,\n                        bs ? log::Level::warn : log::Level::debug,\n                        \"{} while connecting to {} {} @ {}\",\n                        ec == static_cast<uint64_t>(NGTCP2_ERR_HANDSHAKE_TIMEOUT)\n                            ? \"Connection timeout\"\n                            : \"An error occurred (ec={})\"_format(ec),\n                        bs ? \"bootstrap\" : \"testee\",\n                        RouterID{conn.remote_key().first<32>()}.to_network_address(true),\n                        conn.remote());\n                else\n                    log::debug(\n                        bs ? logcat : testcat,\n                        \"Connection to {} {} closed.\",\n                        bs ? \"bootstrap\" : \"testee\",\n                        RouterID{conn.remote_key().first<32>()}.to_network_address(true));\n            });\n\n        control = conn->open_stream<quic::BTRequestStream>();\n\n        return ret;\n    }\n\n    std::pair<std::shared_ptr<quic::Connection>, std::shared_ptr<quic::BTRequestStream>> Endpoint::bootstrap_connect(\n        const RelayContact& rc)\n    {\n        return special_connect_impl(*endpoint, rc, BOOTSTRAP_ALPN);\n    }\n\n    std::pair<std::shared_ptr<quic::Connection>, std::shared_ptr<quic::BTRequestStream>>\n    Endpoint::testing_client_connect(const RelayContact& rc)\n    {\n        return special_connect_impl(*endpoint, rc, CLIENT_ALPN);\n    }\n\n}  // namespace llarp::link\n"
  },
  {
    "path": "llarp/link/endpoint.hpp",
    "content": "#pragma once\n\n#include \"connection.hpp\"\n\n#include <llarp/contact/relay_contact.hpp>\n#include <llarp/contact/router_id.hpp>\n#include <llarp/util/time.hpp>\n\n#include <oxen/quic/btstream.hpp>\n#include <oxen/quic/connection_ids.hpp>\n#include <oxen/quic/endpoint.hpp>\n#include <oxen/quic/gnutls_crypto.hpp>\n\n#include <array>\n#include <chrono>\n#include <memory>\n\nnamespace llarp\n{\n    class Router;\n}  // namespace llarp\n\nnamespace llarp::link\n{\n    class Manager;\n\n    // How long we wait before closing the less-preferred redundant connection when we have\n    // bidirection connections between two routers.  Once both sides have both directions, they\n    // mutually determine the \"winner\" and send all future traffic on the winner connection.  This\n    // buffer is to allow any messages or data to be handled that might have been send down the\n    // less-preferred connection before both directions were established.\n    //\n    // We also use this value as a ticker interval, so redundant connections can stay alive up to\n    // twice this value.\n    inline constexpr auto REDUNDANT_LINGER = 20s;\n\n    // Stores relay-to-relay connections.  In order to not lose stream messages, we temporarily\n    // allow simultaneous connections in both directions between a pair of relays, but then\n    // after a timeout, both sides choose the same winner and drop the other one.  The timeout\n    // ensures that if we race to establish that we don't prematurely close while stream data is\n    // still in flight (i.e. before both sides have aligned to the winning connection stream).\n    struct relay_conn\n    {\n        // Constructor: takes an argument that is true if the inbound connection should take\n        // precedence over the outbound conn when we have both, which is generally performed by\n        // comparing rounter IDs (so as to be consistent on both sides).\n        explicit relay_conn(bool inbound_wins) : inbound_wins{inbound_wins} {}\n\n        bool inbound_wins;\n        std::shared_ptr<link::Connection> inbound;\n        std::shared_ptr<link::Connection> outbound;\n\n        // Pointer to the current preferred connection, or nullptr if there is no current\n        // connection:\n        link::Connection* conn = nullptr;\n\n        // Sets the appropriate inbound/outbound pointer and, if this is the only or the winning\n        // connection, also sets it to `conn`.  If the existing inbound/outbound pointer is\n        // already set, it is quietly closed before being replaced.\n        void set_conn(std::shared_ptr<link::Connection> c, bool is_inbound);\n\n        // Closes either the inbound or outbound connection and drops it from this instance.  If\n        // the other connection still exists then `conn` is updated to point at it, otherwise it\n        // is set to nullptr.  Does nothing if the indicated connection is already closed.\n        void close_quietly(bool direction_inbound);\n\n        // Closes all connections, in both directions (if opened).\n        void close_all_quietly();\n\n        // Closes the \"loser\" connection, if this instance has connections in both directions.\n        void close_redundant();\n    };\n\n    class Endpoint\n    {\n      public:\n        explicit Endpoint(Manager& lm);\n\n        Manager& manager;\n        Router& router;\n\n      private:\n        // Stores established relay-to-relay connections; only used by service nodes.\n        std::unordered_map<RouterID, relay_conn> relay_conns;\n\n        // Stores keys of relay_conns of any relays with which we have bidirectional connections\n        // that will need closing of the less-preferred connection (after a timeout).  The value is\n        // when the latest connection was stored (used for allowing a safety margin before closing\n        // the redundant one).\n        std::unordered_map<RouterID, std::chrono::milliseconds> relay_bidir;\n\n        // Stores not-yet-established outbound connections to relays.  When the connection\n        // established, it is removed from here and inserted into `client_conns` (clients) or\n        // `relay_conns` (relays).\n        std::unordered_map<RouterID, std::shared_ptr<link::Connection>> pending_outbound;\n\n        // Stores established client-to-relay connections (i.e. outbound edge connections).  Client\n        // only.\n        std::unordered_map<RouterID, std::shared_ptr<link::Connection>> client_conns;\n\n        // Stores established inbound client-to-relay connections (i.e. edge connections).  Relay\n        // only.\n        std::unordered_map<quic::ConnectionID, std::shared_ptr<link::Connection>> inbound_clients;\n\n        std::unique_ptr<quic::Loop> loop;\n        std::shared_ptr<quic::Endpoint> endpoint;\n        std::shared_ptr<quic::Ticker> redundancy_ticker;\n        std::shared_ptr<quic::GNUTLSCreds> tls_creds;\n\n      public:\n        void start_tickers();\n\n        // Returns the connection to the given relay.  If there are established connections in both\n        // directions (i.e. when running as a relay), this returns the mutually preferred one.\n        // Returns nullptr if there is no established connection with the given relay at all.\n        link::Connection* get_relay_conn(const RouterID& relay) const;\n\n        // Drops any redundant connections, i.e. where connections between two relays are\n        // established in both directions and sufficient time has passed so ensure that all messages\n        // are flowing on the mutually preferred connection.\n        void close_redundant(std::chrono::milliseconds now = llarp::time_now_ms());\n\n        // Returns an established client->relay connection, if one exists.  Client only.  Returns\n        // nullptr if there is no current established connection to the given relay.\n        link::Connection* get_client_conn(const RouterID& rid) const;\n\n        // Returns a set of all relays with established (or pending, if `include_pending`)\n        // connections.\n        std::unordered_set<RouterID> get_current_relays(bool include_pending = false) const;\n\n        // Returns true if there is an established (or pending, if `include_pending`) connection to\n        // the given RouterID.\n        bool connected_to_relay(const RouterID& relay, bool include_pending = false) const;\n\n        // Returns 5-element array of relay connection counts.  (All values are 0 if this is called\n        // on a client instance):\n        // - number of relays with established connections.  Note that this counts each relay only\n        //   once, i.e. if there are two bidirectional connections between two relays, it is not\n        //   counted here twice.  Note that this also means this can be smaller than the sum of the\n        //   next two values.\n        // - number of established outbound relay-to-relay connections.\n        // - number of inbound relay-to-relay connections.\n        // - number of pending outbound connections (i.e. to other relays).\n        // - number of incoming connections from clients.\n        std::array<int, 5> relay_connection_counts() const;\n\n        // Returns the number of client->relay connections.  The first value is the number of\n        // established connections, the second is the number of pending connections.\n        std::array<int, 2> client_connection_counts() const;\n\n        // Returns the number of relays we are connected to (for relays: relay-to-relay connections,\n        // for clients this is simply the number of connections as all connections are to relays).\n        // Does not double count relays (i.e. if connections exist in both directions, for\n        // relay-to-relay connections).\n        //\n        // If `include_pending` is true then pending connections to relays are always included in\n        // the count (but, as above, without double-counting relays in case the pending connection\n        // would become a parallel connection).\n        int num_relay_conns(bool include_pending = false) const;\n\n        // If all current established outbound client-to-relay edge connections share the same\n        // distinct IP range (according to the unique-hop-mask config setting) then this returns\n        // that range.  If there are multiple ranges (or no established connections, or the unique\n        // range setting is disabled) then this returns nullopt.\n        //\n        // This is used when selecting a random terminus when constructing a new inbound path to\n        // avoid selecting a terminus that would be forced to violate the unique range setting\n        // because of the lack of distinct available edge connection IP networks.\n        //\n        // This method is only meaningful for clients; relays always return std::nullopt.\n        std::optional<quic::ipv4_net> unique_edge_range() const;\n\n        //        bool establish_connection(\n        //            quic::RemoteAddress remote,\n        //            RouterID rid,\n        //            quic::connection_established_callback on_open = nullptr,\n        //            quic::connection_closed_callback on_close = nullptr);\n\n        // If there is no existing or pending connection to the given relay, initiates a new\n        // outbound connection to it, otherwise does nothing.  Returns true a with the remote is\n        // already established, false if it was initiated by this call or was already pending.\n        bool ensure_connection(const RelayContact& rc);\n\n        // Returns a reference to the control stream currently in use to send commands to the given\n        // relay.  If no connection exists yet with that relay, a new one is constructed (and so the\n        // returned control stream might be on an not-yet-established connection).\n        //\n        // You often do *not* want to use this directly, because commands invoked it on it have\n        // their callbacks fired in the network endpoint event loop rather than the router event\n        // loop; instead see control_command for a wrapper that transfers callback execution to the\n        // router loop.\n        quic::BTRequestStream& control_stream_for(const RelayContact& rc);\n\n        // Sends a command on the control stream with `rc`, initiating a new connection if needed to\n        // reach `rc`.  This is almost equivalent to `control_stream_for(rc).command(...)` except\n        // that the callback, when it fires, is wrapped and transferred to the router loop rather\n        // than executing in the endpoint event loop.\n        void send_command(\n            const RelayContact& rc,\n            std::string endpoint,\n            std::vector<std::byte> body,\n            std::function<void(quic::message)> response_handler);\n\n        // Same as above, but takes an relay router id and looks it up.  If lookup fails, returns\n        // false *without* calling the response handler.  Otherwise the message is sent to the\n        // remote and the response (when triggered) fires on the router loop.\n        bool send_command(\n            const RouterID& rid,\n            std::string endpoint,\n            std::vector<std::byte> body,\n            std::function<void(quic::message)> response_handler);\n\n        // Sends a command to a client (i.e. from an edge) on the control stream for the given\n        // incoming client connection ID.  Returns true if the message was queued, false if the\n        // connection is not valid.\n        bool send_command(\n            const quic::ConnectionID& client_cid,\n            std::string endpoint,\n            std::vector<std::byte> body,\n            std::function<void(quic::message)> response_handler);\n\n        // Calls one of the above, based on which thing `target` holds\n        bool send_command(\n            const std::variant<RouterID, quic::ConnectionID>& target,\n            std::string endpoint,\n            std::vector<std::byte> body,\n            std::function<void(quic::message)> response_handler)\n        {\n            return std::visit(\n                [&](const auto& tgt) {\n                    return send_command(tgt, std::move(endpoint), std::move(body), std::move(response_handler));\n                },\n                target);\n        }\n\n        // Send a data message (i.e. datagram) to the given relay, if connected.  Returns true if we\n        // were able to queue the datagram for sending, false otherwise (such as when there is no\n        // fully established connection to the given relay yet).  Unlike `control_command`, this\n        // does not initiate a new connection if there is not already one established.\n        bool send_datagram(const RouterID& relay, std::vector<std::byte> data);\n\n        // Sends a data message (i.e. datagram) on the given inbound client quic connection, if\n        // still connected.  Returns true if we were able to queue it for sending, false otherwise.\n        bool send_datagram(const quic::ConnectionID& client_cid, std::vector<std::byte> data);\n\n        // Calls one of the above, based on which thing `target` holds\n        bool send_datagram(const std::variant<RouterID, quic::ConnectionID>& target, std::vector<std::byte> data)\n        {\n            return std::visit([&](const auto& tgt) { return send_datagram(tgt, std::move(data)); }, target);\n        }\n\n        // Calls `func` for every relay connection.  If any relays have dual inbound/outbound\n        // connections, this is only called for the preferred direction.\n        void for_each_relay_conn(std::function<void(const RouterID&, link::Connection&)> func) const;\n\n        void close_connection(const RouterID& rid);\n\n        // Closes all connections and stops the network event loop\n        void shutdown();\n\n        // Makes a new connection to the given relay as a Lokinet bootstrap client (i.e. using the\n        // special bootstrapping ALPN, even if this node is a relay) *without* using an existing\n        // connection or tracking it in existing connections.  This is primarily used when\n        // bootstrapping to avoid having the connection get treated as a regular relay connection on\n        // either side of the connection.  The connection does not use keep-alive and is expected to\n        // be short lived.\n        //\n        // Returns the connection and the control stream through which a bfetch_rc command can be\n        // issued.  (This is the only command supported under the bootstrap ALPN).\n        std::pair<std::shared_ptr<quic::Connection>, std::shared_ptr<quic::BTRequestStream>> bootstrap_connect(\n            const RelayContact& rc);\n\n        // Makes a new connection to the given relay as a Lokinet client (i.e. using the client\n        // ALPN, even if this node is a relay) *without* using an existing connection or tracking it\n        // in existing connections.  This is primarily used for service node testing to ensure we\n        // can establish a new connection and avoid having the connection get treated as a regular\n        // relay connection on either side of the connection.  The connection does not use\n        // keep-alive and is expected to be short lived.\n        //\n        // Returns the connection and the control stream through which a ping command can be\n        // issued.\n        std::pair<std::shared_ptr<quic::Connection>, std::shared_ptr<quic::BTRequestStream>> testing_client_connect(\n            const RelayContact& rc);\n\n      private:\n        std::shared_ptr<quic::BTRequestStream> make_control(\n            quic::Connection& conn, std::span<const unsigned char> remote_key, std::string_view alpn);\n\n        void on_inbound_conn(std::shared_ptr<quic::Connection> conn, std::shared_ptr<quic::BTRequestStream> control);\n        void on_outbound_conn(std::shared_ptr<quic::Connection> conn);\n\n        void on_conn_established(quic::Connection& conn);\n\n        void on_conn_closed(quic::Connection& conn, uint64_t ec);\n\n        std::pair<bool, quic::BTRequestStream*> ctrl_stream_impl(const RelayContact& rc);\n    };\n\n}  // namespace llarp::link\n"
  },
  {
    "path": "llarp/link/link_manager.cpp",
    "content": "#include \"link_manager.hpp\"\n\n#include <llarp/constants/path.hpp>\n#include <llarp/contact/contactdb.hpp>\n#include <llarp/contact/router_id.hpp>\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/messages/common.hpp>\n#include <llarp/messages/dht.hpp>\n#include <llarp/messages/fetch.hpp>\n#include <llarp/messages/path.hpp>\n#include <llarp/messages/session.hpp>\n#include <llarp/nodedb.hpp>\n#include <llarp/path/path.hpp>\n#include <llarp/path/transit_hop.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/session/session.hpp>\n#include <llarp/util/bspan.hpp>\n#include <llarp/util/random.hpp>\n#include <llarp/util/time.hpp>\n#include <llarp/util/zstd.hpp>\n\n#include <nlohmann/json.hpp>\n#include <oxen/quic/btstream.hpp>\n#include <oxen/quic/connection_ids.hpp>\n#include <oxen/quic/context.hpp>\n#include <oxen/quic/opt.hpp>\n#include <oxenc/bt_producer.h>\n#include <sodium/crypto_generichash_blake2b.h>\n#include <sodium/randombytes.h>\n\n#include <algorithm>\n#include <chrono>\n#include <cstddef>\n#include <exception>\n#include <ranges>\n\n#ifndef LOKINET_EMBEDDED_ONLY\n#include <llarp/rpc/oxend_rpc.hpp>\n#endif\n\nnamespace llarp::link\n{\n    static auto logcat = llarp::log::Cat(\"link.manager\");\n\n    using session::session_tag;\n\n    // These requests come over a path (as a \"path_control\" request),\n    // we may or may not need to make a request to another relay,\n    // then respond (onioned) back along the path.\n    std::unordered_map<\n        std::string_view,\n        void (Manager::*)(std::span<const std::byte> payload, std::function<void(std::string)> respond)>\n        Manager::path_requests = {\n            {\"publish_cc\"sv, &Manager::handle_path_publish_cc},\n            {\"find_cc\"sv, &Manager::handle_path_find_cc},\n            {\"fetch_rcs\"sv, &Manager::handle_path_fetch_rcs},\n            {\"fetch_rids\"sv, &Manager::handle_path_fetch_router_ids},\n            {\"resolve_sns\"sv, &Manager::handle_path_resolve_sns},\n            {\"path_ping\"sv, &Manager::handle_path_ping}};\n\n    void Manager::handle_direct_request(\n        void (Manager::*handler)(std::span<const std::byte>, std::function<void(std::string)>, bool), quic::message m)\n    {\n        auto body_str = m.body<std::byte>();\n        auto resp = [msg = std::move(m)](std::string response) { msg.respond(response, false); };\n        (this->*handler)(body_str, resp, true);\n    }\n\n    void Manager::register_commands(quic::BTRequestStream& s, const std::variant<RouterID, quic::ConnectionID>& remote)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        s.register_handler(\"path_control\"s, [this](quic::message m) {\n            router.loop.call([this, msg = std::move(m)]() mutable { handle_path_control(std::move(msg)); });\n        });\n\n        s.register_handler(\"session_control\"s, [this](quic::message m) {\n            router.loop.call([this, msg = std::move(m)]() mutable { handle_path_session_control(std::move(msg)); });\n        });\n\n        if (not router.is_service_node)\n        {\n            log::trace(logcat, \"Registered all client-only BTStream commands!\");\n            return;\n        }\n\n        s.register_handler(\"path_build\"s, [this, remote](quic::message m) {\n            router.loop.call(\n                [this, remote, msg = std::move(m)]() mutable { handle_path_build(std::move(msg), remote); });\n        });\n\n        s.register_handler(\"fetch_rcs\"s, [this](quic::message m) {\n            router.loop.call([this, msg = std::move(m)]() mutable {\n                handle_direct_request(&Manager::handle_fetch_rcs, std::move(msg));\n            });\n        });\n\n        s.register_handler(\"gossip_rc\"s, [this](quic::message m) {\n            router.loop.call([this, msg = std::move(m)]() mutable { handle_gossip_rc(std::move(msg)); });\n        });\n\n        s.register_handler(\"publish_cc\"s, [this](quic::message m) {\n            router.loop.call([this, msg = std::move(m)]() mutable {\n                handle_direct_request(&Manager::handle_publish_cc, std::move(msg));\n            });\n        });\n\n        s.register_handler(\"find_cc\"s, [this](quic::message m) {\n            router.loop.call([this, msg = std::move(m)]() mutable {\n                handle_direct_request(&Manager::handle_find_cc, std::move(msg));\n            });\n        });\n\n        // Endpoint called to test connectivity by other relays during relay testing.  It simply\n        // replies with \"pong\" (we don't actually need a loop transfer here for the reply, but do it anyway so\n        // that ping requests check that our router loop isn't stuck).\n        s.register_handler(\"ping\"s, [this](quic::message m) {\n            router.loop.call([this, m = std::move(m)] {\n                m.respond(\"pong\");\n                router.on_test_ping();\n            });\n        });\n    }\n\n    void Manager::register_bootstrap_commands(quic::BTRequestStream& s)\n    {\n        s.register_handler(\"bfetch_rcs\"s, [this](quic::message m) {\n            router.loop.call([this, msg = std::move(m)]() mutable { handle_fetch_bootstrap_rcs(std::move(msg)); });\n        });\n\n        log::trace(logcat, \"Registered bootstrap commands for inbound bootstrap connection\");\n    }\n\n    Manager::Manager(Router& r) : router{r}, endpoint{*this} {}\n\n    // void Manager::close_connection(RouterID rid) { return ep->close_connection(rid); }\n\n#if 0\n    /*\n     * TODO FIXME - fix reachability logic (see router.cpp)\n     */\n    void Manager::test_reachability(\n        const RouterID& rid, connection_established_callback on_open, connection_closed_callback on_close)\n    {\n        if (auto rc = router.node_db().get_rc(rid))\n            connect_to(*rc, std::move(on_open), std::move(on_close));\n        else\n            log::warning(logcat, \"Could not find RelayContact for connection to rid:{}\", rid);\n    }\n#endif\n\n    void Manager::stop()\n    {\n        if (is_stopping.exchange(true))\n            return;\n\n        router.loop.call_get([this] { endpoint.shutdown(); });\n    }\n\n    Manager::~Manager() { stop(); }\n\n    // TODO: this\n    nlohmann::json Manager::extract_status() const { return {}; }\n\n    void Manager::connect_to_keep_alive(int num_conns)\n    {\n        auto rcs = router.node_db().get_n_random_edge_rcs(\n            num_conns, false /* shuffling not needed */, [this](const RelayContact& rc) {\n                return not router.link_endpoint().connected_to_relay(rc.router_id(), /*include_pending=*/true);\n            });\n\n        for (const auto* rc : rcs)\n            endpoint.ensure_connection(*rc);\n\n        if (rcs.empty())\n            log::debug(logcat, \"NodeDB query for {} edge RCs returned none\", num_conns);\n    }\n\n    int Manager::gossip_rc(const RelayContact& rc, const quic::ConnectionID* sender)\n    {\n        int count = 0;\n        endpoint.for_each_relay_conn([&rc, &sender, &count](const RouterID& rid, link::Connection& conn) {\n            // Don't gossip this to RC's origin, or back along the connection that sent it to us:\n            if (rid == rc.router_id() or (sender and *sender == conn.conn->reference_id()))\n                return;\n\n            conn.control_stream->command(\"gossip_rc\", rc.view());\n            ++count;\n        });\n\n        return count;\n    }\n\n    void Manager::handle_gossip_rc(quic::message m)\n    {\n        RelayContact rc;\n\n        try\n        {\n            rc = RelayContact{m.body(), router.netid()};\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"Invalid gossipped RC: {}\", e.what());\n            return;\n        }\n\n        if (router.node_db().verify_store_gossip_rc(rc))\n        {\n            log::debug(\n                logcat,\n                \"Received new or significantly updated RC for {}; gossipping to peers\",\n                rc.router_id().short_string());\n            gossip_rc(rc, &m.conn_rid());\n        }\n        else\n            log::debug(\n                logcat,\n                \"Received known or minor RC update for {}; not gossipping to peers\",\n                rc.router_id().short_string());\n    }\n\n    void Manager::handle_fetch_bootstrap_rcs(quic::message m)\n    {\n        // this handler should not be registered for clients\n        assert(router.is_service_node);\n        log::info(logcat, \"Handling bootstrap fetch request\");\n\n        std::vector<std::string_view> rcs;\n        rcs.push_back(\"l\"sv);\n        auto& src = router.node_db().get_known_rcs();\n\n        rcs.reserve(src.size());\n        auto now = llarp::time_now_ms();\n        for (const auto& rc : std::views::values(src))\n            if (not rc.is_expired(now))\n                rcs.push_back(rc.view());\n        rcs.push_back(\"e\"sv);\n\n        if (rcs.size() == 2)\n        {\n            m.respond(\"No RCs\", true);\n            return;\n        }\n\n        if (!compressor)\n            compressor.emplace();\n\n        // Our output is a dict containing a single key `z` which contains the zstd-compressed bytes\n        // of a bt-encoded list of all RCs.\n\n        std::vector<std::byte> response_raw;\n        // We don't know the size in advance, so write a dummy value, compress, then fill it in:\n        constexpr auto compress_prefix = \"d1:z999999999:\"sv;\n        try\n        {\n            response_raw = compressor->compress(rcs, zstd::compressor::DEFAULT_LEVEL, compress_prefix);\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"Bootstrap request RCs compression failed: {}\", e.what());\n            m.respond(\"Compress failed\", true);\n            return;\n        }\n\n        size_t comp_size = response_raw.size() - compress_prefix.size();\n\n#ifndef NDEBUG\n        size_t rcs_size = 0;\n        for (auto& rc : rcs)\n            rcs_size += rc.size();\n        log::debug(\n            logcat,\n            \"compressed RC list to {}B ({:.1f}% of raw {}B)\",\n            comp_size,\n            comp_size * 100.0 / rcs_size,\n            rcs_size);\n#endif\n\n        // Now we need to rewrite the actual `d1:ZNNN:` prefix with the correct NNN for the\n        // compressed data:\n        std::string actual = \"d1:Z{}:\"_format(comp_size);\n        assert(actual.size() <= compress_prefix.size());\n        // Our actual size is almost certainly shorter than the 999999999 value we used, so we skip\n        // however many leading bytes as needed to represent the proper final value without needing\n        // to shift the compressed data around in the buffer:\n        size_t skip = compress_prefix.size() - actual.size();\n        std::memcpy(response_raw.data() + skip, actual.data(), actual.size());\n        // Response dict terminator:\n        response_raw.push_back(std::byte{'e'});\n\n        m.respond(std::span{response_raw.data() + skip, response_raw.size() - skip});\n    }\n\n    void Manager::handle_fetch_rcs(\n        std::span<const std::byte> body,\n        std::function<void(std::string)> respond,\n        [[maybe_unused]] bool source_is_relay)\n    {\n        log::debug(logcat, \"Handling FetchRC request...\");\n        // this handler should not be registered for clients\n        assert(router.is_service_node);\n\n        std::unordered_set<RouterID> explicit_ids;\n\n        try\n        {\n            auto btdc = oxenc::bt_dict_consumer{body};\n            for (auto sublist = btdc.require<oxenc::bt_list_consumer>(\"x\"); !sublist.is_finished();)\n                explicit_ids.emplace(sublist.consume_span<uint8_t, 32>());\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"Exception handling RC Fetch request: {}\", e.what());\n            respond(messages::ERROR_RESPONSE);\n            return;\n        }\n\n        oxenc::bt_dict_producer btdp;\n        {\n            auto sublist = btdp.append_list(\"r\");\n\n            int count = 0;\n            for (const auto& rid : explicit_ids)\n            {\n                if (auto* maybe_rc = router.node_db().get_rc(rid))\n                {\n                    sublist.append_encoded(maybe_rc->view());\n                    ++count;\n                }\n            }\n            log::info(logcat, \"Returning {} RCs for FetchRC request...\", count);\n        }\n\n        respond(std::move(btdp).str());\n    }\n\n    void Manager::handle_path_fetch_rcs(std::span<const std::byte> body, std::function<void(std::string)> respond)\n    {\n        handle_fetch_rcs(std::move(body), std::move(respond), false);\n    }\n\n    void Manager::handle_path_fetch_router_ids(\n        [[maybe_unused]] std::span<const std::byte> body, std::function<void(std::string)> respond)\n    {\n        log::trace(logcat, \"Handling FetchRIDs request...\");\n        // this handler should not be registered for clients\n        assert(router.is_service_node);\n\n        auto known_rids = router.node_db().get_registered_relays();\n        oxenc::bt_dict_producer btdp;\n\n        {\n            auto btlp = btdp.append_list(\"r\");\n\n            for (const auto& rid : known_rids)\n                btlp.append(rid.to_view());\n        }\n\n        log::debug(logcat, \"Returning ALL ({}) locally held RIDs to FetchRIDs request!\", known_rids.size());\n        respond(std::move(btdp).str());\n    }\n\n    void Manager::handle_path_resolve_sns(std::span<const std::byte> body, std::function<void(std::string)> respond)\n    {\n#ifdef LOKINET_EMBEDDED_ONLY\n        throw std::logic_error{\"This lokinet is not a service node!\"};\n#else\n        log::trace(logcat, \"Received request to publish client contact!\");\n\n        std::string name_hash;\n\n        try\n        {\n            name_hash = ResolveSNS::deserialize(oxenc::bt_dict_consumer{body});\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"Exception: {}\", e.what());\n            return respond(messages::ERROR_RESPONSE);\n        }\n\n        assert(router.oxend());\n        router.oxend()->lookup_sns_hash(\n            name_hash, [respond = std::move(respond)](std::optional<EncryptedSNSRecord> maybe_enc) mutable {\n                if (maybe_enc)\n                {\n                    log::info(logcat, \"RPC lookup successfully returned encrypted SNS record!\");\n                    auto resp = ResolveSNS::serialize_response(*maybe_enc);\n                    // FIXME: eventually respond func should take a byte span or something, but\n                    //        string was easier for now\n                    respond(std::string{reinterpret_cast<const char*>(resp.data()), resp.size()});\n                }\n                else\n                {\n                    log::warning(logcat, \"RPC lookup could not find SNS registry!\");\n                    respond(ResolveSNS::NOT_FOUND);\n                }\n            });\n#endif\n    }\n\n    void Manager::handle_publish_cc(\n        std::span<const std::byte> body, std::function<void(std::string)> respond, bool source_is_relay)\n    {\n        log::trace(logcat, \"Received request to publish client contact!\");\n\n        EncryptedClientContact enc;\n        std::optional<int> location;\n\n        try\n        {\n            std::tie(enc, location) = PublishClientContact::deserialize(oxenc::bt_dict_consumer{body});\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"Exception: {}: payload: {}\", e.what(), buffer_printer{body});\n            return respond(messages::ERROR_RESPONSE);\n        }\n\n        if (enc.is_expired())\n        {\n            log::warning(logcat, \"Received expired EncryptedClientContact!\");\n            return respond(PublishClientContact::EXPIRED);\n        }\n\n        if (not router.is_service_node)\n        {\n            // If we aren't a service node then this message is presumably a pushed introset update\n            // pushed to us by someone who we should already have an outbound connection with.\n\n            // TODO FIXME: This previously included an optional \"i\" key containing the sender for\n            // these send-over-session messages, but that seems dumb because 1) it isn't\n            // authenticated, and 2) we should already *know* the sender based on the session the\n            // message arived on.\n\n            log::critical(logcat, \"TODO FIXME STAGENET TOTHINK: fix incoming session CC handling\");\n            respond(\"FIXME!\");\n\n#if 0\n            if (not sender.has_value())\n            {\n                log::warning(logcat, \"Received new EncryptedClientContact from path control with no sender!\");\n                // TODO FIXME - does this client-to-client push actually need a response?\n                return m.respond(messages::ERROR_RESPONSE, true);\n            }\n\n            NetworkAddress sender_addr{*sender, true};\n            auto session = router.session_endpoint().get_session(sender_addr);\n            if (!session || !session->is_outbound)\n            {\n                log::warning(logcat, \"Ignoring pushed ClientContact from {}: no outbound session found\", sender_addr);\n                return m.respond(messages::ERROR_RESPONSE, true);\n            }\n\n            auto intro = enc.decrypt(*sender);\n            if (not intro)\n                // error message already logged in decrypt(...) call\n                return m.respond(messages::ERROR_RESPONSE, true);\n\n            log::debug(logcat, \"Storing ClientContact for remote {}\", sender_addr);\n            router.contact_db().put_cc(std::move(enc));\n\n            // FIXME: this should probably come encrypted.  Need to encrypt it and also handle it here.\n\n            return m.respond(messages::OK_RESPONSE);\n#endif\n        }\n\n        auto cc_blind_pk = enc.key();\n\n        // These messages have two steps: the client sends each message down a path with a 0-3\n        // location value indicating which of the 4 closest locations it should be published to.\n        // The relay receiving it then determines the target relay (based on the input) and forwards\n        // it along.  (Or, if it got lucky and is the requested index, stores it directly).\n        //\n        // The forwarded step here does *not* include the position, and must be a relay-to-relay\n        // direct message: the receiver of this direct message stores it if they are in the top-4+1\n        // locations (the extra +1 is to allow for a slight amount of drift in positions, e.g. in\n        // case of races with oxen block changes or other stale data).\n        //\n        // This two-step process helps ensure that publishes work even if the client has an\n        // incomplete or outdated set of RCs, and doesn't require the client to build extra paths to\n        // the 4 publish locations.\n        auto closest_rids = router.node_db().find_many_closest_to(cc_blind_pk, path::CC_PUBLISH_LOCATIONS + 1);\n        if (closest_rids.size() < path::CC_PUBLISH_LOCATIONS)\n        {\n            respond(messages::serialize_status_response(\"No RCs available!\"));\n            return;\n        }\n\n        if (!source_is_relay)\n        {\n            if (!location || *location < 0 || *location >= path::CC_PUBLISH_LOCATIONS)\n            {\n                log::warning(\n                    logcat,\n                    \"Ignoring ECC publish from a client with {} publish index\",\n                    location ? \"invalid ({})\"_format(*location) : \"missing\");\n                respond(messages::serialize_status_response(\n                    location ? \"INVALID PUBLISH LOCATION\" : \"MISSING PUBLISH LOCATION\"));\n                return;\n            }\n\n            const auto& rid = closest_rids[*location];\n\n            if (rid == router.id())\n            {\n                // Special case: we *are* the intended location\n                router.contact_db().put_cc(std::move(enc));\n                respond(messages::OK_RESPONSE);\n                return;\n            }\n\n            log::debug(\n                logcat,\n                \"Received PublishClientContact (key: {}, index: {}); forwarding to {}\",\n                enc.key(),\n                *location,\n                rid);\n\n            endpoint.send_command(\n                rid,\n                \"publish_cc\",\n                PublishClientContact::serialize(std::move(enc)),\n                [respond = std::move(respond)](quic::message msg) mutable {\n                    log::info(\n                        logcat,\n                        \"Relayed PublishClientContact {}! Relaying response...\",\n                        msg                 ? \"SUCCEEDED\"\n                            : msg.timed_out ? \"timed out\"\n                                            : \"failed\");\n                    log::trace(logcat, \"Relayed PublishClientContact response: {}\", buffer_printer{msg.body()});\n                    respond(std::string{msg.body()});\n                });\n            return;\n        }\n\n        // Otherwise this was forwarded, so we store it if and only if we are one of the\n        // CC_PUBLISH_LOCATIONS closest locations, and we don't forward regardless.\n        //\n        // We don't require that we were strictly in the correct position that the client originally\n        // sent (and thus we don't even include the target location when forwarding), because an\n        // Oxen block update with a new or removed registration at just the wrong time could shift\n        // indices, and we still want to store it even if we shifted (e.g. from 3nd to 2nd).\n        for (auto& rid : closest_rids)\n            if (rid == router.id())\n            {\n                router.contact_db().put_cc(std::move(enc));\n                respond(messages::OK_RESPONSE);\n                return;\n            }\n\n        log::warning(\n            logcat, \"Ignoring forwarded CC publish: we are not in the top {} publish locations\", closest_rids.size());\n        respond(messages::ERROR_RESPONSE);\n        return;\n    }\n\n    void Manager::handle_path_publish_cc(std::span<const std::byte> body, std::function<void(std::string)> respond)\n    {\n        handle_publish_cc(std::move(body), std::move(respond), false);\n    }\n\n    void Manager::handle_find_cc(\n        std::span<const std::byte> body, std::function<void(std::string)> respond, bool source_is_relay)\n    {\n        log::trace(logcat, \"Received request to find client contact!\");\n\n        PubKey blinded_pubkey;\n        try\n        {\n            blinded_pubkey = FindClientContact::deserialize(oxenc::bt_dict_consumer{body});\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"Exception: {}\", e.what());\n            return respond(messages::ERROR_RESPONSE);\n        }\n\n        auto closest_rids = router.node_db().find_many_closest_to(blinded_pubkey, path::CC_PUBLISH_LOCATIONS);\n        if (closest_rids.size() < path::CC_PUBLISH_LOCATIONS)\n            return respond(messages::ERROR_RESPONSE);\n\n        // We don't provide the answer ourselves unless we are in the closest-4 set because it's\n        // possible we *were* in the closest 4 but then dropped out, but still have a stale record\n        // hanging around.\n        auto authoritative = std::ranges::count(closest_rids, router.id());\n        assert(authoritative <= 1);\n\n        if (authoritative)\n        {\n            // TODO FIXME: Do we want to send the requests off to other relays *even if* we have it,\n            // to double-check against other relays in case ours is stale?\n\n            if (auto maybe_cc = router.contact_db().get_encrypted_cc(blinded_pubkey))\n            {\n                log::info(\n                    logcat,\n                    \"Received FindClientContact request (key: {}); returning local EncryptedClientContact...\",\n                    blinded_pubkey);\n                auto resp = FindClientContact::serialize_response(*maybe_cc);\n                // FIXME: eventually respond func should take a byte span or something, but\n                //        string was easier for now\n                return respond(std::string{reinterpret_cast<const char*>(resp.data()), resp.size()});\n            }\n\n            log::debug(\n                logcat,\n                \"Received FindClientContact and we are authoritative, but don't have a matching CC for {}\",\n                blinded_pubkey);\n            // Don't return an error because we can still possibly forward it to other authoritative\n            // nodes, below, and it's perfectly possible for us not to have it if we missed it for\n            // various reasons.\n        }\n\n        // If the optional was nullopt, then this was a relay <-> relay request. As a result, we should NOT\n        // allow it to continue propagating\n        if (source_is_relay)\n        {\n            log::critical(\n                logcat,\n                \"Received relayed FindClientContact request (key: {}); could not find locally, relaying \"\n                \"error...\",\n                blinded_pubkey);\n            return respond(FindClientContact::NOT_FOUND);\n        }\n\n        auto remaining = std::make_shared<size_t>(closest_rids.size() - authoritative);\n        auto hook = [respond = std::move(respond), remaining](quic::message msg) mutable {\n            if (*remaining == 0)\n                return;  // Already answered by an earlier response\n\n            if (msg)\n            {\n                *remaining = 0;\n                log::info(logcat, \"Relayed FindClientContact request SUCCEEDED! Relaying response\");\n                log::trace(logcat, \"Relayed FindClientContact response: {}\", buffer_printer{msg.body()});\n                respond(std::string{msg.body()});\n                return;\n            }\n\n            if (--*remaining == 0)\n                return;  // This was an error, but there are more responses to come back\n\n            log::warning(logcat, \"All FindClientContact requests FAILED! Relaying failure\");\n            respond(msg.timed_out ? messages::TIMEOUT_RESPONSE : std::string{msg.body()});\n        };\n\n        log::debug(logcat, \"Relaying FindClientContactMessage (key: {}) to {} peers\", blinded_pubkey, *remaining);\n\n        auto forwarded_find_cc = FindClientContact::serialize(blinded_pubkey);\n        for (const auto& rid : closest_rids)\n        {\n            if (rid == router.id())\n                continue;\n            endpoint.send_command(rid, \"find_cc\", forwarded_find_cc, hook);\n        }\n    }\n\n    void Manager::handle_path_find_cc(std::span<const std::byte> body, std::function<void(std::string)> respond)\n    {\n        handle_find_cc(std::move(body), std::move(respond), false);\n    }\n\n    void Manager::handle_path_build(quic::message m, const std::variant<RouterID, quic::ConnectionID>& from)\n    {\n        if (!router.path_context.is_transit_allowed())\n        {\n            log::warning(logcat, \"got path build request when not permitting transit\");\n            return m.respond(PATH::BUILD::NO_TRANSIT, true);\n        }\n\n        try\n        {\n            auto frames_in = m.body<std::byte>();\n\n            if (frames_in.size() != path::BUILD_LENGTH * path::BUILD_FRAME_SIZE)\n            {\n                log::info(\n                    logcat,\n                    \"Ignoring path build with invalid length {} != expected {}*{}\",\n                    frames_in.size(),\n                    path::BUILD_LENGTH,\n                    path::BUILD_FRAME_SIZE);\n                m.respond(PATH::BUILD::BAD_FRAMES, true);\n                return;\n            }\n\n            auto now = llarp::time_now_ms();\n            auto [hop, dh_nonce] =\n                path::PathHandler::decrypt_build_frame(frames_in.first<path::BUILD_FRAME_SIZE>(), router, from, now);\n\n            if (hop->expiry > now + path::MAX_LIFETIME_ACCEPTED || hop->expiry <= now)\n                throw path::TransitHopError::INVALID_LIFETIME();\n\n            if (router.path_context.has_transit_hop(hop->rxid) || router.path_context.has_transit_hop(hop->txid))\n                throw path::TransitHopError::HOP_ID_UNAVAILABLE();\n\n            // we are terminal hop and everything is okay\n            if (hop->terminal_hop)\n            {\n                log::info(logcat, \"We are the terminal hop; path build succeeded\");\n                router.path_context.put_transit_hop(std::move(hop));\n                return m.respond(messages::OK_RESPONSE);\n            }\n\n            // rotate remaining frames forward\n            std::vector<std::byte> frames;\n            frames.resize(frames_in.size());\n            std::memcpy(\n                frames.data(), frames_in.data() + path::BUILD_FRAME_SIZE, frames_in.size() - path::BUILD_FRAME_SIZE);\n            // and then fill the frame at the end (where ours would rotate to) with random junk:\n            random_fill(std::span{frames}.last(path::BUILD_FRAME_SIZE));\n\n            // De-onion the remaining frames (not including the known junk frame at the end) for the next hop\n            crypto::xchacha20(\n                std::span{frames}.first((path::BUILD_LENGTH - 1) * path::BUILD_FRAME_SIZE),\n                hop->shared_secret,\n                dh_nonce ^ hop->xor_nonce);\n\n            const auto& upstream = hop->upstream;\n\n            endpoint.send_command(\n                upstream,\n                \"path_build\",\n                std::move(frames),\n                [this, hop = std::move(hop), prev_message = std::move(m)](quic::message m) mutable {\n                    if (m)\n                    {\n                        log::info(\n                            logcat,\n                            \"Upstream returned successful path build response; locally storing Hop ({}) and \"\n                            \"relaying\",\n                            *hop);\n                        router.path_context.put_transit_hop(std::move(hop));\n                        prev_message.respond(messages::OK_RESPONSE);\n                        return;\n                    }\n\n                    log::info(\n                        logcat, \"Upstream ({}) path build {}\", hop->upstream, m.timed_out ? \"timed out\" : \"failed\");\n\n                    if (m.is_error())\n                        prev_message.respond(m.body(), m.is_error());\n                    // else leave it unanswered so that it times out at the request origin\n                });\n        }\n        catch (const path::TransitHopError& e)\n        {\n            log::warning(logcat, \"An error occured during path build request handling: {}\", e.what());\n            return m.respond(messages::serialize_status_response(e.error_code), true);\n        }\n    }\n\n    void Manager::handle_path_control(quic::message m)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        auto body = m.body<std::byte>();\n        if (body.size() <= path::Path::ENCRYPT_PATH_MESSAGE_OVERHEAD_MAC)\n        {\n            log::warning(logcat, \"Received path control message too small to contain valid data.\");\n            m.respond(messages::ERROR_RESPONSE, true);\n            return;\n        }\n\n        HopID hop_id;\n        std::vector<std::byte> payload;\n        SymmNonce nonce;\n\n        payload.assign(body.begin(), body.end());\n\n        static_assert(\n            path::Path::ENCRYPT_PATH_MESSAGE_OVERHEAD_MAC == crypto::MAC_SIZE + SymmNonce::SIZE + HopID::SIZE + 1);\n        auto [inner_payload, bnonce, bhop, msgtype] = split_span_tail<SymmNonce::SIZE, HopID::SIZE, 1>(payload);\n        std::byte type = msgtype[0];\n        if (type != std::byte{0x01})\n        {\n            log::warning(logcat, \"Invalid/unknown path_control encrypted message type {}\", static_cast<int>(type));\n            log::trace(logcat, \"Failed path_control payload: {}\", buffer_printer{body});\n            m.respond(messages::ERROR_RESPONSE, true);\n            return;\n        }\n        nonce.assign(bnonce);\n        hop_id.assign(bhop);\n\n        auto hop = router.path_context.get_transit_hop_ptr(hop_id);\n        if (not hop)\n        {\n            log::warning(logcat, \"Received path control for unknown path (hop ID: {})\", hop_id);\n            m.respond(messages::ERROR_RESPONSE, true);\n            return;\n        }\n\n        nonce ^= hop->xor_nonce;\n\n        // if terminal hop, payload should contain a request (e.g. \"sns_resolve\"); handle and respond.\n        if (hop->terminal_hop)\n        {\n            auto payload_span = crypto::xchacha20_poly1305_decrypt(inner_payload, hop->shared_secret, nonce);\n            auto responder = [hop_weak = std::weak_ptr{hop}, msg = std::move(m), type](std::string response) {\n                auto hop = hop_weak.lock();\n                if (not hop)\n                {\n                    log::info(logcat, \"Received response to path control message, but no transit hop found; dropping.\");\n                    return;\n                }\n                auto& hopid = hop->rxid;\n                auto nonce = SymmNonce::make_random();\n                response.reserve(response.size() + path::Path::ENCRYPT_PATH_MESSAGE_OVERHEAD_MAC);\n                response.resize(response.size() + crypto::MAC_SIZE);\n                try\n                {\n                    crypto::xchacha20_poly1305_encrypt(response, hop->shared_secret, nonce);\n                }\n                catch (const std::exception& e)\n                {\n                    log::warning(logcat, \"Failed encrypting path control message response: {}\", e.what());\n                    return;\n                }\n                nonce ^= hop->xor_nonce;\n                auto inner_size = response.size();\n                response.resize(inner_size + path::Path::ENCRYPT_PATH_MESSAGE_OVERHEAD);\n                auto resp_span = oxen::quic::reinterpret_span<std::byte>(std::span{response});\n\n                static_assert(sizeof(SymmNonce) == SymmNonce::SIZE);\n                static_assert(sizeof(HopID) == HopID::SIZE);\n\n                auto [inner_payload, bnonce, bhop, msgtype] =\n                    split_span_tail<SymmNonce::SIZE, HopID::SIZE, 1>(resp_span);\n                assert(inner_payload.size() == inner_size);\n\n                nonce.copy_to(bnonce);\n                hopid.copy_to(bhop);\n                msgtype[0] = type;\n                msg.respond(response, false);\n            };\n            log::debug(logcat, \"We are terminal hop for path request: {}\", *hop);\n            handle_path_request(payload_span, std::move(responder));\n            return;\n        }\n\n        // intermediate hops chacha the whole payload (MAC included)\n        crypto::xchacha20(inner_payload, hop->shared_secret, nonce);\n\n        log::debug(logcat, \"We are intermediate hop for path request: {}\", *hop);\n\n        const auto [next_target, next_hopid] = hop->next_id(hop_id);\n\n        // We're relaying this message down a path, and we've already done our decryption to the\n        // inner_payload so now we just need to replace the nonce and next hop ID in the outer\n        // payload before passing it along:\n        nonce.copy_to(bnonce);\n        next_hopid.copy_to(bhop);\n\n        endpoint.send_command(\n            next_target,\n            \"path_control\",\n            std::move(payload),\n            [hop_weak = std::weak_ptr{hop}, prev_message = std::move(m), type](quic::message response) mutable {\n                auto hop = hop_weak.lock();\n                if (not hop)\n                {\n                    log::info(logcat, \"Received response to path control message, but no transit hop found; dropping.\");\n                    return;\n                }\n\n                if (response.timed_out)\n                {\n                    log::warning(logcat, \"Path control message response timed out\");\n                    // There's no real point in sending a failure response here because the\n                    // originator is using the same timeout and is going to time out right around\n                    // the same time, so any response we might sent isn't going to be useful (and\n                    // would be treated no differently than the originator hitting their own\n                    // timeout).\n                    return;\n                }\n\n                if (response)\n                    log::debug(logcat, \"Path control message returned successfully\");\n                else\n                {\n                    log::warning(logcat, \"Path control message returned an error!\");\n                    prev_message.respond(response.body(), response.is_error());\n                }\n\n                std::vector<std::byte> payload;\n\n                auto body = response.body<std::byte>();\n\n                if (body.size() <= path::Path::ENCRYPT_PATH_MESSAGE_OVERHEAD)\n                {\n                    prev_message.respond(messages::ERROR_RESPONSE, true);\n                    return;\n                }\n\n                payload.assign(body.begin(), body.end());\n                auto [inner_payload, bnonce, bhop, msgtype] = split_span_tail<SymmNonce::SIZE, HopID::SIZE, 1>(payload);\n                if (msgtype[0] != type)\n                {\n                    log::warning(\n                        logcat,\n                        \"Path control message response type byte mismatch!  Expected {}, got {}\",\n                        static_cast<int>(type),\n                        static_cast<int>(msgtype[0]));\n                    prev_message.respond(messages::ERROR_RESPONSE, true);\n                    return;\n                }\n                HopID recv_hopid;\n                SymmNonce nonce;\n                recv_hopid.assign(bhop);\n                nonce.assign(bnonce);\n                if (recv_hopid != hop->txid)\n                {\n                    log::warning(logcat, \"Path control message response type unexpected hop id...\");\n                    prev_message.respond(messages::ERROR_RESPONSE, true);\n                    return;\n                }\n\n                crypto::xchacha20(inner_payload, hop->shared_secret, nonce);\n\n                nonce ^= hop->xor_nonce;\n                nonce.copy_to(bnonce);\n                hop->rxid.copy_to(bhop);\n\n                prev_message.respond(payload, false);\n            });\n    }\n\n    void Manager::handle_path_session_control(quic::message m)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n        auto body = m.body<std::byte>();\n        if (body.size() <= path::Path::ENCRYPT_PATH_MESSAGE_OVERHEAD_MAC)\n        {\n            log::warning(logcat, \"Received session control message too small to contain valid data.\");\n            return;\n        }\n        std::vector<std::byte> payload;\n        payload.assign(body.begin(), body.end());\n        handle_session_message(std::move(payload), true);\n    }\n\n    // FIXME: overhead for session MAC?\n    static constexpr size_t MIN_PATH_DATA_MESSAGE_SIZE = 0 /*payload*/ + 1 /*packet type*/ + sizeof(HopID) /*pivot*/\n        + path::Path::ENCRYPT_PATH_MESSAGE_OVERHEAD /*nonce, hop, type*/;\n\n    // Removes the message type byte, HopID, and SymmNonce from the end of a path message, returning\n    // the hopid and nonce.  The vector is resized to drop the loaded values (and thus will contain\n    // only the onioned payload after this call).\n    //\n    // Warns and returns nullopt if the input vector is too short or the message type byte is\n    // invalid (and thus the message should be dropped).\n    static std::optional<std::tuple<HopID, SymmNonce, std::byte>> extract_path_message_metadata(\n        std::vector<std::byte>& message)\n    {\n        if (message.size() < MIN_PATH_DATA_MESSAGE_SIZE)\n        {\n            log::warning(logcat, \"Dropping invalid too-short path data message\");\n            return std::nullopt;\n        }\n\n        // Deliberately break compilation if data message overhead changes in path without getting\n        // updated here as well:\n        static_assert(path::Path::ENCRYPT_PATH_MESSAGE_OVERHEAD == SymmNonce::SIZE + HopID::SIZE + 1);\n\n        std::optional<std::tuple<HopID, SymmNonce, std::byte>> result;\n        auto& [hop_id, nonce, msgtype] = result.emplace();\n\n        // For the detailed structure of this encoding, see description in session/session.cpp\n        msgtype = message.back();\n        if (msgtype != std::byte{0x01} && msgtype != std::byte{0x02})\n        {\n            log::warning(logcat, \"Received path message of unknown type: {}\", std::to_integer<int>(msgtype));\n            return std::nullopt;\n        }\n        message.pop_back();\n\n        hop_id.assign(std::span{message}.last<HopID::SIZE>());\n        message.resize(message.size() - HopID::SIZE);\n\n        nonce.assign(std::span{message}.last<SymmNonce::SIZE>());\n        message.resize(message.size() - SymmNonce::SIZE);\n\n        return result;\n    }\n\n    void Manager::handle_session_message(std::vector<std::byte> message, bool control)\n    {\n        auto maybe_hop_nonce = extract_path_message_metadata(message);\n        if (!maybe_hop_nonce)\n            return;\n        auto& [hop_id, nonce, msgtype] = *maybe_hop_nonce;\n\n        // The remainder of `message` is onion-encrypted.\n\n        // We've received a data message down a path.  There are four possible cases to consider\n        // here:\n        //\n        // 1. We are a client and thus the final destination of the message.  We consume it.\n        //\n        // 2. We are a relay and are the path terminus and the target (i.e. a relay session data\n        //    message).  We consume it.\n        //\n        // 3. We are a relay and are the path terminus and the message is to pivot to another path.\n        //    We onion decrypt, then read the pivot it, then onion encrypt for the target aligned\n        //    path and send it along.\n        //\n        // 4. We are a relay along the path but *not* the terminal.  We apply one onion layer and\n        //    pass it along to the next hop.\n\n        // Case 1: client\n        if (not router.is_service_node)\n        {\n            auto path = router.path_context.get_path(hop_id);\n\n            if (not path)\n            {\n                log::warning(logcat, \"Client received path data with unknown rxID: {}\", hop_id);\n                return;\n            }\n\n            // We're receiving this down an aligned path, which means each hop applied xchacha and\n            // nonce mutation so we run through the hops and apply the reverse operation,\n            // repeatedly, in order from nearest (most recently encrypted) back to terminus:\n            for (auto& hop : path->hops)\n            {\n                crypto::xchacha20(message, hop.shared_secret, nonce);\n                nonce ^= hop.xor_nonce;\n            }\n\n            // NB: path switch is a special case, as it comes bundled as 2 messages:\n            //     a path switch session control message, and\n            //     a session init message\n            if (msgtype == path::Path::PATH_SWITCH_MESSAGE_TYPE)\n            {\n                log::debug(logcat, \"client handling path switch/session init message\");\n\n                if (message.size() < sizeof(session::session_tag))\n                {\n                    log::info(logcat, \"invalid path switch / session reinit message received\");\n                    return;\n                }\n                session_tag tag;\n                auto tag_span = std::span{message}.last<sizeof(session_tag)>();\n                tag = oxenc::load_big_to_host<session_tag>(tag_span.data());\n                message.resize(message.size() - sizeof(session::session_tag));\n\n                oxenc::bt_list_consumer btlc{message};\n                auto path_switch = btlc.consume_string_view();\n                auto session_init = btlc.consume_string_view();\n                btlc.finish();\n\n                if (router.session_endpoint().get_session(tag))\n                {\n                    log::debug(logcat, \"Handling incoming session path switch message at the client end of a path\");\n                    // to avoid nonce re-use, mutate by xor factor\n                    nonce ^= session::switch_xor_factor;\n                    std::vector<std::byte> bytes;\n                    bytes.resize(path_switch.size());\n                    std::memcpy(bytes.data(), path_switch.data(), bytes.size());\n                    handle_session_control(std::move(bytes), tag, nonce, path->shared_from_this());\n                }\n                else\n                {\n                    log::debug(logcat, \"Handling incoming session init message at the client end of a path\");\n                    std::vector<std::byte> bytes;\n                    bytes.resize(session_init.size());\n                    std::memcpy(bytes.data(), session_init.data(), bytes.size());\n                    router.session_endpoint().handle_session_init(std::move(bytes), path->shared_from_this());\n                }\n                return;\n            }\n\n            // Client-bound session data has no pivot, just [encrypted][sessiontag], so we extract\n            // and remove the session tag then give the remainder for be session-decrypted.  The\n            // nonce (after the above mutations) also matches the nonce we want to use for the\n            // session encryption.\n            session_tag tag;\n            auto tag_span = std::span{message}.last<sizeof(session_tag)>();\n            tag = oxenc::load_big_to_host<session_tag>(tag_span.data());\n            message.resize(message.size() - sizeof(session_tag));\n\n            if (tag == 0)  // session init\n            {\n                return router.session_endpoint().handle_session_init(std::move(message), path->shared_from_this());\n            }\n            if (control)\n            {\n                log::trace(logcat, \"Handling incoming session control message at the client end of a path\");\n                handle_session_control(std::move(message), tag, nonce, path->shared_from_this());\n            }\n            else\n            {\n                log::trace(logcat, \"Handling incoming data message at the client end of a path\");\n                handle_session_data(std::move(message), tag, nonce);\n            }\n            return;\n        }\n\n        // Cases 2-4: relay.\n        auto hop = router.path_context.get_transit_hop(hop_id);\n        if (not hop)\n        {\n            log::warning(logcat, \"Received path data with unknown next hop (ID: {})\", hop_id);\n            return;\n        }\n\n        // All cases first apply a nonce mutation and then one onion encrypt/decrypt:\n        nonce ^= hop->xor_nonce;\n        crypto::xchacha20(message, hop->shared_secret, nonce);\n\n        if (hop->terminal_hop)\n        {\n            // Case 2 or 3:\n            log::trace(logcat, \"We are terminal hop for path data\");\n\n            // What's left in message after the above xchacha is back to what the data message\n            // creator set up for us: [ENCRYPTED, SESSION_TAG, PIVOT_ID].\n\n            auto [payload, bsession_tag, bpivot_id] = split_span_tail(message, sizeof(session_tag), HopID::SIZE);\n\n            HopID pivot_id;\n            pivot_id.assign(bpivot_id.first<HopID::SIZE>());\n\n            // Identify whether we are in case 2 (relay session) or 3 (pivot) by seeing whether we\n            // were told to \"pivot\" to the identical path (which is a special condition used\n            // explicitly for session data messages):\n            if (pivot_id == hop_id)\n            {\n                // Case 2: this is a session data message to this relay; extract the session tag and\n                // then drop everything down to the session payload for handle_session_data to deal\n                // with.\n\n                // NB: path switch is a special case, as it comes bundled as 2 messages:\n                //     a path switch session control message, and\n                //     a session init message\n                if (msgtype == path::Path::PATH_SWITCH_MESSAGE_TYPE)\n                {\n                    log::debug(logcat, \"client handling path switch/session init message\");\n\n                    oxenc::bt_list_consumer btlc{payload};\n                    auto path_switch = btlc.consume_string_view();\n                    auto session_init = btlc.consume_string_view();\n                    btlc.finish();\n\n                    session_tag tag;\n                    tag = oxenc::load_big_to_host<session_tag>(bsession_tag.data());\n                    if (router.session_endpoint().get_session(tag))\n                    {\n                        log::debug(logcat, \"Handling incoming session path switch message at the relay end of a path\");\n                        // to avoid nonce re-use, mutate by xor factor\n                        nonce ^= session::switch_xor_factor;\n                        std::vector<std::byte> bytes;\n                        bytes.resize(path_switch.size());\n                        std::memcpy(bytes.data(), path_switch.data(), bytes.size());\n                        handle_session_control(\n                            std::move(bytes), tag, nonce, router.path_context.get_transit_hop_ptr(hop_id));\n                    }\n                    else\n                    {\n                        log::debug(logcat, \"Handling incoming session init message at the relay end of a path\");\n                        std::vector<std::byte> bytes;\n                        bytes.resize(session_init.size());\n                        std::memcpy(bytes.data(), session_init.data(), bytes.size());\n                        router.session_endpoint().handle_session_init(\n                            std::move(bytes), router.path_context.get_transit_hop_ptr(hop_id));\n                    }\n                    return;\n                }\n\n                session_tag tag;\n                auto tag_span = bsession_tag.first<sizeof(session_tag)>();\n                tag = oxenc::load_big_to_host<session_tag>(tag_span.data());\n                message.resize(payload.size());\n\n                if (tag == 0)  // session init\n                {\n                    // getting shared_ptr here instead of above saves an atomic op on other messages\n                    return router.session_endpoint().handle_session_init(\n                        std::move(message), router.path_context.get_transit_hop_ptr(hop_id));\n                }\n                if (control)\n                {\n                    log::trace(logcat, \"Incoming control message is a relay session control message\");\n                    handle_session_control(\n                        std::move(message), tag, nonce, router.path_context.get_transit_hop_ptr(hop_id));\n                }\n                else\n                {\n                    log::trace(logcat, \"Incoming data message is a relay session data message\");\n                    handle_session_data(std::move(message), tag, nonce);\n                }\n                return;\n            }\n\n            // Case 3: we are pivoting the message down another path:\n            //\n            auto trans_hop = router.path_context.get_transit_hop(pivot_id);\n            if (not trans_hop)\n            {\n                log::warning(logcat, \"Terminal hop received path data message with unknown pivot id: {}\", pivot_id);\n                return;\n            }\n\n            // We don't want the pivot_id anymore, and don't want to include it in the back-side\n            // relay->client path, so drop it off the back of the message, leaving the session-encrypted-payload +\n            // session tag in place.  However, we *also* need to bebuild this into a path message suitable for sending\n            // down the back path, so we also need to add the path encryption bits:\n            message.resize(message.size() - HopID::SIZE + path::Path::ENCRYPT_PATH_MESSAGE_OVERHEAD);\n\n            // This is essentially a single-iteration version of Path::encrypt_path_data_message,\n            // except that because this is going \"backwards\" (from the perspective of a client), the\n            // xor happens *before* the xchacha.\n\n            static_assert(path::Path::ENCRYPT_PATH_MESSAGE_OVERHEAD == SymmNonce::SIZE + HopID::SIZE + 1);\n            auto [session_payload, bnonce, bhop, bmsgtype] = split_span_tail<SymmNonce::SIZE, HopID::SIZE, 1>(message);\n\n            nonce ^= trans_hop->xor_nonce;\n            crypto::xchacha20(session_payload, trans_hop->shared_secret, nonce);\n\n            nonce.copy_to(bnonce);\n            pivot_id.copy_to(bhop);\n            bmsgtype[0] = msgtype;\n\n            log::trace(logcat, \"Pivoting message down another path\");\n            if (control)\n                endpoint.send_command(trans_hop->downstream, \"session_control\"s, std::move(message), nullptr);\n            else\n                endpoint.send_datagram(trans_hop->downstream, std::move(message));\n            return;\n        }\n\n        // Case 4: we're an intermediate so we forward it to the next hop\n        const auto [next_target, next_hopid] = hop->next_id(hop_id);\n\n        // We chopped off the 0x01, hop_id, and nonce at the top of this function, but now lets put\n        // the new ones back on to make it suitable for the next hop.  (We're just resizing a vector\n        // down and back up, so there's no reallocation or copying happening by the resizing and\n        // little point in trying to avoid it).\n        static_assert(path::Path::ENCRYPT_PATH_MESSAGE_OVERHEAD == SymmNonce::SIZE + HopID::SIZE + 1);\n        message.resize(message.size() + path::Path::ENCRYPT_PATH_MESSAGE_OVERHEAD);\n        auto [enc_data, bnonce, bhop, bmsgtype] = split_span_tail<SymmNonce::SIZE, HopID::SIZE, 1>(message);\n        nonce.copy_to(bnonce);\n        next_hopid.copy_to(bhop);\n        bmsgtype[0] = msgtype;\n\n        if (control)\n            endpoint.send_command(next_target, \"session_control\"s, std::move(message), nullptr);\n        else\n            endpoint.send_datagram(next_target, std::move(message));\n    }\n\n    void Manager::handle_session_data(std::vector<std::byte>&& payload, const session_tag& tag, const SymmNonce& nonce)\n    {\n        if (auto session = router.session_endpoint().get_session(tag))\n            session->recv_session_data_message(std::move(payload), nonce);\n        else\n            log::warning(logcat, \"Could not find session {} to receive session data message!\", tag);\n    }\n\n    void Manager::handle_session_control(\n        std::vector<std::byte>&& payload,\n        const session_tag& tag,\n        const SymmNonce& nonce,\n        std::variant<std::shared_ptr<path::TransitHop>, std::shared_ptr<path::Path>> source)\n    {\n        try\n        {\n            if (auto session = router.session_endpoint().get_session(tag))\n                session->recv_session_control_message(std::move(payload), nonce, source);\n            else\n                log::warning(logcat, \"Could not find session {} to receive session control message!\", tag);\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"Error handling session control message: {}\", e.what());\n        }\n    }\n\n    void Manager::handle_path_request(std::span<const std::byte> payload, std::function<void(std::string)> respond)\n    {\n        std::string endpoint, body;\n\n        try\n        {\n            // FIXME: unnecessary copy\n            std::tie(endpoint, body) = PATH::CONTROL::deserialize(oxenc::bt_dict_consumer{payload});\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"Exception: {}; Payload: {}\", e.what(), buffer_printer{payload});\n            return respond(messages::serialize_status_response(\"ERROR\"));\n        }\n\n        if (auto it = path_requests.find(endpoint); it != path_requests.end())\n        {\n            log::debug(logcat, \"Received path control request (`{}`); invoking endpoint...\", endpoint);\n            (this->*(it->second))(oxen::quic::reinterpret_span<std::byte>(std::span{body}), std::move(respond));\n        }\n        else\n            log::warning(logcat, \"Received path control request (`{}`), which has no local handler!\", endpoint);\n    }\n\n    void Manager::handle_path_ping(std::span<const std::byte>, std::function<void(std::string)> respond)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n        respond(messages::OK_RESPONSE);\n    }\n\n    void Manager::handle_path_latency(quic::message m)\n    {\n        try\n        {\n            oxenc::bt_dict_consumer btdc{m.body()};\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"Exception: {}\", e.what());\n            return m.respond(messages::ERROR_RESPONSE, true);\n        }\n    }\n\n    void Manager::handle_path_latency_response(quic::message m)\n    {\n        try\n        {\n            oxenc::bt_dict_consumer btdc{m.body()};\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"Exception: {}\", e.what());\n            return;\n        }\n    }\n\n}  // namespace llarp::link\n"
  },
  {
    "path": "llarp/link/link_manager.hpp",
    "content": "#pragma once\n\n#include \"connection.hpp\"\n#include \"endpoint.hpp\"\n\n#include <llarp/address/address.hpp>\n#include <llarp/constants/path.hpp>\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/messages/common.hpp>\n#include <llarp/path/transit_hop.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/util/compare_ptr.hpp>\n#include <llarp/util/decaying_hashset.hpp>\n#include <llarp/util/zstd.hpp>\n\n#include <oxen/quic/btstream.hpp>\n#include <oxen/quic/connection.hpp>\n#include <oxen/quic/connection_ids.hpp>\n#include <oxen/quic/endpoint.hpp>\n#include <oxen/quic/format.hpp>\n#include <oxen/quic/loop.hpp>\n#include <oxen/quic/opt.hpp>\n\n#include <atomic>\n#include <chrono>\n#include <unordered_map>\n\nnamespace llarp\n{\n    class Router;\n}\nnamespace llarp::link\n{\n    using session::session_tag;\n    // Keep-alive and idle timeouts.  For relay-to-relay connections, the keep-alive is every 10s;\n    // for client-to-relay connections, we use a longer, 20s keep-alive.\n    //\n    // The related target idle timeout on each is set to 3 times the keep-alive, plus 3s, so that we\n    // have to have no back-and-forth traffic (including the ping) for a little more than three\n    // consecutive pings.\n    //\n    // For inbound connections, relays use the *client* idle timeout: the actual idle timeout for\n    // the connection is negotiated during connection establishing, with both sides using the lower\n    // value, and so client<->relay will use the longer (client) timeout, while relay<->relay will\n    // use the shorter value from the outbound relay connection.\n    inline constexpr auto RELAY_OUTBOUND_KEEP_ALIVE = 10s;\n    inline constexpr auto RELAY_OUTBOUND_IDLE_TIMEOUT = 33s;\n\n    inline constexpr auto CLIENT_KEEP_ALIVE = 20s;\n    inline constexpr auto CLIENT_IDLE_TIMEOUT = 63s;\n\n    inline constexpr auto RELAY_INBOUND_IDLE_TIMEOUT = std::max(CLIENT_IDLE_TIMEOUT, RELAY_OUTBOUND_IDLE_TIMEOUT);\n\n    inline const auto RELAY_ALPN = \"Lokinet_R\"s;\n    inline const auto CLIENT_ALPN = \"Lokinet_C\"s;\n\n    // Special ALPN used when bootstrapping; unlike the above, this does not replace any existing\n    // connection (e.g. if an already-connected pubkey reconnects) and these connections are not\n    // used as general relay or client connections.  This ALPN only supports a single BT stream\n    // command, bfetch_rcs, issued from the client to the server.\n    inline const auto BOOTSTRAP_ALPN = \"Lokinet_BS\"s;\n    inline const auto BOOTSTRAP_IDLE_TIMEOUT = 10s;\n\n    class Manager\n    {\n      public:\n        explicit Manager(Router& r);\n\n        Router& router;\n\n      private:\n        friend class Endpoint;\n        friend class llarp::NodeDB;\n\n        util::DecayingHashSet<RouterID> clients{path::MAX_LIFETIME_ACCEPTED};\n\n        quic::Address addr;\n\n        std::atomic<bool> is_stopping{false};\n\n        std::optional<zstd::compressor> compressor;\n\n        // Registers commands on the client or relay end of a client-relay or relay-relay connection\n        // NB: this could be called from either the network or router loop thread!\n        void register_commands(quic::BTRequestStream& s, const std::variant<RouterID, quic::ConnectionID>& remote);\n\n        // Registered the bootstrap command (bfetch_rcs) on the server (i.e. incoming) bootstrap\n        // connection (i.e.  to the relay being used as a bootstrap).  The client side of such a\n        // connection doesn't have any commands to register.\n        // NB: this could be called from either the network or router loop thread!\n        void register_bootstrap_commands(quic::BTRequestStream& s);\n\n      public:\n        link::Endpoint endpoint;\n\n        const quic::Address& local() { return addr; }\n\n        bool have_client_connection_to(const RouterID& remote) const;\n\n        // TODO FIXME: see comments in router.cpp about required fixes!\n        // void test_reachability(const RouterID& rid, connection_established_callback, connection_closed_callback);\n\n        void connect_to(\n            const RelayContact& rc, connection_established_callback = nullptr, connection_closed_callback = nullptr);\n\n        // Closes all connections and releases the network event loop.\n        void stop();\n\n        nlohmann::json extract_status() const;\n\n        // Attempts to connect to a number of random routers.\n        //\n        // This will try to connect to *up to* num_conns routers, but will not\n        // check if we already have a connection to any of the random set, as making\n        // that thread safe would be slow...I think.\n        void connect_to_keep_alive(int num_conns);\n\n        // Sends the given RC to all our relay peers, excluding connections to the RC pubkey itself,\n        // and (if not-nullptr) the given quic connection.  Returns the number of relay connections\n        // we sent it to.\n        int gossip_rc(const RelayContact& rc, const quic::ConnectionID* sender = nullptr);\n\n        ~Manager();\n\n      private:\n        void handle_gossip_rc(quic::message);\n\n        void handle_fetch_bootstrap_rcs(quic::message m);\n\n        void handle_direct_request(\n            void (Manager::*respond)(std::span<const std::byte>, std::function<void(std::string)>, bool),\n            quic::message m);\n\n        // handlers for requests which could come over a path or a relay request\n        void handle_publish_cc(\n            std::span<const std::byte> body, std::function<void(std::string)> respond, bool source_is_relay = true);\n        void handle_find_cc(\n            std::span<const std::byte> body, std::function<void(std::string)> respond, bool source_is_relay = true);\n        void handle_fetch_rcs(\n            std::span<const std::byte> body, std::function<void(std::string)> respond, bool source_is_relay = true);\n\n        void handle_path_control(quic::message);\n\n        // handlers for path requests\n        void handle_path_publish_cc(std::span<const std::byte> body, std::function<void(std::string)> respond);\n        void handle_path_fetch_router_ids(std::span<const std::byte> body, std::function<void(std::string)> respond);\n        void handle_path_find_cc(std::span<const std::byte> body, std::function<void(std::string)> respond);\n        void handle_path_fetch_rcs(std::span<const std::byte> body, std::function<void(std::string)> respond);\n        void handle_path_resolve_sns(std::span<const std::byte> body, std::function<void(std::string)> respond);\n        void handle_path_ping(std::span<const std::byte> body, std::function<void(std::string)> respond);\n\n        // Path messages\n        void handle_path_build(quic::message, const std::variant<RouterID, quic::ConnectionID>& from);\n        void handle_path_latency(quic::message);\n\n        void handle_path_session_control(quic::message m);\n\n        // These requests come over a path (as a \"path_control\" request),\n        // we may or may not need to make a request to another relay,\n        // then respond (onioned) back along the path.\n        static std::unordered_map<\n            std::string_view,\n            void (Manager::*)(std::span<const std::byte> payload, std::function<void(std::string)> respond)>\n            path_requests;\n\n        // Path relaying\n        void handle_session_message(std::vector<std::byte> msg, bool control = false);\n        void handle_path_request(std::span<const std::byte> payload, std::function<void(std::string)> respond);\n        void handle_session_data(std::vector<std::byte>&& payload, const session_tag& tag, const SymmNonce& nonce);\n        void handle_session_control(\n            std::vector<std::byte>&& payload,\n            const session_tag& tag,\n            const SymmNonce& nonce,\n            std::variant<std::shared_ptr<path::TransitHop>, std::shared_ptr<path::Path>> source);\n\n        // Path responses\n        void handle_path_latency_response(quic::message);\n    };\n}  // namespace llarp::link\n"
  },
  {
    "path": "llarp/linux/dbus.cpp",
    "content": "#ifdef WITH_SYSTEMD\n#include \"dbus.hpp\"\n\nnamespace llarp::linux\n{\n    system_bus_exception::system_bus_exception(int err)\n        : std::runtime_error{\"cannot connect to system bus: \" + std::string{strerror(-err)}}\n    {}\n\n    dbus_call_exception::dbus_call_exception(std::string meth, int err)\n        : std::runtime_error{\"failed to call dbus function '\" + meth + \"': \" + std::string{strerror(-err)}}\n    {}\n\n    DBUS::DBUS(std::string _interface, std::string _instance, std::string _call)\n        : m_interface{std::move(_interface)}, m_instance{std::move(_instance)}, m_call{std::move(_call)}\n    {\n        sd_bus* bus{nullptr};\n        if (auto err = sd_bus_open_system(&bus); err < 0)\n            throw system_bus_exception{err};\n        m_ptr.reset(bus);\n    }\n}  // namespace llarp::linux\n#endif\n"
  },
  {
    "path": "llarp/linux/dbus.hpp",
    "content": "#pragma once\n\nextern \"C\"\n{\n#include <systemd/sd-bus.h>\n}\n#include <memory>\n#include <stdexcept>\n#include <string>\n\nnamespace llarp::linux\n{\n    /// exception for connecting to system bus\n    class system_bus_exception : public std::runtime_error\n    {\n      public:\n        explicit system_bus_exception(int err);\n    };\n\n    /// exception for a failed calling of a dbus method\n    class dbus_call_exception : public std::runtime_error\n    {\n      public:\n        explicit dbus_call_exception(std::string meth, int err);\n    };\n\n    class DBUS\n    {\n        struct sd_bus_deleter\n        {\n            void operator()(sd_bus* ptr) const { sd_bus_unref(ptr); }\n        };\n        std::unique_ptr<sd_bus, sd_bus_deleter> m_ptr;\n        const std::string m_interface;\n        const std::string m_instance;\n        const std::string m_call;\n\n      public:\n        DBUS(std::string _interface, std::string _instance, std::string _call);\n\n        template <typename... T>\n        void operator()(std::string method, const char* arg_format, T... args)\n        {\n            sd_bus_error error = SD_BUS_ERROR_NULL;\n            sd_bus_message* msg = nullptr;\n            auto r = sd_bus_call_method(\n                m_ptr.get(),\n                m_interface.c_str(),\n                m_instance.c_str(),\n                m_call.c_str(),\n                method.c_str(),\n                &error,\n                &msg,\n                arg_format,\n                args...);\n\n            if (r < 0)\n                throw dbus_call_exception{std::move(method), r};\n\n            sd_bus_message_unref(msg);\n            sd_bus_error_free(&error);\n        }\n    };\n}  // namespace llarp::linux\n"
  },
  {
    "path": "llarp/linux/sd_service_manager.cpp",
    "content": "#include <llarp.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/service_manager.hpp>\n\n#include <systemd/sd-daemon.h>\n\n#include <cassert>\n\nnamespace llarp::sys\n{\n    class SD_Manager : public I_SystemLayerManager\n    {\n        llarp::sys::ServiceState m_State{ServiceState::Initial};\n\n      public:\n        /// change our state and report it to the system layer\n        void we_changed_our_state(ServiceState st) override\n        {\n            m_State = st;\n            report_changed_state();\n        }\n\n        void report_changed_state() override\n        {\n            if (m_State == ServiceState::Running)\n            {\n                ::sd_notify(0, \"READY=1\");\n                return;\n            }\n            if (m_State == ServiceState::Stopping)\n            {\n                ::sd_notify(0, \"STOPPING=1\");\n                return;\n            }\n        }\n\n        void report_periodic_stats() override\n        {\n            if (m_Context and m_Context->router and not m_disable)\n            {\n                auto status = fmt::format(\"WATCHDOG=1\\nSTATUS={}\", m_Context->router->status_line());\n                ::sd_notify(0, status.c_str());\n            }\n        }\n\n        void system_changed_our_state(ServiceState) override\n        {\n            // not applicable on systemd\n        }\n    };\n\n    SD_Manager _manager{};\n    I_SystemLayerManager* const service_manager = &_manager;\n\n}  // namespace llarp::sys\n"
  },
  {
    "path": "llarp/lokinet.cpp",
    "content": "#include <llarp.hpp>\n#include <llarp/address/address.hpp>\n#include <llarp/config/config.hpp>\n#include <llarp/net/id.hpp>\n#include <llarp/nodedb.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/logging/buffer.hpp>\n\n#include <lokinet.hpp>\n#include <oxenc/base32z.h>\n\n#include <exception>\n#include <future>\n#include <stdexcept>\n\nusing namespace std::literals;\n\nnamespace\n{\n    static auto logcat = llarp::log::Cat(\"liblokinet\");\n}  // anonymous namespace\n\nnamespace lokinet\n{\n    static auto make_embedded_context() { return std::make_unique<llarp::Context>(/*embedded=*/true); }\n\n    Lokinet::Lokinet(std::string config, std::shared_ptr<oxen::quic::Loop> loop) : context{make_embedded_context()}\n    {\n        context->start(llarp::Config{llarp::config::Type::EmbeddedClient, std::move(config)}, loop);\n    }\n\n    Lokinet::Lokinet(path_ctor, const std::filesystem::path& config, std::shared_ptr<oxen::quic::Loop> loop)\n        : context{make_embedded_context()}\n    {\n        ;\n        context->start(llarp::Config{llarp::config::Type::EmbeddedClient, config}, loop);\n    }\n\n    Lokinet::Lokinet(Network n, std::shared_ptr<oxen::quic::Loop> loop) : context{make_embedded_context()}\n    {\n        llarp::Config conf{llarp::config::Type::EmbeddedClient};\n        switch (n)\n        {\n            case Network::MAINNET:\n                conf.router.net_id = llarp::NetID::MAINNET;\n                break;\n            case Network::TESTNET:\n                conf.router.net_id = llarp::NetID::TESTNET;\n                break;\n            default:\n                throw std::invalid_argument{\"Unknown/unsupported network value passed to Lokinet constructor\"};\n        }\n        context->start(std::move(conf), loop);\n    }\n\n    Lokinet::~Lokinet()\n    {\n        context->stop();\n        context->wait();\n    }\n\n    void Lokinet::on_connected(std::function<void()> callback, bool persist)\n    {\n        context->router->on_connected(std::move(callback), persist);\n    }\n\n    void Lokinet::on_disconnected(std::function<void()> callback, bool persist)\n    {\n        context->router->on_disconnected(std::move(callback), persist);\n    }\n\n    void Lokinet::establish_udp(\n        std::string_view remote,\n        uint16_t port,\n        std::function<void(tunnel_info info)> on_established,\n        std::function<void(std::string errmsg)> failure)\n    {\n        // FIXME: check for valid ONS, and if so, we need to defer the lookup as well\n        llarp::NetworkAddress netaddr;\n        try\n        {\n            netaddr = llarp::NetworkAddress{remote};\n        }\n        catch (const std::exception& e)\n        {\n            failure(\"Invalid remote address: {}\"_format(e.what()));\n            return;\n        }\n\n        llarp::log::info(logcat, \"Creating session for udp connection to {}\", netaddr);\n        context->router->session_endpoint().initiate_remote_session(\n            netaddr,\n            [&r = *context->router,\n             port,\n             netaddr,\n             on_established = std::move(on_established),\n             failure = std::move(failure)](llarp::session::Session& s) {\n                if (!s.is_established())\n                {\n                    auto err = \"Failed to establish remote session to {} for UDP tunnel[port={}]\"_format(netaddr, port);\n                    llarp::log::warning(logcat, \"{}\", err);\n                    failure(std::move(err));\n                    return;\n                }\n\n                // TODO FIXME: 1200 here is just a placeholder, we should be able to pick\n                // something better!\n                tunnel_info ti{.remote = netaddr.to_string(), .remote_port = port, .suggested_mtu = 1200};\n\n                ti.local_port = s.setup_udp_mapping(port);\n                llarp::log::info(\n                    logcat,\n                    \"Session established to {}, with local port {} mapped to remote {}\",\n                    ti.remote,\n                    ti.local_port,\n                    ti.remote_port);\n\n                on_established(std::move(ti));\n            });\n    }\n\n    tunnel_info Lokinet::establish_udp_blocking(std::string_view remote, uint16_t port)\n    {\n        std::promise<tunnel_info> prom;\n        auto fut = prom.get_future();\n        establish_udp(\n            remote,\n            port,\n            [&prom](tunnel_info info) { prom.set_value(std::move(info)); },\n            [&prom](std::string err) {\n                try\n                {\n                    throw std::runtime_error{err};\n                }\n                catch (...)\n                {\n                    prom.set_exception(std::current_exception());\n                }\n            });\n\n        return fut.get();\n    }\n\n}  // namespace lokinet\n"
  },
  {
    "path": "llarp/lokinet_shared.cpp",
    "content": "#include <llarp.hpp>\n#include <llarp/config/config.hpp>\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/nodedb.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/logging/buffer.hpp>\n#include <llarp/util/logging/callback_sink.hpp>\n\n#include <lokinet.h>\n#include <oxenc/base32z.h>\n\n#include <chrono>\n#include <memory>\n#include <mutex>\n#include <stdexcept>\n\n#ifdef _WIN32\n#define EHOSTDOWN ENETDOWN\n#endif\n\nnamespace\n{\n    static auto logcat = oxen::log::Cat(\"liblokinet\");\n\n    struct Context : public llarp::Context\n    {\n        using llarp::Context::Context;\n\n        std::shared_ptr<llarp::NodeDB> makeNodeDB() override { return std::make_shared<llarp::NodeDB>(); }\n    };\n\n    struct UDPFlow\n    {\n        using Clock_t = std::chrono::steady_clock;\n        void* m_FlowUserData;\n        std::chrono::seconds m_FlowTimeout;\n        std::chrono::time_point<Clock_t> m_ExpiresAt;\n        lokinet_udp_flowinfo m_FlowInfo;\n        lokinet_udp_flow_recv_func m_Recv;\n\n        /// call timeout hook for this flow\n        void TimedOut(lokinet_udp_flow_timeout_func timeout) { timeout(&m_FlowInfo, m_FlowUserData); }\n\n        /// mark this flow as active\n        /// updates the expires at timestamp\n        void MarkActive() { m_ExpiresAt = Clock_t::now() + m_FlowTimeout; }\n\n        /// returns true if we think this flow is expired\n        bool IsExpired() const { return Clock_t::now() >= m_ExpiresAt; }\n\n        void HandlePacket(const llarp::net::IPPacket& pkt)\n        {\n            if (auto maybe = pkt.L4Data())\n            {\n                MarkActive();\n                m_Recv(&m_FlowInfo, maybe->first, maybe->second, m_FlowUserData);\n            }\n        }\n    };\n\n    struct UDPHandler\n    {\n        using AddressVariant_t = llarp::AddressVariant_t;\n        int m_SocketID;\n        llarp::nuint16_t m_LocalPort;\n        lokinet_udp_flow_filter m_Filter;\n        lokinet_udp_flow_recv_func m_Recv;\n        lokinet_udp_flow_timeout_func m_Timeout;\n        void* m_User;\n        std::weak_ptr<llarp::service::Endpoint> m_Endpoint;\n\n        std::unordered_map<AddressVariant_t, UDPFlow> m_Flows;\n\n        std::mutex m_Access;\n\n        explicit UDPHandler(\n            int socketid,\n            llarp::nuint16_t localport,\n            lokinet_udp_flow_filter filter,\n            lokinet_udp_flow_recv_func recv,\n            lokinet_udp_flow_timeout_func timeout,\n            void* user,\n            std::weak_ptr<llarp::service::Endpoint> ep)\n            : m_SocketID{socketid},\n              m_LocalPort{localport},\n              m_Filter{filter},\n              m_Recv{recv},\n              m_Timeout{timeout},\n              m_User{user},\n              m_Endpoint{std::move(ep)}\n        {}\n\n        void KillAllFlows()\n        {\n            std::unique_lock lock{m_Access};\n            for (auto& item : m_Flows)\n            {\n                item.second.TimedOut(m_Timeout);\n            }\n            m_Flows.clear();\n        }\n\n        void AddFlow(\n            const AddressVariant_t& from,\n            const lokinet_udp_flowinfo& flow_addr,\n            void* flow_userdata,\n            int flow_timeoutseconds,\n            std::optional<llarp::net::IPPacket> firstPacket = std::nullopt)\n        {\n            std::unique_lock lock{m_Access};\n            auto& flow = m_Flows[from];\n            flow.m_FlowInfo = flow_addr;\n            flow.m_FlowTimeout = std::chrono::seconds{flow_timeoutseconds};\n            flow.m_FlowUserData = flow_userdata;\n            flow.m_Recv = m_Recv;\n            if (firstPacket)\n                flow.HandlePacket(*firstPacket);\n        }\n\n        void ExpireOldFlows()\n        {\n            std::unique_lock lock{m_Access};\n            for (auto itr = m_Flows.begin(); itr != m_Flows.end();)\n            {\n                if (itr->second.IsExpired())\n                {\n                    itr->second.TimedOut(m_Timeout);\n                    itr = m_Flows.erase(itr);\n                }\n                else\n                    ++itr;\n            }\n        }\n\n        void HandlePacketFrom(AddressVariant_t from, llarp::net::IPPacket pkt)\n        {\n            {\n                std::unique_lock lock{m_Access};\n                if (m_Flows.count(from))\n                {\n                    m_Flows[from].HandlePacket(pkt);\n                    return;\n                }\n            }\n            lokinet_udp_flowinfo flow_addr{};\n            // set flow remote address\n            std::string addrstr = var::visit([](auto&& from) { return from.to_string(); }, from);\n\n            std::copy_n(addrstr.data(), std::min(addrstr.size(), sizeof(flow_addr.remote_host)), flow_addr.remote_host);\n            // set socket id\n            flow_addr.socket_id = m_SocketID;\n            // get source port\n            if (const auto srcport = pkt.SrcPort())\n            {\n                flow_addr.remote_port = ToHost(*srcport).h;\n            }\n            else\n                return;  // invalid data so we bail\n            void* flow_userdata = nullptr;\n            int flow_timeoutseconds{};\n            // got a new flow, let's check if we want it\n            if (m_Filter(m_User, &flow_addr, &flow_userdata, &flow_timeoutseconds))\n                return;\n            AddFlow(from, flow_addr, flow_userdata, flow_timeoutseconds, pkt);\n        }\n    };\n}  // namespace\n\nstruct lokinet_context\n{\n    std::mutex m_access;\n\n    std::shared_ptr<llarp::Context> impl = std::make_shared<Context>();\n    std::shared_ptr<llarp::Config> config = llarp::Config::make_embedded_config();\n\n    std::unique_ptr<std::thread> runner;\n\n    int _socket_id = 0;\n\n    ~lokinet_context()\n    {\n        if (runner)\n            runner->join();\n    }\n\n    int next_socket_id()\n    {\n        int id = ++_socket_id;\n        // handle overflow\n        if (id < 0)\n        {\n            _socket_id = 0;\n            id = ++_socket_id;\n        }\n        return id;\n    }\n\n    /// make a udp handler and hold onto it\n    /// return its id\n    [[nodiscard]] std::optional<int> make_udp_handler(\n        const std::shared_ptr<llarp::service::Endpoint>& ep,\n        llarp::net::port_t exposePort,\n        lokinet_udp_flow_filter filter,\n        lokinet_udp_flow_recv_func recv,\n        lokinet_udp_flow_timeout_func timeout,\n        void* user)\n    {\n        if (udp_sockets.empty())\n        {\n            // start udp flow expiration timer\n            impl->router->loop()->call_every(1s, std::make_shared<int>(0), [this]() {\n                std::unique_lock lock{m_access};\n                for (auto& item : udp_sockets)\n                {\n                    item.second->ExpireOldFlows();\n                }\n            });\n        }\n        std::weak_ptr<llarp::service::Endpoint> weak{ep};\n        auto udp = std::make_shared<UDPHandler>(next_socket_id(), exposePort, filter, recv, timeout, user, weak);\n        auto id = udp->m_SocketID;\n        std::promise<bool> result;\n\n        impl->router->loop()->call([ep, &result, udp, exposePort]() {\n            if (auto pkt = ep->EgresPacketRouter())\n            {\n                pkt->AddUDPHandler(llarp::net::ToHost(exposePort), [udp](auto from, auto pkt) {\n                    udp->HandlePacketFrom(std::move(from), std::move(pkt));\n                });\n                result.set_value(true);\n            }\n            else\n                result.set_value(false);\n        });\n\n        if (result.get_future().get())\n        {\n            udp_sockets[udp->m_SocketID] = std::move(udp);\n            return id;\n        }\n        return std::nullopt;\n    }\n\n    void remove_udp_handler(int socket_id)\n    {\n        std::shared_ptr<UDPHandler> udp;\n        {\n            std::unique_lock lock{m_access};\n            if (auto itr = udp_sockets.find(socket_id); itr != udp_sockets.end())\n            {\n                udp = std::move(itr->second);\n                udp_sockets.erase(itr);\n            }\n        }\n        if (udp)\n        {\n            udp->KillAllFlows();\n            // remove packet handler\n            impl->router->loop()->call([ep = udp->m_Endpoint.lock(), localport = llarp::ToHost(udp->m_LocalPort)]() {\n                if (auto pkt = ep->EgresPacketRouter())\n                    pkt->RemoveUDPHandler(localport);\n            });\n        }\n    }\n\n    /// acquire mutex for accessing this context\n    [[nodiscard]] auto acquire() { return std::unique_lock{m_access}; }\n\n    [[nodiscard]] auto endpoint(std::string name = \"default\") const\n    {\n        return impl->router->hidden_service_context().GetEndpointByName(name);\n    }\n\n    std::unordered_map<int, bool> streams;\n    std::unordered_map<int, std::shared_ptr<UDPHandler>> udp_sockets;\n\n    void inbound_stream(int id) { streams[id] = true; }\n\n    void outbound_stream(int id) { streams[id] = false; }\n};\n\nnamespace\n{\n    void stream_error(lokinet_stream_result* result, int err)\n    {\n        std::memset(result, 0, sizeof(lokinet_stream_result));\n        result->error = err;\n    }\n\n    void stream_okay(lokinet_stream_result* result, std::string host, int port, int stream_id)\n    {\n        stream_error(result, 0);\n        std::copy_n(host.c_str(), std::min(host.size(), sizeof(result->local_address) - 1), result->local_address);\n        result->local_port = port;\n        result->stream_id = stream_id;\n    }\n\n    std::pair<std::string, int> split_host_port(std::string data, std::string proto = \"tcp\")\n    {\n        std::string host, portStr;\n        if (auto pos = data.find(\":\"); pos != std::string::npos)\n        {\n            host = data.substr(0, pos);\n            portStr = data.substr(pos + 1);\n        }\n        else\n            throw EINVAL;\n\n        if (auto* serv = getservbyname(portStr.c_str(), proto.c_str()))\n        {\n            return {host, serv->s_port};\n        }\n        return {host, std::stoi(portStr)};\n    }\n\n    int accept_port(const char* remote, uint16_t port, void* ptr)\n    {\n        (void)remote;\n        if (port == *static_cast<uint16_t*>(ptr))\n        {\n            return 0;\n        }\n        return -1;\n    }\n\n    std::optional<lokinet_srv_record> SRVFromData(const llarp::dns::SRVData& data, std::string name)\n    {\n        // TODO: implement me\n        (void)data;\n        (void)name;\n        return std::nullopt;\n    }\n\n}  // namespace\n\nstruct lokinet_srv_lookup_private\n{\n    std::vector<lokinet_srv_record> results;\n\n    int LookupSRV(std::string host, std::string service, lokinet_context* ctx)\n    {\n        std::promise<int> promise;\n        {\n            auto lock = ctx->acquire();\n            if (ctx->impl and ctx->impl->IsUp())\n            {\n                ctx->impl->CallSafe([host, service, &promise, ctx, this]() {\n                    auto ep = ctx->endpoint();\n                    if (ep == nullptr)\n                    {\n                        promise.set_value(ENOTSUP);\n                        return;\n                    }\n                    ep->LookupServiceAsync(host, service, [this, &promise, host](auto results) {\n                        for (const auto& result : results)\n                        {\n                            if (auto maybe = SRVFromData(result, host))\n                                this->results.emplace_back(*maybe);\n                        }\n                        promise.set_value(0);\n                    });\n                });\n            }\n            else\n            {\n                promise.set_value(EHOSTDOWN);\n            }\n        }\n        auto future = promise.get_future();\n        return future.get();\n    }\n\n    void IterateAll(std::function<void(lokinet_srv_record*)> visit)\n    {\n        for (auto& result : results)\n            visit(&result);\n        // null terminator\n        visit(nullptr);\n    }\n};\n\nextern \"C\"\n{\n    void EXPORT lokinet_set_netid(const char* netid)\n    {\n        llarp::NetID::DefaultValue() = llarp::NetID{reinterpret_cast<const uint8_t*>(netid)};\n    }\n\n    const char* EXPORT lokinet_get_netid()\n    {\n        const auto netid = llarp::NetID::DefaultValue().to_string();\n        return strdup(netid.c_str());\n    }\n\n    static auto last_log_set = llarp::log::Level::info;\n\n    int EXPORT lokinet_log_level(const char* level)\n    {\n        try\n        {\n            auto new_level = llarp::log::level_from_string(level);\n            llarp::log::reset_level(new_level);\n            last_log_set = new_level;\n            return 0;\n        }\n        catch (std::invalid_argument& e)\n        {\n            oxen::log::error(logcat, \"{}\", e.what());\n        }\n        return -1;\n    }\n\n    char* EXPORT lokinet_address(struct lokinet_context* ctx)\n    {\n        if (not ctx)\n            return nullptr;\n        auto lock = ctx->acquire();\n        auto ep = ctx->endpoint();\n        const auto addr = ep->GetIdentity().pub.Addr();\n        const auto addrStr = addr.to_string();\n        return strdup(addrStr.c_str());\n    }\n\n    int EXPORT lokinet_add_bootstrap_rc(const char* data, size_t datalen, struct lokinet_context* ctx)\n    {\n        if (data == nullptr or datalen == 0)\n            return -3;\n        llarp_buffer_t buf{data, datalen};\n        if (ctx == nullptr)\n            return -3;\n        auto lock = ctx->acquire();\n        if (data[0] == 'l')\n        {\n            if (not ctx->config->bootstrap.routers.BDecode(&buf))\n            {\n                oxen::log::error(logcat, \"Cannot decode bootstrap list: {}}\", llarp::buffer_printer{buf});\n                return -1;\n            }\n            for (const auto& rc : ctx->config->bootstrap.routers)\n            {\n                if (not rc.Verify(llarp::time_now_ms()))\n                    return -2;\n            }\n        }\n        else\n        {\n            llarp::RelayContact rc{};\n            if (not rc.BDecode(&buf))\n            {\n                oxen::log::error(logcat, \"failed to decode signle RC: {}\", llarp::buffer_printer{buf});\n                return -1;\n            }\n            if (not rc.Verify(llarp::time_now_ms()))\n                return -2;\n            ctx->config->bootstrap.routers.insert(std::move(rc));\n        }\n        return 0;\n    }\n\n    struct lokinet_context* EXPORT lokinet_context_new() { return new lokinet_context{}; }\n\n    void EXPORT lokinet_context_free(struct lokinet_context* ctx)\n    {\n        lokinet_context_stop(ctx);\n        delete ctx;\n    }\n\n    int EXPORT lokinet_context_start(struct lokinet_context* ctx)\n    {\n        if (not ctx)\n            return -1;\n        auto lock = ctx->acquire();\n        ctx->config->router.m_netId = lokinet_get_netid();\n        ctx->config->logging.m_logLevel = last_log_set;\n        ctx->runner = std::make_unique<std::thread>([ctx]() {\n            llarp::util::SetThreadName(\"llarp-mainloop\");\n            ctx->impl->Configure(ctx->config);\n            const llarp::RuntimeOptions opts{};\n            try\n            {\n                ctx->impl->Setup(opts);\n#ifdef SIG_PIPE\n                signal(SIG_PIPE, SIGIGN);\n#endif\n                ctx->impl->Run(opts);\n            }\n            catch (std::exception& ex)\n            {\n                std::cerr << ex.what() << std::endl;\n                ctx->impl->CloseAsync();\n            }\n        });\n        while (not ctx->impl->IsUp())\n        {\n            if (ctx->impl->IsStopping())\n                return -1;\n            std::this_thread::sleep_for(50ms);\n        }\n        return 0;\n    }\n\n    int EXPORT lokinet_status(struct lokinet_context* ctx)\n    {\n        if (ctx == nullptr)\n            return -3;\n        auto lock = ctx->acquire();\n        if (not ctx->impl->IsUp())\n            return -3;\n        if (not ctx->impl->LooksAlive())\n            return -2;\n        return ctx->endpoint()->is_ready() ? 0 : -1;\n    }\n\n    int EXPORT lokinet_wait_for_ready(int ms, struct lokinet_context* ctx)\n    {\n        if (ctx == nullptr)\n            return -1;\n        auto lock = ctx->acquire();\n        auto ep = ctx->endpoint();\n        int iterations = ms / 10;\n        if (iterations <= 0)\n        {\n            ms = 10;\n            iterations = 1;\n        }\n        while (not ep->is_ready() and iterations > 0)\n        {\n            std::this_thread::sleep_for(std::chrono::milliseconds{ms / 10});\n            iterations--;\n        }\n        return ep->is_ready() ? 0 : -1;\n    }\n\n    void EXPORT lokinet_context_stop(struct lokinet_context* ctx)\n    {\n        if (not ctx)\n            return;\n        auto lock = ctx->acquire();\n\n        if (ctx->impl->IsStopping())\n            return;\n\n        ctx->impl->CloseAsync();\n        ctx->impl->Wait();\n\n        if (ctx->runner)\n            ctx->runner->join();\n\n        ctx->runner.reset();\n    }\n\n    void EXPORT lokinet_outbound_stream(\n        struct lokinet_stream_result* result, const char* remote, const char* local, struct lokinet_context* ctx)\n    {\n        if (ctx == nullptr)\n        {\n            stream_error(result, EHOSTDOWN);\n            return;\n        }\n        std::promise<void> promise;\n\n        {\n            auto lock = ctx->acquire();\n\n            if (not ctx->impl->IsUp())\n            {\n                stream_error(result, EHOSTDOWN);\n                return;\n            }\n            std::string remotehost;\n            int remoteport;\n            try\n            {\n                auto [h, p] = split_host_port(remote);\n                remotehost = h;\n                remoteport = p;\n            }\n            catch (int err)\n            {\n                stream_error(result, err);\n                return;\n            }\n            // TODO: make configurable (?)\n            std::string endpoint{\"default\"};\n\n            llarp::SockAddr localAddr;\n            try\n            {\n                if (local)\n                    localAddr = llarp::SockAddr{std::string{local}};\n                else\n                    localAddr = llarp::SockAddr{\"127.0.0.1:0\"};\n            }\n            catch (std::exception& ex)\n            {\n                stream_error(result, EINVAL);\n                return;\n            }\n            auto call =\n                [&promise, ctx, result, router = ctx->impl->router, remotehost, remoteport, endpoint, localAddr]() {\n                    auto ep = ctx->endpoint();\n                    if (ep == nullptr)\n                    {\n                        stream_error(result, ENOTSUP);\n                        promise.set_value();\n                        return;\n                    }\n                    auto* quic = ep->GetQUICTunnel();\n                    if (quic == nullptr)\n                    {\n                        stream_error(result, ENOTSUP);\n                        promise.set_value();\n                        return;\n                    }\n                    try\n                    {\n                        auto [addr, id] = quic->open(remotehost, remoteport, [](auto) {}, localAddr);\n                        auto [host, port] = split_host_port(addr.to_string());\n                        ctx->outbound_stream(id);\n                        stream_okay(result, host, port, id);\n                    }\n                    catch (std::exception& ex)\n                    {\n                        std::cout << ex.what() << std::endl;\n                        stream_error(result, ECANCELED);\n                    }\n                    catch (int err)\n                    {\n                        stream_error(result, err);\n                    }\n                    promise.set_value();\n                };\n\n            ctx->impl->CallSafe([call]() {\n                // we dont want the mainloop to die in case setting the value on the promise fails\n                try\n                {\n                    call();\n                }\n                catch (...)\n                {}\n            });\n        }\n\n        auto future = promise.get_future();\n        try\n        {\n            if (auto status = future.wait_for(std::chrono::seconds{10}); status == std::future_status::ready)\n            {\n                future.get();\n            }\n            else\n            {\n                stream_error(result, ETIMEDOUT);\n            }\n        }\n        catch (std::exception& ex)\n        {\n            stream_error(result, EBADF);\n        }\n    }\n\n    int EXPORT lokinet_inbound_stream(uint16_t port, struct lokinet_context* ctx)\n    {\n        /// FIXME: delete pointer later\n        return lokinet_inbound_stream_filter(&accept_port, (void*)new std::uintptr_t{port}, ctx);\n    }\n\n    int EXPORT\n    lokinet_inbound_stream_filter(lokinet_stream_filter acceptFilter, void* user, struct lokinet_context* ctx)\n    {\n        if (acceptFilter == nullptr)\n        {\n            acceptFilter = [](auto, auto, auto) { return 0; };\n        }\n        if (not ctx)\n            return -1;\n        std::promise<int> promise;\n        {\n            auto lock = ctx->acquire();\n            if (not ctx->impl->IsUp())\n            {\n                return -1;\n            }\n\n            ctx->impl->CallSafe([ctx, acceptFilter, user, &promise]() {\n                auto ep = ctx->endpoint();\n                auto* quic = ep->GetQUICTunnel();\n                auto id =\n                    quic->listen([acceptFilter, user](auto remoteAddr, auto port) -> std::optional<llarp::SockAddr> {\n                        std::string remote{remoteAddr};\n                        if (auto result = acceptFilter(remote.c_str(), port, user))\n                        {\n                            if (result == -1)\n                            {\n                                throw std::invalid_argument{\"rejected\"};\n                            }\n                        }\n                        else\n                            return llarp::SockAddr{\"127.0.0.1:\" + std::to_string(port)};\n                        return std::nullopt;\n                    });\n                promise.set_value(id);\n            });\n        }\n        auto ftr = promise.get_future();\n        auto id = ftr.get();\n        {\n            auto lock = ctx->acquire();\n            ctx->inbound_stream(id);\n        }\n        return id;\n    }\n\n    char* EXPORT lokinet_hex_to_base32z(const char* hex)\n    {\n        std::string_view hexview{hex};\n        if (not oxenc::is_hex(hexview))\n            return nullptr;\n\n        const size_t b32z_len = oxenc::to_base32z_size(oxenc::from_hex_size(hexview.size()));\n        auto buf = std::make_unique<char[]>(b32z_len + 1);\n        buf[b32z_len] = '\\0';  // null terminate\n\n        oxenc::hex_decoder decode{hexview.begin(), hexview.end()};\n        oxenc::base32z_encoder encode{decode, decode.end()};\n        std::copy(encode, encode.end(), buf.get());\n        return buf.release();  // leak the buffer to the caller\n    }\n\n    void EXPORT lokinet_close_stream(int stream_id, struct lokinet_context* ctx)\n    {\n        if (not ctx)\n            return;\n        auto lock = ctx->acquire();\n        if (not ctx->impl->IsUp())\n            return;\n\n        try\n        {\n            std::promise<void> promise;\n            bool inbound = ctx->streams.at(stream_id);\n            ctx->impl->CallSafe([stream_id, inbound, ctx, &promise]() {\n                auto ep = ctx->endpoint();\n                auto* quic = ep->GetQUICTunnel();\n                try\n                {\n                    if (inbound)\n                        quic->forget(stream_id);\n                    else\n                        quic->close(stream_id);\n                }\n                catch (...)\n                {}\n                promise.set_value();\n            });\n            promise.get_future().get();\n        }\n        catch (...)\n        {}\n    }\n\n    int EXPORT\n    lokinet_srv_lookup(char* host, char* service, struct lokinet_srv_lookup_result* result, struct lokinet_context* ctx)\n    {\n        if (result == nullptr or ctx == nullptr or host == nullptr or service == nullptr)\n            return -1;\n        // sanity check, if the caller has not free()'d internals yet free them\n        if (result->internal)\n            delete result->internal;\n        result->internal = new lokinet_srv_lookup_private{};\n        return result->internal->LookupSRV(host, service, ctx);\n    }\n\n    void EXPORT\n    lokinet_for_each_srv_record(struct lokinet_srv_lookup_result* result, lokinet_srv_record_iterator iter, void* user)\n    {\n        if (result and result->internal)\n        {\n            result->internal->IterateAll([iter, user](auto* result) { iter(result, user); });\n        }\n        else\n        {\n            iter(nullptr, user);\n        }\n    }\n\n    void EXPORT lokinet_srv_lookup_done(struct lokinet_srv_lookup_result* result)\n    {\n        if (result == nullptr or result->internal == nullptr)\n            return;\n        delete result->internal;\n        result->internal = nullptr;\n    }\n\n    int EXPORT lokinet_udp_bind(\n        uint16_t exposedPort,\n        lokinet_udp_flow_filter filter,\n        lokinet_udp_flow_recv_func recv,\n        lokinet_udp_flow_timeout_func timeout,\n        void* user,\n        struct lokinet_udp_bind_result* result,\n        struct lokinet_context* ctx)\n    {\n        if (filter == nullptr or recv == nullptr or timeout == nullptr or result == nullptr or ctx == nullptr)\n            return EINVAL;\n\n        auto lock = ctx->acquire();\n        if (auto ep = ctx->endpoint())\n        {\n            if (auto maybe =\n                    ctx->make_udp_handler(ep, llarp::net::port_t::from_host(exposedPort), filter, recv, timeout, user))\n            {\n                result->socket_id = *maybe;\n                return 0;\n            }\n        }\n        return EINVAL;\n    }\n\n    void EXPORT lokinet_udp_close(int socket_id, struct lokinet_context* ctx)\n    {\n        if (ctx)\n        {\n            ctx->remove_udp_handler(socket_id);\n        }\n    }\n\n    int EXPORT lokinet_udp_flow_send(\n        const struct lokinet_udp_flowinfo* remote, const void* ptr, size_t len, struct lokinet_context* ctx)\n    {\n        if (remote == nullptr or remote->remote_port == 0 or ptr == nullptr or len == 0 or ctx == nullptr)\n            return EINVAL;\n        std::shared_ptr<llarp::EndpointBase> ep;\n        llarp::nuint16_t srcport{0};\n        auto dstport = llarp::net::port_t::from_host(remote->remote_port);\n        {\n            auto lock = ctx->acquire();\n            if (auto itr = ctx->udp_sockets.find(remote->socket_id); itr != ctx->udp_sockets.end())\n            {\n                ep = itr->second->m_Endpoint.lock();\n                srcport = itr->second->m_LocalPort;\n            }\n            else\n                return EHOSTUNREACH;\n        }\n        if (auto maybe = llarp::service::parse_address(std::string{remote->remote_host}))\n        {\n            llarp::net::IPPacket pkt = llarp::net::IPPacket::UDP(\n                llarp::nuint32_t{0},\n                srcport,\n                llarp::nuint32_t{0},\n                dstport,\n                llarp_buffer_t{reinterpret_cast<const uint8_t*>(ptr), len});\n\n            if (pkt.empty())\n                return EINVAL;\n            std::promise<int> ret;\n            ctx->impl->router->loop()->call([addr = *maybe, pkt = pkt.to_string(), ep, &ret]() {\n                if (auto tag = ep->GetBestConvoTagFor(addr))\n                {\n                    if (ep->send_to(*tag, pkt))\n                    {\n                        ret.set_value(0);\n                        return;\n                    }\n                }\n                ret.set_value(ENETUNREACH);\n            });\n            return ret.get_future().get();\n        }\n        return EINVAL;\n    }\n\n    int EXPORT lokinet_udp_establish(\n        lokinet_udp_create_flow_func create_flow,\n        void* user,\n        const struct lokinet_udp_flowinfo* remote,\n        struct lokinet_context* ctx)\n    {\n        if (create_flow == nullptr or remote == nullptr or ctx == nullptr)\n            return EINVAL;\n        std::shared_ptr<llarp::EndpointBase> ep;\n        {\n            auto lock = ctx->acquire();\n            if (ctx->impl->router->loop()->inside())\n            {\n                oxen::log::error(logcat, \"Cannot call udp_establish from internal event loop\");\n                return EINVAL;\n            }\n            if (auto itr = ctx->udp_sockets.find(remote->socket_id); itr != ctx->udp_sockets.end())\n            {\n                ep = itr->second->m_Endpoint.lock();\n            }\n            else\n                return EHOSTUNREACH;\n        }\n        if (auto maybe = llarp::service::parse_address(std::string{remote->remote_host}))\n        {\n            {\n                // check for pre existing flow\n                auto lock = ctx->acquire();\n                if (auto itr = ctx->udp_sockets.find(remote->socket_id); itr != ctx->udp_sockets.end())\n                {\n                    auto& udp = itr->second;\n                    if (udp->m_Flows.count(*maybe))\n                    {\n                        // we already have a flow.\n                        return EADDRINUSE;\n                    }\n                }\n            }\n\n            std::promise<bool> gotten;\n\n            ctx->impl->router->loop()->call([maybe_addr = *maybe, ep, &gotten]() {\n                if (auto* addr = std::get_if<llarp::service::Address>(&maybe_addr))\n                {\n                    ep->MarkAddressOutbound(*addr);\n                    auto res =\n                        ep->EnsurePathTo(*addr, [&gotten](auto result) { gotten.set_value(result.has_value()); }, 5s);\n                    if (not res)\n                    {\n                        gotten.set_value(false);\n                    }\n                }\n            });\n            if (gotten.get_future().get())\n            {\n                void* flow_data{nullptr};\n                int flow_timeoutseconds{};\n                create_flow(user, &flow_data, &flow_timeoutseconds);\n                {\n                    auto lock = ctx->acquire();\n                    if (auto itr = ctx->udp_sockets.find(remote->socket_id); itr != ctx->udp_sockets.end())\n                    {\n                        itr->second->AddFlow(*maybe, *remote, flow_data, flow_timeoutseconds);\n                        return 0;\n                    }\n                    return EADDRINUSE;\n                }\n            }\n            else\n                return ETIMEDOUT;\n        }\n        return EINVAL;\n    }\n\n    void EXPORT lokinet_set_syncing_logger(lokinet_logger_func func, lokinet_logger_sync sync, void* user)\n    {\n        llarp::log::clear_sinks();\n        llarp::log::add_sink(std::make_shared<llarp::logging::CallbackSink_mt>(func, sync, user));\n    }\n\n    void EXPORT lokinet_set_logger(lokinet_logger_func func, void* user)\n    {\n        lokinet_set_syncing_logger(func, nullptr, user);\n    }\n}\n"
  },
  {
    "path": "llarp/messages/common.cpp",
    "content": "#include \"common.hpp\"\n\nnamespace llarp::messages\n{\n\n    std::string serialize_status_response(std::string_view value)\n    {\n        oxenc::bt_dict_producer p;\n        p.append(STATUS_KEY, value);\n        return std::move(p).str();\n    }\n\n    const std::string TIMEOUT_RESPONSE = serialize_status_response(\"TIMEOUT\");\n    const std::string ERROR_RESPONSE = serialize_status_response(\"ERROR\");\n    const std::string OK_RESPONSE = serialize_status_response(\"OK\");\n\n}  // namespace llarp::messages\n"
  },
  {
    "path": "llarp/messages/common.hpp",
    "content": "#pragma once\n\n#include <llarp/contact/relay_contact.hpp>\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/path/transit_hop.hpp>\n#include <llarp/util/buffer.hpp>\n#include <llarp/util/logging.hpp>\n\n#include <oxenc/bt_producer.h>\n\nnamespace llarp::messages\n{\n    inline constexpr auto STATUS_KEY = \"!\"sv;\n    std::string serialize_status_response(std::string_view value);\n\n    extern const std::string TIMEOUT_RESPONSE;\n    extern const std::string ERROR_RESPONSE;\n    extern const std::string OK_RESPONSE;\n}  // namespace llarp::messages\n\nnamespace llarp\n{\n\n    // Copies the contents out of a bt_dict_producer into a std::vector<std::byte>.\n    // TODO FIXME - avoid the need to use this, by making bt_dict_producer able to write into and\n    // extract a vector directly.\n    inline std::vector<std::byte> to_bytes(const oxenc::bt_dict_producer& btdp)\n    {\n        auto content = btdp.span<std::byte>();\n        return {content.begin(), content.end()};\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/messages/dht.cpp",
    "content": "#include \"dht.hpp\"\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"dht.msgs\");\n\n    namespace PublishClientContact\n    {\n        const std::string INVALID = messages::serialize_status_response(\"INVALID CC\");\n        const std::string EXPIRED = messages::serialize_status_response(\"EXPIRED CC\");\n\n        /** Bt-encoded contents:\n            - 'e' : EncryptedClientContact\n            - 'n' : (only for network publishes, omitted for distribution through current inbound\n              sessions). 0-3 position indicator.  A client publishes their introset to the 4 closest\n              network locations by sending the introset down their inbound/utility paths to 4 random\n              routers; each message containing a value from 0 to 3 indicating where to forward the\n              CC to.\n\n              E.g.\n                client -> ...path1... -> relayA sends n=0 to ask relayA to forward to the best relay\n                client -> ...path2... -> relayB sends n=1 to ask relayB to forward to the second-best relay\n\n              and so on for n=2 and n=3.  The 4 publishing locations is for redundancy against\n              publishing failures that might miss one, and against service node composition changes\n              that might add or remove a service node (thus changing which 4 are the four best\n              storage locations).\n         */\n        std::vector<std::byte> serialize(const EncryptedClientContact& ecc, std::optional<int> location)\n        {\n            oxenc::bt_dict_producer btdp;\n\n            btdp.append(\"e\", ecc.bt_payload());\n            btdp.append(\"n\", location);\n\n            return to_bytes(btdp);\n        }\n\n        std::pair<EncryptedClientContact, std::optional<int>> deserialize(oxenc::bt_dict_consumer&& btdc)\n        {\n            try\n            {\n                return {EncryptedClientContact{btdc.require_span<std::byte>(\"e\")}, btdc.maybe<int>(\"n\")};\n            }\n            catch (const std::exception& e)\n            {\n                throw std::runtime_error{\"Exception caught deserializing EncryptedClientContact: {}\"_format(e.what())};\n            }\n        }\n    }  // namespace PublishClientContact\n\n    namespace FindClientContact\n    {\n        const std::string NOT_FOUND = messages::serialize_status_response(\"NOT FOUND\");\n        const std::string INSUFFICIENT = messages::serialize_status_response(\"INSUFFICIENT NODES\");\n        const std::string INVALID_ORDER = messages::serialize_status_response(\"INVALID ORDER\");\n\n        /** Bt-encoded contents:\n            - 'k' : blinded pubkey corresponding to client contact\n\n            Note: we are bt-encoding to leave space for future fields (ex: version)\n         */\n        std::vector<std::byte> serialize(const PubKey& location)\n        {\n            oxenc::bt_dict_producer btdp;\n\n            btdp.append(\"k\", location.span());\n\n            return to_bytes(btdp);\n        }\n\n        PubKey deserialize(oxenc::bt_dict_consumer&& btdc)\n        {\n            PubKey key;\n\n            try\n            {\n                key.assign(btdc.require_span<std::byte, PubKey::SIZE>(\"k\"));\n            }\n            catch (const std::exception& e)\n            {\n                log::error(logcat, \"Error: failed to deserialize FindClientContact contents: {}\", e.what());\n                throw;\n            }\n\n            return key;\n        }\n\n        /** Bt-encoded contents:\n            - 'x' : EncryptedClientContact\n\n            Note: we are bt-encoding to leave space for future fields (ex: version)\n         */\n        std::vector<std::byte> serialize_response(const EncryptedClientContact& ecc)\n        {\n            oxenc::bt_dict_producer btdp;\n\n            btdp.append(\"x\", ecc.bt_payload());\n\n            return to_bytes(btdp);\n        }\n\n        EncryptedClientContact deserialize_response(oxenc::bt_dict_consumer&& btdc)\n        {\n            EncryptedClientContact ecc;\n\n            try\n            {\n                ecc = EncryptedClientContact{btdc.require_span<std::byte>(\"x\")};\n            }\n            catch (const std::exception& e)\n            {\n                throw std::runtime_error{\"Exception caught deserializing EncryptedClientContact: {}\"_format(e.what())};\n            }\n\n            return ecc;\n        }\n    }  //  namespace FindClientContact\n\n    namespace ResolveSNS\n    {\n        const std::string NOT_FOUND = messages::serialize_status_response(\"NOT FOUND\");\n\n        /** Bt-encoded contents:\n            - 's' : SNS name\n\n            Note: we are bt-encoding to leave space for future fields (ex: version)\n         */\n        std::vector<std::byte> serialize(std::span<const std::byte, SHORTHASHSIZE> name_hash)\n        {\n            oxenc::bt_dict_producer btdp;\n\n            btdp.append(\"s\", name_hash);\n\n            return to_bytes(btdp);\n        }\n\n        std::string deserialize(oxenc::bt_dict_consumer&& btdc)\n        {\n            try\n            {\n                return btdc.require<std::string>(\"s\");\n            }\n            catch (const std::exception& e)\n            {\n                log::error(logcat, \"Error: failed to deserialize ResolveSNS contents: {}\", e.what());\n                throw;\n            }\n        }\n\n        /** Bt-encoded contents:\n            - 'x' : EncryptedSNSRecord\n\n            Note: we are bt-encoding to leave space for future fields (ex: version)\n         */\n        std::vector<std::byte> serialize_response(const EncryptedSNSRecord& enc)\n        {\n            oxenc::bt_dict_producer btdp;\n\n            btdp.append(\"x\", enc.bt_payload());\n\n            return to_bytes(btdp);\n        }\n\n        EncryptedSNSRecord deserialize_response(oxenc::bt_dict_consumer&& btdc)\n        {\n            EncryptedSNSRecord enc{};\n\n            try\n            {\n                enc = EncryptedSNSRecord::deserialize(btdc.require<std::string_view>(\"x\"));\n            }\n            catch (const std::exception& e)\n            {\n                log::error(logcat, \"Error: failed to deserialize ResolveSNS contents: {}\", e.what());\n                throw;\n            }\n\n            return enc;\n        }\n    }  // namespace ResolveSNS\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/messages/dht.hpp",
    "content": "#pragma once\n\n#include \"common.hpp\"\n\n#include <llarp/contact/client_contact.hpp>\n#include <llarp/contact/sns.hpp>\n\nnamespace llarp\n{\n    namespace PublishClientContact\n    {\n        extern const std::string INVALID;\n        extern const std::string EXPIRED;\n\n        std::vector<std::byte> serialize(const EncryptedClientContact& ecc, std::optional<int> location = std::nullopt);\n\n        std::pair<EncryptedClientContact, std::optional<int>> deserialize(oxenc::bt_dict_consumer&& btdc);\n\n    }  // namespace PublishClientContact\n\n    namespace FindClientContact\n    {\n        extern const std::string NOT_FOUND;\n        extern const std::string INSUFFICIENT;\n        extern const std::string INVALID_ORDER;\n\n        /** Bt-encoded contents:\n            - 'k' : blinded pubkey of the queried client contact\n\n            Note: we are bt-encoding to leave space for future fields (ex: version)\n         */\n        std::vector<std::byte> serialize(const PubKey& location);\n\n        PubKey deserialize(oxenc::bt_dict_consumer&& btdc);\n\n        /** Bt-encoded contents:\n            - 'x' : EncryptedClientContact\n\n            Note: we are bt-encoding to leave space for future fields (ex: version)\n         */\n        std::vector<std::byte> serialize_response(const EncryptedClientContact& ecc);\n\n        EncryptedClientContact deserialize_response(oxenc::bt_dict_consumer&& btdc);\n\n    }  //  namespace FindClientContact\n\n    namespace ResolveSNS\n    {\n        extern const std::string NOT_FOUND;\n\n        /** Bt-encoded contents:\n            - 's' : SNS name\n\n            Note: we are bt-encoding to leave space for future fields (ex: version)\n         */\n        std::vector<std::byte> serialize(std::span<const std::byte, SHORTHASHSIZE> name_hash);\n\n        std::string deserialize(oxenc::bt_dict_consumer&& btdc);\n\n        /** Bt-encoded contents:\n            - 'x' : EncryptedSNSRecord\n\n            Note: we are bt-encoding to leave space for future fields (ex: version)\n         */\n        std::vector<std::byte> serialize_response(const EncryptedSNSRecord& enc);\n\n        EncryptedSNSRecord deserialize_response(oxenc::bt_dict_consumer&& btdc);\n\n    }  // namespace ResolveSNS\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/messages/fetch.cpp",
    "content": "#include \"fetch.hpp\"\n\n#include \"common.hpp\"\n\nnamespace llarp\n{\n    namespace FetchRC\n    {\n        const std::string INVALID_REQUEST = messages::serialize_status_response(\"Invalid relay ID requested\");\n\n        std::vector<std::byte> serialize(std::span<const RouterID> explicit_ids)\n        {\n            oxenc::bt_dict_producer btdp;\n\n            {\n                auto sublist = btdp.append_list(\"x\");\n                for (const auto& rid : explicit_ids)\n                    sublist.append(rid.span());\n            }\n\n            return to_bytes(btdp);\n        }\n\n        std::vector<RelayContact> deserialize_response(NetID netid, oxenc::bt_dict_consumer&& btdc)\n        {\n            std::vector<RelayContact> rcs;\n\n            for (auto sublist = btdc.require<oxenc::bt_list_consumer>(\"r\"); not sublist.is_finished();)\n                rcs.emplace_back(sublist.consume_dict_data(), netid);\n\n            return rcs;\n        }\n    }  // namespace FetchRC\n\n    namespace FetchRID\n    {\n        std::vector<std::byte> serialize(const RouterID& source)\n        {\n            oxenc::bt_dict_producer btdp;\n            btdp.append(\"s\", source.span());\n            return to_bytes(btdp);\n        }\n    }  // namespace FetchRID\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/messages/fetch.hpp",
    "content": "#pragma once\n\n#include <llarp/contact/relay_contact.hpp>\n#include <llarp/contact/router_id.hpp>\n#include <llarp/util/logging/buffer.hpp>\n\nnamespace llarp\n{\n    namespace FetchRC\n    {\n        extern const std::string INVALID_REQUEST;\n\n        std::vector<std::byte> serialize(std::span<const RouterID> explicit_ids);\n\n        std::vector<RelayContact> deserialize_response(NetID netid, oxenc::bt_dict_consumer&& btdc);\n\n    }  // namespace FetchRC\n\n    namespace FetchRID\n    {\n        inline constexpr auto INVALID_REQUEST = \"Invalid relay ID requested to relay response from.\"sv;\n\n        std::vector<std::byte> serialize(const RouterID& source);\n\n    }  // namespace FetchRID\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/messages/path.cpp",
    "content": "#include \"path.hpp\"\n\n#include \"common.hpp\"\n\n#include <llarp/constants/path.hpp>\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/util/bspan.hpp>\n\n#include <ranges>\n#include <stdexcept>\n\nnamespace llarp\n{\n\n    static auto logcat = llarp::log::Cat(\"path.msgs\");\n\n    // FIXME TODO: get rid of this file.  Serialization belongs with the thing being serialized, not\n    // in some header far removed.\n\n    namespace PATH\n    {\n        namespace BUILD\n        {\n\n            const std::string NO_TRANSIT = messages::serialize_status_response(\"NOT ALLOWING TRANSIT\"sv);\n            const std::string BAD_LIFETIME = messages::serialize_status_response(\"BAD PATH LIFETIME (TOO LONG)\"sv);\n            const std::string BAD_FRAMES = messages::serialize_status_response(\"BAD FRAMES\"sv);\n            const std::string BAD_PATHID = messages::serialize_status_response(\"BAD PATH ID\"sv);\n\n        }  // namespace BUILD\n\n        namespace CONTROL\n        {\n            /** Fields for transmitting Path Control:\n                - 'e' : request endpoint being invoked\n                - 'p' : request payload\n            */\n            std::vector<std::byte> serialize(std::string_view endpoint, std::span<const std::byte> payload)\n            {\n                oxenc::bt_dict_producer btdp;\n                btdp.append(\"e\", endpoint);\n                btdp.append(\"p\", payload);\n                return to_bytes(btdp);\n            }\n\n            std::pair<std::string, std::string> deserialize(oxenc::bt_dict_consumer&& btdc)\n            {\n                std::pair<std::string, std::string> ret;\n                auto& [endpoint, payload] = ret;\n\n                try\n                {\n                    endpoint = btdc.require<std::string>(\"e\");\n                    payload = btdc.require<std::string>(\"p\");\n                    return ret;\n                }\n                catch (const std::exception& e)\n                {\n                    throw std::runtime_error{\"Exception caught deserializing path control: {}\"_format(e.what())};\n                }\n            }\n        }  // namespace CONTROL\n\n    }  // namespace PATH\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/messages/path.hpp",
    "content": "#pragma once\n\n#include <llarp/address/address.hpp>\n#include <llarp/constants/path.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/util/logging/buffer.hpp>\n\nnamespace llarp::PATH\n{\n    namespace BUILD\n    {\n        extern const std::string NO_TRANSIT;\n        extern const std::string BAD_LIFETIME;\n        extern const std::string BAD_FRAMES;\n        extern const std::string BAD_PATHID;\n        extern const std::string BAD_CRYPTO;\n\n    }  // namespace BUILD\n\n    namespace CONTROL\n    {\n        std::vector<std::byte> serialize(std::string_view endpoint, std::span<const std::byte> payload);\n\n        std::pair<std::string, std::string> deserialize(oxenc::bt_dict_consumer&& btdc);\n\n    }  // namespace CONTROL\n\n}  // namespace llarp::PATH\n"
  },
  {
    "path": "llarp/messages/session.cpp",
    "content": "#include \"session.hpp\"\n\n#include \"common.hpp\"\n#include \"path.hpp\"\n\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/util/bspan.hpp>\n\nnamespace llarp\n{\n    static auto logcat = llarp::log::Cat(\"session.msgs\");\n\n    namespace InitiateSession\n    {\n        const std::string AUTH_ERROR = messages::serialize_status_response(\"AUTH ERROR\");\n        const std::string BAD_ROUTE = messages::serialize_status_response(\"BAD ROUTE\");\n        const std::string BAD_ADDRESS = messages::serialize_status_response(\"BAD ADDRESS\");\n\n        std::vector<std::byte> serialize(\n            const RouterID& local,\n            HopID local_pivot_txid,\n            HopID remote_pivot_txid,\n            std::optional<std::string_view> auth_token)\n        {\n            try\n            {\n                oxenc::bt_dict_producer btdp;\n\n                btdp.append(\"i\", local.span());\n                // TODO FIXME: include identity proof\n                btdp.append(\"p\", local_pivot_txid.span());\n                btdp.append(\"r\", remote_pivot_txid.span());\n                // TOTHINK: this auth field\n                if (auth_token)\n                    btdp.append(\"u\", *auth_token);\n\n                return to_bytes(btdp);\n            }\n            catch (const std::exception& e)\n            {\n                log::error(logcat, \"Exception caught encrypting session initiation message: {}\", e.what());\n                throw;\n            }\n        }\n\n        std::pair<std::vector<std::byte>, SharedSecret> serialize_encrypt(\n            const RouterID& local,\n            const RouterID& remote,\n            HopID local_pivot_txid,\n            HopID remote_pivot_txid,\n            std::optional<std::string_view> auth_token)\n        {\n            try\n            {\n                auto payload = serialize(local, local_pivot_txid, remote_pivot_txid, std::move(auth_token));\n\n                auto [secret, eph_pk, dh_nonce] = crypto::dh_client_gen(remote);\n                crypto::xchacha20(payload, secret, dh_nonce);\n\n                oxenc::bt_dict_producer btdp;\n\n                btdp.append(\"k\", eph_pk.span());\n                btdp.append(\"n\", dh_nonce.span());\n                btdp.append(\"x\", payload);\n\n                return {to_bytes(btdp), secret};\n            }\n            catch (const std::exception& e)\n            {\n                log::error(logcat, \"Exception caught encrypting session initiation message: {}\", e.what());\n                throw;\n            }\n        }\n\n        Parameters decrypt_deserialize(oxenc::bt_dict_consumer&& outer_btdc, const Ed25519SecretKey& local)\n        {\n            Parameters ret;\n\n            PubKey eph_pubkey;\n            SymmNonce dh_nonce;\n            std::vector<std::byte> payload;\n            eph_pubkey.assign(outer_btdc.require_span<std::byte, PubKey::SIZE>(\"k\"));\n            dh_nonce.assign(outer_btdc.require_span<std::byte, SymmNonce::SIZE>(\"n\"));\n            auto payld = outer_btdc.require<std::span<const std::byte>>(\"x\");\n            payload.assign(payld.begin(), payld.end());\n            outer_btdc.finish();\n\n            crypto::dh_server(ret.session_key, eph_pubkey, local, dh_nonce);\n            crypto::xchacha20(payload, ret.session_key, dh_nonce);\n\n            oxenc::bt_dict_consumer inner{std::move(payload)};\n\n            RouterID remote;\n            remote.assign(inner.require_span<std::byte, RouterID::SIZE>(\"i\"));\n            ret.remote = {remote, true};\n\n            // inverted from serialization order; their local is our remote and vice-versa\n            ret.remote_pivot_txid.assign(inner.require_span<std::byte, HopID::SIZE>(\"p\"));\n            ret.local_pivot_txid.assign(inner.require_span<std::byte, HopID::SIZE>(\"r\"));\n            ret.auth_token = inner.maybe<std::string>(\"u\");\n            inner.finish();\n\n            return ret;\n        }\n\n        std::string serialize_response(session_tag& t)\n        {\n            oxenc::bt_dict_producer btdp;\n            btdp.append(\"t\", t.view());\n            return std::move(btdp).str();\n        }\n\n        session_tag deserialize_response(oxenc::bt_dict_consumer&& btdc)\n        {\n            try\n            {\n                return session_tag{btdc.require_span<std::byte, session_tag::SIZE>(\"t\")};\n            }\n            catch (const std::exception& e)\n            {\n                log::warning(logcat, \"Exception caught deserializing session initiation response:{}\", e.what());\n                throw;\n            }\n        }\n    }  // namespace InitiateSession\n\n    namespace CloseSession\n    {\n        std::string serialize(session_tag& t)\n        {\n            oxenc::bt_dict_producer btdp;\n            btdp.append(\"t\", t.view());\n            return std::move(btdp).str();\n        }\n\n        session_tag deserialize(oxenc::bt_dict_consumer&& btdc)\n        {\n            try\n            {\n                return session_tag{btdc.require_span<std::byte, session_tag::SIZE>(\"t\")};\n            }\n            catch (const std::exception& e)\n            {\n                log::warning(logcat, \"Exception caught deserializing session close response:{}\", e.what());\n                throw;\n            }\n        }\n\n    }  // namespace CloseSession\n\n    namespace SetSessionTag\n    {\n        std::string serialize()\n        {\n            // FIXME TODO no values?  This doesn't seem right.\n            oxenc::bt_dict_producer btdp;\n            return std::move(btdp).str();\n        }\n    }  // namespace SetSessionTag\n\n    namespace SessionPathSwitch\n    {\n        const std::string BAD_TAG = messages::serialize_status_response(\"BAD TAG\");\n        const std::string BAD_ID = messages::serialize_status_response(\"BAD ID\");\n\n        std::string serialize(session_tag t, HopID local_pivot_txid, HopID remote_pivot_txid)\n        {\n            oxenc::bt_dict_producer btdp;\n\n            btdp.append(\"p\", local_pivot_txid.span());\n            btdp.append(\"r\", remote_pivot_txid.span());\n            btdp.append(\"t\", t.view());\n\n            return std::move(btdp).str();\n        }\n\n        std::tuple<session_tag, HopID, HopID> deserialize(oxenc::bt_dict_consumer&& btdc)\n        {\n            std::tuple<session_tag, HopID, HopID> result;\n            auto& [t, remote_pivot_txid, local_pivot_txid] = result;\n\n            try\n            {\n                remote_pivot_txid.assign(btdc.require_span<std::byte, HopID::SIZE>(\"p\"));\n                local_pivot_txid.assign(btdc.require_span<std::byte, HopID::SIZE>(\"r\"));\n                t.assign(btdc.require_span<std::byte, session_tag::SIZE>(\"t\"));\n                return result;\n            }\n            catch (const std::exception& e)\n            {\n                log::warning(logcat, \"Exception caught deserializing PathSwitch message: {}\", e.what());\n                throw;\n            }\n        }\n\n    }  // namespace SessionPathSwitch\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/messages/session.hpp",
    "content": "#pragma once\n\n#include <llarp/address/address.hpp>\n#include <llarp/auth/auth.hpp>\n#include <llarp/path/hopid.hpp>\n#include <llarp/util/logging/buffer.hpp>\n\nnamespace llarp\n{\n    /** Fields for initiating sessions:\n        - 'k' : ephemeral pubkey used to derive shared secret\n        - 'n' : nonce used for key exchange\n        - 'x' : encrypted payload\n            - 'i' : RouterID of initiator\n            - 'p' : HopID at the pivot taken from local ClientIntro\n            - 'r' : HopID at the pivot taken from remote's ClientIntro\n            - 'u' : Authentication field\n                - bt-encoded dict, values TBD\n    */\n\n    /** Fields for switching session paths:\n        - 'p' : HopID at the pivot taken from local ClientIntro\n        - 'r' : HopID at the pivot taken from remote's ClientIntro\n        - 't' : session_tag for current session\n    */\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/net/id.hpp",
    "content": "#pragma once\n#include <fmt/format.h>\n\n#include <string>\n\nnamespace llarp\n{\n\n    enum class NetID\n    {\n        MAINNET = 0,\n        TESTNET = 1\n    };\n\n    inline std::string to_string(NetID n)\n    {\n        switch (n)\n        {\n            case NetID::MAINNET:\n                return \"lokinet\";\n            case NetID::TESTNET:\n                return \"testnet\";\n            default:\n                return fmt::format(\"unknown network {}\", static_cast<int>(n));\n        }\n    }\n\n    inline NetID netid_from_string(std::string_view s)\n    {\n        if (s == \"lokinet\")\n            return NetID::MAINNET;\n        if (s == \"testnet\")\n            return NetID::TESTNET;\n        throw std::invalid_argument{\"Invalid network id\"};\n    }\n\n}  // namespace llarp\n\nnamespace fmt\n{\n    template <>\n    struct formatter<llarp::NetID, char> : formatter<std::string>\n    {\n        template <typename FormatContext>\n        auto format(llarp::NetID n, FormatContext& ctx) const\n        {\n            return formatter<std::string>::format(to_string(n), ctx);\n        }\n    };\n\n}  // namespace fmt\n"
  },
  {
    "path": "llarp/net/ip_headers.hpp",
    "content": "#pragma once\n\n#include \"utils.hpp\"\n\n#include <netinet/ip6.h>\n\nnamespace llarp\n{\n    struct ip_header\n    {\n#if __BYTE_ORDER == __LITTLE_ENDIAN\n        uint8_t header_len : 4;\n        uint8_t version : 4;\n#else\n        uint8_t version : 4;\n        uint8_t header_len : 4;\n#endif\n        uint8_t service_type;\n        uint16_t total_len;  // entire packet size\n        uint16_t id;\n        uint16_t frag_off;  // fragmentation offset\n        uint8_t ttl;\n        uint8_t protocol;\n        uint16_t checksum;\n        uint32_t src;\n        uint32_t dest;\n    };\n\n    static_assert(sizeof(ip_header) == 20);\n\n    struct ipv6_header\n    {\n#if __BYTE_ORDER == __LITTLE_ENDIAN\n        uint8_t tclass_hi : 4;\n        uint8_t version : 4;\n        uint8_t flow_hi : 4;\n        uint8_t tclass_lo : 4;\n#else\n        uint8_t version : 4;\n        uint8_t tclass_hi : 4;\n        uint8_t tclass_lo : 4;\n        uint8_t flow_hi : 4;\n#endif\n        uint16_t flow_lo;\n        uint16_t payload_len;\n        uint8_t protocol;\n        uint8_t hoplimit;\n        in6_addr src;\n        in6_addr dest;\n\n        /// Returns the traffic class value\n        constexpr uint8_t tclass() const { return (tclass_hi << 4) | tclass_lo; }\n\n        /// Sets a traffic class value\n        constexpr void tclass(uint8_t tcl)\n        {\n            tclass_hi = (tcl >> 4);\n            tclass_lo = tcl % 0xf;\n        }\n\n        /// Extracts the host-order decoded flowlabel\n        constexpr uint32_t flowlabel() const { return (flow_hi << 16) | oxenc::big_to_host(flow_lo); }\n\n        /// Sets a host-order flowlabel.\n        constexpr void flowlabel(uint32_t label)\n        {\n            flow_hi = (label >> 16) & 0x0f;\n            flow_lo = oxenc::host_to_big(label & 0xffff);\n        }\n    };\n\n    static_assert(sizeof(ipv6_header) == 40);\n\n    /** TODO: for mobile ipv6, implement ipv6 routing headers\n     */\n\n    enum class TCPFLAG : uint8_t\n    {\n        FIN = 0x01,\n        SYN = 0x02,\n        RST = 0x04,\n        PUSH = 0x08,\n        ACK = 0x10,\n        URG = 0x20\n    };\n\n    struct tcp_header\n    {\n        uint16_t src;    // src addr or port\n        uint16_t dest;   // dst addr or port\n        uint32_t seqno;  // sequence number\n        uint32_t ack;    // ack number\n#if __BYTE_ORDER == __LITTLE_ENDIAN\n        uint8_t : 4;           // unused/reserved\n        uint8_t data_off : 4;  // data offset\n#else\n        uint8_t data_off : 4;  // data offset\n        uint8_t : 4;           // unused/reserved\n#endif\n        uint8_t flags;\n        uint16_t window;\n        uint16_t checksum;\n        uint16_t urg_ptr;  // urgent ptr\n    };\n    static_assert(sizeof(tcp_header) == 20);\n\n    struct udp_header\n    {\n        uint16_t src;\n        uint16_t dest;\n        uint16_t len;  // datagram length\n        uint16_t checksum;\n    };\n    static_assert(sizeof(udp_header) == 8);\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/net/ip_packet.cpp",
    "content": "#include \"ip_packet.hpp\"\n\n#include <llarp/net/policy.hpp>\n#include <llarp/util/buffer.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/logging/buffer.hpp>\n#include <llarp/util/time.hpp>\n\n#include <oxenc/endian.h>\n\n#include <cstddef>\n#include <utility>\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"ip_packet\");\n\n    // constexpr auto IP_CSUM_OFF = offsetof(struct ip_header, checksum);\n    // constexpr auto IP_DST_OFF = offsetof(struct ip_header, dest);\n    // constexpr auto IP_SRC_OFF = offsetof(struct ip_header, src);\n    // constexpr auto IP_PROTO_OFF = offsetof(struct ip_header, protocol);\n    // constexpr auto TCP_DATA_OFF = oxenc::host_to_big<uint32_t>(0xF0000000);\n    constexpr auto TCP_CSUM_OFF = offsetof(struct tcp_header, checksum);\n    constexpr auto UDP_CSUM_OFF = offsetof(struct udp_header, checksum);\n    // auto TCP_DATA_OFFSET = htonl(0xF0000000);\n    // constexpr auto IS_PSEUDO = 0x10;\n\n    IPPacket::IPPacket(size_t sz)\n    {\n        if (sz and sz < MIN_PACKET_SIZE)\n            throw std::invalid_argument{\"Buffer size is too small for an IP packet!\"};\n        _buf.resize(sz, std::byte{0});\n    }\n\n    IPPacket::IPPacket(std::vector<std::byte>&& data) : _buf{std::move(data)}\n    {\n        if (_buf.size() < MIN_PACKET_SIZE)\n            throw std::invalid_argument{\"Buffer data is too small for an IP packet!\"};\n    }\n\n    IPPacket::IPPacket(std::span<const std::byte> buf)\n    {\n        if (buf.size() < MIN_PACKET_SIZE)\n            throw std::invalid_argument{\"Buffer data is too small for an IP packet!\"};\n\n        _buf.resize(buf.size());\n        std::memcpy(_buf.data(), buf.data(), buf.size());\n    }\n\n    std::span<const std::byte> IPPacket::udp_data()\n    {\n        auto proto = protocol();\n        if (proto != net::IPProtocol::UDP || payload_size() < 8)\n            return {};\n        return span().subspan(header_size() + 8);\n    }\n\n    void IPPacket::clear_addresses()\n    {\n        if (is_ipv4())\n            update_ipv4_address(ipv4{}, ipv4{});\n        else if (is_ipv6())\n            update_ipv6_address(ipv6{}, ipv6{});\n    }\n\n    void IPPacket::update_ipv4_address(const ipv4& src, const ipv4& dst)\n    {\n        log::trace(logcat, \"Setting new source ({}) and destination ({}) IPs\", src, dst);\n\n        auto& hdr = header();\n        if (auto ihs = size_t(hdr.header_len * 4), sz = size(); ihs <= sz)\n        {\n            auto* payload = data() + ihs;\n            auto payload_size = sz - ihs;\n            auto frag_off = size_t(oxenc::big_to_host(hdr.frag_off) & 0x1Fff) * 8;\n\n            auto ip_proto = static_cast<net::IPProtocol>(hdr.protocol);\n            switch (ip_proto)\n            {\n                case net::IPProtocol::TCP:\n                    if (frag_off <= TCP_CSUM_OFF && payload_size >= TCP_CSUM_OFF - frag_off + 2)\n                    {\n                        auto* tcp_hdr = reinterpret_cast<tcp_header*>(payload);\n                        tcp_hdr->checksum =\n                            utils::ipv4_tcp_checksum_diff(tcp_hdr->checksum, hdr.src, hdr.dest, src, dst);\n                    }\n                    break;\n                case net::IPProtocol::UDP:\n                case net::IPProtocol::UDP_LITE:  // UDP-Lite - same checksum place, same 0->0xFFff condition\n                    if (frag_off <= UDP_CSUM_OFF && payload_size >= UDP_CSUM_OFF + 2)\n                    {\n                        auto* udp_hdr = reinterpret_cast<udp_header*>(payload);\n                        udp_hdr->checksum =\n                            utils::ipv4_udp_checksum_diff(udp_hdr->checksum, hdr.src, hdr.dest, src, dst);\n                    }\n                    break;\n                case net::IPProtocol::DCCP:\n                    if (frag_off <= UDP_CSUM_OFF || payload_size >= UDP_CSUM_OFF - frag_off + 2)\n                    {\n                        auto* tcp_hdr = reinterpret_cast<tcp_header*>(payload);\n                        tcp_hdr->checksum =\n                            utils::ipv4_tcp_checksum_diff(tcp_hdr->checksum, hdr.src, hdr.dest, src, dst);\n                    }\n                    break;\n                default:\n                    // do nothing (or not implemented)\n                    break;\n            }\n        }\n\n        hdr.checksum = utils::ipv4_checksum_diff(hdr.checksum, hdr.src, hdr.dest, src, dst);\n\n        // set new IP addresses\n        hdr.src = oxenc::host_to_big(src.addr);\n        hdr.dest = oxenc::host_to_big(dst.addr);\n    }\n\n    void IPPacket::update_ipv6_address(const ipv6& src, const ipv6& dst, std::optional<uint32_t> flowlabel)\n    {\n        const auto sz = size();\n        // XXX should've been checked at upper level?\n        if (sz <= sizeof(ipv6_header))\n            return;\n\n        auto& hdr = v6_header();\n        if (flowlabel.has_value())\n            hdr.flowlabel(*flowlabel);\n\n        // IPv6 address\n        hdr.src = static_cast<in6_addr>(src);\n        hdr.dest = static_cast<in6_addr>(dst);\n\n        // TODO IPv6 header options\n        auto* pld = reinterpret_cast<const uint8_t*>(data()) + sizeof(ipv6_header);\n        auto psz = sz - sizeof(ipv6_header);\n\n        size_t fragoff = 0;\n        auto nextproto = hdr.protocol;\n        for (;;)\n        {\n            switch (nextproto)\n            {\n                case 0:   // Hop-by-Hop Options\n                case 43:  // Routing Header\n                case 60:  // Destination Options\n                {\n                    nextproto = pld[0];\n                    auto addlen = (size_t(pld[1]) + 1) * 8;\n                    if (psz < addlen)\n                        return;\n                    pld += addlen;\n                    psz -= addlen;\n                    break;\n                }\n\n                case 44:  // Fragment Header\n                    /*\n           +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n           |  Next Header  |   Reserved    |      Fragment Offset    |Res|M|\n           +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n           |                         Identification                        |\n           +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n                     */\n                    nextproto = pld[0];\n                    fragoff = (uint16_t(pld[2]) << 8) | (uint16_t(pld[3]) & 0xFC);\n                    if (psz < 8)\n                        return;\n                    pld += 8;\n                    psz -= 8;\n\n                    // jump straight to payload processing\n                    if (fragoff != 0)\n                        goto endprotohdrs;\n                    break;\n\n                default:\n                    goto endprotohdrs;\n            }\n        }\n    endprotohdrs:\n\n        uint16_t chksumoff{0};\n        uint16_t chksum{0};\n\n        bool is_udp = false;\n\n        switch (static_cast<net::IPProtocol>(nextproto))\n        {\n            case net::IPProtocol::TCP:\n                chksumoff = 16;\n                [[fallthrough]];\n            case net::IPProtocol::DCCP:\n                chksum = tcp_checksum_ipv6(&hdr.src, &hdr.dest, hdr.payload_len, 0);\n                // ones-complement addition fo 0xFFff is 0; this is verboten\n                if (chksum == 0xFFff)\n                    chksum = 0x0000;\n\n                chksumoff = chksumoff == 16 ? 16 : 6;\n                break;\n            case net::IPProtocol::UDP:\n            case net::IPProtocol::UDP_LITE:  // UDP-Lite - same checksum place, same 0->0xFFff condition\n                chksum = udp_checksum_ipv6(&hdr.src, &hdr.dest, hdr.payload_len, 0);\n                is_udp = true;\n                break;\n            default:\n                // do nothing\n                break;\n        }\n\n        auto check = is_udp ? (uint16_t*)(pld + 6) : (uint16_t*)(pld + chksumoff - fragoff);\n\n        *check = chksum;\n    }\n\n    std::optional<IPPacket> IPPacket::make_icmp_unreachable() const\n    {\n        if (is_ipv4())\n        {\n            const auto& header = *reinterpret_cast<const ip_header*>(data());\n            auto ip_hdr_sz = header.header_len * 4;\n\n            // ICMP unreachable includes a carbon copy of the illiciting packet prefix include *at\n            // least* the header; we also include the first 8 bytes after the header:\n            std::span orig_prefix{_buf.data(), std::min<size_t>(ip_hdr_sz + 8, _buf.size())};\n\n            size_t pkt_size = sizeof(ip_header) + ICMP_HEADER_SIZE + orig_prefix.size();\n\n            IPPacket pkt{pkt_size};\n\n            auto& hdr = pkt.header();\n            hdr.version = 0x04;\n            hdr.header_len = 0x05;\n            hdr.service_type = 0;\n            hdr.checksum = 0;\n            hdr.total_len = ntohs(pkt_size);\n            hdr.src = header.dest;\n            hdr.dest = header.src;\n            hdr.protocol = 1;  // ICMP\n            hdr.ttl = header.ttl;\n            hdr.frag_off = oxenc::host_to_big<uint16_t>(0b01000000'00000000);\n\n            std::byte* itr = pkt.data() + sizeof(ip_header);\n            auto* icmp_begin = itr;\n            *itr++ = std::byte{3};  // ICMP type 3 = 'destination unreachable'\n            *itr++ = std::byte{7};  // ICMP code 7 = 'Destination host unknown error'\n\n            // 2 byte checksum (we'll come back to this later)\n            auto* checksum = reinterpret_cast<uint16_t*>(itr);\n            itr += 2;\n            // optional length byte + unused byte + optional 2-byte next hop MTU (for code 4, i.e.\n            // not us).  We leave this all as zero (and buf is already 0 initialized).\n            itr += (1 + 1 + 2);\n\n            assert(itr == pkt.data() + sizeof(ip_header) + ICMP_HEADER_SIZE);\n\n            // carbon copy the original packet prefix:\n            std::memcpy(itr, orig_prefix.data(), orig_prefix.size());\n            itr += orig_prefix.size();\n\n            assert(itr == pkt.data() + pkt_size);\n\n            // calculate checksum of ip header\n            pkt.header().checksum = utils::ip_checksum(reinterpret_cast<const uint8_t*>(pkt.data()), sizeof(ip_header));\n\n            // calculate icmp checksum from everything from icmp header (inclusive) to the end.\n            *checksum =\n                utils::ip_checksum(reinterpret_cast<const uint8_t*>(icmp_begin), std::distance(icmp_begin, itr));\n\n            log::debug(logcat, \"Constructed ICMP unreachable packet\");\n            return pkt;\n        }\n\n        // TODO FIXME: ipv6\n\n        return std::nullopt;\n    }\n\n    // TODO: ipv6\n    std::vector<std::byte> IPPacket::make_udp_packet(\n        const quic::Address& src, const quic::Address& dest, std::span<const std::byte> payload)\n    {\n        std::vector<std::byte> pkt;\n        pkt.resize(sizeof(ip_header) + sizeof(udp_header) + payload.size());\n        auto* data = pkt.data();\n        data[1] = std::byte{0};  // DSCP and ECN\n        auto* ip_hdr = reinterpret_cast<ip_header*>(data);\n        data += sizeof(ip_header);\n        auto* udp_hdr = reinterpret_cast<udp_header*>(data);\n        data += sizeof(udp_header);\n        std::memcpy(data, payload.data(), payload.size());\n\n        ip_hdr->version = 4;\n        ip_hdr->header_len = 5;\n        ip_hdr->total_len = htons(sizeof(ip_header) + sizeof(udp_header) + payload.size());\n        ip_hdr->protocol = static_cast<uint8_t>(net::IPProtocol::UDP);  // udp\n        ip_hdr->ttl = 64;\n        ip_hdr->frag_off = oxenc::host_to_big<uint16_t>(0b01000000'00000000);\n\n        ip_hdr->src = oxenc::host_to_big(src.to_ipv4().addr);\n        ip_hdr->dest = oxenc::host_to_big(dest.to_ipv4().addr);\n        ip_hdr->checksum = utils::ip_checksum(reinterpret_cast<uint8_t*>(ip_hdr), sizeof(ip_header));\n\n        udp_hdr->src = oxenc::host_to_big(src.port());\n        udp_hdr->dest = oxenc::host_to_big(dest.port());\n        udp_hdr->len = oxenc::host_to_big<uint16_t>(payload.size() + sizeof(udp_header));\n        udp_hdr->checksum = 0;  // FIXME: does this matter?  old lokinet set 0\n\n        return pkt;\n    }\n\n    std::string IPPacket::info_printer::to_string() const\n    {\n        if (pkt.is_ipv4())\n        {\n            return \"IPv4[{}, {}B, src={}, dst={}]\"_format(\n                ip_protocol_name(pkt.protocol()), pkt.size(), *pkt.source_ipv4(), *pkt.dest_ipv4());\n        }\n        if (pkt.is_ipv6())\n        {\n            return \"IPv6[{}, {}B, src={}, dst={}]\"_format(\n                ip_protocol_name(pkt.protocol()), pkt.size(), *pkt.source_ipv6(), *pkt.dest_ipv6());\n        }\n        return \"IPPacket[<unknown-type>, {}B]\"_format(pkt.size());\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/net/ip_packet.hpp",
    "content": "#pragma once\n\n#include \"ip_headers.hpp\"\n#include \"policy.hpp\"\n\n#include <llarp/util/buffer.hpp>\n#include <llarp/util/formattable.hpp>\n#include <llarp/util/time.hpp>\n\n#include <oxen/quic/address.hpp>\n#include <oxen/quic/udp.hpp>\n#include <oxenc/endian.h>\n\nnamespace llarp\n{\n    inline constexpr size_t MAX_PACKET_SIZE{1500};\n    inline constexpr size_t MIN_PACKET_SIZE{20};\n\n    struct IPPacket;\n\n    using net_pkt_hook = std::function<void(quic::Packet&& pkt)>;\n    using ip_pkt_hook = std::function<void(IPPacket)>;\n\n    /** IPPacket\n        This class encapsulates the functionalities and attributes required for data transmission between the local\n        lokinet instance and the surrounding IP landscape. As data enters lokinet from the device/internet/etc, it is\n        transmitted across the network as a quic::Packet via QUIC. As it exits lokinet to the device/internet/etc, it\n        is constructed into an IPPacket.\n\n        This allows for necessary functionalities at the junction that data is entering and exiting the local lokinet\n        instance. For example\n\n    */\n    struct IPPacket\n    {\n      private:\n        std::vector<std::byte> _buf;\n\n      public:\n        IPPacket() : IPPacket{size_t{0}} {}\n        explicit IPPacket(size_t sz);\n        explicit IPPacket(std::vector<std::byte>&& data);\n        explicit IPPacket(std::span<const std::byte> buf);\n\n        // Is this gross thing really needed?\n        static std::optional<IPPacket> try_making(std::span<const std::byte> buf);\n\n        // TESTNET: debug methods\n        // uint16_t checksum() const { return _is_v4 ? header()->checksum : 0; }\n\n        ip_header& header() { return *reinterpret_cast<ip_header*>(data()); }\n        const ip_header& header() const { return *reinterpret_cast<const ip_header*>(data()); }\n\n        ipv6_header& v6_header() { return *reinterpret_cast<ipv6_header*>(data()); }\n        const ipv6_header& v6_header() const { return *reinterpret_cast<const ipv6_header*>(data()); }\n\n        size_t header_size() const\n        {\n            return is_ipv4() ? static_cast<size_t>(header().header_len) * 4 : is_ipv6() ? 40 : 0;\n        }\n        size_t payload_size() const\n        {\n            auto hsz = header_size();\n            return hsz >= _buf.size() ? 0 : _buf.size() - hsz;\n        }\n\n        bool is_ipv4() const { return _buf.size() >= sizeof(ip_header) && header().version == 4; }\n        bool is_ipv6() const { return _buf.size() >= sizeof(ipv6_header) && ipv6_header().version == 6; }\n        bool is_ip() const { return is_ipv4() || is_ipv6(); }\n\n        net::IPProtocol protocol() const\n        {\n            return is_ipv4() ? net::IPProtocol{header().protocol}\n                : is_ipv6()  ? net::IPProtocol{ipv6_header().protocol}\n                             : net::IPProtocol{};\n        }\n\n      private:\n        std::optional<uint16_t> _s_d_port(int offset) const\n        {\n            auto pr = protocol();\n            if (pr == net::IPProtocol::TCP || pr == net::IPProtocol::UDP)\n                if (auto hs = header_size(); _buf.size() >= hs + 4)\n                    return oxenc::load_big_to_host<uint16_t>(_buf.data() + hs + offset);\n            return std::nullopt;\n        }\n\n      public:\n        std::optional<uint16_t> source_port() const { return _s_d_port(0); }\n        std::optional<uint16_t> dest_port() const { return _s_d_port(2); }\n\n        std::optional<ipv4> source_ipv4() const\n        {\n            if (is_ipv4())\n                return ipv4{oxenc::big_to_host(header().src)};\n            return std::nullopt;\n        }\n        std::optional<ipv4> dest_ipv4() const\n        {\n            if (is_ipv4())\n                return ipv4{oxenc::big_to_host(header().dest)};\n            return std::nullopt;\n        }\n\n        std::optional<ipv6> source_ipv6() const\n        {\n            if (is_ipv6())\n                return ipv6{ipv6_header().src};\n            return std::nullopt;\n        }\n        std::optional<ipv6> dest_ipv6() const\n        {\n            if (is_ipv6())\n                return ipv6{ipv6_header().dest};\n            return std::nullopt;\n        }\n\n        std::span<const std::byte> udp_data();\n\n        void clear_addresses();\n\n        void update_ipv4_address(const ipv4& src, const ipv4& dst);\n\n        void update_ipv6_address(const ipv6& src, const ipv6& dst, std::optional<uint32_t> flowlabel = std::nullopt);\n\n        std::optional<IPPacket> make_icmp_unreachable() const;\n\n        static std::vector<std::byte> make_udp_packet(\n            const quic::Address& src, const quic::Address& dest, std::span<const std::byte> payload);\n\n        std::byte* data() { return _buf.data(); }\n        const std::byte* data() const { return _buf.data(); }\n\n        size_t size() const { return _buf.size(); }\n\n        std::span<std::byte> span() { return _buf; }\n        std::span<const std::byte> span() const { return _buf; }\n\n        std::span<uint8_t> u8span() { return {reinterpret_cast<uint8_t*>(data()), size()}; }\n        std::span<const uint8_t> u8span() const { return {reinterpret_cast<const uint8_t*>(data()), size()}; }\n\n        bool empty() const { return _buf.empty(); }\n\n        // Lightweight formattable proxy object:\n        struct info_printer\n        {\n            const IPPacket& pkt;\n            std::string to_string() const;\n            static constexpr bool to_string_formattable = true;\n        };\n\n        info_printer info_line() const { return {*this}; }\n    };\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/net/net.h",
    "content": "#pragma once\n#if defined(_WIN32) || defined(__MINGW32__)\n#include <winsock2.h>\n\n#include <ws2tcpip.h>\n#include <wspiapi.h>\n// because this shit is not defined for Windows NT reeeee\n#ifdef __cplusplus\nextern \"C\"\n{\n#endif\n#if _WIN32_WINNT < 0x600\n    const char* inet_ntop(int af, const void* src, char* dst, size_t size);\n    int inet_pton(int af, const char* src, void* dst);\n#endif\n#ifdef __cplusplus\n}\n#endif\ntypedef unsigned short in_port_t;\ntypedef unsigned int in_addr_t;\n#else\n#include <arpa/inet.h>\n#include <netinet/in.h>\n#include <sys/socket.h>\n#endif\n\n#ifndef __cplusplus\n#include <stdbool.h>\n#endif\n\n#include <sys/types.h>\n\nbool llarp_getifaddr(const char* ifname, int af, struct sockaddr* addr);\n"
  },
  {
    "path": "llarp/net/net_if.hpp",
    "content": "#pragma once\n#ifndef _WIN32\n// this file is a shim include for #include <net/if.h>\n#if defined(__linux__)\n#include <linux/if.h>\nextern \"C\" unsigned int\n#ifndef __GLIBC__\nif_nametoindex(const char* __ifname);\n#else\nif_nametoindex(const char* __ifname) __THROW;\n#endif\n#else\n#include <net/if.h>\n#endif\n#endif\n"
  },
  {
    "path": "llarp/net/platform.hpp",
    "content": "#pragma once\n#include <llarp/address/types.hpp>\n\n#include <oxen/log/format.hpp>\n#include <oxen/quic/address.hpp>\n\n#include <optional>\n\nusing namespace oxen::log::literals;\n\nnamespace llarp::net\n{\n\n    /// network platform (all methods virtual so it can be mocked by unit tests)\n    class Platform\n    {\n      public:\n        Platform() = default;\n        virtual ~Platform() = default;\n        Platform(const Platform&) = delete;\n        Platform(Platform&&) = delete;\n\n        /// get a pointer to our singleton instance used by full lokinet instances.\n        /// embedded clients (and unit test mocks) will not call this\n        static const Platform* Default_ptr();\n\n        virtual bool has_interface_address(ipv4 ip) const = 0;\n        virtual bool has_interface_address(ipv6 ip) const = 0;\n\n        // Attempts to guess a good default public network address from the system's public IP\n        // addresses; the returned Address (if set) will have its port set to the given value.\n        virtual std::optional<quic::Address> get_best_public_address(bool ipv4, uint16_t port) const = 0;\n\n        virtual std::optional<ipv4_net> find_free_ipv4_net(uint8_t mask = 16) const = 0;\n        virtual std::optional<ipv6_net> find_free_ipv6_net(uint8_t /*mask*/ = 64) const { return std::nullopt; }\n\n        // Attempts to find a usable tun device name.  This may return an empty string if naming\n        // cannot be controlled, or a pattern (e.g. \"lokitun%d\") depending on the OS.  Note in\n        // particular that that means the value returned here is merely suitable for creating the\n        // tun, but not necessarily the final name.\n        //\n        // If suggest is given then we try that first before falling back to a generic name.  (Note\n        // that the suggestion will be truncated at the OS name limit, e.g. 15 characters on linux).\n        //\n        // If possible (and no suggestion is given), we try to use lokitun0, lokitun1, etc.\n        virtual std::string find_free_tun(std::string_view suggest = \"\"sv) const = 0;\n\n        // Returns the IPv4 address of an interface, if it exists and has one, nullopt otherwise.\n        virtual std::optional<ipv4> get_interface_ipv4(std::string_view ifname) const = 0;\n\n        // Returns the IPv6 address of an interface, if it exists and has one, nullopt otherwise.\n        virtual std::optional<ipv6> get_interface_ipv6(std::string_view ifname) const = 0;\n\n        virtual std::optional<int> get_interface_index(ipv4 ip) const = 0;\n        virtual std::optional<int> get_interface_index(ipv6 ip) const = 0;\n    };\n\n}  // namespace llarp::net\n"
  },
  {
    "path": "llarp/net/policy.cpp",
    "content": "#include \"policy.hpp\"\n\n#include \"ip_packet.hpp\"\n\n#include <llarp/util/logging.hpp>\n#include <llarp/util/str.hpp>\n\n#include <stdexcept>\n\nextern \"C\"\n{\n#include <netdb.h>\n}\n\nnamespace llarp\n{\n    std::string to_string(protocol_flag p)\n    {\n        auto has_flag = [&p](protocol_flag f) { return (p & f) == f; };\n        return \"<{}{}{}{}>\"_format(\n            has_flag(protocol_flag::EXIT) ? \"exit\" : \"host\",\n            has_flag(protocol_flag::QUIC_TUNNEL) ? \"/quic\" : \"/no-quic\",\n            has_flag(protocol_flag::IPV4) ? \"/ipv4\" : \"/no-ipv4\",\n            has_flag(protocol_flag::IPV6) ? \"/ipv6\" : \"\");\n    }\n    namespace net\n    {\n        static auto logcat = log::Cat(\"TrafficPolicy\");\n\n        // Two functions copied over from llarp/net/ip_packet_old.hpp\n        static std::string ip_proto_str(IPProtocol proto)\n        {\n            if (const auto* ent = getprotobynumber(static_cast<int>(proto)))\n                return ent->p_name;\n\n            throw std::invalid_argument{\"Cannot determine protocol name for IP Protocol: {}\"_format(proto)};\n        }\n\n        bool ProtocolInfo::matches_packet_proto(const IPPacket& pkt) const { return pkt.protocol() == protocol; }\n\n        bool ExitPolicy::allow_ip_traffic(const IPPacket& pkt) const\n        {\n            // ranges are always the allow list (if empty, we route nothing).  Add a 0.0.0.0/0 or\n            // ::/0 if you want to allow everything.\n            auto accept_range = pkt.is_ipv4()\n                ? std::ranges::any_of(ranges, [dest = *pkt.dest_ipv4()](const auto& r) { return r.contains(dest); })\n                : pkt.is_ipv6()\n                ? std::ranges::any_of(ranges_v6, [dest = *pkt.dest_ipv6()](const auto& r) { return r.contains(dest); })\n                : false;\n\n            if (!accept_range)\n                return false;\n\n            // protocols only masks if non-empty:\n            if (!protocols.empty())\n                if (std::ranges::none_of(protocols, [&pkt](const auto& p) { return p.matches_packet_proto(pkt); }))\n                    return false;\n\n            return true;\n        }\n\n        void ProtocolInfo::bt_encode(oxenc::bt_list_producer&& btlp) const\n        {\n            btlp.append(static_cast<uint8_t>(protocol));\n            if (port)\n                btlp.append(*port);\n        }\n\n        ProtocolInfo ProtocolInfo::from_config(std::string_view config_input)\n        {\n            ProtocolInfo pi;\n            auto parts = split(config_input, \"/\");\n            if (parts.size() > 2)\n                throw std::invalid_argument{\"Unparseable IP protocol/port value '{}'\"_format(config_input)};\n            if (const auto* ent = ::getprotobyname(std::string{parts[0]}.c_str()))\n                pi.protocol = static_cast<IPProtocol>(ent->p_proto);\n            else if (uint8_t p; parts[0].starts_with(\"0x\") && parse_int(parts[0], p, 16) && p > 0)\n                pi.protocol = static_cast<IPProtocol>(p);\n            else\n                throw std::invalid_argument{\"No such IP protocol '{}'\"_format(parts[0])};\n\n            if (parts.size() == 2)\n                if (!parse_int(parts[1], pi.port.emplace()))\n                    throw std::invalid_argument{\"Invalid protocol port: '{}'\"_format(parts[1])};\n            return pi;\n        }\n\n        ProtocolInfo::ProtocolInfo(oxenc::bt_list_consumer&& enc)\n        {\n            protocol = IPProtocol{enc.consume_integer<uint8_t>()};\n            if (not enc.is_finished())\n                port = enc.consume_integer<uint16_t>();\n        }\n\n        void ExitPolicy::bt_decode(oxenc::bt_dict_consumer&& btdc)\n        {\n            try\n            {\n                if (auto protos = btdc.maybe<oxenc::bt_list_consumer>(\"p\"))\n                    while (not protos->is_finished())\n                        protocols.emplace(protos->consume_list_consumer());\n\n                if (auto rnges = btdc.maybe<oxenc::bt_list_consumer>(\"r\"))\n                {\n                    while (not rnges->is_finished())\n                    {\n                        auto r = decode_ip_range(rnges->consume_string_view());\n                        if (auto* r4 = std::get_if<ipv4_range>(&r))\n                            ranges.push_back(std::move(*r4));\n                        else\n                            ranges_v6.push_back(std::get<ipv6_range>(r));\n                    }\n                }\n            }\n            catch (const std::exception& e)\n            {\n                log::warning(logcat, \"Failed to parse ExitPolicy: {}\", e.what());\n                throw;\n            }\n        }\n\n        void ExitPolicy::bt_encode(oxenc::bt_dict_producer&& btdp) const\n        {\n            if (!protocols.empty())\n            {\n                auto protos = btdp.append_list(\"p\");\n                for (auto& p : protocols)\n                    p.bt_encode(protos.append_list());\n            }\n            if (!ranges.empty() || !ranges_v6.empty())\n            {\n                auto rnges = btdp.append_list(\"r\");\n                for (const auto& r : ranges)\n                    rnges.append(encode(r));\n                for (const auto& r : ranges_v6)\n                    rnges.append(encode(r));\n            }\n        }\n\n        bool ExitPolicy::bt_decode(std::string_view buf)\n        {\n            try\n            {\n                bt_decode(oxenc::bt_dict_consumer{buf});\n            }\n            catch (const std::exception& e)\n            {\n                // DISCUSS: rethrow or print warning/return false...?\n                auto err = \"TrafficPolicy parsing exception: {}\"_format(e.what());\n                log::warning(logcat, \"{}\", err);\n                throw std::runtime_error{err};\n            }\n\n            return true;\n        }\n    }  // namespace net\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/net/policy.hpp",
    "content": "#pragma once\n\n#include <llarp/address/ip_range.hpp>\n\n#include <oxenc/bt.h>\n\n#include <set>\n\nnamespace llarp\n{\n    struct IPPacket;\n\n    enum class protocol_flag : uint8_t\n    {\n        NONE = 0,\n        EXIT = 1 << 0,         // This client is configured to route (some) exit traffic\n        QUIC_TUNNEL = 1 << 1,  // This client supports QUIC tunnel (currently always enabled)\n        IPV4 = 1 << 2,         // This client support Lokinet raw IPv4 packets (always set for non-embedded clients)\n        IPV6 = 1 << 3,         // This client support Lokinet raw IPv6 packets (not yet supported)\n    };\n    inline constexpr protocol_flag operator&(protocol_flag a, protocol_flag b)\n    {\n        return static_cast<protocol_flag>(static_cast<uint8_t>(a) & static_cast<uint8_t>(b));\n    }\n    inline constexpr protocol_flag& operator&=(protocol_flag& a, protocol_flag b)\n    {\n        a = a & b;\n        return a;\n    }\n    inline constexpr protocol_flag operator|(protocol_flag a, protocol_flag b)\n    {\n        return static_cast<protocol_flag>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));\n    }\n    inline constexpr protocol_flag& operator|=(protocol_flag& a, protocol_flag b)\n    {\n        a = a | b;\n        return a;\n    }\n    // Returns true if `flags` contains all of the set flags in `contains`\n    inline constexpr bool has_flag(protocol_flag flags, protocol_flag contains)\n    {\n        return (flags & contains) == contains;\n    }\n\n    /** proto_mask\n        - When negotiating sessions and advertising client contacts, all flags can be used\n        - When prepended to a datagram, only 2 bits are needed for 4 configurations\n            - host, standard = 00\n            - host, quictun = 01\n            - exit, standard = 10\n            - exit, quictun = 11\n\n        FIXME: What?  I think this is obsolete.\n    */\n    inline constexpr protocol_flag proto_mask = protocol_flag::EXIT | protocol_flag::QUIC_TUNNEL;\n\n    std::string to_string(protocol_flag p);\n\n    namespace net\n    {\n        enum class IPProtocol : uint8_t\n        {\n            ICMP = 0x01,\n            IGMP = 0x02,\n            IPIP = 0x04,\n            TCP = 0x06,\n            UDP = 0x11,\n            DCCP = 0x21,\n            GRE = 0x2F,\n            ICMP6 = 0x3A,\n            OSPF = 0x59,\n            PGM = 0x71,\n            UDP_LITE = 0x88,\n        };\n\n        inline constexpr std::string_view ip_protocol_name(IPProtocol p)\n        {\n            switch (p)\n            {\n                case IPProtocol::ICMP:\n                    return \"ICMP\"sv;\n                case IPProtocol::IGMP:\n                    return \"IGMP\"sv;\n                case IPProtocol::IPIP:\n                    return \"IPIP\"sv;\n                case IPProtocol::TCP:\n                    return \"TCP\"sv;\n                case IPProtocol::UDP:\n                    return \"UDP\"sv;\n                case IPProtocol::DCCP:\n                    return \"DCCP\"sv;\n                case IPProtocol::GRE:\n                    return \"GRE\"sv;\n                case IPProtocol::ICMP6:\n                    return \"ICMP6\"sv;\n                case IPProtocol::OSPF:\n                    return \"OSPF\"sv;\n                case IPProtocol::PGM:\n                    return \"PGM\"sv;\n                case IPProtocol::UDP_LITE:\n                    return \"UDP-Lite\"sv;\n            }\n            return \"<UNKNOWN>\"sv;\n        }\n\n        /// information about an IP protocol\n        struct ProtocolInfo\n        {\n            /// ip protocol of this protocol\n            IPProtocol protocol;\n\n            /// the layer 4 port (TCP and UDP)\n            std::optional<uint16_t> port{std::nullopt};\n\n            ProtocolInfo() = default;\n            ProtocolInfo(oxenc::bt_list_consumer&& enc);\n\n            // Constructs from a user-supplied value typically from the config such as \"tcp\" or\n            // \"udp/53\" or \"0x69\".  Throws on invalid input.\n            static ProtocolInfo from_config(std::string_view config_input);\n\n            void bt_encode(oxenc::bt_list_producer&& btlp) const;\n\n            // Compares packet protocol with protocol info\n            bool matches_packet_proto(const IPPacket& pkt) const;\n\n            auto operator<=>(const ProtocolInfo& other) const = default;\n            bool operator==(const ProtocolInfo& other) const = default;\n        };\n\n        /// information about what exit traffic an endpoint will carry\n        struct ExitPolicy\n        {\n            /// ranges that are allowed.  If empty, allow none.\n            std::vector<ipv4_range> ranges;\n            std::vector<ipv6_range> ranges_v6;\n\n            /// protocols that are explicity allowed.  If empty, allow all.\n            std::set<ProtocolInfo> protocols;\n\n            bool empty() const { return ranges.empty() and protocols.empty(); }\n\n            void bt_encode(oxenc::bt_dict_producer&& btdp) const;\n\n            void bt_decode(oxenc::bt_dict_consumer&& btdc);\n\n            bool bt_decode(std::string_view buf);\n\n            // Verifies if IPPacket traffic is allowed; return true/false\n            bool allow_ip_traffic(const IPPacket& pkt) const;\n\n            bool operator==(const ExitPolicy& other) const = default;\n        };\n    }  // namespace net\n}  // namespace llarp\n\nnamespace fmt\n{\n    template <>\n    struct formatter<llarp::net::IPProtocol, char> : formatter<std::string_view>\n    {\n        template <typename FormatContext>\n        auto format(llarp::net::IPProtocol p, FormatContext& ctx) const\n        {\n            return formatter<std::string_view>::format(ip_protocol_name(p), ctx);\n        }\n    };\n}  // namespace fmt\n"
  },
  {
    "path": "llarp/net/posix.cpp",
    "content": "#include \"net_if.hpp\"\n#include \"platform.hpp\"\n\n#include <llarp/address/ip_range.hpp>\n#include <llarp/util/formattable.hpp>\n#include <llarp/util/logging.hpp>\n\n#include <stdexcept>\n\n#ifdef ANDROID\n#include <llarp/android/ifaddrs.h>\n#else\n#include <ifaddrs.h>\n#endif\n\n#include <oxen/quic/address.hpp>\n\nnamespace llarp::net\n{\n    static auto logcat = log::Cat(\"posix\");\n\n    namespace\n    {\n        struct ifaddrs_deleter\n        {\n            void operator()(ifaddrs* ia) const noexcept { freeifaddrs(ia); }\n        };\n        using ifaddrs_ptr = std::unique_ptr<ifaddrs, ifaddrs_deleter>;\n        ifaddrs_ptr getifaddrs()\n        {\n            if (ifaddrs * ia; 0 == ::getifaddrs(&ia))\n                return ifaddrs_ptr{ia};\n            throw std::runtime_error{\"getifaddrs(): {}\"_format(strerror(errno))};\n        }\n    }  // namespace\n\n    class Platform_Impl : public Platform\n    {\n        // Iterates through interfaces.  If the Visitor returns a bool then a true return means to\n        // break the iteration loop.\n        template <std::invocable<const ifaddrs&> Visitor>\n        void for_each_interface(Visitor&& visit) const\n        {\n            auto addrs = getifaddrs();\n            for (auto next = addrs.get(); next; next = next->ifa_next)\n            {\n                const auto& n = *next;\n                if constexpr (std::same_as<bool, decltype(visit(n))>)\n                {\n                    if (visit(n))\n                        break;\n                }\n                else\n                    visit(n);\n            }\n        }\n\n      public:\n        std::optional<quic::Address> get_best_public_address(bool ipv4, uint16_t port) const override\n        {\n            std::optional<quic::Address> found;\n\n            for_each_interface([&found, family = ipv4 ? AF_INET : AF_INET6, port](const ifaddrs& i) {\n                if (i.ifa_addr and i.ifa_addr->sa_family == family)\n                {\n                    quic::Address a{i.ifa_addr};\n                    if (a.is_public_ip())\n                    {\n                        a.set_port(port);\n                        found = std::move(a);\n                        return true;\n                    }\n                }\n                return false;\n            });\n\n            log::info(logcat, \"get_best_public_address returned: {}\", found);\n\n            return found;\n        }\n\n        std::optional<ipv4_net> find_free_ipv4_net(uint8_t mask) const override\n        {\n            std::vector<ipv4_range> current_ranges;\n\n            for_each_interface([&current_ranges](const ifaddrs& i) {\n                if (i.ifa_addr and i.ifa_addr->sa_family == AF_INET)\n                {\n                    ipv4 addr{reinterpret_cast<sockaddr_in*>(i.ifa_addr)->sin_addr};\n                    auto mask = static_cast<uint8_t>(\n                        std::popcount(reinterpret_cast<sockaddr_in*>(i.ifa_netmask)->sin_addr.s_addr));\n\n                    log::debug(logcat, \"Adding {}/{} to excluded search ranges\", addr, mask);\n                    current_ranges.emplace_back(addr, mask);\n                }\n            });\n\n            return find_private_ipv4_net(std::move(current_ranges), mask);\n        }\n\n        std::optional<ipv6_net> find_free_ipv6_net(uint8_t mask) const override\n        {\n            std::vector<ipv6_range> current_ranges;\n\n            for_each_interface([&current_ranges](const ifaddrs& i) {\n                if (i.ifa_addr and i.ifa_addr->sa_family == AF_INET6)\n                {\n                    ipv6 addr{reinterpret_cast<sockaddr_in6*>(i.ifa_addr)->sin6_addr};\n                    uint8_t mask = 0;\n                    for (auto c : reinterpret_cast<sockaddr_in6*>(i.ifa_netmask)->sin6_addr.s6_addr)\n                        mask += std::popcount(c);\n                    log::debug(logcat, \"Adding {}/{} to excluded search ranges\", addr, mask);\n                    current_ranges.emplace_back(addr, mask);\n                }\n            });\n\n            return find_private_ipv6_net(std::move(current_ranges), mask);\n        }\n\n        std::optional<int> get_interface_index(ipv4 ip) const override\n        {\n            std::optional<int> ret;\n\n            for_each_interface([&ret, &ip](const ifaddrs& i) {\n                if (i.ifa_addr and i.ifa_addr->sa_family == AF_INET)\n                {\n                    auto if_ip = quic::Address{i.ifa_addr}.to_ipv4();\n                    if (if_ip == ip)\n                    {\n                        ret = if_nametoindex(i.ifa_name);\n                        return true;\n                    }\n                }\n                return false;\n            });\n\n            return ret;\n        }\n        std::optional<int> get_interface_index(ipv6 ip) const override\n        {\n            std::optional<int> ret;\n\n            for_each_interface([&ret, &ip](const ifaddrs& i) {\n                if (i.ifa_addr and i.ifa_addr->sa_family == AF_INET6)\n                {\n                    auto if_ip = quic::Address{i.ifa_addr}.to_ipv6();\n                    if (if_ip == ip)\n                    {\n                        ret = if_nametoindex(i.ifa_name);\n                        return true;\n                    }\n                }\n                return false;\n            });\n\n            return ret;\n        }\n\n        std::string find_free_tun([[maybe_unused]] std::string_view suggest) const override\n        {\n#ifdef __linux__\n            if (!suggest.empty())\n            {\n                suggest = suggest.substr(0, IFNAMSIZ - 1);\n                auto addrs = getifaddrs();\n                bool found = false;\n                for (auto next = addrs.get(); next; next = next->ifa_next)\n                {\n                    if (next->ifa_name && next->ifa_name == suggest)\n                    {\n                        found = true;\n                        break;\n                    }\n                }\n                if (!found)\n                    return std::string{suggest};\n            }\n            // Let the kernel choose automatically:\n            return \"lokitun%d\"s;\n#else\n            // On non-linux (e.g. FreeBSD) arbitrary tun device names can't be chosen, so we just\n            // return \"\" to always auto-allocate.\n            return \"\"s;\n#endif\n        }\n\n        std::optional<ipv4> get_interface_ipv4(std::string_view ifname) const override\n        {\n            std::optional<ipv4> addr;\n\n            for_each_interface([&addr, &ifname](const ifaddrs& i) {\n                if (i.ifa_addr and i.ifa_addr->sa_family == AF_INET and i.ifa_name == ifname)\n                {\n                    addr = quic::Address{i.ifa_addr}.to_ipv4();\n                    return true;\n                }\n                return false;\n            });\n\n            return addr;\n        }\n\n        std::optional<ipv6> get_interface_ipv6(std::string_view ifname) const override\n        {\n            std::optional<ipv6> addr;\n\n            for_each_interface([&addr, &ifname](const ifaddrs& i) {\n                if (i.ifa_addr and i.ifa_addr->sa_family == AF_INET6 and i.ifa_name == ifname)\n                {\n                    addr = quic::Address{i.ifa_addr}.to_ipv6();\n                    return true;\n                }\n                return false;\n            });\n\n            return addr;\n        }\n\n        bool has_interface_address(ipv4 ip) const override\n        {\n            bool found{false};\n            for_each_interface([&found, &ip](const ifaddrs& i) {\n                found = i.ifa_addr and i.ifa_addr->sa_family == AF_INET and ip == quic::Address{i.ifa_addr}.to_ipv4();\n                return found;\n            });\n            return found;\n        }\n        bool has_interface_address(ipv6 ip) const override\n        {\n            bool found{false};\n            for_each_interface([&found, &ip](const ifaddrs& i) {\n                found = i.ifa_addr and i.ifa_addr->sa_family == AF_INET6 and ip == quic::Address{i.ifa_addr}.to_ipv6();\n                return found;\n            });\n            return found;\n        }\n    };\n\n    const Platform_Impl g_plat{};\n\n    const Platform* Platform::Default_ptr() { return &g_plat; }\n}  // namespace llarp::net\n"
  },
  {
    "path": "llarp/net/utils.cpp",
    "content": "#include \"utils.hpp\"\n\n#include <llarp/util/logging.hpp>\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"net-utils\");\n\n    namespace utils\n    {\n        static constexpr uint32_t add_u32(uint32_t x) { return uint32_t{x & 0xFFff} + uint32_t{x >> 16}; }\n        // static constexpr uint32_t add_u32(ipv4 x) { return add_u32(oxenc::host_to_big(x.addr)); }\n        static uint32_t add_u32(ipv4 x) { return add_u32(oxenc::host_to_big(x.addr)); }\n\n        static constexpr uint32_t sub_u32(uint32_t x) { return add_u32(~x); }\n        // static constexpr uint32_t sub_u32(ipv4 x) { return sub_u32(oxenc::host_to_big(x.addr)); }\n        static uint32_t sub_u32(ipv4 x) { return sub_u32(oxenc::host_to_big(x.addr)); }\n\n        uint16_t ip_checksum(const uint8_t *buf, size_t sz)\n        {\n            uint32_t sum = 0;\n\n            while (sz > 1)\n            {\n                sum += *(uint16_t *)(buf);\n                sz -= sizeof(uint16_t);\n                buf += sizeof(uint16_t);\n            }\n\n            if (sz != 0)\n            {\n                uint16_t x = 0;\n                *(uint8_t *)&x = *buf;\n                sum += x;\n            }\n\n            sum = (sum & 0xFFff) + (sum >> 16);\n            sum += sum >> 16;\n\n            return uint16_t((~sum) & 0xFFff);\n        }\n\n        uint16_t ipv4_checksum_diff(uint16_t old_sum, uint32_t old_src, uint32_t old_dest, ipv4 new_src, ipv4 new_dest)\n        {\n            uint32_t sum = old_sum + add_u32(old_src) + add_u32(old_dest) + sub_u32(new_src) + sub_u32(new_dest);\n\n            sum = (sum & 0xFFff) + (sum >> 16);\n            sum += sum >> 16;\n\n            return uint16_t(sum & 0xFFff);\n        }\n\n        uint16_t ipv4_tcp_checksum_diff(\n            uint16_t old_sum, uint32_t old_src, uint32_t old_dest, ipv4 new_src, ipv4 new_dest)\n        {\n            auto new_sum = ipv4_checksum_diff(old_sum, old_src, old_dest, new_src, new_dest);\n            return new_sum == 0xFFff ? 0x0000 : new_sum;\n        }\n\n        uint16_t ipv4_udp_checksum_diff(\n            uint16_t old_sum, uint32_t old_src, uint32_t old_dest, ipv4 new_src, ipv4 new_dest)\n        {\n            if (old_sum == 0x0000)\n                return old_sum;\n\n            return ipv4_checksum_diff(old_sum, old_src, old_dest, new_src, new_dest);\n        }\n    }  // namespace utils\n\n    uint16_t csum_add(uint16_t csum, uint16_t rhs)\n    {\n        uint32_t res = csum, other = rhs;\n        res += other;\n        return static_cast<uint16_t>(res + (res < other));\n    }\n\n    uint16_t csum_sub(uint16_t csum, uint16_t rhs) { return csum_add(csum, ~rhs); }\n\n    uint16_t from_32_to_16(uint32_t x)\n    {\n        /* add up 16-bit and 16-bit for 16+c bit */\n        x = (x & 0xffff) + (x >> 16);\n        /* add up carry.. */\n        x = (x & 0xffff) + (x >> 16);\n        return x;\n    }\n\n    uint32_t from_64_to_32(uint64_t x)\n    {\n        /* add up 32-bit and 32-bit for 32+c bit */\n        x = (x & 0xffffffff) + (x >> 32);\n        /* add up carry.. */\n        x = (x & 0xffffffff) + (x >> 32);\n        return x;\n    }\n\n    uint16_t fold_csum(uint32_t csum)\n    {\n        auto sum = csum;\n        sum = (sum & 0xffff) + (sum >> 16);\n        sum = (sum & 0xffff) + (sum >> 16);\n        return static_cast<uint16_t>(~sum);\n    }\n\n    uint16_t ipv6_checksum_magic(\n        const struct in6_addr *saddr, const struct in6_addr *daddr, uint32_t len, uint8_t proto, uint32_t sum)\n    {\n        uint32_t csum = sum;\n\n        // TODO FIXME: this cannot possibly be correct, because it is doing overflow handling of\n        // 32-bit values using little-endian interpretation.\n        auto *s32 = reinterpret_cast<const uint32_t *>(saddr->s6_addr);\n        for (size_t i = 0; i < 4; ++i)\n        {\n            auto &val = s32[i];\n            csum += val;\n            csum += (csum < val);\n        }\n\n        auto *d32 = reinterpret_cast<const uint32_t *>(daddr->s6_addr);\n        for (size_t i = 0; i < 4; ++i)\n        {\n            auto &val = d32[i];\n            csum += val;\n            csum += (csum < val);\n        }\n\n        uint32_t ulen = htonl(len);\n        uint32_t uproto = htonl(proto);\n\n        csum += ulen;\n        csum += (csum < ulen);\n\n        csum += uproto;\n        csum += (csum < uproto);\n\n        return fold_csum(csum);\n    }\n\n    uint32_t tcp_checksum_ipv6(const struct in6_addr *saddr, const struct in6_addr *daddr, uint32_t len, uint32_t csum)\n    {\n        return ~ipv6_checksum_magic(saddr, daddr, len, IPPROTO_TCP, csum);\n    }\n\n    uint32_t udp_checksum_ipv6(const struct in6_addr *saddr, const struct in6_addr *daddr, uint32_t len, uint32_t csum)\n    {\n        return ~ipv6_checksum_magic(saddr, daddr, len, IPPROTO_UDP, csum);\n    }\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/net/utils.hpp",
    "content": "#pragma once\n\n#include <llarp/address/types.hpp>\n\nnamespace llarp\n{\n    inline constexpr uint32_t ipv6_flowlabel_mask = 0b0000'0000'0000'1111'1111'1111'1111'1111;\n\n    inline constexpr size_t ICMP_HEADER_SIZE{8};\n\n    namespace utils\n    {\n        uint16_t ip_checksum(const uint8_t *buf, size_t sz);\n\n        // Parameters:\n        //  - old_sum : old checksum (NETWORK order!)\n        //  - old_{src,dest} : old src and dest IP's (stored internally in HOST order!)\n        //  - new_{src,dest} : new src and dest IP's (stored internally in HOST order!)\n        //\n        // Returns:\n        //  - uint16_t : new checksum (NETWORK order!)\n        uint16_t ipv4_checksum_diff(uint16_t old_sum, uint32_t old_src, uint32_t old_dest, ipv4 new_src, ipv4 new_dest);\n\n        uint16_t ipv4_tcp_checksum_diff(\n            uint16_t old_sum, uint32_t old_src, uint32_t old_dest, ipv4 new_src, ipv4 new_dest);\n\n        uint16_t ipv4_udp_checksum_diff(\n            uint16_t old_sum, uint32_t old_src, uint32_t old_dest, ipv4 new_src, ipv4 new_dest);\n\n        uint16_t ipv6_checksum_diff();\n    }  // namespace utils\n\n    uint32_t tcp_checksum_ipv6(const struct in6_addr *saddr, const struct in6_addr *daddr, uint32_t len, uint32_t csum);\n\n    uint32_t udp_checksum_ipv6(const struct in6_addr *saddr, const struct in6_addr *daddr, uint32_t len, uint32_t csum);\n\n}  //  namespace llarp\n"
  },
  {
    "path": "llarp/net/win32.cpp",
    "content": "#include \"net_if.hpp\"\n#include \"platform.hpp\"\n\n#include <llarp/constants/platform.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/str.hpp>\n#include <llarp/win32/exception.hpp>\n\n#include <iphlpapi.h>\n\n#include <cstdio>\n#include <list>\n#include <stdexcept>\n#include <type_traits>\n\nnamespace llarp::net\n{\n    static auto logcat = log::Cat(\"win32.net\");\n\n    class Platform_Impl : public Platform\n    {\n        template <typename adapter_t>\n        bool adapter_has_ip(adapter_t* a, ipaddr_t ip) const\n        {\n            for (auto* addr = a->FirstUnicastAddress; addr; addr = addr->Next)\n            {\n                quic::Address saddr{*addr->Address.lpSockaddr};\n\n                log::debug(logcat, \"'{}' has address '{}\", a->AdapterName, saddr);\n                if (saddr.getIP() == ip)\n                    return true;\n            }\n            return false;\n        }\n\n        template <typename adapter_t>\n        bool adapter_has_fam(adapter_t* a, int af) const\n        {\n            for (auto* addr = a->FirstUnicastAddress; addr; addr = addr->Next)\n            {\n                quic::Address saddr{*addr->Address.lpSockaddr};\n                if (saddr.Family() == af)\n                    return true;\n            }\n            return false;\n        }\n\n      public:\n        std::optional<int> get_interface_index(ip ip) const override\n        {\n            std::optional<int> found;\n            int af{AF_INET};\n\n            if (std::holds_alternative<ipv6>(ip))\n                af = AF_INET6;\n\n            win32::iter_adapters(\n                [&found, ip, this](auto* adapter) {\n                    if (found)\n                        return;\n\n                    log::debug(\n                        logcat,\n                        \"Visit adapter looking for '{}': '{} idx={}\",\n                        ip,\n                        adapter->AdapterName,\n                        adapter->IfIndex);\n\n                    if (adapter_has_ip(adapter, ip))\n                    {\n                        found = adapter->IfIndex;\n                    }\n                },\n                af);\n\n            return found;\n        }\n\n        std::optional<quic::Address> get_interface_addr(std::string_view name, int af) const override\n        {\n            std::optional<quic::Address> found;\n\n            win32::iter_adapters([name = std::string{name}, af, &found, this](auto* a) {\n                if (found)\n                    return;\n                if (std::string{a->AdapterName} != name)\n                    return;\n\n                if (adapter_has_fam(a, af))\n                    found = quic::Address{*a->FirstUnicastAddress->Address.lpSockaddr};\n            });\n\n            return found;\n        }\n\n        std::optional<quic::Address> all_interfaces(quic::Address fallback) const override\n        {\n            (void)fallback;\n            // windows seems to not give a shit about source address\n            return quic::Address{};\n        }\n\n        std::string find_free_tun() const override { return \"lokitun0\"; }\n\n        std::optional<quic::Address> get_best_public_address(bool, uint16_t) const override\n        {\n            // TODO: implement me ?\n            return std::nullopt;\n        }\n\n        std::optional<IPRange> find_free_range(bool ipv6_enabled) const override\n        {\n            std::list<IPRange> currentRanges;\n\n            win32::iter_adapters([&currentRanges](auto* i) {\n                for (auto* addr = i->FirstUnicastAddress; addr; addr = addr->Next)\n                {\n                    quic::Address saddr{*addr->Address.lpSockaddr};\n\n                    bool is_ipv6 = addr->Address.lpSockaddr->sa_family == AF_INET6 ? true : false;\n\n                    // TOFIX: wtf is this\n                    uint8_t m = is_ipv6 ? reinterpret_cast<sockaddr_in6*>(*addr->Address.lpSockaddr)->sin6_addr.s6_addr\n                                        : reinterpret_cast<sockaddr_in*>(*addr->Address.lpSockaddr)->sin_addr.s_addr;\n\n                    currentRanges.emplace_back(std::move(saddr), m);\n                }\n            });\n\n            return IPRange::find_private_range(currentRanges);\n        }\n\n        bool has_interface_address(ip ip) const override { return get_interface_index(ip) != std::nullopt; }\n    };\n\n    const Platform_Impl g_plat{};\n\n    const Platform* Platform::Default_ptr() { return &g_plat; }\n}  // namespace llarp::net\n"
  },
  {
    "path": "llarp/nodedb-bootstraps.cpp.in",
    "content": "#include <llarp/nodedb.hpp>\n\nnamespace llarp\n{\n    const std::vector<std::pair<NetID, std::string_view>> NodeDB::bootstrap_fallbacks\n    {\n        // clang-format off\n@BOOTSTRAP_FALLBACKS@\n        // clang-format on\n    };\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/nodedb.cpp",
    "content": "#include \"nodedb.hpp\"\n\n#include <llarp/crypto/types.hpp>\n#include <llarp/link/link_manager.hpp>\n#include <llarp/messages/fetch.hpp>\n#include <llarp/util/file.hpp>\n#include <llarp/util/random.hpp>\n#include <llarp/util/time.hpp>\n#include <llarp/util/zstd.hpp>\n\n#include <oxen/quic/btstream.hpp>\n#include <oxen/quic/loop.hpp>\n#include <oxenc/base32z.h>\n#include <sodium/crypto_generichash.h>\n\n#include <algorithm>\n#include <chrono>\n#include <functional>\n#include <iterator>\n#include <random>\n#include <ranges>\n#include <unordered_map>\n#include <utility>\n\nnamespace llarp\n{\n    static auto logcat = llarp::log::Cat(\"nodedb\");\n\n    std::array<int, 3> NodeDB::db_stats() const { return {num_rcs(), num_rids(), num_bootstraps()}; }\n\n    template <typename RCContainer, std::predicate<const RelayContact&> Pred, typename RNG>\n    static std::vector<const RelayContact*> sample_rcs(\n        Router& router, const RCContainer& rcs, int n, const Pred& predicate, RNG& rng, bool shuffle)\n    {\n        auto now = llarp::time_now_ms();\n        const auto& blacklist = router.config().paths.snode_blacklist;\n\n        std::vector<const RelayContact*> rand;\n        rand.resize(n);\n        int admitted = 0;\n\n        for (const RelayContact& rc : rcs)\n        {\n            if (rc.is_expired(now))\n                continue;\n            if (router.is_service_node)\n            {\n                if (rc.router_id() == router.id())\n                    continue;\n            }\n            else if (blacklist.contains(rc.router_id()))\n                continue;\n            if (predicate and not predicate(rc))\n                continue;\n\n            auto pos = admitted < n ? admitted : std::uniform_int_distribution<int>{0, admitted}(rng);\n            admitted++;\n            if (pos < n)\n                rand[pos] = &rc;\n        }\n        if (admitted < n)\n            rand.resize(admitted);\n        if (shuffle && rand.size() > 1)\n            std::ranges::shuffle(rand, rng);\n        return rand;\n    }\n\n    std::vector<const RelayContact*> NodeDB::get_n_random_rcs(\n        int n, bool shuffle, const std::function<bool(const RelayContact&)>& predicate) const\n    {\n        assert(_router.loop.inside());\n#ifdef LOKINET_DEBUG_PATH_SEED\n        if (auto& s = _router.config().paths.debug_path_seed)\n        {\n            std::vector<std::reference_wrapper<const RelayContact>> rcs;\n            rcs.reserve(known_rcs.size());\n            for (const auto& rc : known_rcs | std::views::values)\n                rcs.push_back(std::cref(rc));\n            // We need a sorted list of known rcs because of the potentially non-reproducible order\n            // of elements in an unordered map:\n            std::ranges::sort(\n                rcs, [](const RelayContact& a, const RelayContact& b) { return a.router_id() < b.router_id(); });\n            std::mt19937_64 rng{*s};\n            return sample_rcs(_router, rcs, n, predicate, rng, shuffle);\n        }\n#endif\n\n        return sample_rcs(_router, known_rcs | std::views::values, n, predicate, llarp::csrng, shuffle);\n    }\n\n    const RelayContact* NodeDB::get_random_rc(const std::function<bool(const RelayContact&)>& predicate) const\n    {\n        auto randos = get_n_random_rcs(1, false, predicate);\n        return randos.empty() ? nullptr : randos.front();\n    }\n\n    std::vector<const RelayContact*> NodeDB::get_n_random_edge_rcs(\n        int n, bool shuffle, const std::function<bool(const RelayContact&)>& predicate) const\n    {\n        assert(_router.loop.inside());\n        auto& strict = _router.config().paths.strict_edges;\n        if (_router.is_service_node || strict.empty())\n            return get_n_random_rcs(n, shuffle, predicate);\n\n        n = std::min(n, static_cast<int>(strict.size()));\n\n        // With strict edges the set of edges is typically small, so we iterate through just those\n        // edges rather than sampling from *everything* with an \"is in strict\" lookups added to the\n        // predicate.\n        std::vector<std::reference_wrapper<const RelayContact>> strict_rcs;\n        for (const auto& rid : strict)\n            if (auto* rc = get_rc(rid))\n                strict_rcs.emplace_back(std::cref(*rc));\n\n#ifdef LOKINET_DEBUG_PATH_SEED\n        if (auto& s = _router.config().paths.debug_path_seed)\n        {\n            std::ranges::sort(\n                strict_rcs, [](const RelayContact& a, const RelayContact& b) { return a.router_id() < b.router_id(); });\n            std::mt19937_64 rng{*s};\n            return sample_rcs(_router, strict_rcs, n, predicate, rng, shuffle);\n        }\n#endif\n\n        return sample_rcs(_router, strict_rcs, n, predicate, llarp::csrng, shuffle);\n    }\n\n    void NodeDB::bootstrap()\n    {\n        assert(_router.loop.inside());\n        assert(!_bootstraps.empty());\n        _bootstrap_running = true;\n\n        if (_rc_fetch_ticker)\n            _rc_fetch_ticker->stop();\n        if (_rid_fetch_ticker)\n            _rid_fetch_ticker->stop();\n\n        struct bs_data\n        {\n            NodeDB& nodedb;\n            size_t rc_i = 0;\n            std::string body;\n            RouterID source;\n\n            // Shared pointer to ourself to keep us alive.  This is released once we run out of rcs,\n            // or get a successful fetch.\n            std::shared_ptr<void> keep_alive;\n\n            void try_next()\n            {\n                if (nodedb._router.is_stopping())\n                {\n                    log::debug(logcat, \"Aborting bootstrap because of router stop\");\n                    keep_alive.reset();\n                    return;\n                }\n\n                if (rc_i >= nodedb._bootstraps.size())\n                {\n                    log::debug(logcat, \"Bootstrapping failed: bootstraps list exhausted without any success\");\n                    auto ka = std::move(keep_alive);\n                    nodedb.on_bootstrap_done(false);\n                    return;\n                }\n\n                auto& rc = nodedb._bootstraps[rc_i++];\n                source = rc.router_id();\n                log::debug(\n                    logcat,\n                    \"Initiating bootstrap request to {} @ {}\",\n                    rc.router_id().to_network_address(true),\n                    rc.addr());\n                auto [conn, control] = nodedb._router.link_endpoint().bootstrap_connect(rc);\n                control->command(\"bfetch_rcs\", body, [this, conn](quic::message m) {\n                    nodedb._router.loop.call_soon([this, m = std::move(m)] {\n                        if (not m)\n                            log::warning(logcat, \"Bootstrap fetch failed: {}\", m.timed_out ? \"timeout\" : m.body());\n\n                        else if (nodedb.handle_bootstrap_result(source, m.body()))\n                        {\n                            auto ka = std::move(keep_alive);\n                            nodedb.on_bootstrap_done(true);\n                            return;\n                        }\n\n                        try_next();\n                    });\n\n                    conn->close_connection();\n                });\n            }\n        };\n        auto bs = std::make_shared<bs_data>(*this);\n        bs->keep_alive = bs;\n\n        bs->body = \"de\";\n\n        bs->try_next();\n    }\n\n    void NodeDB::purge_rcs(std::chrono::milliseconds now)\n    {\n        assert(_router.loop.inside());\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        if (_router.is_stopping() || not _router.is_running())\n        {\n            log::debug(logcat, \"NodeDB unable to continue purge ticking -- router is stopped!\");\n            return;\n        }\n\n        remove_rcs_if([this, now](const RelayContact& rc) -> bool {\n            // if for some reason we stored an RC that isn't a valid router\n            // purge this entry\n            if (not rc.addr().is_public())\n            {\n                log::trace(logcat, \"Removing {}: address {} is not public\", rc.router_id(), rc.addr());\n                return true;\n            }\n\n            // clear out a fully expired RC\n            if (rc.is_expired(now))\n            {\n                log::trace(logcat, \"Removing {}: RC is expired\", rc.router_id());\n                return true;\n            }\n\n            if (_router.is_service_node)\n            {\n                // if we don't have the registered relay list yet don't remove the entry\n                if (not has_registered_relays())\n                {\n                    log::trace(\n                        logcat,\n                        \"Skipping check on {}: have not received oxend registered relay list yet\",\n                        rc.router_id());\n                    return false;\n                }\n\n                if (not is_registered(rc.router_id()))\n                {\n                    log::trace(logcat, \"Removing {}: not a valid router\", rc.router_id());\n                    return true;\n                }\n            }\n            else\n            {\n                // Clients do not have an authoritative relay list, so we have no checks equivalent\n                // to the above ones.\n                log::trace(logcat, \"Not removing {}: we are a client and it looks fine\", rc.router_id());\n                return false;\n            }\n\n            return false;\n        });\n\n        if (num_rcs() < MIN_ACTIVE_RCS and not _bootstraps.empty() and not _bootstrap_running)\n        {\n            log::warning(logcat, \"Purging expired relays resulted in too few RCs; falling back to bootstrap mode\");\n            _bootstrap_fails = 0;\n            bootstrap();\n        }\n    }\n\n    std::filesystem::path NodeDB::get_path_by_pubkey(const RouterID& pubkey, const std::filesystem::path& ext) const\n    {\n        return _root / std::filesystem::path{pubkey.to_string()}.replace_extension(ext);\n    }\n\n    void NodeDB::fetch_rcs()\n    {\n        assert(_router.loop.inside());\n        if (_router.is_stopping() || not _router.is_running())\n        {\n            log::debug(logcat, \"NodeDB unable to continue RC fetch -- router is stopped!\");\n            if (_rc_fetch_ticker)\n                _rc_fetch_ticker->stop();\n            return;\n        }\n\n        std::vector<RouterID> to_fetch{};\n        for (const auto& rid : known_rids)\n        {\n            if (!known_rcs.contains(rid))\n                to_fetch.push_back(rid);\n\n            if (to_fetch.size() == RC_FETCH_COUNT)\n                break;\n        }\n\n        if (to_fetch.empty())\n            return;\n\n        path::Path* selected_path = _router.session_endpoint().get_random_active_path();\n        if (!selected_path)\n        {\n            log::debug(logcat, \"NodeDB fetch rcs, skipping because we have no paths.\");\n            return;\n        }\n\n        selected_path->fetch_relay_contacts(to_fetch, [this](auto resp) mutable {\n            std::string error;\n            if (resp.ok())\n            {\n                try\n                {\n                    auto rcs = FetchRC::deserialize_response(_router.netid(), oxenc::bt_dict_consumer{resp.body});\n                    log::debug(logcat, \"RC fetching was successful; processing {} returned RCs...\", rcs.size());\n                    for (auto& rc : rcs)\n                    {\n                        const auto& rid = rc.router_id();\n                        if (!put_rc(std::move(rc)))\n                            log::debug(logcat, \"Not inserting RC for {}, either it is newer or (if relay) ours\", rid);\n                    }\n                }\n                catch (const std::exception& e)\n                {\n                    error = e.what();\n                }\n            }\n            else\n            {\n                error = resp.timed_out ? \"timed out\" : \"failed: {}\"_format(resp.body);\n            }\n        });\n    }\n\n    void NodeDB::fetch_rids()\n    {\n        assert(_router.loop.inside());\n        if (_router.is_stopping() || not _router.is_running())\n        {\n            log::debug(logcat, \"NodeDB skipping RouterID fetch -- router is stopped!\");\n            // FIXME: this *was* calling post_rid_fetch, but that seems wrong (and can segfault),\n            //        might need to see *why* it was doing so, if for any logical reason\n            return;\n        }\n\n        auto results = std::make_shared<std::unordered_map<RouterID, std::unordered_set<RouterID>>>();\n        auto result_count = std::make_shared<size_t>(0);\n        size_t try_count{0};\n        std::vector<path::Path*> selected_paths;\n\n        // In the future, we may want to make paths to selected sources for RID fetching,\n        // but for now just use the first N paths that we already have for simplicity.  If\n        // fetching fails from one, or not all results agree, we probably want to drop that\n        // path anyway.\n        for (auto& path : _router.session_endpoint().active_paths())\n        {\n            if (try_count >= RID_SOURCE_COUNT)\n                break;\n            auto [itr, inserted] = results->emplace(path.terminal_rid(), std::unordered_set<RouterID>{});\n            if (inserted)\n            {\n                try_count++;\n                selected_paths.push_back(&path);\n            }\n        }\n        if (try_count < RID_SOURCE_COUNT)\n            log::info(\n                logcat,\n                \"Fetching RIDs from {} sources (want minimum {}, but not enough paths)\",\n                try_count,\n                RID_SOURCE_COUNT);\n\n        for (auto* path : selected_paths)\n        {\n            auto result_cb = [this, results, result_count, source = path->terminal_rid()](auto resp) {\n                (*result_count)++;\n                if (not resp.ok())\n                {\n                    log::warning(\n                        logcat,\n                        \"RID fetch from {} {}\",\n                        source,\n                        resp.timed_out ? \"timed out\" : \"failed: {}\"_format(resp.body));\n                }\n                else\n                {\n                    try\n                    {\n                        auto& router_ids = results->at(source);\n                        oxenc::bt_dict_consumer btdc{resp.body};\n\n                        btdc.required(\"r\");\n\n                        {\n                            auto sublist = btdc.consume_list_consumer();\n\n                            while (not sublist.is_finished())\n                                router_ids.emplace(sublist.consume_span<uint8_t, 32>());\n                        }\n                    }\n                    catch (const std::exception& e)\n                    {\n                        log::warning(logcat, \"Error handling fetch RouterIDs response: {}\", e.what());\n                        results->at(source).clear();\n                    }\n                }\n                if (*result_count == results->size())\n                {\n                    handle_fetched_router_ids(*results);\n                }\n            };\n            path->send_path_control_message(\"fetch_rids\"sv, {}, std::move(result_cb));\n        }\n    }\n\n    void NodeDB::handle_fetched_router_ids(const std::unordered_map<RouterID, std::unordered_set<RouterID>>& results)\n    {\n        assert(_router.loop.inside());\n        std::unordered_set<RouterID> accepted{};\n\n        auto itr = results.begin();\n        while (itr != results.end())\n        {\n            for (const auto& rid : itr->second)\n            {\n                size_t count{0};\n                auto cur_itr = results.begin();\n                while (cur_itr != results.end())\n                {\n                    if (cur_itr->second.contains(rid))\n                        count++;\n                    cur_itr++;\n                }\n                // FIXME: better than \"half-rounded-up agree\"\n                if (count > (results.size() / 2))\n                    accepted.insert(rid);\n                else\n                    log::info(logcat, \"Received a RouterID that not enough nodes agree is correct: {}\", rid);\n            }\n            itr++;\n        }\n\n        known_rids.clear();\n        for (const auto& rid : accepted)\n            known_rids.insert(rid);\n    }\n\n    void NodeDB::start()\n    {\n        log::trace(logcat, \"NodeDB starting tickers...\");\n\n        _purge_ticker = _router.loop.call_every(PURGE_INTERVAL, [this] { purge_rcs(); });\n\n        auto need_bootstrap = num_rcs() < MIN_ACTIVE_RCS;\n        if (not has_bootstraps())\n        {\n            log::warning(logcat, \"Only {} known RCs, but no bootstrap nodes are configured\", num_rcs());\n            need_bootstrap = false;\n        }\n\n        if (not _router.is_service_node)\n        {\n            _rc_fetch_ticker = _router.loop.call_every(FETCH_INTERVAL, [this] { fetch_rcs(); }, not need_bootstrap);\n\n            _rid_fetch_ticker = _router.loop.call_every(FETCH_INTERVAL, [this] { fetch_rids(); }, not need_bootstrap);\n        }\n\n        _0rtt_saver = _router.disk_loop.make_wakeable([this] { _0rtt_save(); });\n        _0rtt_saver->wake();\n\n        if (need_bootstrap)\n            bootstrap();\n    }\n\n    void NodeDB::on_bootstrap_done(bool success)\n    {\n        if (success)\n        {\n            log::debug(logcat, \"Bootstrap attempt completed successfully\");\n            _bootstrap_fails = 0;\n        }\n        else\n        {\n            _bootstrap_fails++;\n            log::debug(logcat, \"Bootstrap attempt failed ({} consecutive failures)\", _bootstrap_fails);\n        }\n\n        bool need_bootstrap = num_rcs() < MIN_ACTIVE_RCS;\n        if (not need_bootstrap)\n        {\n            // TODO FIXME: we've now completed a bootstrap and so we want to fire off a full RID\n            // fetch.  This current logic, however, doesn't seem right (but isn't specific to here):\n            // we fire off an rid fetch *and* fire off an RC fetch back to back, on separate timers,\n            // when really they should be dependent.\n            //\n            // But I'm not fixing it here because it needs a more significant overhaul.\n            if (_rid_fetch_ticker)\n            {\n                _rid_fetch_ticker->start();\n                fetch_rids();\n            }\n            if (_rc_fetch_ticker)\n            {\n                _rc_fetch_ticker->start();\n                fetch_rcs();\n            }\n            return;\n        }\n\n        auto cooldown = std::min(BOOTSTRAP_COOLDOWN * (success ? 1 : _bootstrap_fails), BOOTSTRAP_COOLDOWN_MAX);\n        log::warning(\n            logcat,\n            \"Not enough RCs ({}) after {} bootstrap attempt; trying again in {}\",\n            num_rcs(),\n            success ? \"successful\" : \"failed\",\n            cooldown);\n        _router.loop.call_later(cooldown, [this] { bootstrap(); });\n    }\n\n    NodeDB::NodeDB(Router& r) : _router{r}, _root{_router.config().router.data_dir / nodedb_dirname}\n    {\n        if (not exists(_root))\n            create_directory(_root);\n        if (not is_directory(_root))\n            throw std::runtime_error{fmt::format(\"nodedb {} is not a directory\", _root)};\n\n        load_bootstraps();\n\n        load_from_disk();\n\n        if (_router.is_service_node)\n        {\n            // Make self.signed a symlink to the full ID.signed file.  (The full file might not\n            // exist if we aren't a relay, but that's okay: we intentionally leave a dangling\n            // symlink in that case).\n            auto self_signed = _root / our_rc_filename;\n            std::error_code ec;  // Ignore errors on the below as this is just for convenience but\n                                 // doesn't otherwise matter.\n            remove(self_signed, ec);\n            create_symlink(\n                std::filesystem::path{_router.id().to_string()}.replace_extension(RC_FILE_EXT), self_signed, ec);\n        }\n    }\n\n    void NodeDB::load_bootstrap(const std::filesystem::path& fpath)\n    {\n        if (not exists(fpath))\n            throw std::runtime_error{\"Bootstrap RC file '{}' does not exist\"_format(fpath)};\n\n        auto content = util::file_to_string(fpath);\n        if (content.empty())\n            throw std::runtime_error{\"Bootstrap RC file '{}' is empty\"_format(fpath)};\n\n        load_bootstrap(content, \"Bootstrap RC file '{}'\"_format(fpath));\n\n        log::debug(logcat, \"Successfully loaded BootstrapRC file {} ({}B)\", fpath, content.size());\n    }\n\n    void NodeDB::load_bootstrap(std::string_view data, std::string_view input_desc)\n    {\n        try\n        {\n            // Bootstrap data can container either a list of bootstraps, or just a single bootstrap RC:\n            if (data.front() == 'l')\n            {\n                // list of bootstrap RCs\n                for (oxenc::bt_list_consumer l{data}; !l.is_finished();)\n                    _bootstraps.emplace_back(l.consume_dict_data(), _router.netid(), /*accept_expired=*/true);\n            }\n            else\n            {\n                // single bootstrap RC\n                _bootstraps.emplace_back(data, _router.netid(), /*accept_expired=*/true);\n            }\n        }\n        catch (const std::exception& e)\n        {\n            log::debug(\n                logcat, \"Failed to load the following bootstrap data from {}: {}\", input_desc, buffer_printer{data});\n            throw std::runtime_error{\"{} does not contain valid bootstrap data: {}\"_format(input_desc, e.what())};\n        }\n    }\n\n    void NodeDB::load_bootstraps()\n    {\n        const auto def = _router.config().router.data_dir / default_bootstrap;\n        for (const auto& f : _router.config().bootstrap.files)\n        {\n            log::debug(logcat, \"Loading BootstrapRC from file {}\", f);\n            load_bootstrap(f);\n        }\n\n        if (_bootstraps.empty() && exists(def))\n        {\n            log::debug(logcat, \"No configured bootstraps; loading from {}\", def);\n            try\n            {\n                load_bootstrap(def);\n            }\n            catch (const std::exception& e)\n            {\n                log::warning(logcat, \"Failed loading from default bootstrap file {}: {}.  Skipping it.\", def, e.what());\n            }\n        }\n\n        auto obsolete = std::erase_if(_bootstraps, [](const auto& bs) { return bs.is_obsolete(); });\n        if (obsolete > 0)\n            log::info(logcat, \"Removed {} obsolete bootstraps RCs\", obsolete);\n\n        if (std::erase_if(_bootstraps, [this](const auto& bs) { return bs.router_id() == _router.id(); }) > 0)\n            log::info(logcat, \"Found and removed ourself ({}) from the bootstrap list\", _router.id());\n\n        if (_bootstraps.empty())\n        {\n            log::debug(logcat, \"Bootstrap list is empty; loading built-in fallbacks\");\n            for (const auto& [n, rc_blob] : bootstrap_fallbacks)\n            {\n                if (n == _router.netid())\n                {\n                    load_bootstrap(rc_blob, \"Fallback bootstrap data\");\n                    break;\n                }\n            }\n\n            log::info(\n                logcat,\n                \"Loaded {} {} default fallback bootstrap router contact(s)\",\n                _bootstraps.size(),\n                _router.netid());\n\n            if (_bootstraps.empty())\n            {\n                log::warning(\n                    logcat,\n                    \"No bootstrap router contacts were loaded.  The default bootstrap file {} does not \"\n                    \"exist, and this lokinet binary does not have any fallback bootstraps for the '{}' network.\",\n                    def,\n                    _router.netid());\n            }\n        }\n\n        std::shuffle(_bootstraps.begin(), _bootstraps.end(), llarp::csrng);\n\n        log::debug(logcat, \"We have {} Bootstrap router(s)!\", _bootstraps.size());\n    }\n\n    void NodeDB::post_rid_fetch(bool shutdown)\n    {\n        assert(_router.loop.inside());\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        fetch_counter = 0;\n        response_counter = 0;\n        fail_counter = 0;\n        fail_sources.clear();\n        rid_result_counters.clear();\n\n        if (shutdown)\n        {\n            _rid_fetch_ticker->stop();\n            log::warning(logcat, \"Client stopped RouterID fetch without a sucessful response!\");\n        }\n        else\n            log::trace(logcat, \"Client successfully completed RouterID fetch!\");\n    }\n\n    bool NodeDB::handle_bootstrap_result(const RouterID& source, std::string_view body)\n    {\n        assert(_router.loop.inside());\n        log::debug(logcat, \"Received response to BootstrapRC fetch request...\");\n\n        int num = 0, n_new = 0;\n\n        try\n        {\n            oxenc::bt_dict_consumer btdc{body};\n\n            auto compressed_rcs = btdc.require_span<std::byte>(\"Z\");\n\n            btdc.finish();\n\n            zstd::decompressor decompressor;\n            // 20M here is just a safety margin so that a malicious bootstrap can't feed us some\n            // tiny data that decompresses into something that exhausts memory:\n            auto rcs_data = zstd::decompressor{}.decompress(compressed_rcs, 20'000'000);\n            if (!rcs_data)\n                throw std::runtime_error{\"Failed to decompress RC list\"};\n\n            oxenc::bt_list_consumer rclist{*rcs_data};\n\n            while (not rclist.is_finished())\n            {\n                RelayContact new_rc{rclist.consume_dict_data(), _router.netid()};\n                // if we're trusting the bootstrap for RCs regardless of RouterID, we\n                // should trust the RouterID as well.\n                known_rids.insert(new_rc.router_id());\n                n_new += put_rc(std::move(new_rc));\n                ++num;\n            }\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(\n                logcat, \"Failed to parse BootstrapRC fetch response from {}: {}\", source.short_string(), e.what());\n            return false;\n        }\n\n        log::info(logcat, \"Bootstrap fetch successfully retrieved {} RCs ({} new)\", num, n_new);\n        return true;\n    }\n\n    bool NodeDB::has_registered_relays() const\n    {\n        std::shared_lock lock{_registered_relays_mutex};\n        return not _registered_relays.empty();\n    }\n\n    void NodeDB::set_registered_relays(std::unordered_set<RouterID> relays)\n    {\n        log::debug(logcat, \"Oxend provided {} whitelisted routers\", relays.size());\n\n        if (relays.empty())\n            return;\n\n        size_t size = relays.size();\n        {\n            std::unique_lock lock{_registered_relays_mutex};\n            std::swap(relays, _registered_relays);\n        }\n\n        if (relays.empty())\n            log::info(logcat, \"Loaded initial SN list from oxend with {} registered relays\", size);\n        else\n            log::debug(logcat, \"Updated SN list from oxend with {} registered relays\", size);\n    }\n\n    void NodeDB::load_registered_relays_fallback()\n    {\n        std::unique_lock lock{_registered_relays_mutex};\n\n        if (not _registered_relays.empty())\n        {\n            // Perhaps a race with a result fetch?\n            log::debug(logcat, \"Not loading registered relay fallback: we already have registered relays\");\n            return;\n        }\n\n        auto now = llarp::time_now_ms();\n        for (auto& [rid, rc] : known_rcs)\n            if (!rc.is_expired(now))\n                _registered_relays.insert(rid);\n    }\n\n    std::vector<RouterID> NodeDB::get_registered_relays() const\n    {\n        std::vector<RouterID> result;\n        std::shared_lock lock{_registered_relays_mutex};\n        result.reserve(_registered_relays.size());\n        result.assign(_registered_relays.begin(), _registered_relays.end());\n        return result;\n    }\n\n    bool NodeDB::is_registered(const RouterID& relay) const\n    {\n        std::shared_lock lock{_registered_relays_mutex};\n        return _registered_relays.contains(relay);\n    }\n\n    std::optional<RouterID> NodeDB::get_random_registered_relay() const\n    {\n        std::optional<RouterID> result;\n        std::shared_lock lock{_registered_relays_mutex};\n        if (!_registered_relays.empty())\n            result = *std::next(\n                _registered_relays.begin(),\n                std::uniform_int_distribution<int>{0, static_cast<int>(_registered_relays.size())}(llarp::csrng));\n        return result;\n    }\n\n    void NodeDB::load_from_disk()\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        if (_root.empty())\n            return;\n\n        std::vector<std::filesystem::path> purge;\n\n        const auto now = time_now_ms();\n        const auto real_now = std::chrono::system_clock::now();\n\n        for (const auto& f : std::filesystem::directory_iterator{_root})\n        {\n            if (not f.is_regular_file())\n                continue;\n\n            RouterID filename_rid;\n            // Ignoring anything that doesn't look like PUBKEY.ext:\n            if (auto no_ext = f.path().stem().u8string(); no_ext.size() == oxenc::to_base32z_size(RouterID::SIZE)\n                and oxenc::is_base32z(no_ext.begin(), no_ext.end()))\n            {\n                oxenc::from_base32z(no_ext.begin(), no_ext.end(), filename_rid.begin());\n            }\n            else\n            {\n                log::debug(logcat, \"Skipping {}: does not match PUBKEY.(ext) filename format\", f.path());\n                continue;\n            }\n\n            auto ext = f.path().extension();\n            if (ext == RC_FILE_EXT)\n            {\n                std::optional<RelayContact> rc;\n                try\n                {\n                    rc.emplace(f.path(), _router.netid());\n                }\n                catch (const std::exception& e)\n                {\n                    log::warning(logcat, \"Failed to load {} from stored RCs: {}\", f.path(), e.what());\n                }\n\n                if (not rc or rc->is_expired(now))\n                {\n                    // try loading it, purge it if it is junk or expired\n                    purge.push_back(f);\n                    continue;\n                }\n\n                auto rid = rc->router_id();\n\n                if (rid != filename_rid)\n                {\n                    log::error(\n                        logcat,\n                        \"Invalid stored RC: RC contains pubkey {} which does not match filename {}\",\n                        rid,\n                        f.path());\n                    purge.push_back(f);\n                    continue;\n                }\n\n                if (not _router.is_service_node)\n                    known_rids.insert(rid);  // Only clients use this container\n                known_rcs.emplace(std::move(rid), std::move(*rc));\n            }\n            else if (ext == ZRTT_FILE_EXT)\n            {\n                std::string content;\n                try\n                {\n                    content = util::file_to_string(f.path());\n                    oxenc::bt_dict_consumer top{content};\n                    RouterID rid{top.require_span<std::byte, RouterID::SIZE>(\"@\")};\n                    if (rid != filename_rid)\n                        throw std::runtime_error{\"pubkey {} does not match filename {}\"_format(rid, f.path())};\n\n                    if (top.skip_until(\"R\"))\n                    {\n                        if (!_router.is_service_node)\n                            throw std::runtime_error{\"ticket is for a relay but we are a client\"};\n\n                        RouterID check_rid{top.consume_span<std::byte, RouterID::SIZE>()};\n                        if (check_rid != _router.id())\n                            throw std::runtime_error{\"ticket is for a different relay\"};\n                    }\n                    else if (_router.is_service_node)\n                        throw std::runtime_error{\"ticket is for a client but we are a relay\"};\n\n                    std::list<std::pair<std::vector<unsigned char>, std::chrono::sys_seconds>> tickets;\n                    auto recs = top.consume_list_consumer();\n                    while (!recs.is_finished())\n                    {\n                        auto rec = recs.consume_dict_consumer();\n                        auto data_sp = rec.require_span<unsigned char>(\"d\");\n                        std::vector<unsigned char> data{data_sp.begin(), data_sp.end()};\n                        std::chrono::sys_seconds expiry{std::chrono::seconds{rec.require<int64_t>(\"e\")}};\n                        if (expiry > real_now)\n                            tickets.emplace_back(std::move(data), expiry);\n                        rec.finish();\n                    }\n                    top.finish();\n\n                    if (tickets.empty())\n                    {\n                        // Everything expired\n                        log::debug(logcat, \"Deleting 0RTT ticket file {}: no unexpired tickets found\", f.path());\n                        purge.push_back(f);\n                        continue;\n                    }\n                    _0rtt_tickets[rid] = std::move(tickets);\n                }\n                catch (const std::exception& e)\n                {\n                    log::warning(\n                        logcat, \"Failed to load {} from stored 0RTT data: {}; deleting it.\", f.path(), e.what());\n                    purge.push_back(f);\n                    continue;\n                }\n            }\n            else\n                log::trace(logcat, \"Skipping file with unknown extension: {}\", f.path());\n        }\n\n        if (not purge.empty())\n        {\n            log::info(logcat, \"removing {} invalid or expired RC/0RTT files from disk\", purge.size());\n            for (const auto& fpath : purge)\n                remove(fpath);\n        }\n\n        log::info(\n            logcat, \"Loaded {} RCs + 0-RTT tickets for {} relays from disk\", known_rcs.size(), _0rtt_tickets.size());\n    }\n\n    void NodeDB::cleanup()\n    {\n        if (_rid_fetch_ticker)\n        {\n            log::trace(logcat, \"NodeDB clearing rid fetch ticker...\");\n            _rid_fetch_ticker->stop();\n            _rid_fetch_ticker.reset();\n        }\n\n        if (_rc_fetch_ticker)\n        {\n            log::trace(logcat, \"NodeDB clearing RC fetch ticker...\");\n            _rc_fetch_ticker->stop();\n            _rc_fetch_ticker.reset();\n        }\n\n        if (_purge_ticker)\n        {\n            log::trace(logcat, \"NodeDB clearing purge ticker...\");\n            _purge_ticker->stop();\n            _purge_ticker.reset();\n        }\n\n        log::debug(logcat, \"NodeDB cleared all tickers...\");\n    }\n\n    const RelayContact* NodeDB::get_rc(const RouterID& pk) const\n    {\n        assert(_router.loop.inside());\n        auto it = known_rcs.find(pk);\n        return it != known_rcs.end() ? &it->second : nullptr;\n    }\n\n    bool NodeDB::put_rc(RelayContact rc)\n    {\n        assert(_router.loop.inside());\n\n        auto [it, new_rc] = known_rcs.try_emplace(rc.router_id(), std::move(rc));\n        auto& stored = it->second;\n        bool should_gossip;\n        if (new_rc)\n        {\n            // If this is a brand new RC then we want to gossip it to make sure everyone gets it.\n            should_gossip = true;\n        }\n        else if (!rc.newer_than(stored, RelayContact::MIN_GOSSIP_RC_AGE))\n        {\n            // The RC is too new since the last one we stored, so drop it.\n            return false;\n        }\n        else\n        {\n            // This RC is an update of one we already have: we only gossip if this RC indicates a\n            // changed address (e.g. port or IP change) or was the first RC from this node in a long\n            // time, both of which are updates we want to waste a little extra network bandwidth for\n            // to get out everywhere ASAP via gossipping.  Otherwise it's a mundane update, and so\n            // we don't gossip it because the full-mesh network connections means it will send it\n            // directly to everyone (and other nodes don't need to update to be able to full mesh\n            // with it).\n            //\n            // For our own RC, we always return true if we get here because we always want to gossip\n            // our *own* RC whenever it gets updated.\n            should_gossip = rc.router_id() == _router.id() || rc.newer_than(stored, RelayContact::OUTDATED_AGE)\n                || rc.address_changed(stored);\n            stored = std::move(rc);\n        }\n\n        // We inserted or updated the record, so queue saving it to disk on the disk loop\n        _router.disk_loop.call_soon([rc = stored, path = get_path_by_pubkey(rc.router_id())] { rc.write(path); });\n\n        return should_gossip;\n    }\n\n    bool NodeDB::verify_store_gossip_rc(RelayContact rc)\n    {\n        assert(_router.loop.inside());\n        if (not is_registered(rc.router_id()) || rc.router_id() == _router.id())\n            return false;\n        return put_rc(std::move(rc));\n    }\n\n    int NodeDB::num_rcs(bool include_self) const\n    {\n        assert(_router.loop.inside());\n        int total = static_cast<int>(known_rcs.size());\n        if (not include_self and _router.is_service_node and known_rcs.contains(_router.id()))\n            --total;\n        return total;\n    }\n\n    int NodeDB::num_rids() const\n    {\n        assert(_router.loop.inside());\n        if (_router.is_service_node)\n        {\n            std::shared_lock lock{_registered_relays_mutex};\n            return static_cast<int>(_registered_relays.size());\n        }\n        return static_cast<int>(known_rids.size());\n    }\n\n    void NodeDB::remove_rcs_if(const std::function<bool(const RelayContact&)>& remove)\n    {\n        assert(_router.loop.inside());\n\n        std::vector<RouterID> removed;\n\n        for (auto it = known_rcs.begin(); it != known_rcs.end();)\n        {\n            const auto& [rid, rc] = *it;\n            if (remove(rc))\n            {\n                removed.push_back(rid);\n                it = known_rcs.erase(it);\n            }\n            else\n                ++it;\n        }\n\n        if (not removed.empty())\n            remove_many_from_disk_async(std::move(removed));\n    }\n\n    void NodeDB::remove_many_from_disk_async(const std::vector<RouterID>& remove) const\n    {\n        assert(_router.loop.inside());\n        if (_root.empty())\n            return;\n\n        // build file list\n        std::vector<std::filesystem::path> files;\n        files.reserve(remove.size());\n        for (const auto& rid : remove)\n            files.push_back(get_path_by_pubkey(rid));\n\n        // remove them from the disk via the diskio thread\n        _router.disk_loop.call_soon([files = std::move(files)] {\n            for (const auto& p : files)\n                std::filesystem::remove(p);\n        });\n    }\n\n    namespace\n    {\n        inline uint64_t xor_condense(const AlignedBuffer<32>& x)\n        {\n            auto* y = reinterpret_cast<const uint64_t*>(x.data());\n            return y[0] ^ y[1] ^ y[2] ^ y[3];\n        }\n\n        // Metric for determining the \"closest\" router ID to a given blinded pubkey, used for\n        // blinded CC publishing.\n        //\n        // This consists of xoring all of the uint64_t chunks of the blinded pubkey with the router\n        // ID and returning the smallest value.\n        struct PublishLocationMetric\n        {\n            PublishLocationMetric(const PubKey& blinded_pk) : pk_xor{xor_condense(blinded_pk)} {}\n\n            const uint64_t pk_xor;  // xor of the blinded PK\n            bool operator()(const RouterID* left, const RouterID* right) const\n            {\n                auto l = xor_condense(*left) ^ pk_xor;\n                auto r = xor_condense(*right) ^ pk_xor;\n                return std::tie(l, *left) < std::tie(r, *right);\n            }\n        };\n    }  // namespace\n\n    std::vector<RouterID> NodeDB::find_many_closest_to(const PubKey& blinded_pk, int num_routers) const\n    {\n        assert(_router.loop.inside());\n        if (num_routers <= 0)\n            return {};\n\n        std::shared_lock lock{_registered_relays_mutex};\n        auto rr = _registered_relays | std::views::transform([](const auto& rid) { return &rid; });\n        std::vector<const RouterID*> rids{rr.begin(), rr.end()};\n        num_routers = std::min(num_routers, static_cast<int>(rids.size()));\n        std::ranges::partial_sort(rids, rids.begin() + num_routers, PublishLocationMetric{blinded_pk});\n        rids.resize(num_routers);\n        std::vector<RouterID> result;\n        result.reserve(rids.size());\n        for (auto* rid : rids)\n            result.push_back(*rid);\n        return result;\n    }\n\n    void NodeDB::store_0rtt(const RouterID& rid, std::vector<unsigned char> data, std::chrono::sys_seconds expiry)\n    {\n        std::lock_guard lock{_0rtt_mutex};\n        auto& tickets = _0rtt_tickets[rid];\n        while (tickets.size() >= MAX_0RTT_TICKETS)\n            tickets.pop_front();\n        tickets.emplace_back(std::move(data), expiry);\n        _0rtt_dirty.insert(rid);\n        _0rtt_saver->wake();\n    }\n\n    std::optional<std::vector<unsigned char>> NodeDB::extract_0rtt(const RouterID& rid)\n    {\n        std::lock_guard lock{_0rtt_mutex};\n        std::optional<std::vector<unsigned char>> ret;\n        auto now = std::chrono::system_clock::now();\n        auto it = _0rtt_tickets.find(rid);\n        if (it != _0rtt_tickets.end())\n        {\n            auto& tickets = it->second;\n            // Delete any expired tickets:\n            while (!tickets.empty() && tickets.front().second < now)\n                tickets.pop_front();\n            if (!tickets.empty())\n            {\n                ret = std::move(tickets.front().first);\n                tickets.pop_front();\n            }\n            _0rtt_dirty.insert(rid);\n            _0rtt_saver->wake();\n        }\n        return ret;\n    }\n\n    void NodeDB::_0rtt_save()\n    {\n        std::list<std::pair<std::filesystem::path, std::string>> rewrite;\n        std::list<std::filesystem::path> erase;\n\n        {\n            std::lock_guard lock{_0rtt_mutex};\n            auto now = std::chrono::system_clock::now();\n            for (const auto& rid : _0rtt_dirty)\n            {\n                auto path = get_path_by_pubkey(rid, ZRTT_FILE_EXT);\n                auto it = _0rtt_tickets.find(rid);\n                if (it == _0rtt_tickets.end())\n                    erase.push_back(std::move(path));\n                else\n                {\n                    auto& tickets = it->second;\n                    // Delete any expired tickets:\n                    while (!tickets.empty() && tickets.front().second < now)\n                        tickets.pop_front();\n\n                    if (!tickets.empty())\n                    {\n                        oxenc::bt_dict_producer top;\n                        // Target relay RID, for verification that this is actually connecting to\n                        // the expected place:\n                        top.append(\"@\", rid.span());\n\n                        // If *we* are a relay then record our own pubkey here (and otherwise\n                        // don't), so that when loading we refuse to load a file that doesn't match\n                        // our pubkey and/or relay state, just in case someone copies the nodedb\n                        // with tickets in it from one data dir to another.\n                        if (_router.is_service_node)\n                            top.append(\"R\", _router.id().to_view());\n\n                        auto recs = top.append_list(\"r\");\n                        for (const auto& [data, exp] : tickets)\n                        {\n                            auto e = recs.append_dict();\n                            e.append(\"d\", std::span{reinterpret_cast<const std::byte*>(data.data()), data.size()});\n                            e.append(\"e\", exp.time_since_epoch().count());\n                        }\n                        rewrite.emplace_back(std::move(path), std::move(top).str());\n                        ++it;\n                    }\n                    else\n                    {\n                        erase.push_back(std::move(path));\n                        it = _0rtt_tickets.erase(it);\n                    }\n                }\n            }\n            _0rtt_dirty.clear();\n        }\n\n        for (const auto& path : erase)\n        {\n            try\n            {\n                std::filesystem::remove(path);\n            }\n            catch (const std::exception& e)\n            {\n                log::error(logcat, \"Failed to remove expired 0RTT ticket file {}: {}\", path, e.what());\n            }\n        }\n        for (const auto& [path, data] : rewrite)\n        {\n            try\n            {\n                util::buffer_to_file(path, data);\n            }\n            catch (const std::exception& e)\n            {\n                log::error(logcat, \"Failed to update 0RTT ticket file {}: {}\", path, e.what());\n            }\n        }\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/nodedb.hpp",
    "content": "#pragma once\n\n#include <llarp/contact/relay_contact.hpp>\n#include <llarp/contact/router_id.hpp>\n#include <llarp/util/thread/threading.hpp>\n\n#include <atomic>\n#include <chrono>\n#include <filesystem>\n#include <optional>\n#include <shared_mutex>\n#include <unordered_set>\n\nnamespace oxen::quic\n{\n    struct message;\n    struct Ticker;\n    class Wakeable;\n}  // namespace oxen::quic\n\nnamespace llarp\n{\n    class Router;\n\n    inline constexpr auto FETCH_INTERVAL{10min};\n    inline constexpr auto PURGE_INTERVAL{5min};\n\n    /*  RC Fetch Constants  */\n    // fallback to bootstrap if we have less than this many RCs\n    inline constexpr int MIN_ACTIVE_RCS{6};\n    // max number of attempts we make in non-bootstrap fetch requests\n    inline constexpr int MAX_FETCH_ATTEMPTS{10};\n\n    // when pro-actively fetching RCs, ask for this many for which we have RouterID but no RC\n    inline constexpr int RC_FETCH_COUNT{5};\n\n    // the total number of accepted returned rids should be above this number\n    inline constexpr size_t MIN_GOOD_RID_FETCH_TOTAL{};\n    // the ratio of accepted:rejected rids must be above this ratio\n    inline constexpr double GOOD_RID_FETCH_THRESHOLD{};\n\n    /*  RID Fetch Constants  */\n    // the number of rid sources that we make rid fetch requests to\n    inline constexpr size_t RID_SOURCE_COUNT{5};\n    // upper limit on how many rid fetch requests to rid sources can fail\n    inline constexpr int MAX_RID_ERRORS{1};\n    // each returned rid must appear this number of times across all responses\n    inline constexpr int MIN_RID_FETCH_FREQ{6};  //  TESTNET:\n\n    /*  Bootstrap Constants  */\n    // the number of rc's we query the bootstrap for; service nodes pass 0, which means\n    // gimme all dat RCs\n    inline constexpr size_t SERVICE_NODE_BOOTSTRAP_SOURCE_COUNT{0};\n    inline constexpr size_t CLIENT_BOOTSTRAP_SOURCE_COUNT{10};\n\n    // After a bootstrap (success or failure) that results in not enough RCs, this is how long we\n    // wait before bootstrapping again.  In the case of repeated failures, we apply an linear\n    // backoff in incrments of this value up to BOOTSTRAP_COOLDOWN_MAX.\n    inline constexpr auto BOOTSTRAP_COOLDOWN = 3s;\n    inline constexpr auto BOOTSTRAP_COOLDOWN_MAX = 60s;\n\n    /*  Other Constants  */\n    // threshold net number of verifications needed to promote an RID to known (positive) or drop\n    // (negative) an unconfirmed rid.  Each observation or omission contributes +1 or -1 vote until\n    // we have ± this threshold.\n    inline constexpr int CONFIRMATION_THRESHOLD{3};\n\n    // Maximum number of 0rtt tickets we will store, per relay.  The server generally sends new ones\n    // shortly after reconnecting so there is no much benefit in storing lots of these.\n    inline constexpr size_t MAX_0RTT_TICKETS = 2;\n\n    inline const std::filesystem::path RC_FILE_EXT{\".signed\"};\n    inline const std::filesystem::path ZRTT_FILE_EXT{\".zrtt\"};\n\n    class NodeDB\n    {\n        friend class Router;\n\n        Router& _router;\n        const std::filesystem::path _root;\n\n        /******** RouterID/RelayContacts ********/\n\n        /** RouterID mappings\n            Both the following are populated in NodeDB startup with RouterID's stored on disk.\n            - known_rids: meant to persist between lokinet sessions, and is only\n              populated during startup and RouterID fetching. This is meant to represent the\n              client instance's most recent perspective of the network, and record which RouterID's\n              were recently \"active\" and connected to\n            - unconfirmed_rids: holds new rids returned in fetch requests to be verified by\n           subsequent fetch requests\n            - known_rcs: populated during startup and when RC's are updated both during gossip\n              and periodic RC fetching\n            - bootstrap_seeds: if we are the seed node, we insert the rc's of bootstrap fetch\n           requests senders into this container to \"introduce\" them to each other\n            - _bootstraps: the standard container for bootstrap RelayContacts\n        */\n        std::unordered_set<RouterID> known_rids;\n        std::unordered_map<RouterID, int> unconfirmed_rids;  // Value is the number of votes: seeing\n                                                             // the rid is +1, missing it is -1.\n\n        std::unordered_map<RouterID, RelayContact> known_rcs;\n\n        static const std::vector<std::pair<NetID, std::string_view>> bootstrap_fallbacks;\n        std::vector<RelayContact> _bootstraps;\n        void load_bootstraps();\n        void load_bootstrap(const std::filesystem::path&);\n        void load_bootstrap(std::string_view data, std::string_view log_desc);\n\n        // All registered relays (service nodes)\n        std::unordered_set<RouterID> _registered_relays;\n        mutable std::shared_mutex _registered_relays_mutex;\n\n        // set of 8 randomly selected RID's from the client's set of routers\n        std::unordered_set<RouterID> rid_sources{};\n        // logs the RID's that resulted in an error during RID fetching\n        std::unordered_set<RouterID> fail_sources{};\n        // tracks the number of times each rid appears in the above responses\n        std::unordered_map<RouterID, std::atomic<int>> rid_result_counters{};\n\n        std::atomic<int> fetch_counter{};\n        std::atomic<int> fail_counter{};\n        std::atomic<int> response_counter{};\n\n        bool _bootstrap_running = false;\n        int _bootstrap_fails = 0;\n\n        /// asynchronously remove the files for a set of rcs on disk given their public ident key\n        void remove_many_from_disk_async(const std::vector<RouterID>& idents) const;\n\n        /// get filename of an RC file (or other, similar file extension) given its public ident key\n        std::filesystem::path get_path_by_pubkey(\n            const RouterID& pk, const std::filesystem::path& extension = RC_FILE_EXT) const;\n\n        std::shared_ptr<quic::Ticker> _rid_fetch_ticker;\n        std::shared_ptr<quic::Ticker> _rc_fetch_ticker;\n\n        std::shared_ptr<quic::Ticker> _purge_ticker;\n\n        std::unordered_map<RouterID, std::list<std::pair<std::vector<unsigned char>, std::chrono::sys_seconds>>>\n            _0rtt_tickets;\n        std::unordered_set<RouterID> _0rtt_dirty;\n        std::mutex _0rtt_mutex;\n        std::shared_ptr<quic::Wakeable> _0rtt_saver;\n        void _0rtt_save();\n\n      public:\n        explicit NodeDB(Router& r);\n\n        // Starts the nodedb tickers for purge and fetch (clients), and initiates a bootstrap if the\n        // nodedb has too few RCs.\n        void start();\n\n        // returns {num_rcs, num_rids, num_bootstraps}\n        std::array<int, 3> db_stats() const;\n\n        const std::unordered_set<RouterID>& get_known_rids() const { return known_rids; }\n\n        const std::unordered_map<RouterID, RelayContact>& get_known_rcs() const { return known_rcs; }\n\n        void purge_rcs(std::chrono::milliseconds now = llarp::time_now_ms());\n\n        void set_registered_relays(std::unordered_set<RouterID> relays);\n        bool has_registered_relays() const;\n        std::vector<RouterID> get_registered_relays() const;\n\n        // Called if our initial oxend SN request fails to load the router IDs of any RCs in our\n        // nodedb as our initial registered relay list until some future oxend update comes along to\n        // correct the list.\n        void load_registered_relays_fallback();\n\n        std::optional<RouterID> get_random_registered_relay() const;\n\n        const std::unordered_set<RouterID>& strict_edges() const;\n\n        int num_bootstraps() const { return static_cast<int>(_bootstraps.size()); }\n\n        bool has_bootstraps() const { return !_bootstraps.empty(); }\n\n        // Returns true if `relay` is a registered relay.  This uses a mutex (rather that event\n        // loop) protection so that it can be safely called from either event loop without risking a\n        // deadlock between the loops.\n        bool is_registered(const RouterID& relay) const;\n\n        /// load all known_rcs from disk synchronously\n        void load_from_disk();\n\n        /// called on close\n        void cleanup();\n\n        /// the number of known RC's currently held.  If `include_self` is false then we subtract\n        /// one if the current service node RC is included in the nodedb.\n        int num_rcs(bool include_self = true) const;\n\n        // The number of known RIDs.  For relays, this is the number of registered relays (as\n        // received from oxend); for clients this is the number of known router IDs fetched from\n        // relays.\n        int num_rids() const;\n\n        /// find the `num_relays` relays with IDs closest to the given blinded pubkey, in order\n        /// from closest to Nth-closest.  Note that this searches all network-registered rids, even\n        /// if we don't have the RC for that relay yet.\n        std::vector<RouterID> find_many_closest_to(const PubKey& blinded_pk, int num_relays) const;\n\n        /// return true if we have an rc by its ident pubkey\n        bool has_rc(const RouterID& pk) const { return get_rc(pk); }\n\n        /// maybe get an rc by its ident pubkey.  Returns nullptr if not found.\n        const RelayContact* get_rc(const RouterID& pk) const;\n\n        /// Selects n random RCs from all known, unexpired, non-blocklisted RCs (if a predicate is\n        /// given, they must also pass the given predicate).  If this is a service node, it will not\n        /// include its own RC.  If there are fewer than `n` admissable RCs then all admissable RCs\n        /// are returned.  The resulting RCs will also be shuffled before being returned, unless the\n        /// shuffle argument is set to false.  The returned pointers are guaranteed to be\n        /// non-nullptr.\n        std::vector<const RelayContact*> get_n_random_rcs(\n            int n, bool shuffle = true, const std::function<bool(const RelayContact&)>& predicate = nullptr) const;\n\n        /// Wrapper around get_n_random_rcs to select a single random RC.  Returns nullptr if there\n        /// are no acceptable RCs.\n        const RelayContact* get_random_rc(const std::function<bool(const RelayContact&)>& predicate = nullptr) const;\n\n        /// Same as `get_n_random_rcs`, except that this only returns RCs that are eligible for\n        /// direct connections.  For a relay, or a client not using strict edges, this is exactly\n        /// the same as `get_n_random_rcs`, but when strict edges are active, only listed strict\n        /// router IDs are considered.\n        std::vector<const RelayContact*> get_n_random_edge_rcs(\n            int n, bool shuffle = true, const std::function<bool(const RelayContact&)>& predicate = nullptr) const;\n\n        /// Stores an RC broadcast to the network.  The return value indicates whether this RC\n        /// should be re-broadcast to all connected relays (true) or not (false).  In particular,\n        /// false does *not* necessarily mean that the RC was not updated, but could also simply\n        /// mean that the RC update was not significant enough to warrant rebroadcasting.\n        ///\n        /// This function does *not* check that the RC's router ID is actually a valid service node:\n        /// call `verify_store_gossip_rc` instead of this to also do that check.\n        ///\n        /// In particular, RC re-gossipping is determined by:\n        /// - The RC must be for a relay we haven't recently received an RC for (i.e. we didn't have\n        ///   it, or what we had was declared outdated (more than 12h old)).\n        /// - Alternatively, an RC will also be gossipped if it is an important update for\n        ///   reachability (i.e. changed IP or port, or other crucial RC properties).\n        /// - Gossips will not be accepted if the currently stored RC for the relay is not at least\n        ///   a minute older than the incoming one.\n        ///\n        /// If storing *our own* RC then this returns true if it was stored, false otherwise,\n        /// because we always want to gossip to our peers when we update our own RC.\n        bool put_rc(RelayContact rc);\n\n        /// Checks of the relay in the given rc is a registered remote network relay (either active\n        /// or decommissioned, and not ourself) and, if so, calls and returns put_rc with it.\n        ///\n        /// Returns true if the router ID is known *and* the rc was updated *and* the RC should be\n        /// re-gossipped (see put_rc); returns false otherwise.\n        bool verify_store_gossip_rc(RelayContact rc);\n\n        /// Stores a 0rtt ticket received from a relay.  This is both written to disk and stored in\n        /// memory so that it can reused quickly in the current session, or after restarting.  (NB:\n        /// this does not have to be called from the router loop).\n        void store_0rtt(const RouterID& rid, std::vector<unsigned char> data, std::chrono::sys_seconds expiry);\n\n        /// Looks up a 0rtt ticket for the given router ID.  If at least one unexpired ticker is\n        /// found, it is removed from storage and returned; otherwise nullopt is returned.  NB: This\n        /// does not have to be called from the router loop.\n        [[nodiscard]] std::optional<std::vector<unsigned char>> extract_0rtt(const RouterID& rid);\n\n      private:\n        void fetch_rcs();\n        void fetch_rids();\n\n        /// Initiate a bootstrap fetch attempt.  This will try to bootstrap once from each\n        /// configured bootstrap node until bootstrapping succeeds, or all bootstraps have been\n        /// tried.  `on_bootstrap_done` will be called when the attempt finishes with a boolean\n        /// indicating whether bootstrapping was successful.\n        ///\n        /// While a bootstrap is running the regular rid- and rc-fetching routines are disabled.\n        void bootstrap();\n        void on_bootstrap_done(bool success);\n\n        bool handle_bootstrap_result(const RouterID& source, std::string_view body);\n\n        void post_rid_fetch(bool shutdown = false);\n\n        /// remove any stored RCs matching the given predicate\n        void remove_rcs_if(const std::function<bool(const RelayContact&)>& remove);\n\n        void handle_fetched_router_ids(const std::unordered_map<RouterID, std::unordered_set<RouterID>>& results);\n\n        // Called on the disk thread to store/update/erase 0rtt tickets for a router id.\n        void save_0rtt(const RouterID& rid);\n    };\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/path/build_stats.cpp",
    "content": "#include \"build_stats.hpp\"\n\n#include <llarp/util/formattable.hpp>\n#include <llarp/util/logging.hpp>\n\n#include <nlohmann/json.hpp>\n\nnamespace llarp::path\n{\n\n    static auto logcat = log::Cat(\"path\");\n\n    nlohmann::json BuildStats::ExtractStatus() const\n    {\n        return nlohmann::json{\n            {\"success\", success}, {\"attempts\", attempts}, {\"timeouts\", timeouts}, {\"fails\", build_fails}};\n    }\n\n    void BuildStats::update(std::chrono::milliseconds now)\n    {\n        if (attempts > 50 && attempts >= (success * 4) && now - last_warn_time > 5s)\n        {\n            log::warning(logcat, \"Low path build success: {}\", *this);\n            last_warn_time = now;\n        }\n    }\n\n    std::string BuildStats::to_string() const\n    {\n        return \"path Stats:[ success:{} | attempts:{} | timeouts:{} | fails:{} ]\"_format(\n            success, attempts, timeouts, build_fails);\n    }\n\n}  // namespace llarp::path\n"
  },
  {
    "path": "llarp/path/build_stats.hpp",
    "content": "#include <nlohmann/json_fwd.hpp>\n\n#include <chrono>\n#include <cstdint>\n\nnamespace llarp::path\n{\n\n    using namespace std::literals;\n\n    /// Stats about all our path builds\n    struct BuildStats\n    {\n        uint64_t attempts{0};\n        uint64_t success{0};\n        uint64_t build_fails{0};  // path build failures\n        uint64_t path_fails{0};   // path failures post-build\n        uint64_t timeouts{0};\n\n        std::chrono::milliseconds last_warn_time{0ms};\n\n        nlohmann::json ExtractStatus() const;\n\n        void update(std::chrono::milliseconds now);\n\n        std::string to_string() const;\n        static constexpr bool to_string_formattable = true;\n    };\n\n}  // namespace llarp::path\n"
  },
  {
    "path": "llarp/path/hopid.cpp",
    "content": "#include \"hopid.hpp\"\n\n#include <sodium/randombytes.h>\n\nnamespace llarp\n{\n    HopID HopID::make_random()\n    {\n        HopID h;\n        randombytes_buf(h.data(), h.size());\n        return h;\n    }\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/path/hopid.hpp",
    "content": "#pragma once\n\n#include <llarp/util/aligned.hpp>\n\nnamespace llarp\n{\n\n    inline constexpr size_t HOPID_SIZE = 16;\n\n    struct HopID final : public AlignedBuffer<HOPID_SIZE>\n    {\n        using AlignedBuffer<HOPID_SIZE>::AlignedBuffer;\n\n        static HopID make_random();\n    };\n\n}  // namespace llarp\n\ntemplate <>\nstruct std::hash<llarp::HopID> : hash<llarp::AlignedBuffer<llarp::HopID::SIZE>>\n{};\n"
  },
  {
    "path": "llarp/path/path.cpp",
    "content": "#include \"path.hpp\"\n\n#include \"path_handler.hpp\"\n\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/link/endpoint.hpp>\n#include <llarp/messages/dht.hpp>\n#include <llarp/messages/fetch.hpp>\n#include <llarp/messages/path.hpp>\n#include <llarp/profiling.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/util/bspan.hpp>\n#include <llarp/util/buffer.hpp>\n\n#include <nlohmann/json.hpp>\n\n#include <chrono>\n#include <ranges>\n\nnamespace llarp::path\n{\n    static auto logcat = log::Cat(\"path\");\n\n    size_t Path::next_path_log_id = 0;\n\n    Path::Path(\n        Router& rtr, std::span<const RelayContact> hop_rcs, PathHandler& handler, std::chrono::milliseconds expiry_ts)\n        : handler{handler.weak_from_this()}, _router{rtr}, _expiry{expiry_ts}, path_log_id{++next_path_log_id}\n    {\n        hops.resize(hop_rcs.size());\n\n        for (size_t i = 0; i < hop_rcs.size(); ++i)\n        {\n            const bool last = i + 1 == hop_rcs.size();\n            auto& hop = hops[i];\n            hop.router_id = hop_rcs[i].router_id();\n            // First hop RXID is unique, the rest are the previous hop TXID\n            hop.rxid = i == 0 ? HopID::make_random() : hops[i - 1].txid;\n            // Pivot hop TXID is not useful, and so is simply set equal to the pivot RXID.\n            hop.txid = last ? hop.rxid : HopID::make_random();\n            // Last hop upstream is it's own RID, the rest are the next hop RID\n            hop.upstream = last ? hop.router_id : hop_rcs[i + 1].router_id();\n            // First hop downstream is client's RID, the rest are the previous hop RID\n            hop.downstream = i == 0 ? _router.id() : hops[i - 1].router_id;\n\n            // hop.shared_secret and hop.xor_nonce are not set yet: they get set via a call to\n            // PathHandler::path_build_onion when we make the actual build path message (because\n            // they also require generating and sending an ephemeral pubkey and dh nonce in the path\n            // build message, which aren't required again once sent in that message).\n        }\n\n        hops.back().terminal_hop = true;\n\n        log::trace(logcat, \"Path populated with hops: {}\", hop_string());\n\n        log::debug(logcat, \"Path successfully constructed: {}\", *this);\n    }\n\n    ClientIntro Path::make_intro() const\n    {\n        ClientIntro intro;\n        intro.relay = hops.back().router_id;\n        intro.hop = hops.back().txid;\n        intro.expiry = std::chrono::sys_seconds{std::chrono::floor<std::chrono::seconds>(_expiry)};\n        return intro;\n    }\n\n    std::string Path::ping_stats_printer::to_string() const\n    {\n        if (p.ping_responses == 0)\n            return \"0.0%\";\n\n        double mean = (double)p.ping_cumulative.count() / p.ping_responses;\n        double success_pct = p.ping_responses / (double)(p.ping_responses + p.ping_timeouts) * 100.0;\n        if (p.ping_responses == 1)\n            return \"{:.1f}%, {:.0f}ms avg\"_format(success_pct, mean);\n\n        double sd = std::sqrt(((double)p.ping_sq_cumulative - p.ping_responses * mean * mean) / (p.ping_responses - 1));\n        return \"{:.1f}%, {:.0f}ms avg, {:.1f}ms s.d.\"_format(success_pct, mean, sd);\n    }\n\n    void Path::do_ping(std::chrono::milliseconds start_time)\n    {\n        if (!is_active() || start_time < next_ping)\n            return;\n\n        // Subtract a few milliseconds so that jitter in the tick processing time doesn't affect the\n        // ping interval:\n        next_ping = start_time + _router.config().paths.ping_interval - 20ms;\n\n        log::trace(logcat, \"Pinging path TXID={}\", edge().txid);\n        send_path_control_message(\n            \"path_ping\", {}, [this, wself = weak_from_this(), start_time](path_control_response resp) {\n                auto sself = wself.lock();\n                if (!sself)\n                    return;\n                std::chrono::milliseconds now = llarp::time_now_ms();\n                auto time_taken = now - start_time;\n                if (resp.ok())\n                {\n                    ping_responses++;\n                    ping_recent_timeouts = 0;\n                    ping_cumulative += time_taken;\n                    ping_sq_cumulative += time_taken.count() * time_taken.count();\n\n                    if (resp.body == messages::OK_RESPONSE)\n                        log::debug(\n                            logcat,\n                            \"Ping response for path {} (txid={}) response received in {} ({})\",\n                            *this,\n                            edge().txid,\n                            time_taken,\n                            printable_ping_stats());\n                    else\n                        log::warning(\n                            logcat,\n                            \"Path {} ping was successful (in {}) but had unexpected response body: {}\",\n                            *this,\n                            time_taken,\n                            buffer_printer(resp.body));\n                }\n                else\n                {\n                    bool expire = true;\n                    if (resp.timed_out)\n                    {\n                        ping_timeouts++;\n                        log::debug(\n                            logcat,\n                            \"Ping response for path {} (txid={}) timed out after {} ({})\",\n                            *this,\n                            edge().txid,\n                            time_taken,\n                            printable_ping_stats());\n                        expire = ++ping_recent_timeouts > _router.config().paths.max_missed_pings;\n                        if (expire)\n                            log::warning(\n                                logcat,\n                                \"Path {} (txid={}) had too many ping timeouts ({}); expiring path.\",\n                                *this,\n                                edge().txid,\n                                ping_recent_timeouts);\n                    }\n                    else\n                        log::warning(\n                            logcat,\n                            \"{} path_ping returned a path error (in {}): {}\",\n                            *this,\n                            time_taken,\n                            buffer_printer(resp.body));\n\n                    if (expire)\n                        _expiry = start_time;\n                }\n            });\n    }\n\n    bool Path::operator==(const Path& other) const\n    {\n        return std::ranges::equal(\n            hops, other.hops, [](const TransitHop& a, const TransitHop& b) { return a.same_transit(b); });\n    }\n\n    void Path::fetch_relay_contact(const RouterID& needed, std::function<void(path_control_response)> func)\n    {\n        send_path_control_message(\"fetch_rcs\", FetchRC::serialize({&needed, 1}), std::move(func));\n    }\n\n    void Path::fetch_relay_contacts(std::span<const RouterID> needed, std::function<void(path_control_response)> func)\n    {\n        send_path_control_message(\"fetch_rcs\", FetchRC::serialize(needed), std::move(func));\n    }\n\n    void Path::find_client_contact(const PubKey& blinded_pk, std::function<void(path_control_response)> func)\n    {\n        send_path_control_message(\"find_cc\", FindClientContact::serialize(blinded_pk), std::move(func));\n    }\n\n    void Path::publish_client_contact(\n        const EncryptedClientContact& ecc, int location, std::function<void(path_control_response)> func)\n    {\n        send_path_control_message(\"publish_cc\", PublishClientContact::serialize(ecc, location), std::move(func));\n    }\n\n    void Path::resolve_sns(\n        std::span<const std::byte, SHORTHASHSIZE> name_hash, std::function<void(path_control_response)> func)\n    {\n        send_path_control_message(\"resolve_sns\", ResolveSNS::serialize(name_hash), std::move(func));\n    }\n\n    void Path::encrypt_path_message(std::vector<std::byte>& data, SymmNonce&& nonce, std::byte type, bool with_mac)\n    {\n        auto& hopid = edge().rxid;\n        auto inner_size = data.size() + (with_mac ? crypto::MAC_SIZE : 0);\n        data.resize(inner_size + ENCRYPT_PATH_MESSAGE_OVERHEAD);\n\n        static_assert(sizeof(SymmNonce) == SymmNonce::SIZE);\n        static_assert(sizeof(HopID) == HopID::SIZE);\n\n        auto [inner_payload, bnonce, bhop, msgtype] = split_span_tail<SymmNonce::SIZE, HopID::SIZE, 1>(data);\n        assert(inner_payload.size() == inner_size);\n\n        bool first{true};\n        for (const auto& hop : std::ranges::reverse_view(hops))\n        {\n            if (first && with_mac)\n            {\n                first = false;\n                crypto::xchacha20_poly1305_encrypt(inner_payload, hop.shared_secret, nonce);\n            }\n            else\n                crypto::xchacha20(inner_payload, hop.shared_secret, nonce);\n\n            nonce ^= hop.xor_nonce;\n        }\n\n        nonce.copy_to(bnonce);\n        hopid.copy_to(bhop);\n        msgtype[0] = type;\n    }\n\n    std::string Path::decrypt_path_message(std::string_view payload)\n    {\n        if (payload.size() <= ENCRYPT_PATH_MESSAGE_OVERHEAD_MAC)\n        {\n            log::warning(logcat, \"received too-short response to path control message.\");\n            return {};\n        }\n        std::string body{payload};\n        std::span<std::byte> body_span{reinterpret_cast<std::byte*>(body.data()), body.size()};\n        auto [inner_payload, bnonce, bhop, msgtype] = split_span_tail<SymmNonce::SIZE, HopID::SIZE, 1>(body_span);\n        SymmNonce nonce;\n        nonce.assign(bnonce);\n        try\n        {\n            for (size_t i = 0; i != hops.size() - 1; i++)\n            {\n                nonce ^= hops[i].xor_nonce;\n                crypto::xchacha20(inner_payload, hops[i].shared_secret, nonce);\n            }\n            const auto& last_hop = hops.back();\n            nonce ^= last_hop.xor_nonce;\n            auto decrypted = crypto::xchacha20_poly1305_decrypt(inner_payload, last_hop.shared_secret, nonce);\n            return {reinterpret_cast<const char*>(decrypted.data()), decrypted.size()};\n        }\n        catch (std::exception& e)\n        {\n            log::warning(logcat, \"path control message response decryption failed: {}\", e.what());\n        }\n        return {};\n    }\n\n    void Path::send_path_data_message(std::vector<std::byte>&& data, SymmNonce&& nonce)\n    {\n        encrypt_path_message(data, std::move(nonce), DATA_MESSAGE_TYPE, false /* mac on session payload */);\n        _router.link_endpoint().send_datagram(edge().router_id, std::move(data));\n    }\n\n    void Path::send_path_control_message(\n        std::string_view method, std::span<const std::byte> body, std::function<void(path_control_response)> func)\n    {\n        auto decryptor = [wself = weak_from_this(), func = std::move(func)](quic::message m) {\n            path_control_response resp;\n\n            auto self = wself.lock();\n            if (!self)\n            {\n                log::info(logcat, \"Path control response received, but path is gone.\");\n                resp.timed_out = true;\n                func(std::move(resp));\n                return;\n            }\n\n            resp.timed_out = m.timed_out;\n            resp.error = !m;\n            if (m.timed_out || m.is_error())\n                resp.body = m.body();\n            else\n                resp.body = self->decrypt_path_message(m.body());\n            func(std::move(resp));\n        };\n\n        auto inner_payload = PATH::CONTROL::serialize(method, body);\n        std::vector<std::byte> payload;\n        payload.reserve(inner_payload.size() + ENCRYPT_PATH_MESSAGE_OVERHEAD_MAC);\n        payload.resize(inner_payload.size());\n        std::memcpy(payload.data(), inner_payload.data(), inner_payload.size());\n        encrypt_path_message(payload, SymmNonce::make_random(), CONTROL_MESSAGE_TYPE, true /* include mac */);\n        _router.link_endpoint().send_command(\n            edge().router_id, \"path_control\", std::move(payload), std::move(decryptor));\n    }\n\n    void Path::send_session_control_message(std::vector<std::byte>&& body, SymmNonce&& nonce, std::byte type)\n    {\n        encrypt_path_message(body, std::move(nonce), type, false /* mac on session payload */);\n        _router.link_endpoint().send_command(edge().router_id, \"session_control\", std::move(body), nullptr);\n    }\n\n    std::string Path::to_string() const { return \"Path{{{}}}[{}]\"_format(path_log_id, hop_string()); }\n\n    std::string path_hop_stringifier::to_string() const\n    {\n        return fmt::to_string(\n            fmt::join(hops | std::views::transform([](auto& h) { return h.router_id.short_string(); }), \"⟷\"));\n    }\n    path_hop_stringifier Path::hop_string() const { return {hops}; }\n\n    nlohmann::json Path::ExtractStatus() const\n    {\n        auto now = llarp::time_now_ms();\n\n        nlohmann::json obj{\n            {\"lastRecvMsg\", to_json(last_recv_msg)},\n            {\"expired\", is_expired(now)},\n            {\"ready\", is_active()},\n        };\n\n        auto json_hops = nlohmann::json::array();\n        for (const auto& hop : hops)\n            json_hops.push_back(hop.ExtractStatus());\n        obj[\"hops\"] = std::move(json_hops);\n\n        return obj;\n    }\n\n    void Path::set_established()\n    {\n        if (_is_established)\n            return;\n\n        log::trace(logcat, \"Path marked as successfully established!\");\n        _is_established = true;\n    }\n\n    std::string Path::name() const { return \"[ TX={} | RX={} ]\"_format(edge().txid, edge().rxid); }\n\n}  // namespace llarp::path\n"
  },
  {
    "path": "llarp/path/path.hpp",
    "content": "#pragma once\n\n#include \"transit_hop.hpp\"\n\n#include <llarp/constants/path.hpp>\n#include <llarp/contact/client_contact.hpp>\n#include <llarp/contact/relay_contact.hpp>\n#include <llarp/crypto/types.hpp>\n#include <llarp/util/aligned.hpp>\n#include <llarp/util/compare_ptr.hpp>\n#include <llarp/util/thread/threading.hpp>\n#include <llarp/util/time.hpp>\n\n#include <chrono>\n#include <functional>\n#include <vector>\n\nnamespace oxen::quic\n{\n    struct message;\n};\n\nnamespace llarp\n{\n    class Router;\n    struct Profiling;\n\n    namespace service\n    {\n        struct EncryptedIntroSet;\n    }\n}  // namespace llarp\n\nnamespace llarp::path\n{\n    class PathHandler;\n\n    /// Proxy object to produce a human readable hop list in log statements on demand.  This\n    /// object is only intended to be used directly in format or log statements and not held.\n    struct path_hop_stringifier\n    {\n        std::span<const TransitHop> hops;\n\n        std::string to_string() const;\n        static constexpr bool to_string_formattable = true;\n    };\n\n    struct path_control_response\n    {\n        std::string body;\n        bool timed_out{false};\n        bool error{false};\n\n        bool ok() { return !timed_out && !error; }\n    };\n\n    class Path final : public std::enable_shared_from_this<Path>\n    {\n      public:\n        Path(\n            Router& rtr,\n            std::span<const RelayContact> hop_rcs,\n            PathHandler& handler,\n            std::chrono::milliseconds expiry_ts);\n\n        // hops on constructed path\n        std::vector<TransitHop> hops;\n\n        // If set, this is an aligned path to a pivot and this value is the hopid required to\n        // send data through the pivot.\n        std::optional<HopID> aligned_hopid;\n\n        std::weak_ptr<PathHandler> handler;\n\n        // Constructs a ClientInfo from this path, i.e. for including in a client contact.\n        ClientIntro make_intro() const;\n\n        nlohmann::json ExtractStatus() const;\n\n        path_hop_stringifier hop_string() const;\n\n        std::chrono::milliseconds LastRemoteActivityAt() const { return last_recv_msg; }\n\n        void do_ping(std::chrono::milliseconds start_time);\n\n        size_t num_hops() const { return hops.size(); }\n\n        const std::chrono::milliseconds& expiry() const { return _expiry; }\n\n        std::chrono::milliseconds expires_in(std::chrono::milliseconds now = llarp::time_now_ms()) const\n        {\n            return _expiry - now;\n        }\n\n        bool is_expired(std::chrono::milliseconds now = llarp::time_now_ms()) const { return _expiry < now; }\n\n        void resolve_sns(\n            std::span<const std::byte, SHORTHASHSIZE> name_hash, std::function<void(path_control_response)> func);\n\n        void fetch_relay_contact(const RouterID& needed, std::function<void(path_control_response)> func);\n\n        void fetch_relay_contacts(std::span<const RouterID> needed, std::function<void(path_control_response)> func);\n\n        void find_client_contact(const PubKey& blinded_pk, std::function<void(path_control_response)> func);\n\n        void publish_client_contact(\n            const EncryptedClientContact& ecc, int location, std::function<void(path_control_response)> func);\n\n        // The constant \"type\" values that we put on the end of control (stream) and data\n        // (datagram) messages.  Data message can overlap since it comes on a different channel\n        static constexpr std::byte DATA_MESSAGE_TYPE{0x01};\n        static constexpr std::byte CONTROL_MESSAGE_TYPE{0x01};\n        static constexpr std::byte PATH_SWITCH_MESSAGE_TYPE{0x02};\n\n        void send_path_data_message(std::vector<std::byte>&& body, SymmNonce&& nonce = SymmNonce::make_random());\n\n        void send_path_control_message(\n            std::string_view method, std::span<const std::byte> body, std::function<void(path_control_response)> func);\n\n        void send_session_control_message(\n            std::vector<std::byte>&& body, SymmNonce&& nonce, std::byte type = CONTROL_MESSAGE_TYPE);\n\n        // The overhead added to encrypted path messages (either data messages or path control\n        // messages) by the `encrypt_path_message` function.  This is the amount that the\n        // `payload` needs to be extended to add encryption metadata, and so callers can use\n        // this value to reserve the vector to be able to store the overhead without additional\n        // allocations.\n        inline static constexpr size_t ENCRYPT_PATH_MESSAGE_OVERHEAD = SymmNonce::SIZE + HopID::SIZE + 1;\n        inline static constexpr size_t ENCRYPT_PATH_MESSAGE_OVERHEAD_MAC =\n            ENCRYPT_PATH_MESSAGE_OVERHEAD + crypto::MAC_SIZE;\n\n        // Takes a payload and encrypts and extends it in-place to make it suitable for sending\n        // down either the datagram channel (carrying traffic) or stream (carrying network\n        // requests such as lookups or path builds).  This is *not* bt-encoded because we want\n        // this to be as low overhead as possible for data messages, in particular.\n        //\n        // The given vector will be extended as part of this operation (to add nonce, hop,\n        // packet type info).  To avoid a need for memory reallocation and copy, the caller\n        // should optimally reserve enough space in the payload vector to ensure it has at least\n        // ENCRYPT_PATH_MESSAGE_OVERHEAD additional bytes.\n        //\n        // nonce will be used if given, otherwise a random nonce is generated and used.  (It is\n        // typically given when this is a session data message; see session.cpp).\n        //\n        // `type` must be a single byte, currently always equal to 0x01 for both data/control\n        // messages.  (All other values are reserved for future versions of the protocol that\n        // may need to change the fundamental structure of encrypted data, or send different\n        // types of data)\n        void encrypt_path_message(\n            std::vector<std::byte>& payload, SymmNonce&& nonce, std::byte type, bool with_mac = false);\n\n        std::string decrypt_path_message(std::string_view payload);\n\n        bool is_active(std::chrono::milliseconds now = llarp::time_now_ms()) const\n        {\n            return _is_established && !is_expired(now);\n        }\n\n        const TransitHop& edge() const { return hops.front(); }\n        const TransitHop& terminus() const { return hops.back(); }\n\n        std::string name() const;\n\n        bool operator==(const Path& other) const;\n\n        std::string to_string() const;\n        static constexpr bool to_string_formattable = true;\n\n        // The router ID at the end of the path.  For an outbound aligned path or inbound\n        // session path, this is the pivot; for an outbound relay path this is the target relay.\n        RouterID terminal_rid() const { return terminus().router_id; }\n\n        // The hop ID of this path used by remotes who want to reach us.  I.e. this is the pivot\n        // hopid we publish in client intros, and is used for return traffic on established\n        // outbound sessions.\n        HopID terminal_hopid() const { return terminus().txid; }\n\n        void set_established();\n\n        // Returns true if a path has been marked established.\n        bool is_established() const { return _is_established; }\n\n        // Marks a path as built.  This is primary used as a way to ensure we only build a Path\n        // object once.  Returns true if the state was successfully changed (i.e. a false return\n        // means the path was already built).\n        bool set_built()\n        {\n            bool was_built = _is_built;\n            _is_built = true;\n            return not was_built;\n        }\n\n        // Returns true if a path has been marked as built.\n        bool is_built() const { return _is_built; }\n\n        // Will be true when the path has been discarded (such as when expiring, or after a\n        // timeout); this is primarily used in the session code that hold onto the path as a shared\n        // pointer (to avoid excessive weak_ptr locks) where the object might live a little longer\n        // as a result, but uses this to detect when it should discard its pointer to the path as\n        // well.\n        bool is_dead{false};\n\n        struct ping_stats_printer\n        {\n            Path& p;\n            std::string to_string() const;\n            static constexpr bool to_string_formattable = true;\n        };\n\n        // Returns ping stats: response rate (0.0-1.0), and average (successful) ping response time\n        ping_stats_printer printable_ping_stats() { return ping_stats_printer{*this}; }\n\n      private:\n        /// call obtained exit hooks\n        bool InformExitResult(std::chrono::milliseconds b);\n\n        bool _is_built{false};\n        bool _is_established{false};\n        bool _is_dead{false};\n\n        Router& _router;\n\n        std::chrono::milliseconds _expiry{0s};\n        std::chrono::milliseconds last_recv_msg{0s};\n\n        static size_t next_path_log_id;\n        const size_t path_log_id;  // Only used for log output\n\n        std::chrono::milliseconds next_ping{0s};\n        int ping_responses{0}, ping_timeouts{0};\n        int ping_recent_timeouts{0};\n        // Cumulative time of all `ping_responses` pings (divide by ping_responses for an average).\n        std::chrono::milliseconds ping_cumulative{0s};\n        int64_t ping_sq_cumulative{0};\n    };\n\n}  // namespace llarp::path\n\ntemplate <>\nstruct std::hash<llarp::path::Path>\n{\n    size_t operator()(const llarp::path::Path& p) const noexcept\n    {\n        return hash<llarp::HopID>{}(p.terminal_hopid()) ^ ((hash<llarp::HopID>{}(p.edge().rxid) << 13) >> 5);\n    }\n};\n"
  },
  {
    "path": "llarp/path/path_context.cpp",
    "content": "#include \"path_context.hpp\"\n\n#include \"path.hpp\"\n\n#include <llarp/router/router.hpp>\n\nnamespace llarp::path\n{\n    static auto logcat = log::Cat(\"pathctx\");\n\n    PathContext::PathContext(Router& r) : _r{r} {}\n\n    void PathContext::allow_transit() { _allow_transit = true; }\n\n    bool PathContext::is_transit_allowed() const { return _allow_transit; }\n\n    void PathContext::add_path(std::shared_ptr<Path> path) { _path_map.emplace(path->edge().rxid, std::move(path)); }\n\n    void PathContext::expire_hops(std::chrono::milliseconds now)\n    {\n        assert(_r.loop.inside());\n        int n = 0;\n        for (auto it = _transit_hops.begin(); it != _transit_hops.end();)\n        {\n            if (it->second && it->second->is_expired(now))\n            {\n                it->second->is_dead = true;\n                it = _transit_hops.erase(it);\n                n++;\n            }\n            else\n                ++it;\n        }\n\n        if (n > 0)\n            log::debug(logcat, \"{} expired TransitHops purged\", n);\n    }\n\n    void PathContext::drop(const Path& path)\n    {\n        assert(_r.loop.inside());\n        auto it = _path_map.find(path.edge().rxid);\n        if (it != _path_map.end())\n        {\n            if (it->second)\n                it->second->is_dead = true;\n            _path_map.erase(it);\n        }\n    }\n\n    void PathContext::drop(const TransitHop& thop)\n    {\n        assert(_r.loop.inside());\n        for (const HopID* h : {&thop.txid, &thop.rxid})\n        {\n            auto it = _transit_hops.find(*h);\n            if (it != _transit_hops.end())\n            {\n                if (it->second)\n                    it->second->is_dead = true;\n                _transit_hops.erase(it);\n            }\n        }\n    }\n\n    std::tuple<size_t, size_t> PathContext::path_ctx_stats() const\n    {\n        assert(_r.loop.inside());\n        return {_path_map.size() / 2, _transit_hops.size() / 2};\n    }\n\n    bool PathContext::has_transit_hop(const TransitHop& hop) const\n    {\n        assert(_r.loop.inside());\n        return has_transit_hop(hop.rxid) or has_transit_hop(hop.txid);\n    }\n\n    bool PathContext::has_transit_hop(const HopID& hop_id) const\n    {\n        assert(_r.loop.inside());\n        return _transit_hops.count(hop_id);\n    }\n\n    void PathContext::put_transit_hop(std::shared_ptr<TransitHop> hop)\n    {\n        assert(_r.loop.inside());\n        _transit_hops.emplace(hop->rxid, hop);\n        _transit_hops.emplace(hop->txid, std::move(hop));\n    }\n\n    template <typename T>\n    static const std::shared_ptr<T> nullshptr{};\n\n    TransitHop* PathContext::get_transit_hop(const HopID& path_id) const\n    {\n        assert(_r.loop.inside());\n        if (auto itr = _transit_hops.find(path_id); itr != _transit_hops.end())\n            return itr->second.get();\n\n        return nullptr;\n    }\n    std::shared_ptr<TransitHop> PathContext::get_transit_hop_ptr(const HopID& path_id) const\n    {\n        assert(_r.loop.inside());\n        if (auto itr = _transit_hops.find(path_id); itr != _transit_hops.end())\n            return itr->second;\n\n        return nullptr;\n    }\n\n    Path* PathContext::get_path(const HopID& hop_id) const\n    {\n        assert(_r.loop.inside());\n        if (auto itr = _path_map.find(hop_id); itr != _path_map.end())\n            return itr->second.get();\n\n        return nullptr;\n    }\n\n}  // namespace llarp::path\n"
  },
  {
    "path": "llarp/path/path_context.hpp",
    "content": "#pragma once\n\n#include \"hopid.hpp\"\n#include \"path_handler.hpp\"\n#include \"transit_hop.hpp\"\n\n#include <llarp/contact/client_contact.hpp>\n#include <llarp/util/compare_ptr.hpp>\n#include <llarp/util/decaying_hashset.hpp>\n\n#include <memory>\n#include <unordered_map>\n\nnamespace llarp\n{\n    class Router;\n}\n\nnamespace llarp::path\n{\n    // This class is the top-level holder of all paths and transit hops, and has a primary purpose\n    // of being able to look up the associated path/transit hop on incoming traffic.\n    class PathContext\n    {\n      private:\n        Router& _r;\n\n        using Lock_t = util::NullLock;\n        mutable util::NullMutex paths_mutex;\n\n        // Paths/TransitHops are 1:1 with edge rxIDs\n        std::unordered_map<HopID, std::shared_ptr<Path>> _path_map;\n        std::unordered_map<HopID, std::shared_ptr<TransitHop>> _transit_hops;\n\n        bool _allow_transit{false};\n\n      public:\n        explicit PathContext(Router& r);\n\n        std::tuple<size_t, size_t> path_ctx_stats() const;\n\n        bool has_transit_hop(const TransitHop& hop) const;\n\n        bool has_transit_hop(const HopID& hop_id) const;\n\n        void put_transit_hop(std::shared_ptr<TransitHop> hop);\n\n        Path* get_path(const HopID& hop_id) const;\n\n        TransitHop* get_transit_hop(const HopID&) const;\n        std::shared_ptr<TransitHop> get_transit_hop_ptr(const HopID&) const;\n\n        void add_path(std::shared_ptr<Path> p);\n\n        void drop(const Path& p);\n\n        // TODO FIXME: currently unused, but it would be nice to allow clients to terminate a path\n        // early (i.e. just before dropping all their connections), which will need this:\n        void drop(const TransitHop& thop);\n\n        void expire_hops(std::chrono::milliseconds now);\n\n        void allow_transit();\n\n        void reject_transit();\n\n        bool is_transit_allowed() const;\n    };\n}  // namespace llarp::path\n"
  },
  {
    "path": "llarp/path/path_handler.cpp",
    "content": "#include \"path_handler.hpp\"\n\n#include \"path.hpp\"\n#include \"path_context.hpp\"\n\n#include <llarp/constants/path.hpp>\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/link/link_manager.hpp>\n#include <llarp/messages/path.hpp>\n#include <llarp/nodedb.hpp>\n#include <llarp/profiling.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/util/bspan.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/random.hpp>\n#include <llarp/util/time.hpp>\n\n#include <nlohmann/json.hpp>\n#include <sodium/randombytes.h>\n\n#include <chrono>\n#include <functional>\n#include <random>\n\nnamespace llarp::path\n{\n    static auto logcat = log::Cat(\"pathhandler\");\n\n    PathHandler::PathHandler(Router& r, int target_paths, int num_hops)\n        : router{r}, _running{true}, _num_hops{num_hops}, _target_paths{target_paths}\n    {}\n\n    void PathHandler::add_path(Path& p)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n        Lock_t l(paths_mutex);\n\n        _paths.insert_or_assign(p.edge().rxid, p.shared_from_this());\n        router.path_context.add_path(p.shared_from_this());\n    }\n\n    void PathHandler::drop_path(const Path& p)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        _paths.erase(p.edge().rxid);\n\n        router.path_context.drop(p);\n    }\n\n    Path* PathHandler::get_random_active_path() const\n    {\n        int n_paths = num_active_paths();\n        if (!n_paths)\n            return nullptr;\n\n        return &*std::next(active_paths().begin(), std::uniform_int_distribution<int>{0, n_paths - 1}(llarp::csrng));\n    }\n\n    void PathHandler::ping_paths(std::chrono::milliseconds now)\n    {\n        Lock_t l{paths_mutex};\n\n        for (const auto& [h, p] : _paths)\n            if (p)\n                p->do_ping(now);\n    }\n\n    void PathHandler::expire_paths(std::chrono::milliseconds now)\n    {\n        Lock_t lock{paths_mutex};\n\n        int n = 0;\n        for (auto itr = _paths.begin(); itr != _paths.end();)\n        {\n            if (itr->second and itr->second->is_expired(now))\n            {\n                router.path_context.drop(*itr->second);\n                itr = _paths.erase(itr);\n                n++;\n            }\n            else\n                ++itr;\n        }\n\n        if (n)\n            log::debug(logcat, \"{} expired paths dropped\", n);\n    }\n\n    void PathHandler::invalidate_paths()\n    {\n        log::trace(logcat, \"{} dropping all paths\", __PRETTY_FUNCTION__);\n        Lock_t lock{paths_mutex};\n        for (auto itr = _paths.begin(); itr != _paths.end();)\n        {\n            router.path_context.drop(*itr->second);\n            itr = _paths.erase(itr);\n        }\n    }\n\n    Path* PathHandler::get_path_by_edge(const HopID& edge_hop_id)\n    {\n        if (auto it = _paths.find(edge_hop_id); it != _paths.end())\n            return it->second.get();\n        return nullptr;\n    }\n\n    Path* PathHandler::get_path_by_terminus(const HopID& terminal_hop_id)\n    {\n        for (auto& p : std::views::values(_paths))\n            if (p && p->terminal_hopid() == terminal_hop_id)\n                return p.get();\n        return nullptr;\n    }\n\n    void PathHandler::tick(std::chrono::milliseconds now)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        Lock_t l{paths_mutex};\n\n        expire_paths(now);\n\n        if (not router.is_service_node and not router.is_connected())\n            // If we are not yet fully connected then we can't initiate path builds.  (In theory we\n            // could whe not yet fully connected, but don't want to because that would bias edge\n            // router selection towards faster ones).\n            return;\n\n        if (!is_stopped())\n            update_paths(now);\n\n        router.path_builds.update(now);\n\n        ping_paths(now);\n    }\n\n    nlohmann::json PathHandler::ExtractStatus() const\n    {\n        auto paths = nlohmann::json::array();\n        for (auto& [h, path] : _paths)\n            if (path)\n                paths.push_back(path->ExtractStatus());\n\n        return nlohmann::json{{\"numHops\", _num_hops}, {\"targetPaths\", _target_paths}, {\"paths\", std::move(paths)}};\n    }\n\n    const RelayContact* PathHandler::select_first_hop(std::function<bool(const RelayContact&)> pred) const\n    {\n#ifdef LOKINET_DEBUG_PATH_SEED\n        auto current_remotes_unsorted =\n#else\n        auto current_remotes =\n#endif\n            router.link_endpoint().get_current_relays();\n\n#ifdef LOKINET_DEBUG_PATH_SEED\n        std::vector<RouterID> current_remotes;\n        current_remotes.reserve(current_remotes_unsorted.size());\n        current_remotes.assign(current_remotes_unsorted.begin(), current_remotes_unsorted.end());\n        std::optional<std::mt19937_64> rng;\n        if (router.config().paths.debug_path_seed)\n        {\n            rng.emplace(*router.config().paths.debug_path_seed);\n            std::sort(current_remotes.begin(), current_remotes.end());\n        }\n#endif\n\n        const RelayContact* selected = nullptr;\n        int acceptable = 0;\n        for (auto& rid : current_remotes)\n        {\n            auto* rc = router.node_db().get_rc(rid);\n            if (!rc)\n            {\n                log::debug(logcat, \"Skipping {}: missing RC\", rid);\n                continue;\n            }\n\n            if (pred && !pred(*rc))\n                log::trace(\n                    logcat, \"Not considering {} for first hop selection because it failed the given predicate\", rid);\n            else if (router.router_profiling().is_bad_for_path(rid))  // always returns false on testnet\n                log::trace(logcat, \"Not considering {} for first hop because of router profiling\", rid);\n            else\n            {\n                log::trace(logcat, \"Router {} is an acceptable first hop\", rid);\n\n                // DIY reservoir sample because doing this with a filter and a view calls the filter\n                // code multiple times, which we don't want.\n                if (acceptable == 0\n                    || (\n#ifdef LOKINET_DEBUG_PATH_SEED\n                        rng ? std::uniform_int_distribution<int>{0, acceptable}(*rng) :\n#endif\n                            std::uniform_int_distribution<int>{0, acceptable}(llarp::csrng) == 0))\n                    selected = rc;\n                acceptable++;\n            }\n        }\n\n        if (!selected)\n            log::debug(logcat, \"Failed to select first hop: no acceptable candidates found\");\n\n        return selected;\n    }\n\n    int PathHandler::num_active_paths(std::chrono::milliseconds expiry_ts) const\n    {\n        Lock_t l(paths_mutex);\n\n        int n = 0;\n        for (const auto& [_, p] : _paths)\n            if (p and p->is_active() and not p->is_expired(expiry_ts))\n                n++;\n        return n;\n    }\n\n    int PathHandler::num_paths(std::chrono::milliseconds expiry_ts) const\n    {\n        Lock_t l(paths_mutex);\n\n        int n = 0;\n        for (const auto& [_, p] : _paths)\n            // TODO FIXME: what does a nullptr path mean?\n            if (p and not p->is_expired(expiry_ts))\n                n++;\n        return n;\n    }\n\n    void PathHandler::stop()\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n        _running = false;\n\n        if (_path_rotater)\n        {\n            _path_rotater.reset();\n            log::trace(logcat, \"Path rotation ticker stopped!\");\n        }\n\n        _paths.clear();\n    }\n\n    bool PathHandler::is_stopped() const { return !_running.load(); }\n\n    std::optional<std::vector<RelayContact>> PathHandler::select_hops_to_remote(const RouterID& pivot)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n        assert(_num_hops);\n\n        int hops_needed = _num_hops;\n\n        auto hops = std::make_optional<std::vector<RelayContact>>();\n\n        auto* pivot_rc = router.node_db().get_rc(pivot);\n        if (!pivot_rc)\n        {\n            log::warning(logcat, \"Failed to select path hops: no RC found for requested pivot {}\", pivot);\n            return std::nullopt;\n        }\n\n        if (--hops_needed <= 0)\n        {\n            // if we only need one hop then we're done!\n            hops->push_back(std::move(*pivot_rc));\n            return hops;\n        }\n\n        auto netmask = router.config().paths.unique_hop_netmask;\n        std::unordered_set<RouterID> to_exclude{{pivot}};\n        std::vector<ipv4_net> excluded_ranges{};\n        if (netmask)\n            excluded_ranges.reserve(_num_hops);\n\n        auto exclude = [&netmask, &to_exclude, &excluded_ranges](const RelayContact& rc) {\n            to_exclude.insert(rc.router_id());\n            if (netmask)\n                excluded_ranges.push_back(rc.addr().to_ipv4() % netmask);\n        };\n\n        exclude(*pivot_rc);\n\n        auto filter =\n            [&rp = router.router_profiling(), &excluded_ranges, &to_exclude, &netmask](const RelayContact& rc) {\n                auto& rid = rc.router_id();\n                if (to_exclude.contains(rid))\n                    return false;\n\n                if (netmask)\n                {\n                    auto v4 = rc.addr().to_ipv4();\n                    for (auto& r : excluded_ranges)\n                        if (r.contains(v4))\n                            return false;\n                }\n\n                if (rp.is_bad_for_path(rc.router_id(), 1))\n                    return false;\n\n                return true;\n            };\n\n        // Edge selection has its own distinct criteria: we can only select from edge routers we\n        // have established connections to.  We take up to three passes to find a suitable edge:\n        // 1. Try finding one with exclusion of `pivot`'s net range (when netmask exclusions are\n        //    enabled).\n        // 2. Try finding one that only excludes the pivot itself.\n        // 3. Use the pivot as the first hop, and force path length of at least 3 (because\n        //    client-A-A is not a valid path, so we need to force client-A-B-A).\n        //\n        // Ideally we want to avoid cases 2 or 3 but sometimes that is simply impossible: for\n        // example if you need to build a path to A specifically, and A happens to be your only\n        // pinned edge.  If we do find ourselves in case 2 or 3, we extend the path length by 1 (if\n        // possible) to compensate for the repeated network in the path.\n        //\n        // We avoid cases 2/3 for inbound/utility paths by detecting when we have all edges in the\n        // same range and avoiding that range for pivot selection.  For outbound paths it is\n        // sometimes unavoidable because outbound paths need a specific terminus, leaving us with no\n        // choice.\n        const RelayContact* maybe_first = nullptr;\n        bool extend_path = false;\n        if (netmask)\n        {\n            maybe_first = select_first_hop(filter);\n\n            if (!maybe_first)\n                // We failed to find any edge that doesn't overlap with pivot's range, so extend the\n                // path length by one to compensate.\n                extend_path = true;\n        }\n\n        // Without a netmask filter, or if we couldn't find anything with the filter applied, open\n        // up the selection to simply any relay that isn't the pivot itself:\n        if (!maybe_first)\n            maybe_first = select_first_hop([&pivot](const RelayContact& rc) { return rc.router_id() != pivot; });\n\n        // If even that failed then we have to use the pivot itself.  This seems weird, but can\n        // happen if you are connected to only a single router (e.g. with a pinned edge) *and* want\n        // to build a path to that router.\n        if (!maybe_first)\n        {\n            extend_path = true;\n            maybe_first = router.node_db().get_rc(pivot);\n        }\n\n        // If that failed, retry first hop selection *without* the IP range exclusion being applied:\n        // this is so that if the pivot happens to be in the same range as all your current (or\n        // allowed) edges, you can still connect to it.\n        if (!maybe_first)\n            maybe_first = select_first_hop();\n\n        if (!maybe_first)\n        {\n            log::warning(logcat, \"No suitable first hop candidate for path to {}\", pivot);\n            return std::nullopt;\n        }\n\n        --hops_needed;\n        hops->push_back(std::move(*maybe_first));\n        exclude(hops->back());\n\n        // If we're in two-hop mode, and went through the last selection fallback above, then we\n        // could have just selected a path (client-A-A) but that is not valid: so in that special\n        // case we forcible extend the path length by 1 to construct a path client-A-B-A instead.\n        if (extend_path and hops_needed < BUILD_LENGTH - 2)\n        {\n            log::debug(\n                logcat,\n                \"Extending path length from {} to {} because of unavoidable edge/terminus network overlap\"\n                \" (edge: {}, terminus: {})\",\n                hops_needed + 2,\n                hops_needed + 3,\n                maybe_first->addr(),\n                pivot_rc->addr());\n            ++hops_needed;\n        }\n\n        log::trace(logcat, \"First/last hop selected, {} hops remaining to select\", hops_needed);\n\n        for (; hops_needed > 0; hops_needed--)\n        {\n            // We can't use get_n_random_rcs here to select hops_needed all at once because as we\n            // select each one that affects the selection criteria for the next one, not *only*\n            // because of no-replacement but also because of the unique range setting.  (And we\n            // can't use a mutating filter because the random selection potentially calls the filter\n            // for every possible node, whether or not they end up being in the final selection).\n            auto* maybe_hop = router.node_db().get_random_rc(filter);\n            if (!maybe_hop)\n            {\n                log::warning(\n                    logcat,\n                    \"Failed to find enough acceptable RCs for aligned path to pivot {}: {} required but only found {}\",\n                    pivot,\n                    _num_hops,\n                    _num_hops - hops_needed);\n                return std::nullopt;\n            }\n\n            hops->push_back(std::move(*maybe_hop));\n            auto& hop = hops->back();\n            to_exclude.insert(hop.router_id());\n            if (netmask)\n                excluded_ranges.push_back(hop.addr().to_ipv4() % netmask);\n        }\n\n        hops->push_back(*pivot_rc);\n        return hops;\n    }\n\n    Path* PathHandler::build_path_to_remote(const RouterID& remote, std::chrono::seconds lifetime)\n    {\n        Lock_t l(paths_mutex);\n\n        if (auto maybe_hops = select_hops_to_remote(remote))\n        {\n            return build(*maybe_hops, llarp::time_now_ms() + lifetime);\n        }\n\n        log::warning(logcat, \"Failed to get hops for path-build to {}\", remote);\n        return nullptr;\n    }\n\n    bool PathHandler::can_build(std::span<const RelayContact> hops)\n    {\n        if (is_stopped())\n        {\n            log::debug(logcat, \"Path builder is stopped, aborting path build...\");\n            return false;\n        }\n\n        if (hops.empty())\n        {\n            log::error(logcat, \"Error: cannot build an empty path!\");\n            return false;\n        }\n\n        if (hops.size() > path::BUILD_LENGTH)\n        {\n            log::error(\n                logcat,\n                \"Error: cannot build a path of size {} (exceeds max path length {})\",\n                hops.size(),\n                path::BUILD_LENGTH);\n            return false;\n        }\n\n        _last_build = llarp::time_now_ms();\n\n        return true;\n    }\n\n    std::shared_ptr<Path> PathHandler::build_init_path(\n        std::span<const RelayContact> hops, std::chrono::milliseconds expiry_ts)\n    {\n        auto path = std::make_shared<path::Path>(router, hops, *this, expiry_ts);\n\n        Lock_t l{paths_mutex};\n\n        if (auto [it, b] = _paths.try_emplace(path->edge().rxid, path); not b)\n        {\n            // TODO FIXME: doesn't this mean we somehow selected an invalid rxid for the path\n            // build, not that there is a path to the same remote?\n            log::debug(logcat, \"Pending build to {} already underway... aborting...\", path->edge().rxid);\n            return nullptr;\n        }\n\n        log::debug(logcat, \"Building -> {}\", *path);\n\n        return path;\n    }\n\n    static consteval size_t bt_pair_bytes(size_t value_len, size_t key_len = 1)\n    {\n        if (value_len > 999)\n            throw std::invalid_argument{\"value_len too big\"};\n        if (key_len > 9)\n            throw std::invalid_argument{\"key_len too big\"};\n        return (2 + key_len /*n:KEY*/) + (value_len < 10 ? 2 : value_len < 100 ? 3 : 4) + value_len /*NN:DATA*/;\n    }\n    static_assert(\n        BUILD_FRAME_SIZE\n        == 2 /*de*/ + bt_pair_bytes(PubKey::SIZE) /*k*/ + bt_pair_bytes(SymmNonce::SIZE) /*n*/ + /*x*/\n            bt_pair_bytes(\n                2 /*de*/ + bt_pair_bytes(sizeof(uint32_t)) /*l*/ + bt_pair_bytes(HopID::SIZE) /*r*/\n                + bt_pair_bytes(HopID::SIZE) /*t*/ + bt_pair_bytes(RouterID::SIZE) /*u*/));\n\n    std::vector<std::byte> PathHandler::path_build_onion(Path& path)\n    {\n        // Note: we aren't using bt serialization here of the actual build frames, mainly because\n        // one step of the build is to onion en/decrypt all following frame data, and that is much\n        // simpler if it's just packed together without another serialization layer in it.\n\n        if (not path.set_built())\n            throw std::logic_error{\"Cannot build a path from a Path object ({}) multiple times!\"_format(path)};\n\n        std::vector<std::byte> result;\n        result.resize(BUILD_FRAME_SIZE * BUILD_LENGTH);\n        std::span rspan{result};\n\n        auto& path_hops = path.hops;\n        int n_hops = static_cast<int>(path.num_hops());\n\n        auto path_expiry = oxenc::host_to_little(\n            static_cast<uint32_t>(std::chrono::round<std::chrono::seconds>(path.expires_in()).count()));\n        std::span<const std::byte, 4> path_expiry_encoded{\n            reinterpret_cast<const std::byte*>(&path_expiry), sizeof(path_expiry)};\n\n        if (n_hops < BUILD_LENGTH)\n        {\n            // append junk data when our path is shorter than the max path length: path build\n            // request must always have exactly BUILD_LENGTH frames\n            random_fill(rspan.last(BUILD_FRAME_SIZE * (BUILD_LENGTH - n_hops)));\n        }\n\n        // each hop will be able to read the outer part of its frame and decrypt\n        // the inner part with that information.  It will then do an onion step on the\n        // remaining frame data so the next hop can read the outer part of its frame,\n        // and so on.  As this de-onion happens from hop 1 to n, we create and onion\n        // the frames from hop n downto 1 (i.e. reverse order).  The first frame is\n        // not onioned.\n        //\n        // Onion-ing the frames in this way will prevent relays controlled by\n        // the same entity from knowing they are part of the same path\n        // (unless they're adjacent in the path; nothing we can do about that obviously).\n\n        for (int i = n_hops - 1; i >= 0; --i)\n        {\n            /** For each hop:\n                - Generate an Ed keypair for the hop (`shared_key`)\n                - Generate a symmetric nonce for subsequent DH\n                - Derive the shared secret (`hop.shared`) for DH key-exchange using the Ed keypair, hop pubkey, and\n                    symmetric nonce\n                - Encrypt the hop info in-place using `hop.shared` and the generated symmetric nonce from DH\n                - Generate the XOR nonce by hashing the symmetric key from DH (`hop.shared`) and truncating\n\n                Bt-encoded contents:\n                - 'k' : ephemeral pubkey used to derive DH shared secret for this hop\n                - 'n' : nonce used for DH secret calculation *and* for the encrypted payload (next item)\n                - 'x' : encrypted payload\n                    - 'l' : path lifetime in seconds, as a 4-byte, little-endian encoded integer (because we require an\n               exact size)\n                    - 'r' : rxID (the path ID for messages going *to* the hop)\n                    - 't' : txID (the path ID for messages coming *from* the client/path origin)\n                    - 'u' : upstream hop RouterID\n\n                All of these frames are inserted sequentially into the list and padded with any needed dummy frames\n            */\n            // TODO FIXME: poly1305 MAC for path build encryption\n            auto& hop = path_hops[i];\n\n            std::string hop_payload;\n            {\n                oxenc::bt_dict_producer info;\n                info.append(\"l\", path_expiry_encoded);\n                info.append(\"r\", hop.rxid.to_view());\n                info.append(\"t\", hop.txid.to_view());\n                info.append(\"u\", hop.upstream.to_view());\n                hop_payload = std::move(info).str();\n            }\n\n            auto dh_nonce = SymmNonce::make_random();\n            auto eph_key = crypto::generate_ed25519();\n\n            if (!crypto::dh_client(hop.shared_secret, hop.router_id, eph_key, dh_nonce))\n                throw std::runtime_error{\"Client DH failed for hop[{}] with rid {}\"_format(i, hop.router_id)};\n\n            hop.xor_nonce.assign(crypto::shorthash(hop.shared_secret).first<SymmNonce::SIZE>());\n\n            crypto::xchacha20(as_bspan(hop_payload), hop.shared_secret, dh_nonce);\n\n            oxenc::bt_dict_producer btdp;\n            btdp.append(\"k\", eph_key.pubkey_span());\n            btdp.append(\"n\", dh_nonce.span());\n            btdp.append(\"x\", hop_payload);\n            auto frame = btdp.view();\n\n            if (frame.size() != BUILD_FRAME_SIZE)\n            {\n                assert(frame.size() == BUILD_FRAME_SIZE);\n                log::critical(logcat, \"Internal error: unexpected path build frame size!\");\n                throw std::runtime_error{\"Internal error: frame size mismatch in path build!\"};\n            }\n\n            auto mine = rspan.subspan(i * BUILD_FRAME_SIZE, BUILD_FRAME_SIZE);\n            std::memcpy(mine.data(), frame.data(), BUILD_FRAME_SIZE);\n\n            if (auto following_frames = n_hops - 1 - i; following_frames > 0)\n                // We only onion the real frames that follow this one, not the junk frames, because\n                // the junk frames are never actually used.  When *de*-onioning we deonion all\n                // following frames because we have no idea where the real frames end and junk\n                // frames begin (because of frame rotation), which also has a nice side effect of\n                // scrambling the junk frames so that junk values don't link path builds.  (This\n                // also means the junk recovered isn't the same junk we produced, but that's fine).\n                crypto::xchacha20(\n                    rspan.subspan((i + 1) * BUILD_FRAME_SIZE, following_frames * BUILD_FRAME_SIZE),\n                    hop.shared_secret,\n                    dh_nonce ^ hop.xor_nonce);\n        }\n\n        router.path_builds.attempts++;\n\n        return result;\n    }\n\n    // Constructs a TransitHop from a serialized path build frame, i.e. undoing one layer of the\n    // path build onioning, above.  Returns the constructed TransitHop and the dh_nonce for the path\n    // build.\n    std::pair<std::shared_ptr<path::TransitHop>, SymmNonce> PathHandler::decrypt_build_frame(\n        std::span<const std::byte, path::BUILD_FRAME_SIZE> frame,\n        const Router& r,\n        const std::variant<RouterID, quic::ConnectionID>& src,\n        std::chrono::milliseconds now)\n    {\n        std::pair<std::shared_ptr<path::TransitHop>, SymmNonce> ret;\n        auto& [hop_ptr, dh_nonce] = ret;\n        auto& hop = *(hop_ptr = std::make_shared<path::TransitHop>());\n        hop.downstream = src;\n\n        PubKey eph_pubkey;\n        std::vector<std::byte> payload;\n        try\n        {\n            oxenc::bt_dict_consumer btdc{frame};\n            eph_pubkey.assign(btdc.require_span<std::byte, PubKey::SIZE>(\"k\"));\n            dh_nonce.assign(btdc.require_span<std::byte, SymmNonce::SIZE>(\"n\"));\n            // Need to copy this because we decrypt in place below:\n            auto payld = btdc.require<std::span<const std::byte>>(\"x\");\n            payload.assign(payld.begin(), payld.end());\n            btdc.finish();\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"Exception caught deserializing hop dict: {}\", e.what());\n            throw path::TransitHopError::INVALID_DATA();\n        }\n\n        if (!crypto::dh_server(hop.shared_secret, eph_pubkey, r.secret_key(), dh_nonce))\n        {\n            log::warning(logcat, \"Failed to derive shared secret!\");\n            throw path::TransitHopError::DH_PUBKEY();\n        }\n\n        crypto::xchacha20(payload, hop.shared_secret, dh_nonce);\n        hop.xor_nonce.assign(crypto::shorthash(hop.shared_secret).first<SymmNonce::SIZE>());\n\n        try\n        {\n            oxenc::bt_dict_consumer inner{std::move(payload)};\n\n            std::chrono::seconds lifetime{\n                oxenc::load_little_to_host<uint32_t>(inner.require_span<std::byte, sizeof(uint32_t)>(\"l\").data())};\n            if (lifetime > path::MAX_LIFETIME_ACCEPTED)\n                throw std::runtime_error{\"Path lifetime {} exceeds maximum allowed path lifetime {}\"_format(\n                    lifetime, path::MAX_LIFETIME_ACCEPTED)};\n            hop.expiry = now + lifetime;\n            hop.rxid.assign(inner.require_span<std::byte, HopID::SIZE>(\"r\"));\n            hop.txid.assign(inner.require_span<std::byte, HopID::SIZE>(\"t\"));\n            hop.upstream.assign(inner.require_span<std::byte, RouterID::SIZE>(\"u\"));\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"TransitHop caught bt parsing exception: {}\", e.what());\n            throw path::TransitHopError::INVALID_PAYLOAD();\n        }\n\n        // If we are a terminal hop then two things must be true: upstream must be this router, and\n        // the rxid and txid must be equal.  If *not* a terminal hop, then both must be false.\n        hop.terminal_hop = hop.upstream == r.id();\n        bool terminal_mismatch = hop.terminal_hop != (hop.txid == hop.rxid);\n        if (hop.txid.is_zero() || hop.rxid.is_zero() || terminal_mismatch)\n            throw path::TransitHopError::INVALID_HOP_ID();\n\n        log::trace(logcat, \"TransitHop data successfully decrypted/deserialized: {}\", hop);\n\n        return ret;\n    }\n\n    // TODO FIXME: investigate return type?\n    Path* PathHandler::build(std::span<const RelayContact> hops, std::chrono::milliseconds expiry_ts)\n    {\n        Lock_t lock{paths_mutex};\n\n        // error message logs in function scope\n        if (can_build(hops))\n        {\n            if (auto new_path = build_init_path(hops, expiry_ts))\n            {\n                auto ptr = new_path.get();\n                auto id = ++_path_counter;\n                send_path_build(std::move(new_path), id);\n                // send_path_build calls the appropriate success/failure method\n                return ptr;\n            }\n        }\n\n        path_build_failed(0, nullptr, false);\n        return nullptr;\n    }\n\n    void PathHandler::send_path_build(const std::shared_ptr<Path>& new_path, int64_t id)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        auto payload = path_build_onion(*new_path);\n        const auto& upstream = new_path->edge().router_id;\n\n        router.link_endpoint().send_command(\n            upstream, \"path_build\", std::move(payload), [this, new_path, id](quic::message m) {\n                if (m)\n                {\n                    log::info(logcat, \"PATH ESTABLISHED: {}\", *new_path);\n                    log::trace(logcat, \"path build response: {}\", buffer_printer{m.body()});\n                    return path_build_succeeded(id, *new_path);\n                }\n\n                if (m.timed_out)\n                    log::warning(logcat, \"Path-build request timed out!\");\n                else\n                    try\n                    {\n                        oxenc::bt_dict_consumer d{m.body()};\n                        auto status = d.require<std::string_view>(messages::STATUS_KEY);\n                        log::warning(logcat, \"Onepass path-build returned failure status: {}\", status);\n                    }\n                    catch (const std::exception& e)\n                    {\n                        log::warning(\n                            logcat,\n                            \"Exception caught parsing path_build response: {}; response body: {}\",\n                            e.what(),\n                            llarp::buffer_printer{m.body()});\n                    }\n\n                return path_build_failed(id, new_path.get(), m.timed_out);\n            });\n    }\n\n    void PathHandler::path_build_failed(int64_t build_id, Path* p, bool timeout)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        if (p)\n            drop_path(*p);\n\n        if (timeout)\n        {\n            if (p)\n                router.router_profiling().path_timeout(*p);\n            router.path_builds.timeouts++;\n        }\n        else\n            router.path_builds.build_fails++;\n\n        _last_failure = llarp::time_now_ms();\n        _consecutive_failures++;\n\n        on_path_build_failure(build_id, p, timeout);\n    }\n\n    void PathHandler::path_build_succeeded(int64_t build_id, Path& p)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        p.set_established();\n        add_path(p);\n        router.router_profiling().path_success(p);\n        router.path_builds.success++;\n\n        _consecutive_failures = 0;\n\n        on_path_build_success(build_id, p);\n    }\n\n    bool PathHandler::cooldown(std::chrono::milliseconds now) const\n    {\n        if (_consecutive_failures < BACKOFF_THRESHOLD)\n            return false;\n\n        return now < _last_failure + BACKOFF_INCREMENT * (1 + _consecutive_failures - BACKOFF_THRESHOLD);\n    }\n\n    // TODO FIXME: something should be calling this!\n    void PathHandler::path_died(const Path& p)\n    {\n        log::warning(logcat, \"Path {} died post-build\", p);\n        router.path_builds.path_fails++;\n    }\n}  // namespace llarp::path\n"
  },
  {
    "path": "llarp/path/path_handler.hpp",
    "content": "#pragma once\n\n#include \"hopid.hpp\"\n\n#include <llarp/address/address.hpp>\n#include <llarp/contact/client_intro.hpp>\n#include <llarp/path/path.hpp>\n#include <llarp/util/decaying_hashset.hpp>\n#include <llarp/util/thread/threading.hpp>\n#include <llarp/util/time.hpp>\n\n#include <atomic>\n#include <chrono>\n#include <ranges>\n#include <unordered_map>\n\nnamespace oxen::quic\n{\n    struct Ticker;\n}\n\nnamespace llarp\n{\n    class Router;\n    namespace path\n    {\n        /// We start delaying path builds once we hit this many consecutive path build failures:\n        inline constexpr int BACKOFF_THRESHOLD = 3;\n\n        /// Once we've met the above threshold, we apply a linear backoff starting with this delay\n        /// and then increase the delay by this amount again for each additional path build failure.\n        inline constexpr auto BACKOFF_INCREMENT = 1s;\n\n        class PathHandler : public std::enable_shared_from_this<PathHandler>\n        {\n            void path_build_backoff();\n\n          public:\n            Router& router;\n\n          protected:\n            std::shared_ptr<quic::Ticker> _path_rotater;\n\n            /// flag for ::Stop()\n            std::atomic<bool> _running;\n\n            int _num_hops;\n            int _target_paths;\n            int64_t _path_counter = 0;\n\n            int _consecutive_failures = 0;\n            std::chrono::milliseconds _last_failure = 0ms;\n            std::chrono::milliseconds _last_build = 0ms;\n\n            using Lock_t = util::NullLock;\n            mutable util::NullMutex paths_mutex;\n\n            // Container of paths.  The key is the hopid used by the edge when relaying messages\n            // back to us along this path, i.e. the same as `value->edge().rxid`.\n            std::unordered_map<HopID, std::shared_ptr<Path>> _paths;\n\n            // Returns true if we are currently in the cooldown period because of path build\n            // failures and thus should not currently be trying new path builds.\n            bool cooldown(std::chrono::milliseconds now = llarp::time_now_ms()) const;\n\n            void drop_path(const Path& p);\n\n            virtual void path_died(const Path& p);\n\n            /// Called when a path build fails.  The first argument is a unique non-zero integer as\n            /// returned by build() and can be used to disambiguate the path that fails.  `path` is\n            /// a pointer to the path object, but can be nullptr in the case of immediate failure\n            /// (see below).  `timeout` will be true if the path build timed out, false if there was\n            /// some other error.\n            ///\n            /// Note that this method can be called from within the build() call itself if a path\n            /// build cannot even be attempted (i.e. for some immediate failure).  In such a case,\n            /// the build_id will be 0 and the Path pointer will be nullptr.  For all other failure\n            /// cases, the build_id value will be non-zero and the pointer will be non-nullptr.\n            void path_build_failed(int64_t build_id, Path* path, bool timeout);\n\n            /// Called during path_build_failed after performing basic path handling for subclasses\n            /// to hook into path build failures.  The base class implementation does nothing.\n            virtual void on_path_build_failure(int64_t /*build_id*/, Path* /*path*/, bool /*timeout*/) {}\n\n            /// Called when a path build is successful and confirmed.  build_id is the non-zero\n            /// integer as returned by the build() call that initiated the path build.\n            void path_build_succeeded(int64_t build_id, Path& p);\n\n            /// Called during path_build_succeeded after performing basic path handling for\n            /// subclasses to hook into path build successes.  The base class implementation does\n            /// nothing.\n            virtual void on_path_build_success(int64_t /*build_id*/, Path& /*p*/) {}\n\n          public:\n            PathHandler(Router& router, int target_paths, int num_hops);\n\n            virtual ~PathHandler() = default;\n\n            Path* get_path_by_edge(const HopID& edge_hop_id);\n            Path* get_path_by_terminus(const HopID& terminal_hop_id);\n\n            nlohmann::json ExtractStatus() const;\n\n            void expire_paths(std::chrono::milliseconds now);\n\n            // In case we know none of our paths are still valid, e.g. we received a close on a\n            // relay session so we assume it's restarting.\n            void invalidate_paths();\n\n            void add_path(Path& path);\n\n            // Returns a random path, or nullptr if there are no paths.\n            Path* get_random_active_path() const;\n\n            /// get the number of ACTIVE, unexpired paths.  An future expiry value other than now\n            /// can be given to query the number of active paths that will not have expired at the\n            /// given timestamp.\n            int num_active_paths(std::chrono::milliseconds expiry_ts = llarp::time_now_ms()) const;\n\n            /// get the number of ALL unexpired paths (both active and those being currently built).\n            /// If an expiry value is given then this returns the number of paths that will not have\n            /// expired at that timestamp (i.e. passing in `llarp::time_now_ms() + 10s` will omit\n            /// any paths expiring within the next 10 seconds).\n            int num_paths(std::chrono::milliseconds expiry_ts = llarp::time_now_ms()) const;\n\n            /// get the number of paths (active or currently building) to the given terminus relay\n            int num_paths_to(const RouterID& terminus) const;\n\n            /// Returns the target number of paths we attempt to maintain\n            const int& target_paths() const { return _target_paths; }\n\n            /// Returns the number of hops used for paths built by this object\n            const int& num_hops() const { return _num_hops; }\n\n            // TODO FIXME: this seems like an entangled mess: I don't think *anything* ever calls\n            // this, except for Router calling SessionHandler::stop (which overrides this but then\n            // calls it from the override).  But \"send_close\" has no apparent meaning here, and is\n            // only in the base class because SessionHandler::stop's override uses it.\n            void stop();\n\n            bool is_stopped() const;\n\n            /// Called each path handler tick to allow subclasses to perform path checks, updates,\n            /// rotations, start new paths, etc. as needed.  If not overridden this does nothing.\n            virtual void update_paths(std::chrono::milliseconds /*now*/) {}\n\n            virtual void tick(std::chrono::milliseconds now);\n\n            void ping_paths(std::chrono::milliseconds now);\n\n            Path* build_path_to_remote(const RouterID& remote, std::chrono::seconds lifetime = path::MAX_LIFETIME);\n\n            std::optional<std::vector<RelayContact>> select_hops_to_remote(const RouterID& pivot);\n\n            /// Attempts to build the given path and send it to the network, initiating the path\n            /// build.  When the build is done it calls either path_build_succeeded or\n            /// path_build_failed.  It is possible for path_build_failed to fire *before* this\n            /// function returns if the given path cannot currently be built (such as when shutting\n            /// down, or if the rate limiter is hit).\n            ///\n            /// The return value is a unique id for the path that is passed into the\n            /// path_build_failed/_succeeded methods to uniquely identify the path, or 0 if the path\n            /// build is not currently possible.\n            Path* build(\n                std::span<const RelayContact> hops,\n                std::chrono::milliseconds expiry_ts = llarp::time_now_ms() + path::MAX_LIFETIME);\n\n            /// Returns a view over all current paths (as `Path&` references)\n            auto paths() const\n            {\n                return std::views::values(_paths)  //\n                    | std::views::filter(&std::shared_ptr<Path>::operator bool)\n                    | std::views::transform(&std::shared_ptr<Path>::operator*);\n            }\n\n            /// Returns a view over all active paths (i.e. established and not expired)\n            auto active_paths(std::chrono::milliseconds now = llarp::time_now_ms()) const\n            {\n                return std::views::values(_paths)  //\n                    | std::views::filter([now](const std::shared_ptr<Path>& p) { return p && p->is_active(now); })\n                    | std::views::transform(&std::shared_ptr<Path>::operator*);\n            }\n\n            /// pick a first hop; if predicate is given, only routers for which it returns true are\n            /// permitted.  (Note that the path build limiter and router profile are always checked,\n            /// regardless of the predicate).  Returns nullptr if no acceptable first hops are\n            /// found.\n            const RelayContact* select_first_hop(std::function<bool(const RelayContact&)> pred = nullptr) const;\n\n          private:\n            /// Checks whether we are currently able to build the given path (e.g. not stopped, the\n            /// path edge is not build limited, valid number of hops).\n            bool can_build(std::span<const RelayContact> hops);\n\n            /// Takes a set of path hops (edge, hop1, hop2, ..., pivot) and initializes a Path\n            /// following those hops, including generating path IDs that will be used along the\n            /// path.\n            std::shared_ptr<Path> build_init_path(\n                std::span<const RelayContact> hops, std::chrono::milliseconds expiry_ts);\n\n            /// Takes a path as constructed by build_init_path and constructs an encoded network\n            /// path build message containing the frames required to build the path.\n            std::vector<std::byte> path_build_onion(Path& path);\n\n            /// Takes the path build (from encode_path_build) and fires it down the path.  When the\n            /// path build finishes it calls either path_build_succeeded on success, or\n            /// path_build_failed on failure.\n            void send_path_build(const std::shared_ptr<Path>& new_path, int64_t id);\n\n          public:\n            // Counterpart to path_build_onion that decrypts a single path build frame; this is only\n            // actually called from link_manager.cpp, but is here to be alongside the\n            // path_build_onion that builds the frames.\n            static std::pair<std::shared_ptr<path::TransitHop>, SymmNonce> decrypt_build_frame(\n                std::span<const std::byte, path::BUILD_FRAME_SIZE> frame,\n                const Router& r,\n                const std::variant<RouterID, quic::ConnectionID>& src,\n                std::chrono::milliseconds now);\n        };\n    }  // namespace path\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/path/transit_hop.cpp",
    "content": "#include \"transit_hop.hpp\"\n\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/link/endpoint.hpp>\n#include <llarp/messages/common.hpp>\n#include <llarp/messages/path.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/util/bspan.hpp>\n#include <llarp/util/buffer.hpp>\n#include <llarp/util/time.hpp>\n\n#include <nlohmann/json.hpp>\n#include <oxen/quic/connection_ids.hpp>\n#include <sodium/randombytes.h>\n\n#include <stdexcept>\n\nnamespace llarp::path\n{\n    static auto logcat = log::Cat(\"transit-hop\");\n\n    TransitHopError::TransitHopError(std::string err_code)\n        : std::runtime_error{\"TransitHop construction failed: {}\"_format(err_code)}, error_code{std::move(err_code)}\n    {}\n\n    std::pair<std::variant<RouterID, quic::ConnectionID>, HopID> TransitHop::next_id(const HopID& h) const\n    {\n        std::pair<std::variant<RouterID, quic::ConnectionID>, HopID> ret;\n\n        assert(h == rxid or h == txid);\n        if (h == rxid)\n            return {upstream, txid};\n        return {downstream, rxid};\n    }\n\n    nlohmann::json TransitHop::ExtractStatus() const\n    {\n        return {\n            {\"rid\", router_id.ToHex()}, {\"rxid\", rxid.ToHex()}, {\"txid\", txid.ToHex()}, {\"expiry\", to_json(expiry)}};\n    }\n\n    static std::string short_string(const std::variant<RouterID, quic::ConnectionID>& downstream)\n    {\n        if (auto* rid = std::get_if<RouterID>(&downstream))\n            return rid->short_string().to_string();\n        return std::get<quic::ConnectionID>(downstream).to_string();\n    }\n\n    std::string TransitHop::to_string() const\n    {\n        return \"TransitHop:[ Terminal:{} | TX:{} | RX:{} | Upstream:{} | Downstream:{} | Expiry:{} ]\"_format(\n            terminal_hop, txid, rxid, upstream.short_string(), short_string(downstream), expiry.count());\n    }\n\n}  // namespace llarp::path\n"
  },
  {
    "path": "llarp/path/transit_hop.hpp",
    "content": "#pragma once\n\n#include \"hopid.hpp\"\n\n#include <llarp/constants/path.hpp>\n#include <llarp/contact/router_id.hpp>\n#include <llarp/util/aligned.hpp>\n#include <llarp/util/compare_ptr.hpp>\n\n#include <oxen/quic/connection_ids.hpp>\n\nnamespace llarp\n{\n    class Router;\n}  // namespace llarp\n\nnamespace llarp::path\n{\n    class TransitHopError : public std::runtime_error\n    {\n      public:\n        std::string error_code;\n        TransitHopError(std::string err_code);\n\n        /// Pre-defined error codes:\n        inline static TransitHopError INVALID_DATA() { return \"INVALID DATA\"s; }\n        inline static TransitHopError DH_PUBKEY() { return \"INVALID DH PUBKEY\"s; }\n        inline static TransitHopError INVALID_PAYLOAD() { return \"INVALID TRANSIT HOP PAYLOAD\"s; }\n        inline static TransitHopError INVALID_HOP_ID() { return \"INVALID TRANSIT HOP IDS\"s; }\n        inline static TransitHopError HOP_ID_UNAVAILABLE() { return \"TRANSIT HOP ID ALREADY IN USE\"s; }\n        inline static TransitHopError INVALID_LIFETIME() { return \"INVALID PATH LIFETIME\"s; }\n    };\n\n    // TransitHop holds the raw data associated with a single hop in a path, e.g. hop ids, keys,\n    // expiry, and so on.  It is primarily just a container to hold this data, and lives at the\n    // relevant hop on the relay.\n    struct TransitHop\n    {\n        HopID txid, rxid;\n\n        // Along a path \"upstream\" is the next router away from the client, \"downstream\" is the hop\n        // towards the client (or the connection itself, at the edge).  The pivot (which has no\n        // upstream) is identified by the upstream value being equal to itself.\n        //\n        // For an example path client-A-B-C-pivot, then:\n        //\n        // A: downstream=client's connection id; upstream=B\n        // B: downstream=A, upstream=C\n        // C: downstream=B, upstream=pivot\n        // pivot: downstream=C, upstream=pivot\n        //\n        // txid and rxid are joined in the same way, i.e. the txid of A equals the rxid of B,\n        // and so on up the path.\n        // TODO FIXME: this mixed terminology (\"tx/rx\" for hop ids, but \"upstream/downstream\"\n        // for pubkeys) is needlessly confusing and should be unified, probably by renaming\n        // txid and rxid to upstream_id and downstream_id.\n        // TODO FIXME: why is router_id here at all?\n        RouterID upstream;\n        RouterID router_id;\n        std::variant<RouterID, oxen::quic::ConnectionID> downstream;\n\n        TransitHop() = default;\n\n        // Shared secret between the client and this hop used for this hop's onion encryption\n        SharedSecret shared_secret;\n\n        // Used by each hop to mutate the encryption nonce used for the onion encryption of a\n        // datum down a path.  This isn't cryptographically necessary (the same nonce could be\n        // used all along) but rather is used to make traffic correlation more difficult.\n        SymmNonce xor_nonce;\n\n        std::chrono::milliseconds expiry{0s};\n        std::chrono::milliseconds last_activity{0s};\n\n        uint8_t version;\n        bool terminal_hop{false};\n\n        // Will be set to true immediately before being dropped from the path_context container;\n        // this is primarily used by InboundRelaySession which also holds a shared_ptr (to avoid\n        // needing to lock a weak ptr on every packet) to allow detection of when the TransitHop has\n        // been dropped.\n        bool is_dead{false};\n\n        std::pair<std::variant<RouterID, oxen::quic::ConnectionID>, HopID> next_id(const HopID& h) const;\n\n        // Returns true if this TransitHop matches the same transit components as other, that is,\n        // has the same tx/rxids and upstream/downstream.  This is not equality, however, as this\n        // returns true even if the shared secret, xor_nonce, expiry, etc. are different.\n        bool same_transit(const TransitHop& other) const\n        {\n            return std::tie(txid, rxid, upstream, downstream)\n                == std::tie(other.txid, other.rxid, other.upstream, other.downstream);\n        }\n\n        bool is_expired(std::chrono::milliseconds now = llarp::time_now_ms()) const { return now >= expiry; };\n\n        nlohmann::json ExtractStatus() const;\n\n        std::string to_string() const;\n        static constexpr bool to_string_formattable = true;\n    };\n\n}  // namespace llarp::path\n"
  },
  {
    "path": "llarp/profiling.cpp",
    "content": "#include \"profiling.hpp\"\n\n#include \"path/path.hpp\"\n#include \"router/router.hpp\"\n#include \"util/file.hpp\"\n\n#include <oxen/quic/loop.hpp>\n#include <oxenc/bt_producer.h>\n#include <oxenc/bt_serialize.h>\n\n#include <stdexcept>\n\nusing oxenc::bt_dict_consumer;\nusing oxenc::bt_dict_producer;\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"profiling\");\n\n    RouterProfile::RouterProfile(bt_dict_consumer&& btdc)\n    {\n        try\n        {\n            bt_decode(std::move(btdc));\n        }\n        catch (const std::exception& e)\n        {\n            auto err = \"RouterProfile parsing exception: {}\"_format(e.what());\n            log::warning(logcat, \"{}\", err);\n            throw std::runtime_error{err};\n        }\n    }\n\n    void RouterProfile::bt_encode(bt_dict_producer&& btdp) const\n    {\n        btdp.append(\"g\", conn_success);\n        btdp.append(\"p\", path_success);\n        btdp.append(\"q\", path_timeout);\n        btdp.append(\"s\", path_fail);\n        btdp.append(\"t\", conn_timeout);\n        btdp.append(\"u\", last_update.count());\n        btdp.append(\"v\", version);\n    }\n\n    void RouterProfile::bt_decode(bt_dict_consumer&& btdc)\n    {\n        try\n        {\n            conn_success = btdc.require<uint64_t>(\"g\");\n            path_success = btdc.require<uint64_t>(\"p\");\n            path_timeout = btdc.require<uint64_t>(\"q\");\n            path_fail = btdc.require<uint64_t>(\"s\");\n            conn_timeout = btdc.require<uint64_t>(\"t\");\n            last_update = std::chrono::milliseconds{btdc.require<uint64_t>(\"u\")};\n            version = btdc.require<uint64_t>(\"v\");\n        }\n        catch (...)\n        {\n            log::critical(logcat, \"RouterProfile failed to decode contents\");\n            throw;\n        }\n    }\n\n    bool RouterProfile::bt_decode(std::string_view buf)\n    {\n        try\n        {\n            bt_decode(oxenc::bt_dict_consumer{buf});\n        }\n        catch (const std::exception& e)\n        {\n            // DISCUSS: rethrow or print warning/return false...?\n            auto err = \"RouterProfile parsing exception: {}\"_format(e.what());\n            log::warning(logcat, \"{}\", err);\n            throw std::runtime_error{err};\n        }\n\n        return true;\n    }\n\n    void RouterProfile::decay()\n    {\n        conn_success /= 2;\n        conn_timeout /= 2;\n        path_success /= 2;\n        path_fail /= 2;\n        path_timeout /= 2;\n        last_decay = llarp::time_now_ms();\n    }\n\n    void RouterProfile::tick()\n    {\n        static constexpr auto updateInterval = 30s;\n        const auto now = llarp::time_now_ms();\n        if (last_decay < now && now - last_decay > updateInterval)\n            decay();\n    }\n\n    bool RouterProfile::is_good(uint64_t chances) const\n    {\n        if (conn_timeout > chances)\n            return conn_timeout < conn_success && (path_success * chances) > path_fail;\n        return (path_success * chances) > path_fail;\n    }\n\n    static constexpr bool checkIsGood(uint64_t fails, uint64_t success, uint64_t chances)\n    {\n        if (fails > 0 && (fails + success) >= chances)\n            return (success / fails) > 1;\n        if (success == 0)\n            return fails < chances;\n        return true;\n    }\n\n    bool RouterProfile::is_good_for_connect(uint64_t chances) const\n    {\n        return checkIsGood(conn_timeout, conn_success, chances);\n    }\n\n    bool RouterProfile::is_good_for_path(uint64_t chances) const\n    {\n        if (path_timeout > chances)\n            return false;\n        return checkIsGood(path_fail, path_success, chances);\n    }\n\n    void Profiling::disable() { _profiling_disabled.store(true); }\n\n    void Profiling::enable() { _profiling_disabled.store(false); }\n\n    bool Profiling::is_enabled() const { return not _profiling_disabled.load(); }\n\n    bool Profiling::is_bad_for_connect(const RouterID& r, uint64_t chances)\n    {\n        if (_profiling_disabled.load())\n            return false;\n        util::Lock lock{_m};\n        auto itr = _profiles.find(r);\n        if (itr == _profiles.end())\n            return false;\n        return not itr->second.is_good_for_connect(chances);\n    }\n\n    bool Profiling::is_bad_for_path(const RouterID& r, uint64_t chances)\n    {\n        if (_profiling_disabled.load())\n            return false;\n        util::Lock lock{_m};\n        auto itr = _profiles.find(r);\n        if (itr == _profiles.end())\n            return false;\n        return not itr->second.is_good_for_path(chances);\n    }\n\n    bool Profiling::is_bad(const RouterID& r, uint64_t chances)\n    {\n        if (_profiling_disabled.load())\n            return false;\n        util::Lock lock{_m};\n        auto itr = _profiles.find(r);\n        if (itr == _profiles.end())\n            return false;\n        return not itr->second.is_good(chances);\n    }\n\n    void Profiling::tick()\n    {\n        if (_profiling_disabled.load())\n            return;\n        util::Lock lock(_m);\n        for (auto& [rid, profile] : _profiles)\n            profile.tick();\n    }\n\n    void Profiling::connect_timeout(const RouterID& r)\n    {\n        util::Lock lock{_m};\n        auto& profile = _profiles[r];\n        profile.conn_timeout += 1;\n        profile.last_update = llarp::time_now_ms();\n    }\n\n    void Profiling::connect_succeess(const RouterID& r)\n    {\n        util::Lock lock{_m};\n        auto& profile = _profiles[r];\n        profile.conn_success += 1;\n        profile.last_update = llarp::time_now_ms();\n    }\n\n    void Profiling::clear_profile(const RouterID& r)\n    {\n        util::Lock lock{_m};\n        _profiles.erase(r);\n    }\n\n    void Profiling::hop_fail(const RouterID& r)\n    {\n        if (_profiling_disabled.load())\n            return;\n\n        util::Lock lock{_m};\n        auto& profile = _profiles[r];\n        profile.path_fail += 1;\n        profile.last_update = llarp::time_now_ms();\n    }\n\n    void Profiling::path_fail(path::Path& p)\n    {\n        if (_profiling_disabled.load())\n            return;\n\n        util::Lock lock{_m};\n        bool first = true;\n        for (const auto& hop : p.hops)\n        {\n            // don't mark first hop as failure because we are connected to it directly\n            if (first)\n                first = false;\n            else\n            {\n                auto& profile = _profiles[hop.router_id];\n                profile.path_fail += 1;\n                profile.last_update = llarp::time_now_ms();\n            }\n        }\n    }\n\n    void Profiling::path_timeout(path::Path& p)\n    {\n        if (_profiling_disabled.load())\n            return;\n\n        util::Lock lock{_m};\n        for (const auto& hop : p.hops)\n        {\n            auto& profile = _profiles[hop.router_id];\n            profile.path_timeout += 1;\n            profile.last_update = llarp::time_now_ms();\n        }\n    }\n\n    void Profiling::path_success(path::Path& p)\n    {\n        if (_profiling_disabled.load())\n            return;\n\n        util::Lock lock{_m};\n        for (const auto& hop : p.hops)\n        {\n            auto& profile = _profiles[hop.router_id];\n            // redeem previous fails by halfing the fail count and setting timeout to zero\n            profile.path_fail /= 2;\n            profile.path_timeout = 0;\n            // mark success at hop\n            profile.path_success += p.hops.size();\n            profile.last_update = llarp::time_now_ms();\n        }\n    }\n\n    void Profiling::stop_save_ticker()\n    {\n        if (_disk_saver)\n        {\n            log::trace(logcat, \"Stopping router profile disk saving\");\n            _disk_saver->stop();\n            _disk_saver.reset();\n        }\n    }\n\n    void Profiling::start_save_ticker(Router& r)\n    {\n        _disk_saver = r.disk_loop.call_every(SAVE_INTERVAL, [this] {\n            log::debug(logcat, \"Writing router profiles to disk...\");\n            save_to_disk();\n        });\n    }\n\n    bool Profiling::save_to_disk()\n    {\n        std::string buf;\n        {\n            util::Lock lock{_m};\n            try\n            {\n                buf = BEncode();\n            }\n            catch (const std::exception& e)\n            {\n                log::warning(logcat, \"Failed to encode profiling data: {}\", e.what());\n                return false;\n            }\n        }\n\n        try\n        {\n            util::buffer_to_file(_profile_file, buf);\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"Failed to save profiling data to {}: {}\", _profile_file, e.what());\n            return false;\n        }\n\n        _last_save = llarp::time_now_ms();\n        return true;\n    }\n\n    std::string Profiling::BEncode() const\n    {\n        bt_dict_producer dict;\n        for (const auto& [r_id, profile] : _profiles)\n            profile.bt_encode(dict.append_dict(r_id.to_view()));\n        return std::move(dict).str();\n    }\n\n    void Profiling::BDecode(bt_dict_consumer&& dict)\n    {\n        _profiles.clear();\n        while (dict)\n        {\n            auto [rid, subdict] = dict.next_dict_consumer();\n            if (rid.size() != RouterID::SIZE)\n                throw std::invalid_argument{\n                    \"Invalid profiling data: expected {}-byte pubkey, found {}-byte value\"_format(\n                        RouterID::SIZE, rid.size())};\n            std::span<const uint8_t, RouterID::SIZE> rdata{reinterpret_cast<const uint8_t*>(rid.data()), 32};\n            _profiles.emplace(rdata, std::move(subdict));\n        }\n    }\n\n    bool Profiling::load_from_disk()\n    {\n        try\n        {\n            std::string data = util::file_to_string(_profile_file);\n            util::Lock lock{_m};\n            BDecode(bt_dict_consumer{data});\n        }\n        catch (const std::exception& e)\n        {\n            log::warning(logcat, \"failed to load router profiles from {}: {}\", _profile_file, e.what());\n            return false;\n        }\n        _last_save = llarp::time_now_ms();\n        return true;\n    }\n\n    bool Profiling::should_save(std::chrono::milliseconds now) const\n    {\n        auto dlt = now - _last_save;\n        return dlt > 1min;\n    }\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/profiling.hpp",
    "content": "#pragma once\n\n#include \"constants/proto.hpp\"\n#include \"contact/router_id.hpp\"\n#include \"util/thread/threading.hpp\"\n\n#include <filesystem>\n#include <map>\n\nnamespace oxenc\n{\n    class bt_dict_consumer;\n    class bt_dict_producer;\n}  // namespace oxenc\n\nnamespace oxen::quic\n{\n    struct Ticker;\n}\n\nnamespace llarp\n{\n    class Router;\n\n    namespace path\n    {\n        class Path;\n    }\n\n    struct RouterProfile\n    {\n        static constexpr size_t MaxSize{256};\n\n        uint64_t conn_timeout{};\n        uint64_t conn_success{};\n        uint64_t path_success{};\n        uint64_t path_fail{};\n        uint64_t path_timeout{};\n        std::chrono::milliseconds last_update{0s};\n        std::chrono::milliseconds last_decay{0s};\n        uint64_t version = llarp::constants::proto_version;\n\n        RouterProfile() = default;\n        RouterProfile(oxenc::bt_dict_consumer&& btdc);\n\n        void bt_encode(oxenc::bt_dict_producer&& btdp) const;\n\n        void bt_decode(oxenc::bt_dict_consumer&& btdc);\n\n        bool bt_decode(std::string_view buf);\n\n        bool is_good(uint64_t chances) const;\n\n        bool is_good_for_connect(uint64_t chances) const;\n\n        bool is_good_for_path(uint64_t chances) const;\n\n        /// decay stats\n        void decay();\n\n        // rotate stats if timeout reached\n        void tick();\n    };\n\n    struct Profiling\n    {\n        static constexpr std::chrono::milliseconds SAVE_INTERVAL{10min};\n\n        friend class Router;\n\n        Profiling() = default;\n\n        inline static const int profiling_chances{4};\n\n        /// generic variant\n        bool is_bad(const RouterID& r, uint64_t chances = profiling_chances);\n\n        /// check if this router should have paths built over it\n        bool is_bad_for_path(const RouterID& r, uint64_t chances = profiling_chances);\n\n        /// check if this router should be connected directly to\n        bool is_bad_for_connect(const RouterID& r, uint64_t chances = profiling_chances);\n\n        void connect_timeout(const RouterID& r);\n\n        void connect_succeess(const RouterID& r);\n\n        void path_timeout(path::Path& p);\n\n        void path_fail(path::Path& p);\n\n        void path_success(path::Path& p);\n\n        void hop_fail(const RouterID& r);\n\n        void clear_profile(const RouterID& r);\n\n        void tick();\n\n        bool load_from_disk();\n\n        bool save_to_disk();\n\n        bool should_save(std::chrono::milliseconds now) const;\n\n        void disable();\n\n        void enable();\n\n        bool is_enabled() const;\n\n      private:\n        void start_save_ticker(Router& r);\n\n        void stop_save_ticker();\n\n        std::string BEncode() const;\n\n        void BDecode(oxenc::bt_dict_consumer&& dict);\n\n        std::shared_ptr<oxen::quic::Ticker> _disk_saver;\n\n        mutable util::Mutex _m;\n        std::filesystem::path _profile_file;\n        std::map<RouterID, RouterProfile> _profiles;\n        std::chrono::milliseconds _last_save{0s};\n        std::atomic<bool> _profiling_disabled{false};\n    };\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/router/route_poker.cpp",
    "content": "#include \"route_poker.hpp\"\n\n#include \"router.hpp\"\n\n#include <llarp/link/link_manager.hpp>\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"route_poker\");\n\n    RoutePoker::RoutePoker(Router& r) : _router{r} {}\n\n    template <IP46 IP>\n    void RoutePoker::add_route(const IP& ip)\n    {\n        if (not _up)\n            return;\n\n        // set up route and apply as needed\n        auto& poked_rs = poked_routes<IP>();\n        auto [it, new_route] = poked_rs.emplace(ip, IP{});\n        auto& gw = it->second;\n\n        auto& current_gw = current_gateway<IP>();\n        if (!current_gw)\n        {\n            gw = IP{};\n            return;\n        }\n\n        // remove existing mapping as needed\n        if (!new_route)\n            disable_route(ip, gw);\n        // update and add new mapping\n        gw = *current_gw;\n\n        log::info(logcat, \"Added route to {} via {}\", ip, gw);\n\n        enable_route(ip, gw);\n    }\n    template void RoutePoker::add_route(const ipv4& ip);\n    template void RoutePoker::add_route(const ipv6& ip);\n\n    template <IP46 IP>\n    void RoutePoker::disable_route(const IP& ip, const IP& gateway)\n    {\n        if (_enabled and ip != IP{})\n        {\n            log::info(logcat, \"Deleting route to {} via {}\", ip, gateway);\n            _router.vpn_platform()->RouteManager().delete_route(ip, gateway);\n        }\n    }\n    template void RoutePoker::disable_route(const ipv4&, const ipv4&);\n    template void RoutePoker::disable_route(const ipv6&, const ipv6&);\n\n    template <IP46 IP>\n    void RoutePoker::enable_route(const IP& ip, const IP& gateway)\n    {\n        if (_enabled and ip != IP{})\n            _router.vpn_platform()->RouteManager().add_route(ip, gateway);\n    }\n    template void RoutePoker::enable_route(const ipv4&, const ipv4&);\n    template void RoutePoker::enable_route(const ipv6&, const ipv6&);\n\n    void RoutePoker::delete_all_routes()\n    {\n        for (auto it = poked_routes4.begin(); it != poked_routes4.end();)\n            it = delete_route(it);\n        for (auto it = poked_routes6.begin(); it != poked_routes6.end();)\n            it = delete_route(it);\n    }\n\n    void RoutePoker::start()\n    {\n        if (not _enabled)\n        {\n            log::info(logcat, \"Route poker is NOT enabled for this lokinet instance!\");\n            return;\n        }\n\n        // TODO FIXME: this should be enabled again, but it doesn't seem very nice to have this on\n        // such a tight timer.  Perhaps we could trigger it from the appropriate place, and if not,\n        // at least reduce the timer frequency.\n\n        // router.loop()->call_every(100ms, weak_from_this(), [self = weak_from_this()]() {\n        //     if (auto ptr = self.lock())\n        //         ptr->update();\n        // });\n    }\n\n    void RoutePoker::disable_all_routes()\n    {\n        for (const auto& [ip, gateway] : poked_routes4)\n            disable_route(ip, gateway);\n        for (const auto& [ip, gateway] : poked_routes6)\n            disable_route(ip, gateway);\n    }\n\n    void RoutePoker::refresh_all_routes()\n    {\n        for (const auto& [ip, gw] : poked_routes4)\n            add_route(ip);\n        for (const auto& [ip, gw] : poked_routes6)\n            add_route(ip);\n    }\n\n    RoutePoker::~RoutePoker()\n    {\n        if (not _router.vpn_platform())\n            return;\n\n        delete_all_routes();\n        _router.vpn_platform()->RouteManager().delete_blackhole();\n    }\n\n    void RoutePoker::update()\n    {\n        // TODO FIXME\n        //\n        // // ensure we have an endpoint\n        // auto ep = router.hidden_service_context().GetDefault();\n        // if (ep == nullptr)\n        //   return;\n        // // ensure we have a vpn platform\n        // auto* platform = router.vpn_platform();\n        // if (platform == nullptr)\n        //   return;\n        // // ensure we have a vpn interface\n        // auto* vpn = ep->GetVPNInterface();\n        // if (vpn == nullptr)\n        //   return;\n\n        // auto& route = platform->RouteManager();\n\n        // // get current gateways, assume sorted by lowest metric first\n        // auto gateways = route.get_non_interface_gateways(*vpn);\n        // std::optional<quic::Address> next_gw;\n\n        // for (auto& g : gateways)\n        // {\n        //   if (g.is_ipv4())\n        //   {\n        //     next_gw = g;\n        //     break;\n        //   }\n        // }\n\n        // // update current gateway and apply state changes as needed\n        // if (!(current_gateway == next_gw))\n        // {\n        //   if (next_gw and current_gateway)\n        //   {\n        //     log::info(logcat, \"default gateway changed from {} to {}\", *current_gateway,\n        //     *next_gw); current_gateway = next_gw; refresh_all_routes();\n        //   }\n        //   else if (current_gateway)\n        //   {\n        //     log::warning(logcat, \"default gateway {} has gone away\", *current_gateway);\n        //     current_gateway = next_gw;\n        //   }\n        //   else  // next_gw and not m_CurrentGateway\n        //   {\n        //     log::info(logcat, \"default gateway found at {}\", *next_gw);\n        //     current_gateway = next_gw;\n        //   }\n        // }\n        // else if (router.HasClientExit())\n        //   put_up();\n    }\n\n    template <IP46 IP>\n    inline static constexpr auto ip_name = std::same_as<IP, ipv4> ? \"IPv4\"sv : \"IPv6\"sv;\n\n    void RoutePoker::put_up()\n    {\n        if (_up)\n            return;\n        _up = true;\n\n        if (!_enabled)\n        {\n            log::warning(logcat, \"RoutePoker coming up, but route poking is disabled by config\");\n            return;\n        }\n\n        if (!current_gateway4 && !current_gateway6)\n        {\n            log::warning(logcat, \"RoutePoker came up, but we don't appear to have any gateways!\");\n            return;\n        }\n\n        log::info(logcat, \"RoutePoker coming up; poking routes\");\n\n        vpn::AbstractRouteManager& route = _router.vpn_platform()->RouteManager();\n\n        // black hole all routes if enabled\n        if (_router.config().network.blackhole_routes)\n            route.add_blackhole();\n\n        // explicit route pokes for first hops\n        _router.link_manager().endpoint.for_each_relay_conn([this](const RouterID&, link::Connection& conn) {\n            auto remote = conn.conn->remote();\n            if (remote.is_ipv4())\n                add_route(remote.to_ipv4());\n            else\n                add_route(remote.to_ipv6());\n        });\n\n        auto& local = _router.link_manager().local();\n        if (local.is_ipv4())\n            add_route(local.to_ipv4());\n        else\n            add_route(local.to_ipv6());\n        // add default route\n        //\n        // TODO FIXME -- with this commented out exit mode cannot work!!\n        //\n        log::critical(logcat, \"FIXME TODO: not adding default route yet because ???\");\n        // const auto ep = router.hidden_service_context().GetDefault();\n        // if (auto* vpn = ep->GetVPNInterface())\n        //   route.add_default_route_via_interface(*vpn);\n        log::info(logcat, \"route poker up\");\n\n        // TODO FIXME\n        // set_dns_mode(true);\n    }\n\n    void RoutePoker::put_down()\n    {\n        if (!_up)\n            return;\n\n        // unpoke routes for first hops\n        _router.link_manager().endpoint.for_each_relay_conn([this](const RouterID&, link::Connection& conn) {\n            auto remote = conn.conn->remote();\n            if (remote.is_ipv4())\n                delete_route(remote.to_ipv4());\n            else\n                delete_route(remote.to_ipv6());\n        });\n\n        if (_enabled)\n        {\n            // TODO FIXME\n            // vpn::AbstractRouteManager& route = router.vpn_platform()->RouteManager();\n            // const auto ep = router.hidden_service_context().GetDefault();\n            // if (auto* vpn = ep->GetVPNInterface())\n            //   route.delete_default_route_via_interface(*vpn);\n\n            // delete route blackhole\n            // route.delete_blackhole();\n            // log::info(logcat, \"route poker down\");\n        }\n\n        // set_dns_mode(false);\n        _up = false;\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/router/route_poker.hpp",
    "content": "#pragma once\n\n#include <llarp/address/types.hpp>\n\n#include <optional>\n#include <unordered_map>\n\nnamespace llarp\n{\n    class Router;\n\n    template <typename IP>\n    concept IP46 = std::same_as<IP, ipv4> || std::same_as<IP, ipv6>;\n\n    class RoutePoker\n    {\n      public:\n        RoutePoker(Router& r);\n        RoutePoker(const RoutePoker&) = delete;\n        RoutePoker(RoutePoker&&) = delete;\n        RoutePoker& operator=(const RoutePoker&) = delete;\n        RoutePoker& operator=(RoutePoker&&) = delete;\n\n        template <IP46 IP>\n        void add_route(const IP& ip);\n\n        template <IP46 IP>\n        void delete_route(const IP& ip)\n        {\n            auto& pr = poked_routes<IP>();\n            if (auto it = pr.find(ip); it != pr.end())\n                delete_route(it);\n        }\n\n        void start();\n\n        ~RoutePoker();\n\n        /// explicitly put routes up\n        void put_up();\n\n        /// explicitly put routes down\n        void put_down();\n\n        /// set dns resolver\n        /// pass in if we are using exit node mode right now  as a bool\n        // void set_dns_mode(bool using_exit_mode) const;\n\n        bool enabled() const { return _enabled; }\n\n      private:\n        void update();\n\n        void delete_all_routes();\n\n        void disable_all_routes();\n\n        void refresh_all_routes();\n\n        template <IP46 IP>\n        void enable_route(const IP& ip, const IP& gateway);\n\n        template <IP46 IP>\n        void disable_route(const IP& ip, const IP& gateway);\n\n        std::unordered_map<ipv4, ipv4> poked_routes4;\n        std::unordered_map<ipv6, ipv6> poked_routes6;\n        std::optional<ipv4> current_gateway4;\n        std::optional<ipv6> current_gateway6;\n\n        template <IP46 IP>\n        std::unordered_map<IP, IP>& poked_routes()\n        {\n            if constexpr (std::same_as<IP, ipv4>)\n                return poked_routes4;\n            else\n                return poked_routes6;\n        }\n        template <IP46 IP>\n        std::optional<IP>& current_gateway()\n        {\n            if constexpr (std::same_as<IP, ipv4>)\n                return current_gateway4;\n            else\n                return current_gateway6;\n        }\n\n        template <IP46 IP>\n        using poked_iterator = std::unordered_map<IP, IP>::iterator;\n\n        template <typename It>\n            requires std::same_as<It, poked_iterator<ipv4>> || std::same_as<It, poked_iterator<ipv6>>\n        auto delete_route(It it)\n        {\n            disable_route(it->first, it->second);\n            using IP = std::conditional_t<std::same_as<It, poked_iterator<ipv4>>, ipv4, ipv6>;\n            return poked_routes<IP>().erase(it);\n        }\n\n        Router& _router;\n        bool _up{false};\n        bool _enabled{false};\n    };\n\n    extern template void RoutePoker::add_route(const ipv4&);\n    extern template void RoutePoker::add_route(const ipv6&);\n    extern template void RoutePoker::enable_route(const ipv4& ip, const ipv4& gateway);\n    extern template void RoutePoker::enable_route(const ipv6& ip, const ipv6& gateway);\n    extern template void RoutePoker::disable_route(const ipv4& ip, const ipv4& gateway);\n    extern template void RoutePoker::disable_route(const ipv6& ip, const ipv6& gateway);\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/router/router.cpp",
    "content": "#include \"router.hpp\"\n\n#include <llarp/config/config.hpp>\n#include <llarp/consensus/reachability_testing.hpp>\n#include <llarp/constants/platform.hpp>\n#include <llarp/constants/proto.hpp>\n#include <llarp/constants/version.hpp>\n#include <llarp/contact/contactdb.hpp>\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/link/link_manager.hpp>\n#include <llarp/messages/dht.hpp>\n#include <llarp/nodedb.hpp>\n#include <llarp/util/formattable.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/random.hpp>\n#include <llarp/util/service_manager.hpp>\n#include <llarp/util/time.hpp>\n\n#include <nlohmann/json.hpp>\n#include <oxen/log.hpp>\n\n#include <chrono>\n\n#ifndef LOKINET_EMBEDDED_ONLY\n#include <llarp/handlers/tun.hpp>\n#include <llarp/rpc/oxend_rpc.hpp>\n#include <llarp/rpc/rpc_server.hpp>\n\n#include <oxenmq/oxenmq.h>\n#endif\n\n#include <cstdlib>\n#include <memory>\n#include <stdexcept>\n#include <utility>\n\n#if defined(ANDROID) || defined(IOS)\n#include <unistd.h>\n#endif\n\n#if defined(WITH_SYSTEMD)\n#include <systemd/sd-daemon.h>\n#endif\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"router\");\n\n    Router::Router(\n        Config conf, std::shared_ptr<quic::Loop> loop, std::shared_ptr<vpn::Platform> vpnPlatform, std::promise<void> p)\n        : _config{std::move(conf)},\n          _loop{std::move(loop)},\n          _vpn{std::move(vpnPlatform)},\n          _close_promise{std::move(p)},\n          _contact_db{std::make_unique<ContactDB>(*this)},\n          _last_tick{llarp::time_now_ms()}\n    {\n#ifndef LOKINET_EMBEDDED_ONLY\n        // Not actually shared, but unique_ptr would require destructor visibility which\n        // embedded-only won't have:\n        _omq = std::make_shared<oxenmq::OxenMQ>();\n        // for oxend, so we don't close the connection when syncing the registered relay (which can\n        // exceed the defaut 1MB limit).\n        _omq->MAX_MSG_SIZE = -1;\n        if (_config.router.worker_threads > 0)\n            _omq->set_general_threads(_config.router.worker_threads);\n\n        _router_testing = std::make_shared<consensus::reachability_testing>(*this);\n#endif\n\n        init_logging();\n\n        _loop->call_get([this] {\n            log::debug(logcat, \"Inside router loop, initializing router\");\n            configure();\n            start();\n        });\n    }\n\n    // Default, but we define it here because some of the unique_ptrs are for forward-declared types\n    // in router.hpp which aren't available for destruction, but are available here.\n    Router::~Router() = default;\n\n    nlohmann::json Router::ExtractStatus() const\n    {\n        if (not _is_running)\n            nlohmann::json{{\"running\", false}};\n\n        nlohmann::json links;\n        if (is_service_node)\n        {\n            auto [relays, out, in, pending, clients] = _link_endpoint->relay_connection_counts();\n            links = {\n                {\"relays\", relays},\n                {\"relays_in\", in},\n                {\"relays_out\", out},\n                {\"pending_out\", pending},\n                {\"clients_in\", clients}};\n        }\n        else\n        {\n            auto [nconns, pending] = _link_endpoint->client_connection_counts();\n            links = {{\"relays\", nconns}, {\"relays_out\", nconns}, {\"pending_out\", pending}};\n        }\n        auto [rcs, rids, bootstraps] = node_db().db_stats();\n        auto [npaths, nhops] = path_context.path_ctx_stats();\n        auto [s_in, s_out_r, s_out_c, s_out_r_pending, s_out_c_pending] = _session_endpoint->session_stats();\n        auto ccs = contact_db().num_ccs();\n\n        return {\n            {\"instance\",\n             {{\"id\", id().to_network_address(is_service_node).to_string()},\n              {\"running\", true},\n              {\"relay\", is_service_node}}},\n            {\"links\", std::move(links)},\n            {\"sessions\",\n             {{\"in\", s_in},\n              {\"relay_out\", s_out_r},\n              {\"client_out\", s_out_c},\n              {\"relay_pending\", s_out_r_pending},\n              {\"client_pending\", s_out_c_pending}}},\n            {\"nodedb\", {{\"rcs\", rcs}, {\"rids\", rids}}},\n            {\"contactdb\", {{\"ccs\", ccs}}},\n            {\"path_ctx\", {{\"paths\", npaths}, {\"hops\", nhops}}}};\n    }\n\n    nlohmann::json Router::ExtractSummaryStatus() const\n    {\n        // if (!is_running)\n        //   return nlohmann::json{{\"running\", false}};\n\n        // auto services = _hidden_service_context.ExtractStatus();\n\n        // auto link_types = _link_manager->extract_status();\n\n        // uint64_t tx_rate = 0;\n        // uint64_t rx_rate = 0;\n        // uint64_t peers = 0;\n        // for (const auto& links : link_types)\n        // {\n        //   for (const auto& link : links)\n        //   {\n        //     if (link.empty())\n        //       continue;\n        //     for (const auto& peer : link[\"sessions\"][\"established\"])\n        //     {\n        //       tx_rate += peer[\"tx\"].get<uint64_t>();\n        //       rx_rate += peer[\"rx\"].get<uint64_t>();\n        //       peers++;\n        //     }\n        //   }\n        // }\n\n        // // Compute all stats on all path builders on the default endpoint\n        // // Merge snodeSessions, remoteSessions and default into a single array\n        // std::vector<nlohmann::json> builders;\n\n        // if (services.is_object())\n        // {\n        //   const auto& serviceDefault = services.at(\"default\");\n        //   builders.push_back(serviceDefault);\n\n        //   auto snode_sessions = serviceDefault.at(\"snodeSessions\");\n        //   for (const auto& session : snode_sessions)\n        //     builders.push_back(session);\n\n        //   auto remote_sessions = serviceDefault.at(\"remoteSessions\");\n        //   for (const auto& session : remote_sessions)\n        //     builders.push_back(session);\n        // }\n\n        // // Iterate over all items on this array to build the global pathStats\n        // uint64_t pathsCount = 0;\n        // uint64_t success = 0;\n        // uint64_t attempts = 0;\n        // for (const auto& builder : builders)\n        // {\n        //   if (builder.is_null())\n        //     continue;\n\n        //   const auto& paths = builder.at(\"paths\");\n        //   if (paths.is_array())\n        //   {\n        //     for (const auto& [key, value] : paths.items())\n        //     {\n        //       if (value.is_object() && value.at(\"status\").is_string()\n        //           && value.at(\"status\") == \"established\")\n        //         pathsCount++;\n        //     }\n        //   }\n\n        //   const auto& buildStats = builder.at(\"buildStats\");\n        //   if (buildStats.is_null())\n        //     continue;\n\n        //   success += buildStats.at(\"success\").get<uint64_t>();\n        //   attempts += buildStats.at(\"attempts\").get<uint64_t>();\n        // }\n        // double ratio = static_cast<double>(success) / (attempts + 1);\n\n        nlohmann::json stats{\n            {\"running\", true},\n            {\"version\", llarp::LOKINET_VERSION_FULL},\n            {\"uptime\", to_json(Uptime())},\n            // {\"numPathsBuilt\", pathsCount},\n            // {\"numPeersConnected\", peers},\n            {\"numRoutersKnown\", _node_db->num_rcs()},\n            // {\"ratio\", ratio},\n            // {\"txRate\", tx_rate},\n            // {\"rxRate\", rx_rate},\n        };\n\n        // if (services.is_object())\n        // {\n        //   stats[\"authCodes\"] = services[\"default\"][\"authCodes\"];\n        //   stats[\"exitMap\"] = services[\"default\"][\"exitMap\"];\n        //   stats[\"networkReady\"] = services[\"default\"][\"networkReady\"];\n        //   stats[\"lokiAddress\"] = services[\"default\"][\"identity\"];\n        // }\n        return stats;\n    }\n\n    void Router::start_tickers()\n    {\n        if (_tun)\n            _tun->start_poller();\n\n#ifndef LOKINET_EMBEDDED_ONLY\n        if (!embedded())\n            _service_stat_ticker = _loop->call_every(\n                SERVICE_MANAGER_REPORT_INTERVAL, []() { sys::service_manager->report_periodic_stats(); });\n#endif\n\n        _node_db->start();\n        _contact_db->start_tickers();\n        _link_endpoint->start_tickers();\n\n#ifndef LOKINET_EMBEDDED_ONLY\n        if (is_service_node)\n        {\n            _oxend->start_pings();\n\n            auto delay = uniform_duration_distribution{10s, 15s}(llarp::csrng);\n            if (auto* rc = _node_db->get_rc(id());\n                rc && rc->age(llarp::time_now_ms()) < RelayContact::MIN_GOSSIP_RC_AGE)\n            {\n                // If we already have our own RC, and it's very new then most likely the network\n                // won't accept it right now anyway because we will have just sent it and restarted,\n                // so delay by additional minimum acceptable gossip age before first sending it out.\n                delay += RelayContact::MIN_GOSSIP_RC_AGE;\n            }\n            log::debug(logcat, \"Delaying initial RC broadcast for {}\", delay);\n            loop.call_later(delay, [this] {\n                regenerate_rc();\n                log::debug(logcat, \"Starting RC regen ticker\");\n                _gossip_ticker = loop.call_every(RC_UPDATE_INTERVAL, [this] { regenerate_rc(); });\n            });\n\n            if (not _config.lokid.disable_testing)\n                _router_testing->start();\n        }\n        else\n#endif\n        {\n            // Resolve needed ONS values now that we have the necessary things prefigured\n            _session_endpoint->resolve_sns_mappings();\n        }\n    }\n\n    bool Router::is_fully_meshed() const { return link_endpoint().num_relay_conns() >= _node_db->num_rcs(); }\n\n    void Router::fetch_snode_keys()\n    {\n        assert(is_service_node);\n#ifndef LOKINET_EMBEDDED_ONLY\n\n        our_rc_file = _config.router.data_dir / our_rc_filename;\n\n#if defined(ANDROID) || defined(IOS)\n        log::critical(logcat, \"running a service node on mobile devices is not possible.\");\n        throw std::runtime_error{\"Invalid SNode configuration\"};\n#elif defined(_WIN32)\n        log::critical(logcat, \"running a service node on windows is not possible.\");\n        throw std::runtime_error{\"Invalid SNode configuration\"};\n#endif\n        constexpr int maxTries = 5;\n        int numTries = 0;\n        while (numTries < maxTries)\n        {\n            numTries++;\n            try\n            {\n                key_manager.update_idkey(_oxend->obtain_identity_key());\n                log::info(log_global, \"Obtained service node identity from oxend: {}\", key_manager.router_id());\n                break;\n            }\n            catch (const std::exception& e)\n            {\n                log::warning(\n                    log_global, \"Failed attempt {} of {} to get oxend id keys: \", numTries, maxTries, e.what());\n\n                if (numTries == maxTries)\n                    throw;\n            }\n        }\n#endif\n    }\n\n    void Router::init_logging()\n    {\n        if (_config.logging.type)\n        {\n            auto log_type = *_config.logging.type;\n\n            // Backwards compat: before 0.9.10 we used `type=file` with `file=|-|stdout` for print mode\n            if (log_type == log::Type::File\n                && (_config.logging.file == \"stdout\" || _config.logging.file == \"-\" || _config.logging.file.empty()))\n                log_type = log::Type::Print;\n\n#ifndef NDEBUG\n            std::string debug_pattern =\n                log_type == log::Type::Print ? log::DEFAULT_PATTERN_COLOR : log::DEFAULT_PATTERN_MONO;\n            // In a debug build replace YYYY-MM-DD with \"t=THREADID\"\n            if (auto pos = debug_pattern.find(\"%Y-%m-%d\"); pos != std::string::npos)\n                debug_pattern = debug_pattern.substr(0, pos) + \"t=%t\" + debug_pattern.substr(pos + 8);\n            else\n                debug_pattern = \"[t=%t] \" + debug_pattern;\n#endif\n\n            log::clear_sinks();\n            log::add_sink(\n                log_type,\n                log_type == log::Type::System ? \"lokinet\" : _config.logging.file\n#ifndef NDEBUG\n                ,\n                debug_pattern\n#endif\n            );\n        }\n\n        log::extract_categories(_config.logging.levels).apply([](log::Level global_level) {\n            // Override the global category to always print at info level, even when the\n            // overall/default level is set higher.\n            if (global_level > log::Level::info)\n                log::set_level(log_global, log::Level::info);\n        });\n\n#ifndef LOKINET_EMBEDDED_ONLY\n        // re-add rpc log sink if rpc enabled, else free it\n        if (_config.api.enable_rpc_server and llarp::logRingBuffer)\n            log::add_sink(llarp::logRingBuffer, llarp::log::DEFAULT_PATTERN_MONO);\n        else\n#endif\n            llarp::logRingBuffer.reset();\n    }\n\n    void Router::process_config()\n    {\n        if (is_service_node && embedded())\n            throw std::runtime_error{\"Invalid config: service node and embedded modes are incompatible!\"};\n\n        if (is_service_node)\n        {\n            auto paddr = _config.router.public_addr;\n            if (!paddr)\n                paddr = _config.links.public_addr;\n\n            // Treat 0.0.0.0:0 as not specified:\n            if (_config.links.listen_addr && _config.links.listen_addr->is_any_addr()\n                && _config.links.listen_addr->is_any_port())\n                _config.links.listen_addr.reset();\n\n            // If our given public address has a port of 0 then set port to the listen port (if\n            // one was explicitly given) or otherwise the default relay port.\n            if (paddr)\n            {\n                if (paddr->is_any_port() && _config.links.listen_addr)\n                    paddr->set_port(_config.links.listen_addr->port());\n                if (paddr->is_any_port())\n                    paddr->set_port(DEFAULT_RELAY_PORT);\n\n                _public_address = paddr;\n                if (!_public_address->is_public())\n                    throw std::runtime_error{\"Invalid public-ip: given IP address is not a public IP address\"};\n            }\n\n            bool auto_detect = false;\n            uint16_t auto_port = DEFAULT_RELAY_PORT;\n\n            if (!_config.links.listen_addr)\n            {\n                if (_public_address)\n                    // No listen address, but a public ip/port were given so use that\n                    _listen_address = *_public_address;\n                else\n                    auto_detect = true;\n            }\n            else if (_config.links.listen_addr->is_any_addr())\n            {\n                assert(!_config.links.listen_addr->is_any_port());  // Should be assured from above\n                // port given but not IP: if we have a public ip then use that, else go search\n                if (paddr)\n                    _listen_address = quic::Address{*paddr, _config.links.listen_addr->port()};\n                else\n                {\n                    auto_detect = true;\n                    auto_port = _config.links.listen_addr->port();\n                }\n            }\n            else if (_config.links.listen_addr->is_any_port())\n            {\n                // IP given but not port.  If we have a public port use that, otherwise use the\n                // default.\n                _listen_address = *_config.links.listen_addr;\n                _listen_address.set_port(paddr ? paddr->port() : DEFAULT_RELAY_PORT);\n            }\n            else\n            {\n                assert(_config.links.listen_addr->is_addressable());\n                _listen_address = *_config.links.listen_addr;\n            }\n\n            if (auto_detect)\n            {\n                assert(net());\n                if (auto maybe_addr = net()->get_best_public_address(true, auto_port))\n                    _public_address = _listen_address = std::move(*maybe_addr);\n                else\n                    throw std::runtime_error{\n                        \"Unable to determine a public IP on this system; you likely need to set \"\n                        \"[router]:public-ip/public-port config settings\"};\n            }\n\n            if (!_public_address)\n            {\n                if (_listen_address.is_public())\n                    _public_address = _listen_address;\n                else\n                    throw std::runtime_error{\"When listening on a non-public IP, public-ip must be specified\"};\n            }\n\n            if (_listen_address == *_public_address)\n                log::info(logcat, \"Using {} for Lokinet communications\", _listen_address);\n            else if (!_listen_address.is_public())\n                log::info(\n                    logcat,\n                    \"Listening on private address {} with publicly reachable address {}\",\n                    _listen_address,\n                    *_public_address);\n            else\n                // Binding to a different public IP/port than we actually advertise in RCs is not\n                // allowed, as it is almost certainly a configuration error (e.g. such as moving the\n                // config from one server to another and updating only one of the two values).\n                throw std::runtime_error{\n                    \"public-ip/port ({}) and listen address ({}) are both public addresses but do not match!\"_format(\n                        _public_address, _listen_address)};\n\n            log::info(\n                log_global,\n                \"Lokinet relay listening on {}{}\",\n                _listen_address,\n                _public_address ? \" with public address {}\"_format(*_public_address) : \"\");\n        }\n        else  // Not a service node:\n        {\n            _listen_address = _config.links.listen_addr.value_or(DEFAULT_CLIENT_ADDR);\n\n            // default listen port for clients is a specific port, we want 0 for embedded,\n            // but perhaps the default should be 0 for all clients?\n            if (_config.links.listen_addr && embedded())\n                _listen_address.set_port(0);\n\n            log::info(log_global, \"Lokinet client connection using {}\", _listen_address);\n        }\n\n        RelayContact::BLOCK_BOGONS = _config.router.block_bogons;\n\n        auto& netconf = _config.network;\n\n        if (!embedded())\n        {\n            assert(net());\n\n            if (!netconf._if_name)\n                netconf._if_name = net()->find_free_tun();\n\n            if (!(netconf._local_ip_net && netconf._local_ip_net->ip.addr))\n            {\n                if (auto maybe = net()->find_free_ipv4_net(netconf._local_ip_net ? netconf._local_ip_net->mask : 16))\n                    netconf._local_ip_net = std::move(*maybe);\n                else\n                    throw std::runtime_error(\"cannot find free IPv4 address range!\");\n            }\n            log::info(logcat, \"Lokinet IPv4 local network is {}\", *netconf._local_ip_net);\n\n            if (netconf.enable_ipv6)\n            {\n                if (!netconf._local_ipv6_net || (!netconf._local_ipv6_net->ip.hi && !netconf._local_ipv6_net->ip.lo))\n                {\n                    if (auto maybe =\n                            net()->find_free_ipv6_net(netconf._local_ipv6_net ? netconf._local_ipv6_net->mask : 64))\n                        netconf._local_ipv6_net = std::move(*maybe);\n                    else\n                        throw std::runtime_error(\"cannot find free IPv6 address range!\");\n                }\n                log::info(logcat, \"Lokinet IPv6 local network is {}\", *netconf._local_ipv6_net);\n                log::warning(\n                    logcat,\n                    \"Lokinet IPv6 support is a work-in-progress and unsupported; enabling it is not recommended\");\n            }\n\n            // Make sure any reserved addresses are within our local network range:\n            std::erase_if(netconf._reserved_local_ipv4, [&netconf](const auto& addr_ip) {\n                return !netconf._local_ip_net->contains(addr_ip.second);\n            });\n            if (netconf._local_ipv6_net)\n                std::erase_if(netconf._reserved_local_ipv6, [&netconf](const auto& addr_ip) {\n                    return !netconf._local_ipv6_net->contains(addr_ip.second);\n                });\n        }\n\n        if (not is_service_node)\n        {\n            auto& pathconf = _config.paths;\n\n            if (int conf_edges = static_cast<int>(_config.paths.strict_edges.size()); conf_edges > 0)\n            {\n                if (pathconf.edge_connections > conf_edges)\n                {\n                    log::warning(\n                        logcat,\n                        \"[paths]:edge-connections is set to {0}, but only {1} strict edges are defined; lowering \"\n                        \"edge-connections to {1}\",\n                        pathconf.edge_connections,\n                        conf_edges);\n                    pathconf.edge_connections = conf_edges;\n                }\n                else\n                    log::debug(\n                        logcat,\n                        \"Local client configured to maintain {} of {} possible strict edge relays\",\n                        pathconf.edge_connections,\n                        conf_edges);\n            }\n            else\n                log::debug(\n                    logcat,\n                    \"Local client configured to maintain {} random router edge connections\",\n                    config().paths.edge_connections);\n        }\n    }\n\n    void Router::configure()\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n#ifndef LOKINET_EMBEDDED_ONLY\n        if (!embedded())\n            sys::service_manager->starting();\n#endif\n\n        if (_is_exit_node and is_service_node)\n            throw std::runtime_error{\n                \"Lokinet cannot simultaneously operate as a service node and client-operated exit node service!\"};\n\n        if (_config.lokid.disable_testing && netid() == NetID::MAINNET)\n            throw std::runtime_error{\"Error: reachability testing can only be disabled on testnet!\"};\n\n        auto net_id = netid();\n        log::log(logcat, net_id == NetID::MAINNET ? log::Level::debug : log::Level::warn, \"Network ID is {}\", net_id);\n\n        log::trace(logcat, \"Configuring router...\");\n\n        log::info(log_global, \"Operating as a Lokinet {}\", is_service_node ? \"relay (service node)\" : \"client\");\n\n#ifndef LOKINET_EMBEDDED_ONLY\n        if (is_service_node)\n        {\n            log::debug(logcat, \"Starting oxend RPC client\");\n            _oxend = std::make_shared<rpc::OxendRPC>(*_omq, *this);\n        }\n\n        if (_config.api.enable_rpc_server)\n        {\n            log::debug(logcat, \"Starting RPC server\");\n            //\n            _rpc_server = std::make_shared<rpc::RPCServer>(*_omq, *this);\n        }\n\n        log::debug(logcat, \"Starting OMQ server\");\n        _omq->start();\n\n        if (is_service_node)\n        {\n            log::debug(logcat, \"Connecting to oxend @ {}\", _config.lokid.rpc_addr);\n            _oxend->connect_async(oxenmq::address(_config.lokid.rpc_addr));\n        }\n#endif\n\n        log::debug(logcat, \"Initializing key manager\");\n\n        if (is_service_node)\n            fetch_snode_keys();\n        else\n            key_manager = KeyManager{_config, is_service_node};\n\n        log::trace(logcat, \"Initializing from configuration\");\n\n        process_config();\n\n        _node_db = std::make_unique<NodeDB>(*this);\n\n#ifndef LOKINET_EMBEDDED_ONLY\n        if (is_service_node)\n        {\n            // Wait, synchronously, for the oxend SN list update, for up to 10s.  If we still don't\n            // get it, then fall back to using our current nodedb list.\n            auto on_update = std::make_shared<std::promise<void>>();\n            auto fut = on_update->get_future();\n            _oxend->update_service_node_list(std::move(on_update));\n            bool fallback = false;\n            try\n            {\n                if (fut.wait_for(10s) == std::future_status::timeout)\n                    throw std::runtime_error{\"request timed out\"};\n                fut.get();\n            }\n            catch (std::exception& e)\n            {\n                log::warning(\n                    log_global,\n                    \"Oxend SN request failed: {}. Proceeding with stored RC database as a fallback, which may be \"\n                    \"out of date\",\n                    e.what());\n                fallback = true;\n            }\n\n            if (fallback)\n                _node_db->load_registered_relays_fallback();\n        }\n#endif\n\n        _session_endpoint = std::make_unique<handlers::SessionEndpoint>(*this);\n\n        log::debug(logcat, \"Creating QUIC link manager\");\n        _link_manager = std::make_unique<link::Manager>(*this);\n        _link_endpoint = &_link_manager->endpoint;\n\n        if (!embedded())\n        {\n#ifdef LOKINET_EMBEDDED_ONLY\n            log::critical(logcat, \"This lokinet build only supports embedded configurations!\");\n            throw std::runtime_error{\"This lokinet build only supports embedded configurations!\"};\n#else\n            log::debug(logcat, \"Initializing TUN device\");\n            auto tun = _loop->make_shared<handlers::TunEndpoint>(*this);\n            tun->setup_dns();\n\n            log::info(\n                log_global, \"Lokinet internal network: {} on device {}\", tun->get_ipv4_network(), tun->get_if_name());\n\n            _tun = std::move(tun);\n#endif\n        }\n        else\n            log::debug(logcat, \"Not initializing TUN device; running as an embedded client\");\n    }\n\n    bool Router::is_exit_node() const { return _is_exit_node; }\n\n    bool Router::insufficient_peers() const\n    {\n        constexpr int KnownPeerWarningThreshold = 5;\n        return node_db().num_rcs() < KnownPeerWarningThreshold;\n    }\n\n    std::optional<std::string> Router::OxendErrorState() const\n    {\n        // If we're in the registered list then we *should* be establishing connections to other\n        // routers, so if we have almost no peers then something is almost certainly wrong.\n        if (insufficient_peers() and not _config.lokid.disable_testing)\n            return \"too few peer connections; lokinet is not adequately connected to the network\";\n        return std::nullopt;\n    }\n\n    bool Router::appears_registered() const { return is_service_node and node_db().is_registered(id()); }\n\n    void Router::regenerate_rc()\n    {\n        if (not appears_registered())\n        {\n            log::debug(logcat, \"Not regenerating RC: not currently a registered service node\");\n            return;\n        }\n\n        RelayContact rc{*this};\n        if (_node_db->put_rc(std::move(rc)))\n        {\n            auto* rc = _node_db->get_rc(id());\n            assert(rc);\n            int count = _link_manager->gossip_rc(*rc);\n            log::debug(logcat, \"Regenerated RC and gossiped to {} peers\", count);\n        }\n        else\n        {\n            log::debug(logcat, \"NodeDB refused our own RC; perhaps we restarted too soon since the last regeneration?\");\n        }\n    }\n\n    bool Router::should_report_stats(std::chrono::milliseconds now) const\n    {\n        return now >= _started_at + 10s\n            and now >= _last_stats_report\n                + (log::get_level(logcat) <= log::Level::debug ? REPORT_STATS_INTERVAL_DEBUG : REPORT_STATS_INTERVAL);\n    }\n\n    std::string Router::_stats_line(std::chrono::milliseconds now) const\n    {\n        using namespace fmt::literals;\n        auto [rcs, rids, bs] = _node_db->db_stats();\n        auto [s_in, s_out_r, s_out_c, s_out_r_pending, s_out_c_pending] = _session_endpoint->session_stats();\n        auto [in_paths, out_r_paths, out_c_paths] = _session_endpoint->path_stats(now);\n        if (is_service_node)\n        {\n            auto [relays, rout, rin, rpending, clients] = link_endpoint().relay_connection_counts();\n            return fmt::format(\n                \"relays: {relays} conns ({rin}↓, {rout}↑{pending}{full_mesh}), RC/RIDs: {rcs}/{rids}; \"\n                \"{clients} clients; sessions: {sess_in}↓; paths: {paths_in}\",\n\n                \"relays\"_a = relays,\n                \"rin\"_a = rin,\n                \"rout\"_a = rout,\n                \"pending\"_a = rpending ? \", {} pending\"_format(rpending) : \"\",\n                \"full_mesh\"_a = relays >= rids - 1 ? \", #\" : \"\",\n                \"clients\"_a = clients,\n                \"sess_in\"_a = s_in,\n                \"paths_in\"_a = in_paths,\n                \"rcs\"_a = rcs,\n                \"rids\"_a = rids);\n        }\n\n        auto [nconns, npending] = link_endpoint().client_connection_counts();\n        return fmt::format(\n            \"conns: {conns}; sessions: {sess_in}↓/{sess_out_c}↑(c)/{sess_out_r}↑(r); \"\n            \"paths: {paths_in}↓/{paths_out}↑, {path_percent:.1f}% of {path_total} attempts; \"\n            \"RC/RIDs: {rcs}/{rids}\",\n\n            \"conns\"_a = nconns + npending,\n            \"sess_in\"_a = s_in,\n            \"sess_out_c\"_a = s_out_c,\n            \"sess_out_r\"_a = s_out_r,\n            \"paths_in\"_a = in_paths,\n            \"paths_out\"_a = out_r_paths + out_c_paths,\n            \"path_percent\"_a = (double)path_builds.success * 100.0 / (double)path_builds.attempts,\n            \"path_total\"_a = path_builds.attempts,\n            \"rcs\"_a = rcs,\n            \"rids\"_a = rids);\n    }\n\n    void Router::report_stats()\n    {\n        const auto now = llarp::time_now_ms();\n\n        log::info(log_global, \"Local {}: {}\", is_service_node ? \"Relay\" : \"Client\", _stats_line(now));\n\n        _last_stats_report = now;\n\n        oxen::log::flush();\n    }\n\n    std::string Router::status_line()\n    {\n        auto now = llarp::time_now_ms();\n        return \"v{} {}: {}\"_format(llarp::LOKINET_VERSION_FULL, is_service_node ? \"relay\" : \"client\", _stats_line(now));\n    }\n\n    void Router::_relay_tick([[maybe_unused]] std::chrono::milliseconds now)\n    {\n        assert(_config.relay());\n#ifndef LOKINET_EMBEDDED_ONLY\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        if (should_report_stats(now))\n            report_stats();\n\n        bool registered = appears_registered();\n\n        if (now >= _next_dereg_warning)\n        {\n            if (not registered)\n            {\n                // complain about being deregistered/decommed\n                log::error(logcat, \"We are running as a relay but are not a registered service node\");\n                _next_dereg_warning = now + DECOMM_WARNING_INTERVAL;\n            }\n            else if (insufficient_peers())\n            {\n                log::error(\n                    logcat, \"We are an active service node, but have too few ({}) known peers!\", node_db().num_rcs());\n                _next_dereg_warning = now + DECOMM_WARNING_INTERVAL;\n            }\n        }\n\n        if (registered)\n        {\n            int want = std::min(\n                node_db().num_rcs(/*include_self=*/false) - link_endpoint().num_relay_conns(/*include_pending=*/true),\n                RELAY_CONNECTS_PER_TICK);\n            if (want > 0)\n            {\n                log::debug(logcat, \"Service Node connecting to {} random routers to achieve full mesh\", want);\n                _link_manager->connect_to_keep_alive(want);\n            }\n        }\n\n        path_context.expire_hops(now);\n#endif\n    }\n\n    void Router::_client_tick(std::chrono::milliseconds now)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        _router_profiling.tick();\n\n        if (should_report_stats(now))\n            report_stats();\n\n        // if we need more sessions to routers we shall connect out to others\n        if (int n_conns = link_endpoint().num_relay_conns(/*include_pending=*/true);\n            n_conns < config().paths.edge_connections)\n        {\n            auto num_needed = config().paths.edge_connections - n_conns;\n\n            log::debug(\n                logcat,\n                \"Client connecting to {} random routers to keep alive (current:{}, target:{})\",\n                num_needed,\n                n_conns,\n                config().paths.edge_connections);\n            _link_manager->connect_to_keep_alive(num_needed);\n        }\n\n        _session_endpoint->tick(now);\n    }\n\n    void Router::tick()\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        if (_is_stopping)\n        {\n            log::debug(logcat, \"Router is stopping; exiting ::tick()...\");\n            return;\n        }\n\n        const auto now = llarp::time_now_ms();\n\n        if (const auto delta = now - _last_tick;\n            _last_tick != 0s and (delta > NETWORK_RESET_SKIP_INTERVAL || delta < -NETWORK_RESET_SKIP_INTERVAL))\n        {\n            // TODO: this, if needed?\n            // we detected a time skip into the futre, thaw the network\n            log::error(logcat, \"Timeskip of {}ms detected, resetting network state!\", delta.count());\n        }\n\n        if (is_service_node)\n            _relay_tick(now);\n        else\n            _client_tick(now);\n\n        // update tick timestamp\n        _last_tick = llarp::time_now_ms();\n    }\n\n    void Router::start()\n    {\n        log::debug(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        if (is_service_node)\n        {\n            log::debug(logcat, \"Router accepting transit traffic\");\n            path_context.allow_transit();\n\n            // relays do not use profiling\n            _router_profiling.disable();\n        }\n        else if (netid() == NetID::MAINNET and _config.network.enable_profiling)\n        {\n            _router_profiling._profile_file = _config.router.data_dir / \"profiles.dat\";\n\n            log::debug(logcat, \"Router profiling enabled\");\n            if (not std::filesystem::exists(_router_profiling._profile_file))\n            {\n                log::debug(logcat, \"No profiles file found at {}; skipping...\", _router_profiling._profile_file);\n            }\n            else\n            {\n                log::debug(logcat, \"Loading router profiles from {}\", _router_profiling._profile_file);\n                _router_profiling.load_from_disk();\n            }\n\n            if (_config.network.save_profiles)\n            {\n                log::debug(logcat, \"Router profile saving enabled\");\n                _router_profiling.start_save_ticker(*this);\n            }\n        }\n        else\n        {\n            _config.network.enable_profiling = false;\n            _router_profiling.disable();\n            log::info(logcat, \"Router profiling disabled\");\n        }\n\n        log::debug(logcat, \"Starting Router main tick interval\");\n        _loop_ticker = _loop->call_every(ROUTER_TICK_INTERVAL, [this] { tick(); });\n\n        _started_at = llarp::time_now_ms();\n\n        start_tickers();\n        _is_running = true;\n\n#ifndef LOKINET_EMBEDDED_ONLY\n        if (!embedded())\n            llarp::sys::service_manager->ready();\n#endif\n\n        log::info(\n            log_global,\n            \"{} started @ {}\",\n            is_service_node ? \"Relay\" : \"Client\",\n            id().to_network_address(is_service_node));\n\n        // Fire a tick right now to start making connections immediately (rather than waiting until\n        // the first tick):\n        tick();\n    }\n\n    std::chrono::milliseconds Router::Uptime() const\n    {\n        const std::chrono::milliseconds now = llarp::time_now_ms();\n        if (_started_at > 0s && now > _started_at)\n            return now - _started_at;\n        return 0s;\n    }\n\n    bool Router::is_connected() const\n    {\n        return loop.call_get([this] { return _is_connected; });\n    }\n\n    void Router::on_connected(std::function<void()> callback, bool persistent)\n    {\n        if (!callback)\n            return;\n        loop.call([this, callback = std::move(callback), persistent] {\n            if (_is_connected)\n                try\n                {\n                    callback();\n                }\n                catch (const std::exception& e)\n                {\n                    log::error(logcat, \"Uncaught exception calling on_connected callback: {}\", e.what());\n                }\n\n            if (persistent or not _is_connected)\n                _on_connected.emplace_back(std::move(callback), persistent);\n        });\n    }\n\n    void Router::on_disconnected(std::function<void()> callback, bool persistent)\n    {\n        if (!callback)\n            return;\n        loop.call([this, callback = std::move(callback), persistent] {\n            if (not _is_connected)\n                try\n                {\n                    callback();\n                }\n                catch (const std::exception& e)\n                {\n                    log::error(logcat, \"Uncaught exception calling on_disconnected callback: {}\", e.what());\n                }\n\n            if (persistent or _is_connected)\n                _on_disconnected.emplace_back(std::move(callback), persistent);\n        });\n    }\n\n    static void process_on_conn_callbacks(\n        std::list<std::pair<std::function<void()>, bool>> callbacks, std::string_view type)\n    {\n        for (auto it = callbacks.begin(); it != callbacks.end();)\n        {\n            auto& [f, persist] = *it;\n            try\n            {\n                f();\n            }\n            catch (const std::exception& e)\n            {\n                log::error(logcat, \"Uncaught exception calling {} callback: {}\", type, e.what());\n            }\n            if (persist)\n                ++it;\n            else\n                it = callbacks.erase(it);\n        }\n    }\n\n    void Router::on_edge_conn_change()\n    {\n        assert(loop.inside());\n\n        int conns = link_endpoint().num_relay_conns();\n        if (conns == 0 and _is_connected)\n        {\n            _is_connected = false;\n\n            log::warning(log_global, \"Lokinet is no longer connected to the network!\");\n\n            process_on_conn_callbacks(_on_disconnected, \"on_disconnected\");\n        }\n        else if (\n            not _is_connected\n            and conns * CLIENT_CONNECTED_THRESHOLD::den\n                >= config().paths.edge_connections * CLIENT_CONNECTED_THRESHOLD::num)\n        {\n            _is_connected = true;\n\n            log::info(\n                log_global,\n                \"Lokinet is now connected to the network ({}) with {}/{} relay connections\",\n                config().network.is_reachable ? id().to_network_address(false).to_string() : \"outgoing-only\",\n                conns,\n                config().paths.edge_connections);\n\n            process_on_conn_callbacks(_on_connected, \"on_connected\");\n        }\n    }\n\n    void Router::on_test_ping()\n    {\n#ifndef LOKINET_EMBEDDED_ONLY\n        _router_testing->incoming_ping();\n#endif\n    }\n\n    void Router::stop()\n    {\n        if (!_is_running)\n        {\n            log::debug(logcat, \"Stop called, but not running\");\n            return;\n        }\n        if (_is_stopping)\n        {\n            log::debug(logcat, \"Stop called, but already stopping\");\n            return;\n        }\n\n        if (_is_stopping.exchange(true))\n            return;  // Lost a race with something else trying to stop\n\n        _loop->call([this] {\n#ifndef LOKINET_EMBEDDED_ONLY\n            if (!embedded())\n            {\n                log::debug(logcat, \"stopping service manager...\");\n                llarp::sys::service_manager->stopping();\n            }\n\n            _router_testing->stop();\n#endif\n\n            _session_endpoint->stop(true);\n\n            if (not is_service_node)\n                _router_profiling.stop_save_ticker();\n\n            log::debug(logcat, \"closing all connections\");\n            _link_manager->stop();\n\n            auto rv = _loop_ticker->stop();\n            log::debug(logcat, \"router loop ticker stopped {}successfully!\", rv ? \"\" : \"un\");\n            _loop_ticker.reset();\n\n            if (_service_stat_ticker)\n            {\n                rv = _service_stat_ticker->stop();\n                log::debug(logcat, \"service stat ticker stopped {}successfully!\", rv ? \"\" : \"un\");\n                _service_stat_ticker.reset();\n            }\n\n            if (_reachability_ticker)\n            {\n                log::debug(logcat, \"clearing reachability ticker...\");\n                _reachability_ticker->stop();\n                _reachability_ticker.reset();\n            }\n\n            log::debug(logcat, \"stopping nodedb events\");\n            node_db().cleanup();\n\n            // Submit a dummy job to the disk loop that we wait on to ensure that we've cleared out\n            // any pending disk write jobs.\n            log::debug(logcat, \"flushing disk loop jobs\");\n            disk_loop.call_get([] {});\n\n            log::debug(logcat, \"cleaning up link_manager\");\n            _link_endpoint = nullptr;\n            _link_manager.reset();\n\n            if (_router_close_cb)\n                _router_close_cb();\n\n            _is_running.store(false);\n\n            _omq.reset();\n\n            _close_promise.set_value();\n            log::info(log_global, \"Lokinet has stopped\");\n        });\n    }\n\n    const llarp::net::Platform* Router::net() const\n    {\n#ifndef LOKINET_EMBEDDED_ONLY\n        if (!embedded())\n            return llarp::net::Platform::Default_ptr();\n#endif\n        return nullptr;\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/router/router.hpp",
    "content": "#pragma once\n\n#include \"route_poker.hpp\"\n\n#include <llarp/constants/link_layer.hpp>\n#include <llarp/contact/relay_contact.hpp>\n#include <llarp/crypto/key_manager.hpp>\n#include <llarp/handlers/session.hpp>\n#include <llarp/handlers/tun_base.hpp>\n#include <llarp/path/build_stats.hpp>\n#include <llarp/path/path_context.hpp>\n#include <llarp/profiling.hpp>\n#include <llarp/util/buffer.hpp>\n#include <llarp/util/mem.hpp>\n#include <llarp/util/str.hpp>\n#include <llarp/util/time.hpp>\n#include <llarp/vpn/platform.hpp>\n\n#include <oxen/quic/loop.hpp>\n\n#include <chrono>\n#include <functional>\n#include <memory>\n\nnamespace oxenmq\n{\n    class OxenMQ;\n}\n\nnamespace llarp\n{\n\n    namespace link\n    {\n        struct Connection;\n        class Endpoint;\n        class Manager;\n    }  // namespace link\n\n    namespace rpc\n    {\n        class RPCServer;\n        class OxendRPC;\n    }  // namespace rpc\n\n    namespace consensus\n    {\n        class reachability_testing;\n    }  // namespace consensus\n\n    namespace quic = oxen::quic;\n\n    inline constexpr std::chrono::milliseconds ROUTER_TICK_INTERVAL{250ms};\n\n    inline constexpr std::chrono::milliseconds RC_UPDATE_INTERVAL{10min};\n\n    // Upon startup, relays will attempt to connect to this many nodes per second (divided into the\n    // number of ticks per second) to try to reach full mesh as quickly as possible.  Note that a\n    // single node restarting will full mesh almost instantly regardless of this setting (because\n    // all other nodes will want to re-connect to it), and so this mainly affects how quickly the\n    // network reestablishes after a significant number of nodes restart or regain connectivity all\n    // at once.\n    inline constexpr int RELAY_CONNECTS_PER_TICK{10};\n\n    // DISCUSS: ask tom and jason about this\n    // how big of a time skip before we reset network state\n    inline constexpr std::chrono::milliseconds NETWORK_RESET_SKIP_INTERVAL{1min};\n\n    inline constexpr std::chrono::milliseconds REPORT_STATS_INTERVAL{1min};\n    inline constexpr std::chrono::milliseconds REPORT_STATS_INTERVAL_DEBUG{10s};\n\n    inline constexpr std::chrono::milliseconds DECOMM_WARNING_INTERVAL{5min};\n\n    inline constexpr auto SERVICE_MANAGER_REPORT_INTERVAL{5s};\n\n    // The proportion of its target number of edge connections a client needs to have established\n    // connections with before we consider it \"connected\" to the network.  We allow less than full\n    // connectivity so that a single relay connection timeout doesn't stall connectivity for the\n    // full timeout duration, but generally want more than 1 so that we don't end up clustering all\n    // initial path builds through a single edge.\n    using CLIENT_CONNECTED_THRESHOLD = std::ratio<2, 3>;\n\n    class ContactDB;\n    class NodeDB;\n\n    class Router\n    {\n      public:\n        // Starts Lokinet immediately upon construction.\n        explicit Router(\n            Config conf,\n            std::shared_ptr<quic::Loop> loop,\n            std::shared_ptr<vpn::Platform> vpnPlatform,\n            std::promise<void> close_promise);\n\n        ~Router();\n\n      private:\n        // Internal functions called during construction:\n        void configure();\n        void start();\n\n        Config _config;\n        const std::shared_ptr<quic::Loop> _loop;\n\n        // path to write our self signed rc to\n        std::filesystem::path our_rc_file;\n\n        std::shared_ptr<oxenmq::OxenMQ> _omq{};\n\n        std::atomic<bool> _is_stopping{false};\n        std::atomic<bool> _is_running{false};\n\n        bool _is_connected{false};\n\n        // FIXME: we probably don't need two separate config options for this!\n        bool _is_exit_node{_config.network.allow_exit || _config.exit.exit_enabled};\n\n        // Not actually shared, but not available at all in non-full builds.\n        std::shared_ptr<consensus::reachability_testing> _router_testing;\n\n        // The actual network address we use for communications:\n        quic::Address _listen_address;\n\n        // The advertised public IP address for relays.  This is often the same as _listen_address,\n        // but can be different in exotic setups (e.g. where a known public IP is forwarded to an\n        // internal IP).  Always set for a relay.\n        std::optional<quic::Address> _public_address;\n\n        std::unique_ptr<handlers::SessionEndpoint> _session_endpoint;\n\n        std::unique_ptr<link::Manager> _link_manager;\n        link::Endpoint* _link_endpoint = nullptr;\n\n        // These are only created in full platform mode (not embedded clients)\n        std::shared_ptr<handlers::TunEPBase> _tun;\n        std::shared_ptr<vpn::Platform> _vpn;\n        std::shared_ptr<RoutePoker> _route_poker;\n\n        std::promise<void> _close_promise;\n\n      public:\n        // Tiny event loop + thread for handling disk I/O jobs without affecting other loops.  (It\n        // is up here because it must destroy after _node_db, which uses it.)\n        quic::Loop disk_loop;\n\n      private:\n        std::unique_ptr<ContactDB> _contact_db;\n        std::unique_ptr<NodeDB> _node_db;\n\n        std::shared_ptr<quic::Ticker> _loop_ticker;\n\n        // Might not be set/used, depending on the platform:\n        std::shared_ptr<quic::Ticker> _service_stat_ticker;\n        std::shared_ptr<quic::Ticker> _reachability_ticker;\n\n        std::shared_ptr<quic::Ticker> _gossip_ticker;\n\n        std::chrono::milliseconds _started_at;\n        std::chrono::milliseconds _last_stats_report{0s};\n        std::chrono::milliseconds _next_dereg_warning{time_now_ms() + 15s};\n\n        // Application callback(s) to fire as soon as we reach \"connected\" or \"disconnected\" status,\n        // which means when we have established our target number of edge connections or lost all\n        // edge connections, respectively.  Typically used as a \"ready-to-go\" callback during\n        // initialization.  The bool value indicates whether the callback is persistent (true) or\n        // one-time (false).  Note that callbacks are only called when the connected state changes:\n        // that is when we were disconnected and became connected, or were connected and became\n        // disconnected.\n        std::list<std::pair<std::function<void()>, bool>> _on_connected, _on_disconnected;\n\n        // These aren't actually shared, but we unique_ptr requires destructor visibility, which\n        // embedded-only clients won't have as they don't compile any RPC code.\n        std::shared_ptr<rpc::RPCServer> _rpc_server;\n        std::shared_ptr<rpc::OxendRPC> _oxend;\n\n        Profiling _router_profiling;\n\n        bool should_report_stats(std::chrono::milliseconds now) const;\n\n        std::string _stats_line(std::chrono::milliseconds now) const;\n\n        void report_stats();\n\n        bool insufficient_peers() const;\n\n        void init_logging();\n\n        void process_config();\n\n        void _relay_tick(std::chrono::milliseconds now);\n\n        void _client_tick(std::chrono::milliseconds now);\n\n        void tick();\n\n        void start_tickers();\n\n      public:\n        path::PathContext path_context{*this};\n        path::BuildStats path_builds{};\n        KeyManager key_manager;\n\n        const bool is_service_node{_config.router.is_relay};\n\n        bool is_fully_meshed() const;\n\n        const std::shared_ptr<handlers::TunEPBase>& tun_endpoint() { return _tun; }\n\n        // Returns the net Platform pointer, or nullptr if this is an embedded client.\n        const llarp::net::Platform* net() const;\n\n        const std::shared_ptr<vpn::Platform>& vpn_platform() const { return _vpn; }\n\n        handlers::SessionEndpoint& session_endpoint() { return *_session_endpoint; }\n        const handlers::SessionEndpoint& session_endpoint() const { return *_session_endpoint; }\n\n        link::Manager& link_manager() { return *_link_manager; }\n        const link::Manager& link_manager() const { return *_link_manager; }\n        link::Endpoint& link_endpoint()\n        {\n            assert(_link_endpoint);\n            return *_link_endpoint;\n        }\n        const link::Endpoint& link_endpoint() const\n        {\n            assert(_link_endpoint);\n            return *_link_endpoint;\n        }\n\n        const Config& config() const { return _config; }\n\n        ContactDB& contact_db()\n        {\n            assert(_contact_db);\n            return *_contact_db;\n        }\n        const ContactDB& contact_db() const\n        {\n            assert(_contact_db);\n            return *_contact_db;\n        }\n\n        NodeDB& node_db()\n        {\n            assert(_node_db);\n            return *_node_db;\n        }\n        const NodeDB& node_db() const\n        {\n            assert(_node_db);\n            return *_node_db;\n        }\n\n        NetID netid() const { return _config.router.net_id; }\n\n        bool embedded() const { return _config.embedded(); }\n\n        oxenmq::OxenMQ* omq() { return _omq.get(); }\n        const oxenmq::OxenMQ* omq() const { return _omq.get(); }\n\n        rpc::OxendRPC* oxend() const { return _oxend.get(); }\n\n        const Ed25519SecretKey& secret_key() const { return key_manager.secret_key; }\n        const RouterID& id() const { return key_manager.router_id(); }\n\n        Profiling& router_profiling() { return _router_profiling; }\n\n        quic::Loop& loop{*_loop};\n\n        // If this router is not a registered service node, does nothing.  Otherwise this regenerate\n        // the RC for this router, add it to the nodedb, saves it to disk, and gossips it.\n        void regenerate_rc();\n\n        const quic::Address& listen_addr() const { return _listen_address; }\n\n        // Returns the relay's advertised public address.  MUST NOT BE CALLED ON A CLIENT INSTANCE!\n        const quic::Address& public_addr() const\n        {\n            assert(_public_address);\n            return *_public_address;\n        }\n\n        nlohmann::json ExtractStatus() const;\n\n        nlohmann::json ExtractSummaryStatus() const;\n\n        /// return true if we a registered service node (either active or decommissioned).\n        bool appears_registered() const;\n\n        std::chrono::milliseconds Uptime() const;\n\n        std::chrono::milliseconds _last_tick;\n\n        std::function<void(void)> _router_close_cb;\n\n        void set_router_close_cb(std::function<void(void)> hook) { _router_close_cb = hook; }\n\n        bool looks_alive() const { return llarp::time_now_ms() - _last_tick <= 30s; }\n\n        // RoutePoker& route_poker() { return *_route_poker; }\n        // const RoutePoker& route_poker() const { return *_route_poker; }\n\n        std::string status_line();\n\n        // Returns the client connectivity status: we enter \"connected\" state once the target number\n        // of edge router connections is reached, and we lose connected state when we lose all edge\n        // connections.  Application code can monitor this state by setting callbacks via\n        // `on_connected`/`on_disconnected`.\n        bool is_connected() const;\n\n        // Adds an application callback to invoke when the connectivity state changes to\n        // \"connected\".  If the state is already connected when this is called, the callback will be\n        // invoked immediately.  If `persistent` is true then the callback will be stored and called\n        // again if the state leaves and re-enters the connected state.\n        void on_connected(std::function<void()> callback, bool persistent);\n\n        // Like `is_connected`, but fires on disconnections.\n        void on_disconnected(std::function<void()> callback, bool persistent);\n\n        // Internal method: called from link::Endpoint to re-check and possibly change connected\n        // state when a client edge connection is established or lost.\n        void on_edge_conn_change();\n\n        // Called when we get a relay testing ping to pass through to the router tester so that it\n        // can warn if we haven't received pings in a long time.\n        void on_test_ping();\n\n        bool is_running() const { return _is_running; }\n\n        bool is_stopping() const { return _is_stopping; }\n\n        bool is_exit_node() const;\n\n        std::optional<std::string> OxendErrorState() const;\n\n        void close();\n\n        /// stop running the router logic gracefully\n        void stop();\n\n        void fetch_snode_keys();\n\n        void teardown();\n    };\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/rpc/json_binary_proxy.cpp",
    "content": "#include \"json_binary_proxy.hpp\"\n\n#include <oxenc/base64.h>\n#include <oxenc/hex.h>\n\nusing namespace std::literals;\n\nnamespace llarp::rpc\n{\n\n    void load_binary_parameter_impl(std::string_view bytes, size_t raw_size, bool allow_raw, uint8_t* val_data)\n    {\n        if (allow_raw && bytes.size() == raw_size)\n        {\n            std::memcpy(val_data, bytes.data(), bytes.size());\n            return;\n        }\n        else if (bytes.size() == raw_size * 2)\n        {\n            if (oxenc::is_hex(bytes))\n            {\n                oxenc::from_hex(bytes.begin(), bytes.end(), val_data);\n                return;\n            }\n        }\n        else\n        {\n            const size_t b64_padded = (raw_size + 2) / 3 * 4;\n            const size_t b64_padding = raw_size % 3 == 1 ? 2 : raw_size % 3 == 2 ? 1 : 0;\n            const size_t b64_unpadded = b64_padded - b64_padding;\n            const std::string_view b64_padding_string = b64_padding == 2 ? \"==\"sv : b64_padding == 1 ? \"=\"sv : \"\"sv;\n            if (bytes.size() == b64_unpadded\n                || (b64_padding > 0 && bytes.size() == b64_padded && bytes.substr(b64_unpadded) == b64_padding_string))\n            {\n                if (oxenc::is_base64(bytes))\n                {\n                    oxenc::from_base64(bytes.begin(), bytes.end(), val_data);\n                    return;\n                }\n            }\n        }\n\n        throw std::runtime_error{\"Invalid binary value: unexpected size and/or encoding\"};\n    }\n\n    nlohmann::json& json_binary_proxy::operator=(std::string_view binary_data)\n    {\n        switch (format)\n        {\n            case fmt::bt:\n                return e = binary_data;\n            case fmt::hex:\n                return e = oxenc::to_hex(binary_data);\n            case fmt::base64:\n                return e = oxenc::to_base64(binary_data);\n        }\n        throw std::runtime_error{\"Internal error: invalid binary encoding\"};\n    }\n\n}  // namespace llarp::rpc\n"
  },
  {
    "path": "llarp/rpc/json_binary_proxy.hpp",
    "content": "#pragma once\n\n#include <nlohmann/json.hpp>\n\n#include <string_view>\n#include <unordered_set>\n\nnamespace llarp::rpc\n{\n\n    // Binary types that we support for rpc input/output.  For json, these must be specified as hex\n    // or base64; for bt-encoded requests these can be accepted as binary, hex, or base64.\n    template <typename T>\n    inline constexpr bool json_is_binary = false;\n\n    template <typename T>\n    inline constexpr bool json_is_binary_container = false;\n    template <typename T>\n    inline constexpr bool json_is_binary_container<std::vector<T>> = json_is_binary<T>;\n    template <typename T>\n    inline constexpr bool json_is_binary_container<std::unordered_set<T>> = json_is_binary<T>;\n\n    // De-referencing wrappers around the above:\n    template <typename T>\n    inline constexpr bool json_is_binary<const T&> = json_is_binary<T>;\n    template <typename T>\n    inline constexpr bool json_is_binary<T&&> = json_is_binary<T>;\n    template <typename T>\n    inline constexpr bool json_is_binary_container<const T&> = json_is_binary_container<T>;\n    template <typename T>\n    inline constexpr bool json_is_binary_container<T&&> = json_is_binary_container<T>;\n\n    void load_binary_parameter_impl(std::string_view bytes, size_t raw_size, bool allow_raw, uint8_t* val_data);\n\n    // Loads a binary value from a string_view which may contain hex, base64, and (optionally) raw\n    // bytes.\n    template <typename T, typename = std::enable_if_t<json_is_binary<T>>>\n    void load_binary_parameter(std::string_view bytes, bool allow_raw, T& val)\n    {\n        load_binary_parameter_impl(bytes, sizeof(T), allow_raw, reinterpret_cast<uint8_t*>(&val));\n    }\n\n    // Wrapper around a nlohmann::json that assigns a binary value either as binary (for\n    // bt-encoding); or as hex or base64 (for json-encoding).\n    class json_binary_proxy\n    {\n      public:\n        nlohmann::json& e;\n        enum class fmt\n        {\n            bt,\n            hex,\n            base64\n        } format;\n        explicit json_binary_proxy(nlohmann::json& elem, fmt format) : e{elem}, format{format} {}\n        json_binary_proxy() = delete;\n\n        json_binary_proxy(const json_binary_proxy&) = default;\n        json_binary_proxy(json_binary_proxy&&) = default;\n\n        /// Dereferencing a proxy element accesses the underlying nlohmann::json\n        nlohmann::json& operator*() { return e; }\n        nlohmann::json* operator->() { return &e; }\n\n        /// Descends into the json object, returning a new binary value proxy around the child\n        /// element.\n        template <typename T>\n        json_binary_proxy operator[](T&& key)\n        {\n            return json_binary_proxy{e[std::forward<T>(key)], format};\n        }\n\n        /// Returns a binary value proxy around the first/last element (requires an underlying list)\n        json_binary_proxy front() { return json_binary_proxy{e.front(), format}; }\n        json_binary_proxy back() { return json_binary_proxy{e.back(), format}; }\n\n        /// Assigns binary data from a string_view/string/etc.\n        nlohmann::json& operator=(std::string_view binary_data);\n\n        /// Assigns binary data from a string_view over a 1-byte, non-char type (e.g. unsigned char\n        /// or uint8_t).\n        template <typename Char, std::enable_if_t<sizeof(Char) == 1 && !std::is_same_v<Char, char>, int> = 0>\n        nlohmann::json& operator=(std::basic_string_view<Char> binary_data)\n        {\n            return *this = std::string_view{reinterpret_cast<const char*>(binary_data.data()), binary_data.size()};\n        }\n\n        /// Takes a trivial, no-padding data structure (e.g. a crypto::hash) as the value and dumps\n        /// its contents as the binary value.\n        template <typename T, std::enable_if_t<json_is_binary<T>, int> = 0>\n        nlohmann::json& operator=(const T& val)\n        {\n            return *this = std::string_view{reinterpret_cast<const char*>(&val), sizeof(val)};\n        }\n\n        /// Takes a vector of some json_binary_proxy-assignable type and builds an array by\n        /// assigning each one into a new array of binary values.\n        template <typename T, std::enable_if_t<json_is_binary_container<T>, int> = 0>\n        nlohmann::json& operator=(const T& vals)\n        {\n            auto a = nlohmann::json::array();\n            for (auto& val : vals)\n                json_binary_proxy{a.emplace_back(), format} = val;\n            return e = std::move(a);\n        }\n        /// Emplaces a new nlohman::json to the end of an underlying list and returns a\n        /// json_binary_proxy wrapping it.\n        ///\n        /// Example:\n        ///\n        ///     auto child = wrappedelem.emplace_back({\"key1\": 1}, {\"key2\": 2});\n        ///     child[\"binary-key\"] = some_binary_thing;\n        template <typename... Args>\n        json_binary_proxy emplace_back(Args&&... args)\n        {\n            return json_binary_proxy{e.emplace_back(std::forward<Args>(args)...), format};\n        }\n\n        /// Adds an element to an underlying list, then copies or moves the given argument onto it\n        /// via json_binary_proxy assignment.\n        template <typename T>\n        void push_back(T&& val)\n        {\n            emplace_back() = std::forward<T>(val);\n        }\n    };\n\n}  // namespace llarp::rpc\n"
  },
  {
    "path": "llarp/rpc/json_bt.hpp",
    "content": "#pragma once\n\n#include <nlohmann/json.hpp>\n#include <oxenc/bt_value.h>\n\nusing nlohmann::json;\n\nnamespace llarp::rpc\n{\n\n    inline oxenc::bt_value json_to_bt(json&& j)\n    {\n        if (j.is_object())\n        {\n            oxenc::bt_dict res;\n            for (auto& [k, v] : j.items())\n            {\n                if (v.is_null())\n                    continue;  // skip k-v pairs with a null v (for other nulls we fail).\n                res[k] = json_to_bt(std::move(v));\n            }\n            return res;\n        }\n        if (j.is_array())\n        {\n            oxenc::bt_list res;\n            for (auto& v : j)\n                res.push_back(json_to_bt(std::move(v)));\n            return res;\n        }\n        if (j.is_string())\n        {\n            return std::move(j.get_ref<std::string&>());\n        }\n        if (j.is_boolean())\n            return j.get<bool>() ? 1 : 0;\n        if (j.is_number_unsigned())\n            return j.get<uint64_t>();\n        if (j.is_number_integer())\n            return j.get<int64_t>();\n        throw std::domain_error{\"internal error: encountered some unhandled/invalid type in json-to-bt translation\"};\n    }\n\n}  // namespace llarp::rpc\n"
  },
  {
    "path": "llarp/rpc/json_conversions.cpp",
    "content": "#include \"json_conversions.hpp\"\n\n#include <nlohmann/json.hpp>\n\nnamespace llarp\n{\n    static auto logcat = log::Cat(\"RPC-conversions\");\n\n    void to_json(nlohmann::json& j, const ipv4_net& ipr) { j = ipr.to_string(); }\n\n    void from_json(const nlohmann::json& j, ipv4_net& ipr)\n    {\n        try\n        {\n            ipr = parse_ipv4_net(j.get<std::string_view>());\n        }\n        catch (const std::exception& e)\n        {\n            log::error(logcat, \"Failed to parse ipv4 network from json: {}\", e.what());\n            throw;\n        }\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/rpc/json_conversions.hpp",
    "content": "#pragma once\n\n#include \"json_binary_proxy.hpp\"\n\n#include <llarp/address/ip_range.hpp>\n#include <llarp/util/logging.hpp>\n\n#include <nlohmann/json_fwd.hpp>\n\nnamespace llarp\n{\n    void to_json(nlohmann::json& j, const ipv4_net& ipr);\n    void from_json(const nlohmann::json& j, ipv4_net& ipr);\n}  // namespace llarp\n\nnamespace nlohmann\n{\n    // Specializations of binary types for deserialization; when receiving these from json we expect\n    // them encoded in hex or base64.  These may *not* be used for serialization, and will throw if\n    // so invoked; for serialization you need to use RPC_COMMAND::response_hex (or _b64) instead.\n    template <typename T>\n    struct adl_serializer<T, std::enable_if_t<llarp::rpc::json_is_binary<T>>>\n    {\n        static_assert(std::is_trivially_copyable_v<T> && std::has_unique_object_representations_v<T>);\n\n        static void to_json(json&, const T&)\n        {\n            throw std::logic_error{\"Internal error: binary types are not directly serializable\"};\n        }\n        static void from_json(const json& j, T& val)\n        {\n            llarp::rpc::load_binary_parameter(j.get<std::string_view>(), false /*no raw*/, val);\n        }\n    };\n\n}  // namespace nlohmann\n"
  },
  {
    "path": "llarp/rpc/oxend_rpc.cpp",
    "content": "#include \"oxend_rpc.hpp\"\n\n#include <llarp/nodedb.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/util/logging.hpp>\n\n#include <nlohmann/json.hpp>\n#include <oxenc/hex.h>\n\n#include <exception>\n#include <stdexcept>\n\nnamespace llarp::rpc\n{\n    static auto logcat = log::Cat(\"rpc.oxend\");\n\n    OxendRPC::OxendRPC(oxenmq::OxenMQ& omq, Router& r) : _omq{omq}, _router{r}\n    {\n        // new block handler\n        _omq.add_category(\"notify\", oxenmq::Access{oxenmq::AuthLevel::none})\n            .add_command(\"block\", [this](oxenmq::Message& m) { handle_new_block(m); });\n\n        // TODO: proper auth here\n        auto lokidCategory = _omq.add_category(\"lokid\", oxenmq::Access{oxenmq::AuthLevel::none});\n        _is_updating_list = false;\n    }\n\n    void OxendRPC::connect_async(oxenmq::address url)\n    {\n        if (not _router.is_service_node)\n        {\n            throw std::runtime_error(\"we cannot talk to lokid while not a service node\");\n        }\n\n        log::info(logcat, \"RPC client connecting to oxend at {}\", url.full_address());\n\n        _conn = _omq.connect_remote(\n            url,\n            [](oxenmq::ConnectionID) {},\n            [this, url](oxenmq::ConnectionID, std::string_view f) {\n                log::info(logcat, \"Failed to connect to oxend at {}\", f);\n                _router.loop.call([this, url]() { connect_async(url); });\n            });\n    }\n\n    void OxendRPC::command(std::string_view cmd)\n    {\n        log::debug(logcat, \"Oxend command: {}\", cmd);\n        _omq.send(*_conn, std::move(cmd));\n    }\n\n    void OxendRPC::handle_new_block(oxenmq::Message& msg)\n    {\n        if (msg.data.size() != 2)\n        {\n            log::error(\n                logcat,\n                \"Received invalid new block notification with {} parts (expected 2); not updating service node list!\",\n                msg.data.size());\n\n            return;  // bail\n        }\n        try\n        {\n            _block_height = std::stoll(std::string{msg.data[0]});\n        }\n        catch (std::exception& ex)\n        {\n            log::error(logcat, \"Bad block height: {}\", ex.what());\n\n            return;  // bail\n        }\n\n        log::trace(logcat, \"new block at height {}\", _block_height);\n        update_service_node_list();\n    }\n\n    void OxendRPC::update_service_node_list(std::shared_ptr<std::promise<void>> on_updated)\n    {\n        if (_is_updating_list.exchange(true))\n        {\n            assert(!on_updated);  // When using a promise it should be the first call\n            return;               // update already in progress\n        }\n\n        nlohmann::json req{{\"fields\", {\"pubkey_ed25519\", \"block_hash\"}}};\n        if (!_last_hash_update.empty())\n            req[\"poll_block_hash\"] = _last_hash_update;\n\n        request(\n            \"rpc.get_service_nodes\",\n            [this, on_updated = std::move(on_updated)](bool success, std::vector<std::string> data) mutable {\n                std::string fail_msg;\n                if (not success)\n                    fail_msg = \"Failed to update service node list\";\n                else if (data.size() < 2)\n                    fail_msg = \"Oxend gave empty reply for service node list\";\n                else\n                {\n                    try\n                    {\n                        auto json = nlohmann::json::parse(std::move(data[1]));\n                        if (json.at(\"status\") != \"OK\")\n                            throw std::runtime_error{\"get_service_nodes did not return 'OK' status\"};\n                        if (auto it = json.find(\"unchanged\"); it != json.end() and it->is_boolean() and it->get<bool>())\n                            log::trace(logcat, \"service node list unchanged\");\n                        else\n                        {\n                            handle_new_service_node_list(json.at(\"service_node_states\"));\n                            if (auto it = json.find(\"block_hash\"); it != json.end() and it->is_string())\n                                _last_hash_update = it->get<std::string>();\n                            else\n                                _last_hash_update.clear();\n                            if (on_updated)\n                                on_updated->set_value();\n                        }\n                    }\n                    catch (const std::exception& ex)\n                    {\n                        fail_msg = fmt::format(\"Failed to process service node list: {}\", ex.what());\n                        log::error(logcat, \"{}\", fail_msg);\n                    }\n                }\n\n                // set down here so that the 1) we don't start updating until we're completely\n                // finished with the previous update; and 2) so that m_UpdatingList also guards\n                // m_LastUpdateHash\n                _is_updating_list = false;\n\n                if (!fail_msg.empty() && on_updated)\n                {\n                    try\n                    {\n                        throw std::runtime_error{fail_msg};\n                    }\n                    catch (const std::runtime_error& e)\n                    {\n                        on_updated->set_exception(std::current_exception());\n                    }\n                }\n            },\n            req.dump());\n    }\n\n    void OxendRPC::ping()\n    {\n        // send a ping\n        auto pk = _router.id();\n\n        nlohmann::json payload = {\n            {\"pubkey_ed25519\", oxenc::to_hex(pk.begin(), pk.end())},\n            {\"version\", {LOKINET_VERSION[0], LOKINET_VERSION[1], LOKINET_VERSION[2]}}};\n\n        if (auto err = _router.OxendErrorState())\n            payload[\"error\"] = *err;\n\n        request(\n            \"admin.lokinet_ping\",\n            [](bool success, std::vector<std::string> /* data */) {\n                log::debug(logcat, \"Received response for ping. Successful: {}\", success);\n            },\n            payload.dump());\n\n        // subscribe to block updates\n        request(\"sub.block\", [](bool success, std::vector<std::string> data) {\n            if (data.empty() or not success)\n                log::error(logcat, \"Failed to subscribe to new blocks\");\n            else\n                log::debug(logcat, \"Subscribed to new blocks: {}\", data[0]);\n        });\n\n        // Trigger an update on a regular timer as well in case we missed a block notify for\n        // some reason (e.g. oxend restarts and loses the subscription); we poll using the last\n        // known hash so that the poll is very cheap (basically empty) if the block hasn't\n        // advanced.\n        update_service_node_list();\n    }\n\n    void OxendRPC::start_pings()\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        log::info(logcat, \"Starting OxendRPC ping ticker...\");\n        ping();\n        _ping_ticker = _router.loop.call_every(PING_INTERVAL, [this] { ping(); });\n    }\n\n    void OxendRPC::handle_new_service_node_list(const nlohmann::json& j)\n    {\n        std::unordered_set<RouterID> registered;\n        if (not j.is_array())\n            throw std::runtime_error{\"Invalid service node list: expected array of service node states\"};\n\n        for (auto& snode : j)\n        {\n            const auto ed_itr = snode.find(\"pubkey_ed25519\");\n            if (ed_itr == snode.end() or not ed_itr->is_string())\n                continue;\n\n            RouterID rid;\n            if (rid.FromHex(ed_itr->get<std::string_view>()))\n                registered.insert(rid);\n        }\n\n        if (registered.empty())\n        {\n            log::warning(logcat, \"Ignoring empty/invalid service node list received from oxend\");\n            return;\n        }\n\n        // Thread-safe; doesn't need to be in a loop call:\n        _router.node_db().set_registered_relays(std::move(registered));\n    }\n\n    void OxendRPC::inform_connection(RouterID router, bool success)\n    {\n        _router.loop.call([router, success, this]() {\n            const nlohmann::json req = {{\"passed\", success}, {\"pubkey\", router.ToHex()}, {\"type\", \"lokinet\"}};\n            request(\n                \"admin.report_peer_status\",\n                [](bool success, std::vector<std::string>) {\n                    if (not success)\n                    {\n                        log::error(logcat, \"Failed to report connection status to oxend\");\n                        return;\n                    }\n                    log::debug(logcat, \"Reported connection status to core\");\n                },\n                req.dump());\n        });\n    }\n\n    Ed25519SecretKey OxendRPC::obtain_identity_key()\n    {\n        std::promise<Ed25519SecretKey> promise;\n        request(\"admin.get_service_privkeys\", [&promise](bool success, std::vector<std::string> data) {\n            try\n            {\n                if (not success)\n                    throw std::runtime_error(\"Failed to get private key request\");\n\n                if (data.empty() or data.size() < 2)\n                    throw std::runtime_error(\"Failed to get private key request: data empty\");\n\n                const auto j = nlohmann::json::parse(data[1]);\n                Ed25519SecretKey k;\n\n                if (not k.FromHex(j.at(\"service_node_ed25519_privkey\").get<std::string>()))\n                    throw std::runtime_error(\"failed to parse private key\");\n\n                promise.set_value(k);\n            }\n            catch (const std::exception& e)\n            {\n                log::warning(logcat, \"Caught exception while trying to request admin keys: {}\", e.what());\n                promise.set_exception(std::current_exception());\n            }\n            catch (...)\n            {\n                log::warning(logcat, \"Caught non-standard exception while trying to request admin keys\");\n                promise.set_exception(std::current_exception());\n            }\n        });\n\n        auto ftr = promise.get_future();\n        return ftr.get();\n    }\n\n    void OxendRPC::lookup_sns_hash(\n        std::string namehash, std::function<void(std::optional<EncryptedSNSRecord>)> resultHandler)\n    {\n        log::debug(logcat, \"Looking Up ONS NameHash {}\", namehash);\n        const nlohmann::json req{{\"type\", 2}, {\"name_hash\", oxenc::to_hex(namehash)}};\n        request(\n            \"rpc.sns_resolve\",\n            [this, resultHandler](bool success, std::vector<std::string> data) {\n                std::optional<EncryptedSNSRecord> maybe = std::nullopt;\n                if (success)\n                {\n                    try\n                    {\n                        EncryptedSNSRecord result;\n                        const auto j = nlohmann::json::parse(data[1]);\n                        j.dump();\n                        result.ciphertext = oxenc::from_hex(j[\"encrypted_value\"].get<std::string>());\n                        const auto nonce = oxenc::from_hex(j[\"nonce\"].get<std::string>());\n                        if (nonce.size() != result.nonce.size())\n                        {\n                            throw std::invalid_argument{\n                                fmt::format(\"nonce size mismatch: {} != {}\", nonce.size(), result.nonce.size())};\n                        }\n\n                        std::copy_n(nonce.data(), nonce.size(), result.nonce.data());\n                        maybe = result;\n                    }\n                    catch (std::exception& ex)\n                    {\n                        log::error(logcat, \"Failed to parse response from ONS lookup: {}\", ex.what());\n                    }\n                }\n                _router.loop.call([resultHandler, maybe = std::move(maybe)]() { resultHandler(std::move(maybe)); });\n            },\n            req.dump());\n    }\n\n}  // namespace llarp::rpc\n"
  },
  {
    "path": "llarp/rpc/oxend_rpc.hpp",
    "content": "#pragma once\n\n#include <llarp/contact/router_id.hpp>\n#include <llarp/contact/sns.hpp>\n#include <llarp/crypto/types.hpp>\n#include <llarp/util/logging.hpp>\n\n#include <oxenmq/address.h>\n#include <oxenmq/oxenmq.h>\n\nnamespace oxen::quic\n{\n    struct Ticker;\n}\nnamespace llarp\n{\n    class Router;\n}\n\nnamespace llarp::rpc\n{\n    inline constexpr auto PING_INTERVAL{30s};\n\n    /// OxenMQ RPC client for a relay to talk to its oxend to obtain info about and report on\n    /// the service node network.\n    class OxendRPC\n    {\n      public:\n        OxendRPC(oxenmq::OxenMQ& omq, Router& r);\n\n        /// Connect to lokid async\n        void connect_async(oxenmq::address url);\n\n        /// blocking request identity secret key from lokid\n        /// throws on failure\n        Ed25519SecretKey obtain_identity_key();\n\n        /// get what the current block height is according to oxend\n        uint64_t block_height() const { return _block_height; }\n\n        void lookup_sns_hash(\n            std::string namehash, std::function<void(std::optional<EncryptedSNSRecord>)> resultHandler);\n\n        /// inform that if connected to a router successfully\n        void inform_connection(RouterID router, bool success);\n\n        void start_pings();\n\n        /// triggers a service node list refresh from oxend; thread-safe and will do nothing if\n        /// an update is already in progress.  The promise is for router.cpp to attempt a\n        /// synchronous update on startup, and should not be used otherwise.\n        void update_service_node_list(std::shared_ptr<std::promise<void>> on_update = nullptr);\n\n      private:\n        void ping();\n\n        /// do a lmq command on the current connection\n        void command(std::string_view cmd);\n\n        template <typename HandlerFunc_t, typename Args_t>\n        void request(std::string_view cmd, HandlerFunc_t func, const Args_t& args)\n        {\n            _omq.request(*_conn, std::move(cmd), std::move(func), args);\n        }\n\n        template <typename HandlerFunc_t>\n        void request(std::string_view cmd, HandlerFunc_t func)\n        {\n            _omq.request(*_conn, std::move(cmd), std::move(func));\n        }\n\n        // Handles a service node list update; takes the \"service_node_states\" object of an\n        // oxend \"get_service_nodes\" rpc request.\n        void handle_new_service_node_list(const nlohmann::json& json);\n\n        // Handles notification of a new block\n        void handle_new_block(oxenmq::Message& msg);\n\n        std::shared_ptr<oxen::quic::Ticker> _ping_ticker;\n\n        std::optional<oxenmq::ConnectionID> _conn;\n        oxenmq::OxenMQ& _omq;\n\n        Router& _router;\n        std::atomic<bool> _is_updating_list;\n        std::string _last_hash_update;\n\n        std::unordered_map<RouterID, PubKey> _key_map;\n\n        uint64_t _block_height;\n    };\n\n}  // namespace llarp::rpc\n"
  },
  {
    "path": "llarp/rpc/param_parser.hpp",
    "content": "#pragma once\n\n#include \"json_binary_proxy.hpp\"\n#include \"json_bt.hpp\"\n#include \"json_conversions.hpp\"\n\n#include <nlohmann/json.hpp>\n#include <oxenc/bt_serialize.h>\n\n#include <optional>\n#include <stdexcept>\n#include <string>\n#include <unordered_map>\n\nnamespace llarp::rpc\n{\n    using json_range = std::pair<nlohmann::json::const_iterator, nlohmann::json::const_iterator>;\n    using rpc_input = std::variant<std::monostate, nlohmann::json, oxenc::bt_dict_consumer>;\n\n    // Checks that key names are given in ascending order\n    template <typename... Ignore>\n    void check_ascending_names(std::string_view name1, std::string_view name2, const Ignore&...)\n    {\n        if (!(name2 > name1))\n            throw std::runtime_error{\"Internal error: request values must be retrieved in ascending order\"};\n    }\n\n    // Wrapper around a reference for get_values that is used to indicate that the value is\n    // required, in which case an exception will be raised if the value is not found.  Usage:\n    //\n    //     int a_optional = 0, b_required;\n    //     get_values(input,\n    //         \"a\", a_optional,\n    //         \"b\", required{b_required},\n    //         // ...\n    //     );\n    template <typename T>\n    struct required\n    {\n        T& value;\n        required(T& ref) : value{ref} {}\n    };\n    template <typename T>\n    constexpr bool is_required_wrapper = false;\n    template <typename T>\n    constexpr bool is_required_wrapper<required<T>> = true;\n\n    template <typename T>\n    constexpr bool is_std_optional = false;\n    template <typename T>\n    constexpr bool is_std_optional<std::optional<T>> = true;\n\n    // Wrapper around a reference for get_values that adds special handling to act as if the value\n    // was not given at all if the value is given as an empty string.  This sucks, but is necessary\n    // for backwards compatibility (especially with wallet2 clients).\n    //\n    // Usage:\n    //\n    //     std::string x;\n    //     get_values(input,\n    //         \"x\", ignore_empty_string{x},\n    //         // ...\n    //     );\n    template <typename T>\n    struct ignore_empty_string\n    {\n        T& value;\n        ignore_empty_string(T& ref) : value{ref} {}\n\n        bool should_ignore(oxenc::bt_dict_consumer& d)\n        {\n            if (d.is_string())\n            {\n                auto d2{d};  // Copy because we want to leave d intact\n                if (d2.consume_string_view().empty())\n                    return true;\n            }\n            return false;\n        }\n\n        bool should_ignore(json_range& it_range)\n        {\n            auto& e = *it_range.first;\n            return (e.is_string() && e.get<std::string_view>().empty());\n        }\n    };\n\n    template <typename T>\n    constexpr bool is_ignore_empty_string_wrapper = false;\n    template <typename T>\n    constexpr bool is_ignore_empty_string_wrapper<ignore_empty_string<T>> = true;\n\n    // Advances the dict consumer to the first element >= the given name.  Returns true if found,\n    // false if it advanced beyond the requested name.  This is exactly the same as\n    // `d.skip_until(name)`, but is here so we can also overload an equivalent function for json\n    // iteration.\n    inline bool skip_until(oxenc::bt_dict_consumer& d, std::string_view name) { return d.skip_until(name); }\n    // Equivalent to the above but for a json object iterator.\n    inline bool skip_until(json_range& it_range, std::string_view name)\n    {\n        auto& [it, end] = it_range;\n        while (it != end && it.key() < name)\n            ++it;\n        return it != end && it.key() == name;\n    }\n\n    // List types that are expandable; for these we emplace_back for each element of the input\n    template <typename T>\n    constexpr bool is_expandable_list = false;\n    template <typename T>\n    constexpr bool is_expandable_list<std::vector<T>> = true;\n\n    // Fixed size elements: tuples, pairs, and std::array's; we accept list input as long as the\n    // list length matches exactly.\n    template <typename T>\n    constexpr bool is_tuple_like = false;\n    template <typename T, size_t N>\n    constexpr bool is_tuple_like<std::array<T, N>> = true;\n    template <typename S, typename T>\n    constexpr bool is_tuple_like<std::pair<S, T>> = true;\n    template <typename... T>\n    constexpr bool is_tuple_like<std::tuple<T...>> = true;\n\n    // True if T is a `std::unordered_map<std::string, ANYTHING...>`\n    template <typename T>\n    constexpr bool is_unordered_string_map = false;\n    template <typename... ValueEtc>\n    constexpr bool is_unordered_string_map<std::unordered_map<std::string, ValueEtc...>> = true;\n\n    template <typename TupleLike, size_t... Is>\n    void load_tuple_values(oxenc::bt_list_consumer&, TupleLike&, std::index_sequence<Is...>);\n\n    // Consumes the next value from the dict consumer into `val`\n    template <\n        typename BTConsumer,\n        typename T,\n        std::enable_if_t<\n            std::is_same_v<BTConsumer, oxenc::bt_dict_consumer> || std::is_same_v<BTConsumer, oxenc::bt_list_consumer>,\n            int> = 0>\n    void load_value(BTConsumer& c, T& target)\n    {\n        if constexpr (std::is_integral_v<T>)\n            target = c.template consume_integer<T>();\n        else if constexpr (std::is_same_v<T, std::string> || std::is_same_v<T, std::string_view>)\n            target = c.consume_string_view();\n        else if constexpr (llarp::rpc::json_is_binary<T>)\n            llarp::rpc::load_binary_parameter(c.consume_string_view(), true /*allow raw*/, target);\n        else if constexpr (is_expandable_list<T>)\n        {\n            auto lc = c.consume_list_consumer();\n            target.clear();\n            while (!lc.is_finished())\n                load_value(lc, target.emplace_back());\n        }\n        else if constexpr (is_tuple_like<T>)\n        {\n            auto lc = c.consume_list_consumer();\n            load_tuple_values(lc, target, std::make_index_sequence<std::tuple_size_v<T>>{});\n        }\n        else if constexpr (is_unordered_string_map<T>)\n        {\n            auto dc = c.consume_dict_consumer();\n            target.clear();\n            while (!dc.is_finished())\n                load_value(dc, target[std::string{dc.key()}]);\n        }\n        else\n            static_assert(std::is_same_v<T, void>, \"Unsupported load_value type\");\n    }\n\n    // Copies the next value from the json range into `val`, and advances the iterator.  Throws\n    // on unconvertible values.\n    template <typename T>\n    void load_value(json_range& range_itr, T& target)\n    {\n        auto& key = range_itr.first.key();\n        auto& current = *range_itr.first;  // value currently pointed to by range_itr.first\n        if constexpr (std::is_same_v<T, bool>)\n        {\n            if (current.is_boolean())\n                target = current.get<bool>();\n            else if (current.is_number_unsigned())\n            {\n                // Also accept 0 or 1 for bools (mainly to be compatible with bt-encoding which\n                // doesn't have a distinct bool type).\n                auto b = current.get<uint64_t>();\n                if (b <= 1)\n                    target = b;\n                else\n                    throw std::domain_error{\"Invalid value for '\" + key + \"': expected boolean\"};\n            }\n            else\n            {\n                throw std::domain_error{\"Invalid value for '\" + key + \"': expected boolean\"};\n            }\n        }\n        else if constexpr (std::is_unsigned_v<T>)\n        {\n            if (!current.is_number_unsigned())\n                throw std::domain_error{\"Invalid value for '\" + key + \"': non-negative value required\"};\n            auto i = current.get<uint64_t>();\n            if (sizeof(T) < sizeof(uint64_t) && i > std::numeric_limits<T>::max())\n                throw std::domain_error{\"Invalid value for '\" + key + \"': value too large\"};\n            target = i;\n        }\n        else if constexpr (std::is_integral_v<T>)\n        {\n            if (!current.is_number_integer())\n                throw std::domain_error{\"Invalid value for '\" + key + \"': value is not an integer\"};\n            auto i = current.get<int64_t>();\n            if (sizeof(T) < sizeof(int64_t))\n            {\n                if (i < std::numeric_limits<T>::lowest())\n                    throw std::domain_error{\"Invalid value for '\" + key + \"': negative value magnitude is too large\"};\n                if (i > std::numeric_limits<T>::max())\n                    throw std::domain_error{\"Invalid value for '\" + key + \"': value is too large\"};\n            }\n            target = i;\n        }\n        else if constexpr (std::is_same_v<T, std::string> || std::is_same_v<T, std::string_view>)\n        {\n            target = current.get<std::string_view>();\n        }\n        else if constexpr (\n            llarp::rpc::json_is_binary<T> || is_expandable_list<T> || is_tuple_like<T> || is_unordered_string_map<T>)\n        {\n            try\n            {\n                current.get_to(target);\n            }\n            catch (const std::exception& e)\n            {\n                throw std::domain_error{\"Invalid values in '\" + key + \"'\"};\n            }\n        }\n        else\n        {\n            static_assert(std::is_same_v<T, void>, \"Unsupported load type\");\n        }\n        ++range_itr.first;\n    }\n\n    template <typename TupleLike, size_t... Is>\n    void load_tuple_values(oxenc::bt_list_consumer& c, TupleLike& val, std::index_sequence<Is...>)\n    {\n        (load_value(c, std::get<Is>(val)), ...);\n    }\n\n    // Takes a json object iterator or bt_dict_consumer and loads the current value at the iterator.\n    // This calls itself recursively, if needed, to unwrap optional/required/ignore_empty_string\n    // wrappers.\n    template <typename In, typename T>\n    void load_curr_value(In& in, T& val)\n    {\n        if constexpr (is_required_wrapper<T>)\n        {\n            load_curr_value(in, val.value);\n        }\n        else if constexpr (is_ignore_empty_string_wrapper<T>)\n        {\n            if (!val.should_ignore(in))\n                load_curr_value(in, val.value);\n        }\n        else if constexpr (is_std_optional<T>)\n        {\n            load_curr_value(in, val.emplace());\n        }\n        else\n        {\n            load_value(in, val);\n        }\n    }\n\n    // Gets the next value from a json object iterator or bt_dict_consumer.  Leaves the iterator at\n    // the next value, i.e.  found + 1 if found, or the next greater value if not found.  (NB:\n    // nlohmann::json objects are backed by an *ordered* map and so both nlohmann iterators and\n    // bt_dict_consumer behave analogously here).\n    template <typename In, typename T>\n    void get_next_value(In& in, [[maybe_unused]] std::string_view name, T& val)\n    {\n        if constexpr (std::is_same_v<std::monostate, In>)\n            ;\n        else if (skip_until(in, name))\n            load_curr_value(in, val);\n        else if constexpr (is_required_wrapper<T>)\n            throw std::runtime_error{\"Required key '\" + std::string{name} + \"' not found\"};\n    }\n\n    // Accessor for simple, flat value retrieval from a json or bt_dict_consumer.  In the later\n    // case note that the given bt_dict_consumer will be advanced, so you *must* take care to\n    // process keys in order, both for the keys passed in here *and* for use before and after this\n    // call.\n    template <typename Input, typename T, typename... More>\n    void get_values(Input& in, std::string_view name, T&& val, More&&... more)\n    {\n        if constexpr (std::is_same_v<rpc_input, Input>)\n        {\n            if (auto* json_in = std::get_if<nlohmann::json>(&in))\n            {\n                json_range r{json_in->cbegin(), json_in->cend()};\n                get_values(r, name, val, std::forward<More>(more)...);\n            }\n            else if (auto* dict = std::get_if<oxenc::bt_dict_consumer>(&in))\n            {\n                get_values(*dict, name, val, std::forward<More>(more)...);\n            }\n            else\n            {\n                // A monostate indicates that no parameters field was provided at all\n                get_values(std::get<std::monostate>(in), name, val, std::forward<More>(more)...);\n            }\n        }\n        else if constexpr (std::is_same_v<std::string_view, Input>)\n        {\n            if (in.front() == 'd')\n            {\n                oxenc::bt_dict_consumer d{in};\n                get_values(d, name, val, std::forward<More>(more)...);\n            }\n            else\n            {\n                auto json_in = nlohmann::json::parse(in);\n                json_range r{json_in.cbegin(), json_in.cend()};\n                get_values(r, name, val, std::forward<More>(more)...);\n            }\n        }\n        else\n        {\n            static_assert(\n                std::is_same_v<json_range, Input> || std::is_same_v<oxenc::bt_dict_consumer, Input>\n                || std::is_same_v<std::monostate, Input>);\n            get_next_value(in, name, val);\n            if constexpr (sizeof...(More) > 0)\n            {\n                check_ascending_names(name, more...);\n                get_values(in, std::forward<More>(more)...);\n            }\n        }\n    }\n}  // namespace llarp::rpc\n"
  },
  {
    "path": "llarp/rpc/rpc_request.hpp",
    "content": "#pragma once\n\n#include \"json_bt.hpp\"\n#include \"rpc_request_decorators.hpp\"\n#include \"rpc_request_definitions.hpp\"\n#include \"rpc_request_parser.hpp\"\n#include \"rpc_server.hpp\"\n\n#include <llarp/config/config.hpp>\n#include <llarp/router/router.hpp>\n\n#include <oxen/log/omq_logger.hpp>\n#include <oxenmq/address.h>\n#include <oxenmq/oxenmq.h>\n\n#include <string_view>\n\nnamespace llarp::rpc\n{\n    using nlohmann::json;\n\n    template <typename RPC>\n    auto make_invoke()\n    {\n        return [](oxenmq::Message& m, RPCServer& server) {\n            EndpointHandler<RPC> handler{server, m.send_later()};\n            auto& rpc = handler.rpc;\n\n            if (m.data.size() > 1)\n            {\n                m.send_reply(nlohmann::json{\n                    {\"error\",\n                     \"Bad Request: RPC requests must have at most one data part (received {})\"_format(m.data.size())}}\n                                 .dump());\n                return;\n            }\n            // parsing input as bt or json\n            //    hand off to parse_request (overloaded versions)\n            try\n            {\n                if (m.data.empty() or m.data[0].empty())\n                {\n                    parse_request(rpc, nlohmann::json::object());\n                }\n                else if (m.data[0].front() == 'd')\n                {\n                    rpc.set_bt();\n                    parse_request(rpc, oxenc::bt_dict_consumer{m.data[0]});\n                }\n                else\n                {\n                    parse_request(rpc, nlohmann::json::parse(m.data[0]));\n                }\n            }\n            catch (const std::exception& e)\n            {\n                m.send_reply(nlohmann::json{{\"Failed to parse request parameters: \"s + e.what()}}.dump());\n                return;\n            }\n\n            if (not std::is_base_of_v<Immediate, RPC>)\n            {\n                server._router.loop.call_soon(std::move(handler));\n            }\n            else\n            {\n                handler();\n            }\n        };\n    }\n\n}  // namespace llarp::rpc\n"
  },
  {
    "path": "llarp/rpc/rpc_request_decorators.hpp",
    "content": "#pragma once\n\n#include \"json_binary_proxy.hpp\"\n#include \"json_bt.hpp\"\n\n#include <llarp/config/config.hpp>\n\n#include <nlohmann/json_fwd.hpp>\n#include <oxen/log/omq_logger.hpp>\n#include <oxenmq/address.h>\n#include <oxenmq/oxenmq.h>\n\n#include <string_view>\n\nnamespace tools\n{\n    //  Type wrapper that contains an arbitrary list of types.\n    template <typename...>\n    struct type_list\n    {};\n}  // namespace tools\n\nnamespace llarp::rpc\n{\n    //  Base class that all RPC requests will expand for each endpoint type\n    struct RPCRequest\n    {\n      private:\n        bool bt = false;\n\n      public:\n        //  Returns true if response is bt-encoded, and false for json\n        //  Note: do not set value\n        bool is_bt() const { return bt; }\n\n        //  Callable method to indicate request is bt-encoded\n        void set_bt()\n        {\n            bt = true;\n            response_b64.format = llarp::rpc::json_binary_proxy::fmt::bt;\n            response_hex.format = llarp::rpc::json_binary_proxy::fmt::bt;\n        }\n\n        //  Invoked if this.replier is still present. If it is \"stolen\" by endpoint (moved from\n        //  RPC struct), then endpoint handles sending reply\n        void send_response()\n        {\n            replier->reply(is_bt() ? oxenc::bt_serialize(json_to_bt(std::move(response))) : response.dump());\n        }\n\n        void send_response(nlohmann::json _response)\n        {\n            response = std::move(_response);\n            send_response();\n        }\n\n        //  Response Data:\n        //  bt-encoded are converted in real-time\n        //    - bool becomes 0 or 1\n        //    - key:value where value == null are omitted\n        //    - other nulls will raise an exception if found in json\n        //    - no doubles\n        //      - to store doubles: encode bt in endpoint-specific way\n        //    - binary strings will fail json serialization; caller must\n        //\n        //        std::string binary = some_binary_data();\n        //        request.response[\"binary_value\"] = is_bt ? binary : oxenmq::to_hex(binary)\n        //\n        nlohmann::json response;\n\n        //  Proxy Object:\n        //  Sets binary data in \"response\"\n        //    - if return type is json, encodes as hex\n        //    - if return type is bt, then binary is untouched\n        //\n        //  Usage:\n        //    std::string data = \"abc\";\n        //    request.response_hex[\"foo\"][\"bar\"] = data; // json: \"616263\", bt: \"abc\"\n        //\n        llarp::rpc::json_binary_proxy response_hex{response, llarp::rpc::json_binary_proxy::fmt::hex};\n\n        //  Proxy Object:\n        //  Encodes binary data as base_64 for json-encoded responses, leaves as binary for\n        //  bt-encoded responses\n        //\n        //    Usage:\n        //      std::string data = \"abc\"\n        //      request.response_b64[\"foo\"][\"bar\"] = data; json: \"YWJj\", bt: \"abc\"\n        //\n        llarp::rpc::json_binary_proxy response_b64{response, llarp::rpc::json_binary_proxy::fmt::base64};\n\n        //  The oxenmq deferred send object into which the response will be sent when the `invoke`\n        //  method returns.  If the response needs to happen later (i.e. not immediately after\n        //  `invoke` returns) then you should call `defer()` to extract and clear this and then send\n        //  the response via the returned DeferredSend object yourself.\n        std::optional<oxenmq::Message::DeferredSend> replier;\n\n        // Called to clear the current replier and return it.  After this call the automatic reply\n        // will not be generated; the caller is responsible for calling `->reply` on the returned\n        // optional itself.  This is typically used where a call has to be deferred, for example\n        // because it depends on some network response to build the reply.\n        oxenmq::Message::DeferredSend move()\n        {\n            auto r{std::move(*replier)};\n            replier.reset();\n            return r;\n        }\n    };\n\n    //  Tag types that are inherited to set RPC endpoint properties\n\n    //  RPC call wil take no input arguments\n    //    Parameter dict can be passed, but will be ignored\n    struct NoArgs : virtual RPCRequest\n    {};\n\n    // RPC call will be executed immediately\n    struct Immediate : virtual RPCRequest\n    {};\n\n}  // namespace llarp::rpc\n"
  },
  {
    "path": "llarp/rpc/rpc_request_definitions.hpp",
    "content": "#pragma once\n\n#include \"rpc_request_decorators.hpp\"\n\n#include <llarp/address/ip_range.hpp>\n#include <llarp/config/config.hpp>\n#include <llarp/router/route_poker.hpp>\n\n#include <oxen/log/omq_logger.hpp>\n#include <oxenmq/address.h>\n#include <oxenmq/oxenmq.h>\n\n#include <string_view>\n#include <unordered_map>\n#include <vector>\n\nnamespace llarp::rpc\n{\n    //  RPC: halt\n    //    Stops lokinet router\n    //\n    //  Inputs: none\n    //\n    struct Halt : NoArgs, Immediate\n    {\n        static constexpr auto name = \"halt\"sv;\n    };\n\n    //  RPC: version\n    //    Returns version and uptime information\n    //\n    //  Inputs: none\n    //\n    //  Returns: \"OK\"\n    //    \"uptime\"\n    //    \"version\"\n    //\n    struct Version : NoArgs, Immediate\n    {\n        static constexpr auto name = \"version\"sv;\n    };\n\n    //  RPC: status\n    //    Returns that current activity status of lokinet router\n    //    Calls router::extractstatus\n    //\n    //  Inputs: none\n    //\n    //  Returns: massive dump of status info\n    //\n    struct Status : NoArgs\n    {\n        static constexpr auto name = \"status\"sv;\n    };\n\n    //  RPC: get_status\n    //    Returns current summary status\n    //\n    //  Inputs: none\n    //\n    //  Returns: slightly smaller dump of status info including\n    //    \"authcodes\"\n    //    \"exitMap\"\n    //    \"lokiAddress\"\n    //    \"networkReady\"\n    //    \"numPathsBuilt\"\n    //    \"numPeersConnected\"\n    //    etc\n    //\n    struct GetStatus : NoArgs\n    {\n        static constexpr auto name = \"get_status\"sv;\n    };\n\n    //  RPC: quic_connect\n    //    Initializes QUIC connection tunnel\n    //    Passes request parameters in nlohmann::json format\n    //\n    //  Inputs:\n    //    \"endpoint\" : endpoint id (string)\n    //    \"bindAddr\" : bind address (string, ex: \"127.0.0.1:1142\")\n    //    \"host\" : remote host ID (string)\n    //    \"port\" : port to bind to (int)\n    //    \"close\" : close connection to port or host ID\n    //\n    //  Returns:\n    //    \"id\" : connection ID\n    //    \"addr\" : connection local address\n    //\n    struct QuicConnect : RPCRequest\n    {\n        static constexpr auto name = \"quic_connect\"sv;\n\n        struct request_parameters\n        {\n            std::string bindAddr;\n            int closeID;\n            std::string endpoint;\n            uint16_t port;\n            std::string remoteHost;\n        } request;\n    };\n\n    //  RPC: quic_listener\n    //    Connects to QUIC interface on local endpoint\n    //    Passes request parameters in nlohmann::json format\n    //\n    //  Inputs:\n    //    \"endpoint\" : endpoint id (string)\n    //    \"host\" : remote host ID (string)\n    //    \"port\" : port to bind to (int)\n    //    \"close\" : close connection to port or host ID\n    //    \"srv-proto\" :\n    //\n    //  Returns:\n    //    \"id\" : connection ID\n    //    \"addr\" : connection local address\n    //\n    struct QuicListener : RPCRequest\n    {\n        static constexpr auto name = \"quic_listener\"sv;\n\n        struct request_parameters\n        {\n            int closeID;\n            std::string endpoint;\n            uint16_t port;\n            std::string remoteHost;\n            std::string srvProto;\n        } request;\n    };\n\n    //  RPC: lookup_snode\n    //    Look up service node\n    //    Passes request parameters in nlohmann::json format\n    //\n    //  Inputs:\n    //    \"routerID\" : router ID to query (string)\n    //\n    //  Returns:\n    //    \"ip\" : snode IP address\n    //\n    struct LookupSnode : RPCRequest\n    {\n        static constexpr auto name = \"lookup_snode\"sv;\n\n        struct request_parameters\n        {\n            std::string routerID;\n        } request;\n    };\n\n    //  RPC: map_exit\n    //    Map a new connection to an exit node\n    //\n    //  Inputs:\n    //    \"address\" : ID of endpoint to map\n    //    \"range\" : IP range to map to exit node\n    //    \"token\" : auth token\n    //\n    //  Returns:\n    //\n    struct MapExit : RPCRequest\n    {\n        static constexpr auto name = \"map_exit\"sv;\n\n        struct request_parameters\n        {\n            std::string address;\n            std::vector<std::string> ip_ranges;\n            std::string token;\n        } request;\n    };\n\n    //  RPC: list_exits\n    //    List all currently mapped exit node connections\n    //\n    //  Inputs: none\n    //\n    //  Returns:\n    //\n    struct ListExits : NoArgs\n    {\n        static constexpr auto name = \"list_exits\"sv;\n    };\n\n    //  RPC: unmap_exit\n    //    Unmap a connection to an exit node\n    //\n    //  Inputs:\n    //    \"endpoint\" : ID of endpoint to map\n    //\n    //  Returns:\n    //\n    struct UnmapExit : RPCRequest\n    {\n        static constexpr auto name = \"unmap_exit\"sv;\n\n        struct request_parameters\n        {\n            std::string address;\n        } request;\n    };\n\n    //  RPC: swap_exit\n    //    Swap a connection from one exit to another\n    //\n    //  Inputs:\n    //    \"exits\" : exit nodes to swap mappings from (index 0 = old exit, index 1 = new exit)\n    //\n    //  Returns:\n    //\n    struct SwapExits : RPCRequest\n    {\n        static constexpr auto name = \"swap_exits\"sv;\n\n        struct request_parameters\n        {\n            std::vector<std::string> exit_addresses;\n            std::string token;\n        } request;\n    };\n\n#if 0\n    //  RPC: dns_query\n    //    Attempts to query endpoint by domain name\n    //\n    //  Inputs:\n    //    \"endpoint\" : endpoint ID to query (string)\n    //    \"qname\" : query name (string)\n    //    \"qtype\" : query type (int)\n    //\n    //  Returns:\n    //\n    struct DNSQuery : Immediate\n    {\n        static constexpr auto name = \"dns_query\"sv;\n\n        struct request_parameters\n        {\n            std::string endpoint;\n            uint16_t qtype;\n            std::string qname;\n        } request;\n    };\n#endif\n\n    //  RPC: config\n    //    Runs lokinet router using .ini config file passed as path\n    //\n    //  Inputs:\n    //    \"filename\" : name of .ini file to either save or delete\n    //    \"ini\" : .ini chunk to save in new file\n    //    \"del\" : boolean specifying whether to delete file \"filename\" or save it\n    //\n    //  Returns:\n    //\n    struct Config : Immediate\n    {\n        static constexpr auto name = \"config\"sv;\n\n        struct request_parameters\n        {\n            bool del;\n            std::string filename;\n            std::string ini;\n        } request;\n    };\n\n    //  RPC: find_cc\n    //    Lookup client contact via path request\n    //\n    //  Inputs:\n    //    \"pk\" : client pubkey\n    //\n    //  Returns:\n    //    \"cc\" : client contact, or error string\n    struct FindCC : RPCRequest\n    {\n        static constexpr auto name = \"find_cc\"sv;\n\n        struct request_parameters\n        {\n            std::string pk;\n        } request;\n    };\n\n    //  RPC: session_init\n    //    Initiate session to remote instance\n    //\n    //  Inputs:\n    //    \"pk\" : remote pubkey terminating with `.loki` or `.snode`\n    //\n    //  Returns:\n    //    \"ip\" : mapped IP address, or error string\n    struct SessionInit : RPCRequest\n    {\n        static constexpr auto name = \"session_init\"sv;\n\n        struct request_parameters\n        {\n            std::string pk;\n        } request;\n    };\n\n    //  RPC: session_close\n    //    Close session to remote instance\n    //\n    //  Inputs:\n    //    \"pk\" : remote pubkey\n    //\n    //  Returns:\n    //    \"b\" : T/F if close was successful\n    struct SessionClose : RPCRequest\n    {\n        static constexpr auto name = \"session_close\"sv;\n\n        struct request_paramters\n        {\n            std::string pk;\n        } request;\n    };\n\n    // List of all RPC request structs to allow compile-time enumeration of all supported types\n    using rpc_request_types = tools::type_list<\n        Halt,\n        Version,\n        Status,\n        GetStatus,\n        QuicConnect,   // debug\n        QuicListener,  // debug\n        LookupSnode,\n        FindCC,\n        SessionInit,\n        SessionClose,\n        MapExit,\n        ListExits,\n        SwapExits,\n        UnmapExit,\n        // DNSQuery,\n        Config>;\n\n}  // namespace llarp::rpc\n"
  },
  {
    "path": "llarp/rpc/rpc_request_parser.cpp",
    "content": "#include \"rpc_request_parser.hpp\"\n\n#include \"param_parser.hpp\"\n\nnamespace llarp::rpc\n{\n    using nlohmann::json;\n\n    void parse_request(QuicConnect& quicconnect, rpc_input input)\n    {\n        get_values(\n            input,\n            \"bindAddr\",\n            quicconnect.request.bindAddr,\n            \"closeID\",\n            quicconnect.request.closeID,\n            \"endpoint\",\n            quicconnect.request.endpoint,\n            \"port\",\n            quicconnect.request.port,\n            \"remoteHost\",\n            quicconnect.request.remoteHost);\n    }\n\n    void parse_request(QuicListener& quiclistener, rpc_input input)\n    {\n        get_values(\n            input,\n            \"closeID\",\n            quiclistener.request.closeID,\n            \"endpoint\",\n            quiclistener.request.endpoint,\n            \"port\",\n            quiclistener.request.port,\n            \"remoteHost\",\n            quiclistener.request.remoteHost,\n            \"srvProto\",\n            quiclistener.request.srvProto);\n    }\n\n    void parse_request(FindCC& findcc, rpc_input input) { get_values(input, \"pk\", findcc.request.pk); }\n\n    void parse_request(SessionInit& sessioninit, rpc_input input) { get_values(input, \"pk\", sessioninit.request.pk); }\n\n    void parse_request(SessionClose& sessionclose, rpc_input input)\n    {\n        get_values(input, \"pk\", sessionclose.request.pk);\n    }\n\n    void parse_request(LookupSnode& lookupsnode, rpc_input input)\n    {\n        get_values(input, \"routerID\", lookupsnode.request.routerID);\n    }\n\n    void parse_request(MapExit& mapexit, rpc_input input)\n    {\n        get_values(\n            input,\n            \"address\",\n            mapexit.request.address,\n            \"ip_ranges\",\n            mapexit.request.ip_ranges,\n            \"token\",\n            mapexit.request.token);\n    }\n\n    void parse_request(UnmapExit& unmapexit, rpc_input input)\n    {\n        get_values(input, \"address\", unmapexit.request.address);\n    }\n\n    void parse_request(SwapExits& swapexits, rpc_input input)\n    {\n        get_values(input, \"exit_addresses\", swapexits.request.exit_addresses, \"token\", swapexits.request.token);\n    }\n\n#if 0\n    void parse_request(DNSQuery& dnsquery, rpc_input input)\n    {\n        get_values(\n            input,\n            \"endpoint\",\n            dnsquery.request.endpoint,\n            \"qname\",\n            dnsquery.request.qname,\n            \"qtype\",\n            dnsquery.request.qtype);\n    }\n#endif\n\n    void parse_request(Config& config, rpc_input input)\n    {\n        get_values(input, \"delete\", config.request.del, \"filename\", config.request.filename, \"ini\", config.request.ini);\n    }\n\n}  // namespace llarp::rpc\n"
  },
  {
    "path": "llarp/rpc/rpc_request_parser.hpp",
    "content": "#pragma once\n\n#include \"rpc_request_definitions.hpp\"\n\n#include <llarp/config/config.hpp>\n\n#include <oxenmq/address.h>\n#include <oxenmq/oxenmq.h>\n\nnamespace llarp::rpc\n{\n    using rpc_input = std::variant<std::monostate, nlohmann::json, oxenc::bt_dict_consumer>;\n\n    inline void parse_request(NoArgs&, rpc_input) {}\n\n    void parse_request(FindCC& findcc, rpc_input input);\n    void parse_request(SessionInit& sessioninit, rpc_input input);\n    void parse_request(SessionClose& sessionclose, rpc_input input);\n\n    void parse_request(QuicConnect& quicconnect, rpc_input input);\n    void parse_request(QuicListener& quiclistener, rpc_input input);\n    void parse_request(LookupSnode& lookupsnode, rpc_input input);\n    void parse_request(MapExit& mapexit, rpc_input input);\n    void parse_request(UnmapExit& unmapexit, rpc_input input);\n    void parse_request(SwapExits& swapexits, rpc_input input);\n    // void parse_request(DNSQuery& dnsquery, rpc_input input);\n    void parse_request(Config& config, rpc_input input);\n\n}  // namespace llarp::rpc\n"
  },
  {
    "path": "llarp/rpc/rpc_server.cpp",
    "content": "#include \"rpc_server.hpp\"\n\n#include \"rpc_request.hpp\"\n\n#include <llarp/config/config.hpp>\n#include <llarp/config/ini.hpp>\n#include <llarp/constants/version.hpp>\n#include <llarp/contact/client_contact.hpp>\n#include <llarp/dns/dns.hpp>\n#include <llarp/dns/server.hpp>\n#include <llarp/messages/common.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/rpc/rpc_request_definitions.hpp>\n#include <llarp/util/logging/buffer.hpp>\n\n#include <nlohmann/json.hpp>\n#include <oxenc/base32z.h>\n\n#include <exception>\n#include <vector>\n\nnamespace llarp::rpc\n{\n    static auto logcat = llarp::log::Cat(\"rpc-server\");\n\n    template <typename T>\n        requires std::derived_from<T, RPCRequest>\n    static void log_print_rpc(T& req)\n    {\n        log::info(logcat, \"RPC Server received request for endpoint `{}`\", req.name);\n    }\n\n    // Fake packet source that serializes repsonses back into dns\n    class DummyPacketSource final : public dns::PacketSource\n    {\n        std::function<void(std::optional<dns::Message>)> func;\n\n      public:\n        explicit DummyPacketSource(std::function<void(std::optional<dns::Message>)> func) : func{std::move(func)} {}\n\n        bool would_loop(const quic::Address&, const quic::Address&) const override { return false; };\n\n        /// send packet with src and dst address containing buf on this packet source\n        void send_udp(const quic::Address&, const quic::Address&, std::span<const std::byte> payload) const override\n        {\n            func(dns::maybe_parse_dns_msg(payload));\n        }\n\n        /// returns the sockaddr we are bound on if applicable\n        std::optional<quic::Address> bound_on() const override { return std::nullopt; }\n    };\n\n    bool check_path(std::string path)\n    {\n        for (auto c : path)\n        {\n            if (not((c >= '0' and c <= '9') or (c >= 'A' and c <= 'Z') or (c >= 'a' and c <= 'z') or (c == '_')\n                    or (c == '-')))\n            {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    template <typename RPC>\n    void register_rpc_command(std::unordered_map<std::string, rpc_callback>& regs)\n    {\n        static_assert(std::is_base_of_v<RPCRequest, RPC>);\n        rpc_callback cback{};\n\n        cback.invoke = make_invoke<RPC>();\n\n        regs.emplace(RPC::name, std::move(cback));\n    }\n\n    RPCServer::RPCServer(oxenmq::OxenMQ& omq, Router& r) : _omq{omq}, _router(r)\n    {\n        if (llarp::logRingBuffer)\n            log_subs.emplace(_omq, llarp::logRingBuffer);\n\n        for (const auto& addr : _router.config().api.rpc_bind_addrs)\n        {\n            _omq.listen_plain(addr);\n            log::debug(logcat, \"Bound RPC server to {}\", addr);\n        }\n\n        AddCategories();\n    }\n\n    template <typename... RPC>\n    std::unordered_map<std::string, rpc_callback> register_rpc_requests(tools::type_list<RPC...>)\n    {\n        std::unordered_map<std::string, rpc_callback> regs;\n\n        (register_rpc_command<RPC>(regs), ...);\n\n        return regs;\n    }\n\n    const std::unordered_map<std::string, rpc_callback> rpc_request_map =\n        register_rpc_requests(rpc::rpc_request_types{});\n\n    void RPCServer::AddCategories()\n    {\n        _omq.add_category(\"llarp\", oxenmq::AuthLevel::none).add_request_command(\"logs\", [this](oxenmq::Message& msg) {\n            HandleLogsSubRequest(msg);\n        });\n\n        for (auto& req : rpc_request_map)\n        {\n            _omq.add_request_command(\n                \"llarp\", req.first, [name = std::string_view{req.first}, &call = req.second, this](oxenmq::Message& m) {\n                    call.invoke(m, *this);\n                });\n        }\n    }\n\n    void RPCServer::invoke(Halt& halt)\n    {\n        log_print_rpc(halt);\n\n        if (not _router.is_running())\n        {\n            SetJSONError(\"Router is not running\", halt.response);\n            return;\n        }\n\n        _router.loop.call_soon([&]() { _router.stop(); });\n\n        SetJSONResponse(\"OK\", halt.response);\n    }\n\n    void RPCServer::invoke(Version& version)\n    {\n        log_print_rpc(version);\n\n        nlohmann::json result{{\"version\", llarp::LOKINET_VERSION_FULL}, {\"uptime\", to_json(_router.Uptime())}};\n\n        SetJSONResponse(result, version.response);\n    }\n\n    void RPCServer::invoke(Status& status)\n    {\n        log_print_rpc(status);\n\n        (_router.is_running()) ? SetJSONResponse(_router.ExtractStatus(), status.response)\n                               : SetJSONError(\"Router is not yet ready\", status.response);\n    }\n\n    void RPCServer::invoke(GetStatus& getstatus)\n    {\n        log_print_rpc(getstatus);\n\n        SetJSONResponse(_router.ExtractSummaryStatus(), getstatus.response);\n    }\n\n    void RPCServer::invoke(QuicConnect& quicconnect)\n    {\n        log_print_rpc(quicconnect);\n\n        auto& req = quicconnect.request;\n\n        if (req.port == 0 and req.closeID == 0)\n        {\n            SetJSONError(\"Port not provided\", quicconnect.response);\n            return;\n        }\n\n        if (req.remoteHost.empty() and req.closeID == 0)\n        {\n            SetJSONError(\"Host not provided\", quicconnect.response);\n            return;\n        }\n\n        // auto endpoint =\n        //     (req.endpoint.empty()) ? GetEndpointByName(_router, \"default\") : GetEndpointByName(_router,\n        //     req.endpoint);\n\n        // if (not endpoint)\n        // {\n        //     SetJSONError(\"No such local endpoint found.\", quicconnect.response);\n        //     return;\n        // }\n\n        // auto quic = endpoint->GetQUICTunnel();\n\n        // if (not quic)\n        // {\n        //     SetJSONError(\"No quic interface available on endpoint \" + req.endpoint, quicconnect.response);\n        //     return;\n        // }\n\n        if (req.closeID)\n        {\n            // TODO:\n            // quic->forget(req.closeID);\n            SetJSONResponse(\"OK\", quicconnect.response);\n            return;\n        }\n\n        quic::Address laddr{req.bindAddr, req.port};\n\n        try\n        {\n            // TODO:\n            // auto [addr, id] = quic->open(\n            //     req.remoteHost, req.port, [](auto&&) {}, laddr);\n\n            nlohmann::json status;\n            // status[\"addr\"] = addr.to_string();\n            // status[\"id\"] = id;\n\n            SetJSONResponse(status, quicconnect.response);\n        }\n        catch (std::exception& e)\n        {\n            SetJSONError(e.what(), quicconnect.response);\n        }\n    }\n\n    void RPCServer::invoke(QuicListener& quiclistener)\n    {\n        log_print_rpc(quiclistener);\n\n        auto req = quiclistener.request;\n\n        if (req.port == 0 and req.closeID == 0)\n        {\n            SetJSONError(\"Invalid arguments\", quiclistener.response);\n            return;\n        }\n\n        // auto endpoint =\n        //     (req.endpoint.empty()) ? GetEndpointByName(_router, \"default\") : GetEndpointByName(_router,\n        //     req.endpoint);\n\n        // if (not endpoint)\n        // {\n        //     SetJSONError(\"No such local endpoint found\", quiclistener.response);\n        //     return;\n        // }\n\n        // auto quic = endpoint->GetQUICTunnel();\n\n        // if (not quic)\n        // {\n        //     SetJSONError(\"No quic interface available on endpoint \" + req.endpoint, quiclistener.response);\n        //     return;\n        // }\n\n        if (req.closeID)\n        {\n            // TODO:\n            // quic->forget(req.closeID);\n            SetJSONResponse(\"OK\", quiclistener.response);\n            return;\n        }\n\n        if (req.port)\n        {\n            auto id = 0;\n            try\n            {\n                quic::Address addr{req.remoteHost, req.port};\n                // TODO:\n                // id = quic->listen(addr);\n            }\n            catch (std::exception& e)\n            {\n                SetJSONError(e.what(), quiclistener.response);\n                return;\n            }\n\n            nlohmann::json result;\n            result[\"id\"] = id;\n            std::string localAddress;\n            // var::visit([&](auto&& addr) { localAddress = addr.to_string(); }, endpoint->local_address());\n            result[\"addr\"] = localAddress + \":\" + std::to_string(req.port);\n\n            if (not req.srvProto.empty())\n            {\n                dns::SRVData srvData{req.srvProto, 1, 1, req.port, \"\"};\n                // endpoint->put_srv_record(std::move(srvData));\n            }\n\n            SetJSONResponse(result, quiclistener.response);\n            return;\n        }\n    }\n\n    static std::optional<NetworkAddress> try_netaddr(std::string_view x)\n    {\n        try\n        {\n            return NetworkAddress{x};\n        }\n        catch (...)\n        {\n            return std::nullopt;\n        }\n    }\n\n    void RPCServer::invoke(FindCC& findcc)\n    {\n        log_print_rpc(findcc);\n\n        if (_router.is_service_node)\n        {\n            SetJSONError(\"Not supported\", findcc.response);\n            return;\n        }\n\n        if (findcc.request.pk.empty())\n        {\n            SetJSONError(\"No pubkey provided!\", findcc.response);\n            return;\n        }\n\n        auto maybe_netaddr = try_netaddr(findcc.request.pk);\n\n        if (not maybe_netaddr)\n        {\n            SetJSONError(\"Invalid pubkey provided: {}\"_format(findcc.request.pk), findcc.response);\n            return;\n        }\n\n        _router.loop.call([this, netaddr = *maybe_netaddr, replier = findcc.move()]() mutable {\n            _router.session_endpoint().lookup_client_intro(\n                netaddr.router_id(), [&replier](std::optional<llarp::ClientContact> cc) {\n                    nlohmann::json result;\n                    if (cc)\n                    {\n                        auto cc_str = \"{}\"_format(*cc);\n                        result.emplace(\"cc\", cc_str);\n                        log::info(logcat, \"RPC call to `find_cc` returned successfully: {}\", cc_str);\n                    }\n                    else\n                    {\n                        log::warning(logcat, \"RPC call to `find_cc` failed!\");\n                        result.emplace(\"cc\", \"ERROR\");\n                    }\n                    replier.reply(result.dump());\n                });\n        });\n    }\n\n    void RPCServer::invoke(SessionInit& sessioninit)\n    {\n        log_print_rpc(sessioninit);\n\n        if (_router.is_service_node)\n        {\n            SetJSONError(\"Not supported\", sessioninit.response);\n            return;\n        }\n\n        if (sessioninit.request.pk.empty())\n        {\n            SetJSONError(\"No pubkey provided!\", sessioninit.response);\n            return;\n        }\n\n        auto maybe_netaddr = try_netaddr(sessioninit.request.pk);\n\n        if (not maybe_netaddr)\n        {\n            SetJSONError(\"Invalid pubkey provided: {}\"_format(sessioninit.request.pk), sessioninit.response);\n            return;\n        }\n\n        _router.loop.call([this, netaddr = *maybe_netaddr]() {\n            try\n            {\n                log::debug(logcat, \"Beginning session init to remote instance: {}\", netaddr);\n                _router.session_endpoint().initiate_remote_session(netaddr, nullptr);\n                /*[replier = sessioninit.move()](auto success) mutable {\n                    // FIXME: needs redone, initiate remote session no longer returns an ip\n                    nlohmann::json result;\n                    std::string a = std::holds_alternative<ipv4>(ip) ? std::get<ipv4>(ip).to_string()\n                                                                     : std::get<ipv6>(ip).to_string();\n                    result.emplace(\"ip\", a);\n                    log::info(logcat, \"RPC call to `session_init` succeeded: {}\", a);\n                    replier.reply(result.dump());\n                });\n                */\n                log::info(logcat, \"RPC Server dispatched `session_init` to remote:{}\", netaddr);\n            }\n            catch (const std::exception& e)\n            {\n                log::critical(logcat, \"Failed to parse remote instance netaddr: {}\", e.what());\n            }\n        });\n    }\n\n    void RPCServer::invoke(SessionClose& sessionclose)\n    {\n        log_print_rpc(sessionclose);\n\n        if (sessionclose.request.pk.empty())\n        {\n            SetJSONError(\"No pubkey provided!\", sessionclose.response);\n            return;\n        }\n\n        auto maybe_netaddr = try_netaddr(sessionclose.request.pk);\n\n        if (not maybe_netaddr)\n        {\n            SetJSONError(\"Invalid pubkey provided: {}\"_format(sessionclose.request.pk), sessionclose.response);\n            return;\n        }\n\n        auto& netaddr = *maybe_netaddr;\n\n        _router.loop.call([&]() {\n            try\n            {\n                if (auto session = _router.session_endpoint().get_session(netaddr))\n                {\n                    auto hook = [replier = sessionclose.move()](quic::message m) mutable {\n                        nlohmann::json result;\n\n                        if (m)\n                        {\n                            result.emplace(\"result\", \"OK\");\n                            log::info(logcat, \"RPC call to `session_close` succeeded!\");\n                            return replier.reply(result.dump());\n                        }\n\n                        std::string status{\"<none given>\"};\n\n                        try\n                        {\n                            oxenc::bt_dict_consumer btdc{m.body()};\n\n                            if (auto s = btdc.maybe<std::string>(messages::STATUS_KEY))\n                                status = std::move(*s);\n                        }\n                        catch (const std::exception& e)\n                        {\n                            status = \"Exception: {}\"_format(e.what());\n                        }\n\n                        log::critical(logcat, \"Call to `session_close` FAILED; reason: {}\", status);\n                        result.emplace(\"result\", std::move(status));\n                        replier.reply(result.dump());\n                    };\n\n                    // session->stop_session(true, std::move(hook));\n\n                    log::info(logcat, \"RPC Server dispatched `session_close` to remote:{}\", netaddr);\n                }\n            }\n            catch (const std::exception& e)\n            {\n                log::critical(logcat, \"Failed to parse remote instance netaddr: {}\", e.what());\n            }\n        });\n    }\n\n    // TODO: fix this because it's bad\n    void RPCServer::invoke(LookupSnode& lookupsnode)\n    {\n        log_print_rpc(lookupsnode);\n\n        if (not _router.is_service_node)\n        {\n            SetJSONError(\"Not supported\", lookupsnode.response);\n            return;\n        }\n\n        RouterID routerID;\n\n        if (lookupsnode.request.routerID.empty())\n        {\n            SetJSONError(\"No remote ID provided\", lookupsnode.response);\n            return;\n        }\n\n        if (not routerID.from_relay_address(lookupsnode.request.routerID))\n        {\n            SetJSONError(\"Invalid remote: \" + lookupsnode.request.routerID, lookupsnode.response);\n            return;\n        }\n\n        // _router.loop()->call([&]() {\n        //   auto endpoint = _router.exit_context().get_exit_endpoint(\"default\");\n\n        //   if (endpoint == nullptr)\n        //   {\n        //     SetJSONError(\"Cannot find local endpoint: default\", lookupsnode.response);\n        //     return;\n        //   }\n\n        //   endpoint->ObtainSNodeSession(routerID, [&](auto session) {\n        //     if (session and session->IsReady())\n        //     {\n        //       const auto ip = net::TruncateV6(endpoint->GetIPForIdent(PubKey{routerID}));\n        //       nlohmann::json status{{\"ip\", ip.to_string()}};\n        //       SetJSONResponse(status, lookupsnode.response);\n        //       return;\n        //     }\n\n        //     SetJSONError(\"Failed to obtain snode session\", lookupsnode.response);\n        //     return;\n        //   });\n        // });\n    }\n\n    void RPCServer::invoke(MapExit& mapexit)\n    {\n        log_print_rpc(mapexit);\n\n        MapExit exit_request;\n        // steal replier from exit RPC endpoint\n        exit_request.replier.emplace(mapexit.move());\n\n        // TODO: connect this to remote service session management (service::Handler)\n        // _router.hidden_service_context().GetDefault()->map_exit(\n        //     mapexit.request.address,\n        //     mapexit.request.token,\n        //     mapexit.request.ip_range,\n        //     [exit = std::move(exit_request)](bool success, std::string result) mutable {\n        //       if (success)\n        //         exit.send_response({{\"result\"}, std::move(result)});\n        //       else\n        //         exit.send_response({{\"error\"}, std::move(result)});\n        //     });\n    }\n\n    void RPCServer::invoke(ListExits& listexits)\n    {\n        log_print_rpc(listexits);\n\n        (void)listexits;\n        // if (not _router.hidden_service_context().hasEndpoints())\n        // {\n        //   SetJSONError(\"No mapped endpoints found\", listexits.response);\n        //   return;\n        // }\n\n        // auto status = _router.hidden_service_context().GetDefault()->ExtractStatus()[\"exitMap\"];\n\n        // SetJSONResponse((status.empty()) ? \"No exits\" : status, listexits.response);\n    }\n\n    void RPCServer::invoke(UnmapExit& unmapexit)\n    {\n        log_print_rpc(unmapexit);\n\n        try\n        {\n            // for (auto& ip : unmapexit.request.ip_range)\n            //   _router.hidden_service_context().GetDefault()->UnmapExitRange(ip);\n        }\n        catch (std::exception& e)\n        {\n            SetJSONError(\"Unable to unmap to given range\", unmapexit.response);\n            return;\n        }\n\n        SetJSONResponse(\"OK\", unmapexit.response);\n    }\n\n    //  Sequentially calls map_exit and unmap_exit to hotswap mapped connection from old exit\n    //  to new exit. Similar to how map_exit steals the oxenmq deferredsend object, swapexit\n    //  moves the replier object to the unmap_exit struct, as that is called second. Rather than\n    //  the nested lambda within map_exit making the reply call, it instead calls the unmap_exit\n    //  logic and leaves the message handling to the unmap_exit struct\n    void RPCServer::invoke(SwapExits& swapexits)\n    {\n        log_print_rpc(swapexits);\n\n        (void)swapexits;\n        // MapExit map_request;\n        // UnmapExit unmap_request;\n        // auto endpoint = _router.hidden_service_context().GetDefault();\n        // auto current_exits = endpoint->ExtractStatus()[\"exitMap\"];\n\n        // if (current_exits.empty())\n        // {\n        //   SetJSONError(\"Cannot swap to new exit: no exits currently mapped\", swapexits.response);\n        //   return;\n        // }\n\n        // if (swapexits.request.exit_addresses.size() < 2)\n        // {\n        //   SetJSONError(\"Exit addresses not passed\", swapexits.response);\n        //   return;\n        // }\n\n        // // steal replier from swapexit RPC endpoint\n        // unmap_request.replier.emplace(swapexits.move());\n\n        // // set map_exit request to new address\n        // map_request.request.address = swapexits.request.exit_addresses[1];\n\n        // // set token for new exit node mapping\n        // if (not swapexits.request.token.empty())\n        //   map_request.request.token = swapexits.request.token;\n\n        // // populate map_exit request with old IP ranges\n        // for (auto& [range, exit] : current_exits.items())\n        // {\n        //   if (exit.get<std::string>() == swapexits.request.exit_addresses[0])\n        //   {\n        //     map_request.request.ip_range.emplace_back(range);\n        //     unmap_request.request.ip_range.emplace_back(range);\n        //   }\n        // }\n\n        // if (map_request.request.ip_range.empty() or unmap_request.request.ip_range.empty())\n        // {\n        //   SetJSONError(\"No mapped ranges found matching requested swap\", swapexits.response);\n        //   return;\n        // }\n\n        // endpoint->map_exit(\n        //     map_request.request.address,\n        //     map_request.request.token,\n        //     map_request.request.ip_range,\n        //     [unmap = std::move(unmap_request),\n        //      ep = endpoint,\n        //      old_exit = swapexits.request.exit_addresses[0]](bool success, std::string result)\n        //      mutable {\n        //       if (not success)\n        //         unmap.send_response({{\"error\"}, std::move(result)});\n        //       else\n        //       {\n        //         try\n        //         {\n        //           for (auto& ip : unmap.request.ip_range)\n        //             ep->UnmapRangeByExit(ip, old_exit);\n        //         }\n        //         catch (std::exception& e)\n        //         {\n        //           SetJSONError(\"Unable to unmap to given range\", unmap.response);\n        //           return;\n        //         }\n\n        //         SetJSONResponse(\"OK\", unmap.response);\n        //         unmap.send_response();\n        //       }\n        //     });\n    }\n\n#if 0\n    void RPCServer::invoke(DNSQuery& dnsquery)\n    {\n        log_print_rpc(dnsquery);\n\n        std::string qname = (dnsquery.request.qname.empty()) ? \"\" : dnsquery.request.qname;\n        dns::QType_t qtype = (dnsquery.request.qtype) ? dnsquery.request.qtype : dns::qTypeA;\n\n        dns::Message msg{dns::Question{qname, qtype}};\n\n        // auto endpoint = (dnsquery.request.endpoint.empty()) ? GetEndpointByName(_router, \"default\")\n        //                                                     : GetEndpointByName(_router, dnsquery.request.endpoint);\n\n        // if (endpoint == nullptr)\n        // {\n        //     SetJSONError(\"No such endpoint found for dns query\", dnsquery.response);\n        //     return;\n        // }\n\n        // if (auto dns = endpoint->DNS())\n        // {\n        //     auto packet_src = std::make_shared<DummyPacketSource>([&](auto result) {\n        //         if (result)\n        //             SetJSONResponse(result->ToJSON(), dnsquery.response);\n        //         else\n        //             SetJSONError(\"No response from DNS\", dnsquery.response);\n        //     });\n        //     if (not dns->maybe_handle_packet(packet_src, packet_src->dumb, packet_src->dumb,\n        //     IPPacket{msg.to_buffer()}))\n        //         SetJSONError(\"DNS query not accepted by endpoint\", dnsquery.response);\n        // }\n        // else\n        //     SetJSONError(\"Endpoint does not have dns\", dnsquery.response);\n        return;\n    }\n#endif\n\n    void RPCServer::invoke(Config& config)\n    {\n        log_print_rpc(config);\n\n        if (config.request.filename.empty() and not config.request.ini.empty())\n        {\n            SetJSONError(\"No filename specified for .ini file\", config.response);\n            return;\n        }\n        if (config.request.ini.empty() and not config.request.filename.empty())\n        {\n            SetJSONError(\"No .ini chunk provided\", config.response);\n            return;\n        }\n\n        if (config.request.filename.ends_with(\".ini\"))\n        {\n            SetJSONError(\"Must append '.ini' to filename\", config.response);\n            return;\n        }\n\n        if (not check_path(config.request.filename))\n        {\n            SetJSONError(\"Bad filename passed\", config.response);\n            return;\n        }\n\n        std::filesystem::path conf_d{\"conf.d\"};\n\n        if (config.request.del and not config.request.filename.empty())\n        {\n            try\n            {\n                if (exists(conf_d / config.request.filename))\n                    remove(conf_d / config.request.filename);\n            }\n            catch (std::exception& e)\n            {\n                SetJSONError(e.what(), config.response);\n                return;\n            }\n        }\n        else\n        {\n            try\n            {\n                if (not exists(conf_d))\n                    create_directory(conf_d);\n\n                auto parser = ConfigParser();\n                parser.load_new_from_str(config.request.ini);\n                parser.set_filename(conf_d / config.request.filename);\n                parser.save_new();\n            }\n            catch (std::exception& e)\n            {\n                SetJSONError(e.what(), config.response);\n                return;\n            }\n        }\n\n        SetJSONResponse(\"OK\", config.response);\n    }\n\n    void RPCServer::HandleLogsSubRequest(oxenmq::Message& m)\n    {\n        if (m.data.size() != 1)\n        {\n            m.send_reply(\"Invalid subscription request: no log receipt endpoint given\");\n            return;\n        }\n\n        if (!log_subs)\n        {\n            m.send_reply(\"This Lokinet instance is not capturing logs\");\n            return;\n        }\n\n        auto endpoint = std::string{m.data[0]};\n\n        if (endpoint == \"unsubscribe\")\n        {\n            log::debug(logcat, \"New logs unsubscribe request from conn {}@{}\", m.conn.to_string(), m.remote);\n            log_subs->unsubscribe(m.conn);\n            m.send_reply(\"OK\");\n            return;\n        }\n\n        auto is_new = log_subs->subscribe(m.conn, endpoint);\n\n        if (is_new)\n        {\n            log::debug(logcat, \"New logs subscription request from conn {}@{}\", m.conn.to_string(), m.remote);\n            m.send_reply(\"OK\");\n            log_subs->send_all(m.conn, endpoint);\n        }\n        else\n        {\n            log::debug(logcat, \"Renewed logs subscription request from conn id {}@{}\", m.conn.to_string(), m.remote);\n            m.send_reply(\"ALREADY\");\n        }\n    }\n\n}  // namespace llarp::rpc\n"
  },
  {
    "path": "llarp/rpc/rpc_server.hpp",
    "content": "#pragma once\n\n#include \"json_bt.hpp\"\n#include \"rpc_request_definitions.hpp\"\n\n#include <llarp/config/config.hpp>\n\n#include <nlohmann/json_fwd.hpp>\n#include <oxen/log/omq_logger.hpp>\n#include <oxenmq/address.h>\n#include <oxenmq/message.h>\n#include <oxenmq/oxenmq.h>\n\n// #include <stdexcept>\n// #include <string_view>\n\nnamespace llarp\n{\n    class Router;\n\n    namespace rpc\n    {\n\n        class RPCServer;\n\n        //  Stores RPC request callback\n        struct rpc_callback\n        {\n            using result_type = std::variant<oxenc::bt_value, nlohmann::json, std::string>;\n            //  calls with incoming request data; returns response body or throws exception\n            void (*invoke)(oxenmq::Message&, RPCServer&);\n        };\n\n        //  RPC request registration\n        //    Stores references to RPC requests in a unordered map for ease of reference\n        //    when adding to server. To add endpoints, define in rpc_request_definitions.hpp\n        //    and register in rpc_server.cpp\n        extern const std::unordered_map<std::string, rpc_callback> rpc_request_map;\n\n        //  Exception used to signal various types of errors with a request back to the caller.  This\n        //  exception indicates that the caller did something wrong: bad data, invalid value, etc., but\n        //  don't indicate a local problem (and so we'll log them only at debug).  For more serious,\n        //  internal errors a command should throw some other stl error (e.g. std::runtime_error or\n        //  perhaps std::logic_error), which will result in a local daemon warning (and a generic\n        //  internal error response to the user).\n        //\n        //  For JSON RPC these become an error response with the code as the error.code value and the\n        //  string as the error.message.\n        //  For HTTP JSON these become a 500 Internal Server Error response with the message as the\n        //  body. For OxenMQ the code becomes the first part of the response and the message becomes the\n        //  second part of the response.\n        struct rpc_error : std::runtime_error\n        {\n            /// \\param message - a message to send along with the error code (see general description\n            /// above).\n            rpc_error(std::string message) : std::runtime_error{\"RPC error: \" + message}, message{std::move(message)} {}\n\n            std::string message;\n        };\n\n        template <typename Result_t>\n        void SetJSONResponse(Result_t result, json& j)\n        {\n            j[\"result\"] = result;\n        }\n\n        inline void SetJSONError(std::string_view msg, json& j) { j[\"error\"] = msg; }\n\n        class RPCServer\n        {\n          public:\n            explicit RPCServer(oxenmq::OxenMQ&, Router&);\n\n            void HandleLogsSubRequest(oxenmq::Message& m);\n\n            void AddCategories();\n\n            void invoke(FindCC& findcc);\n            void invoke(SessionInit& sessioninit);\n            void invoke(SessionClose& sessionclose);\n            void invoke(Status& status);\n\n            void invoke(LookupSnode& lookupsnode);\n            void invoke(Halt& halt);\n            void invoke(Version& version);\n            void invoke(GetStatus& getstatus);\n            void invoke(QuicConnect& quicconnect);\n            void invoke(QuicListener& quiclistener);\n            void invoke(MapExit& mapexit);\n            void invoke(ListExits& listexits);\n            void invoke(UnmapExit& unmapexit);\n            void invoke(SwapExits& swapexits);\n            // void invoke(DNSQuery& dnsquery);\n            void invoke(Config& config);\n\n            oxenmq::OxenMQ& _omq;\n            Router& _router;\n            std::optional<oxen::log::PubsubLogger> log_subs;\n        };\n\n        template <typename RPC>\n        class EndpointHandler\n        {\n          public:\n            RPCServer& server;\n            RPC rpc{};\n\n            EndpointHandler(RPCServer& _server, oxenmq::Message::DeferredSend _replier) : server{_server}\n            {\n                rpc.replier.emplace(std::move(_replier));\n            }\n\n            void operator()()\n            {\n                try\n                {\n                    server.invoke(rpc);\n                }\n                catch (const rpc_error& e)\n                {\n                    SetJSONError(\"RPC request 'rpc.{}' failed with: {}\"_format(rpc.name, e.what()), rpc.response);\n                }\n                catch (const std::exception& e)\n                {\n                    SetJSONError(\n                        \"RPC request 'rpc.{}' raised an exception: {}\"_format(rpc.name, e.what()), rpc.response);\n                }\n\n                if (rpc.replier.has_value())\n                {\n                    rpc.send_response();\n                }\n            }\n        };\n    }  // namespace rpc\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/session/session.cpp",
    "content": "#include \"session.hpp\"\n\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/handlers/session.hpp>\n#include <llarp/handlers/tun.hpp>\n#include <llarp/link/endpoint.hpp>\n#include <llarp/messages/dht.hpp>\n#include <llarp/messages/path.hpp>\n#include <llarp/net/policy.hpp>\n#include <llarp/path/transit_hop.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/util/bspan.hpp>\n#include <llarp/util/formattable.hpp>\n#include <llarp/util/random.hpp>\n#include <llarp/util/time.hpp>\n\n#include <nlohmann/json.hpp>\n#include <oxen/quic/context.hpp>\n#include <oxen/quic/gnutls_crypto.hpp>\n#include <oxen/quic/udp.hpp>\n#include <oxenc/endian.h>\n#include <oxenc/hex.h>\n\n#include <chrono>\n#include <limits>\n#include <random>\n#include <utility>\n\nnamespace\n{\n    using namespace oxenc::literals;\n    // GNUTLS Creds tunnel default keys until we implement null-crypto in libquic\n    inline constexpr auto TUNNEL_SEED = \"0000000000000000000000000000000000000000000000000000000000000000\"_hex;\n    inline constexpr auto TUNNEL_PUBKEY = \"3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29\"_hex;\n}  // anonymous namespace\n\nnamespace llarp::session\n{\n    namespace quic = oxen::quic;\n\n    static auto logcat = log::Cat(\"session\");\n    struct TCPTunnel\n    {\n        const quic::Address FAKE_QUIC_ADDR{\"127.86.75.30\"s, 9};\n        const quic::Path FAKE_QUIC_PATH{FAKE_QUIC_ADDR, FAKE_QUIC_ADDR};\n\n        std::shared_ptr<quic::GNUTLSCreds> tls_creds = quic::GNUTLSCreds::make_from_ed_keys(TUNNEL_SEED, TUNNEL_PUBKEY);\n\n        std::shared_ptr<quic::Endpoint> quic_ep{nullptr};\n        std::shared_ptr<quic::Connection> quic_conn{nullptr};\n\n        // (session initiator) TCPHandle listeners mapped to the destination port they are mapped for\n        std::unordered_map<uint16_t, std::shared_ptr<TCPHandle>> tcp_handles;\n\n        // (session remote) QUIC stream ID to TCP connection\n        std::vector<std::shared_ptr<TCPConnection>> _tcp_conns;\n\n        Session& session;\n\n        std::shared_ptr<bool> destructor_canary{std::make_shared<bool>(true)};\n\n        ~TCPTunnel() { reset(); }\n\n        // the QUIC endpoint should be fine if the QUIC connection closes, but\n        // TCP conns and port mappings will need to be restarted.\n        void reset()\n        {\n            log::critical(logcat, \"TCPTunnel::reset()\");\n            quic_conn.reset();\n            _tcp_conns.clear();\n            tcp_handles.clear();\n            log::critical(logcat, \"TCPTunnel::reset() END\");\n        }\n\n        TCPTunnel(Session& _session) : session(_session)\n        {\n            quic::opt::manual_routing quic_send{[this](const quic::Path&, std::span<const std::byte> data) {\n                session.send_session_data_message(data, traffic_type::TUNNELED_QUIC);\n            }};\n            quic::connection_established_callback new_conn{[this](quic::Connection& conn) {\n                if (quic_conn)\n                {\n                    log::error(logcat, \"Already have connection for QUIC tunnel for session to {}!\", session._remote);\n                    return;\n                }\n                log::debug(logcat, \"New connection for QUIC tunnel for session to {}!\", session._remote);\n                quic_conn = conn.shared_from_this();\n            }};\n            quic::connection_closed_callback conn_closed{\n                [this, canary = std::weak_ptr{destructor_canary}](quic::Connection&, uint64_t) {\n                    if (!quic_conn)\n                    {\n                        log::warning(\n                            logcat,\n                            \"Received conn closed, but this session's QUIC tunnel does not seem to have an open \"\n                            \"connection, remote: {}\",\n                            session._remote);\n                        return;\n                    }\n                    log::debug(logcat, \"QUIC TCP tunnel conn to {} closed.\", session._remote);\n\n                    // this could fire from quic::Endpoint destructor, at which point\n                    // the members `reset` would reset may no longer be valid objects\n                    if (canary.lock())\n                        reset();\n                }};\n\n            auto stream_opened = [this](quic::Stream& stream) {\n#if 0\n                stream.set_stream_data_cb([this, prev_byte = std::optional<std::byte>{std::nullopt}](\n                                              quic::Stream& stream, std::span<const std::byte> data) mutable {\n                    uint16_t dest_port{0};\n\n                                        if (data.empty())\n                                        {\n                                            log::error(logcat, \"QUIC stream data callback with no data!\");\n                                            return;\n                                        }\n                                        if (prev_byte)\n                                        {\n                                            std::array<std::byte, 2> buf;\n                                            buf[0] = *prev_byte;\n                                            buf[1] = data[0];\n                                            dest_port = oxenc::load_big_to_host<uint16_t>(buf.data());\n                                            data = data.subspan(1);\n                                        }\n                                        else if (data.size() >= 2)\n                                        {\n                                            dest_port = oxenc::load_big_to_host<uint16_t>(data.data());\n                                            data = data.subspan(2);\n                                        }\n                                        else\n                                        {  // only got 1 byte total so far, need 2 for dest port\n                                            prev_byte = data[0];\n                                            return;\n                                        }\n\n                                        stream.pause();\n\n                                        // FIXME: TCPHandle::connect replaces the stream's data callback.  Perhaps\n                                        // that should happen here instead.\n                                        // FIXME: the connection should probably come from tun bind address, if\n                                        // available, rather than always 127.0.0.1\n                                        auto tcp_conn = TCPHandle::connect(\n                                            session._r.loop.get_event_base(), FAKE_QUIC_ADDR, stream.shared_from_this(),\n                       dest_port); if (!tcp_conn)\n                                        {\n                                            stream.close(11223322);  // TODO: meaningful error code\n                                            return;\n                                        }\n\n                                        _tcp_conns.push_back(tcp_conn);\n\n                                        if (data.size())\n                                        {\n                                            // put any remaining stream data on the tcp socket\n                                            stream.data_callback(stream, data);\n                                        }\n\n                                        stream.enable_watermarks(\n                                            500'000,\n                                            [this, tcp_conn](auto&) { tcp_conn->stop_reading(); },\n                                            50'000,\n                                            [this, tcp_conn](auto&) { tcp_conn->resume_reading(); });\n                });\n#endif\n                return 0;\n            };\n\n            quic_ep = quic::Endpoint::endpoint(\n                // TODO FIXME: this should probably attach to the network loop rather than the logic loop:\n                session._r.loop,\n                FAKE_QUIC_ADDR,\n                std::move(quic_send),\n                std::move(new_conn),\n                std::move(conn_closed),\n                quic::opt::disable_mtu_discovery{});\n\n            // TODO: only listen if we support inbound tunneled traffic\n            quic_ep->listen(tls_creds, std::move(stream_opened));\n        }\n\n        void open_connection()\n        {\n            if (quic_conn)\n            {\n                log::error(\n                    logcat, \"Cannot create more than one QUIC connection over TPC tunnel, remote: {}\", session._remote);\n                return;\n            }\n\n            quic_conn = quic_ep->connect(\n                quic::RemoteAddress{TUNNEL_PUBKEY, FAKE_QUIC_ADDR},\n                tls_creds,\n                [this](quic::Connection& conn) {\n                    log::debug(logcat, \"Outbound QUIC TCP Tunnel connection established to {}\", session._remote);\n                    if (!quic_conn)\n                    {\n                        quic_conn = conn.shared_from_this();\n                    }\n                },  // connection established\n                [this, canary = std::weak_ptr{destructor_canary}](quic::Connection&, uint64_t) /* connection closed*/ {\n                    // this could fire from quic::Endpoint destructor, at which point\n                    // the members referenced below may no longer be valid objects\n                    if (!canary.lock())\n                        return;\n                    if (!quic_conn)\n                        log::error(logcat, \"QUIC TPC tunnel connection to {} failed!\", session._remote);\n                    else\n                        log::debug(logcat, \"QUIC TPC tunnel connection to {} closed.\", session._remote);\n                    reset();\n                });\n        }\n\n        uint16_t map_tcp_remote_port(uint16_t dest_port)\n        {\n            if (!session.is_established())\n                return 0;\n            if (!quic_conn)\n            {\n                open_connection();\n            }\n\n            auto _handle = TCPHandle::make_server(\n                // TODO FIXME: this should probably attach to the network loop rather than the logic loop:\n                session._r.loop,\n                [this, dest_port](struct bufferevent* _bev, evutil_socket_t _fd) -> TCPConnection* {\n                    auto s =\n                        quic_conn->open_stream<quic::Stream>([_bev](quic::Stream& s, std::span<const std::byte> data) {\n                            auto rv = bufferevent_write(_bev, data.data(), data.size());\n\n                            log::debug(\n                                logcat,\n                                \"Stream (id:{}) {} {}B to TCP buffer\",\n                                s.stream_id(),\n                                rv < 0 ? \"failed to write\" : \"successfully wrote\",\n                                data.size());\n                        });\n                    if (!s)\n                    {\n                        log::error(logcat, \"Failed to open stream for TCP tunnel...\");\n                        return nullptr;\n                    }\n                    std::string p;\n                    p.resize(2);\n                    oxenc::write_host_as_big(dest_port, p.data());\n                    s->send(std::move(p));\n\n                    auto tcp_conn = std::make_shared<TCPConnection>(_bev, _fd, std::move(s));\n\n                    auto* ptr = tcp_conn.get();\n                    _tcp_conns.push_back(std::move(tcp_conn));\n\n                    return ptr;\n                });\n\n            auto bound_port = _handle->port();\n            if (bound_port == 0)\n            {\n                log::error(logcat, \"Failed to bind TCP port for tunneled session.\");\n                return 0;\n            }\n\n            log::debug(logcat, \"Bound TCP tunneled session, dest_port: {}, local_port: {}\", dest_port, bound_port);\n            tcp_handles.emplace(dest_port, std::move(_handle));\n            return bound_port;\n        }\n    };\n\n    void InboundSession::init(std::vector<std::byte>&& request)\n    {\n        oxenc::bt_dict_consumer outer_btdc{request};\n        PubKey eph_pubkey;\n        SymmNonce dh_nonce;\n        eph_pubkey.assign(outer_btdc.require_span<std::byte, PubKey::SIZE>(\"k\"));\n        dh_nonce.assign(outer_btdc.require_span<std::byte, SymmNonce::SIZE>(\"n\"));\n\n        // this is a bit ugly, but bt_dict_consumer only gives const spans/views, and\n        // it is safe here to have non-const and avoid a copy.\n        auto inner_payload_const = outer_btdc.require_span<std::byte>(\"x\");\n        std::span<std::byte> inner_payload{\n            const_cast<std::byte*>(inner_payload_const.data()), inner_payload_const.size()};\n        outer_btdc.finish();\n\n        crypto::dh_server(_shared_secret, eph_pubkey, _r.secret_key(), dh_nonce);\n        auto decrypted = crypto::xchacha20_poly1305_decrypt(inner_payload, _shared_secret, dh_nonce);\n\n        oxenc::bt_dict_consumer inner_btdc{decrypted};\n        RouterID remote_rid;\n        remote_rid.assign(inner_btdc.require_span<std::byte, RouterID::SIZE>(\"i\"));\n        _remote = {remote_rid, true};\n        _remote_pivot_txid.assign(inner_btdc.require_span<std::byte, HopID::SIZE>(\"p\"));\n        _outbound_tag = inner_btdc.require<session_tag>(\"t\");\n\n        inner_btdc.require_signature(\"~\", [remote_rid](std::span<const std::byte> msg, std::span<const std::byte> sig) {\n            if (sig.size() != SIGSIZE)\n                throw std::runtime_error{fmt::format(\"Invalid signature: not {} bytes\", SIGSIZE)};\n\n            if (not crypto::verify(remote_rid, msg, sig.first<SIGSIZE>()))\n                throw std::runtime_error{\"Failed to verify session_init identity signature\"};\n        });\n        inner_btdc.finish();\n        _inbound_tag = _parent.next_tag();\n    }\n\n    Session::Session(\n        Router& r, handlers::SessionEndpoint& parent, const NetworkAddress& remote, session_tag inbound_tag)\n        : _r{r},\n          _parent{parent},\n          _inbound_tag{inbound_tag},\n          _remote{remote},\n          is_outbound{true},\n          is_relay_session{_remote.relay()}\n    {\n        // Maybe we should make this on demand rather than on construction?\n        tcp_tunnel = std::make_unique<TCPTunnel>(*this);\n    }\n\n    Session::Session(Router& r, handlers::SessionEndpoint& parent)\n        : _r{r},\n          _parent{parent},\n          _is_established{true},  // Inbound sessions are established from construction\n          is_outbound{false},\n          is_relay_session{_r.is_service_node}\n    {\n        // Maybe we should make this on demand rather than on construction?\n        tcp_tunnel = std::make_unique<TCPTunnel>(*this);\n    }\n\n    Session::~Session()\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n        close(false);\n    }\n\n    void Session::update_active() { last_activity = llarp::time_now_ms(); }\n\n    bool Session::send_session_control_message(std::string_view method, std::span<const std::byte> body)\n    {\n        if (!_is_established)\n        {\n            log::warning(logcat, \"Session not yet established: should not send control messages yet.\");\n            return false;\n        }\n        if (_dead_path)\n        {\n            log::warning(logcat, \"Dropping session control message: session has no current path\");\n            return false;\n        }\n        auto inner_body = PATH::CONTROL::serialize(method, body);\n\n        send_session_data_message(std::move(inner_body), 0, true);\n\n        return true;\n    }\n\n    void Session::recv_session_control_message(\n        std::vector<std::byte>&& message,\n        const SymmNonce& nonce,\n        [[maybe_unused]] std::variant<std::shared_ptr<path::TransitHop>, std::shared_ptr<path::Path>> source)\n    {\n        last_inbound_activity = llarp::time_now_ms();\n        update_active();\n        auto decrypted = crypto::xchacha20_poly1305_decrypt(message, _shared_secret, nonce);\n        if (decrypted.size() == 0)\n        {\n            log::warning(logcat, \"Received unauthenticated session message on session from {}\", _remote);\n            return;\n        }\n        auto btdc = oxenc::bt_dict_consumer{decrypted};\n\n        auto method = btdc.require<std::string_view>(\"e\"sv);\n        auto params = btdc.require<std::span<const std::byte>>(\"p\"sv);\n        log::debug(logcat, \"Received session control message for {} of type {}\", _remote, method);\n\n        if (method == \"session_accept\"sv)\n            handle_session_accept(params);\n        else if (method == \"session_close\")\n            recv_close();\n        else if (method == \"publish_cc\"sv)\n            handle_client_contact(params);\n        else if (method == \"path_switch\"sv)\n        {\n            if (is_outbound)\n            {\n                log::warning(logcat, \"Received path switch on outbound path, dropping.\");\n                return;\n            }\n            auto inner_btdc = oxenc::bt_dict_consumer{params};\n            auto hop_span = inner_btdc.require_span<std::byte, HopID::SIZE>(\"p\"sv);\n            HopID hopid;\n            hopid.assign(hop_span);\n            inner_btdc.finish();\n            if (is_relay_session)\n            {\n                auto p = std::get<std::shared_ptr<path::TransitHop>>(std::move(source));\n                if (hopid != p->rxid)\n                {\n                    log::warning(logcat, \"Received relay session path switch, hopid mismatch with receiving path.\");\n                    return;\n                }\n                static_cast<InboundRelaySession*>(this)->handle_path_switch(std::move(hopid), std::move(p));\n            }\n            else\n            {\n                auto p = std::get<std::shared_ptr<path::Path>>(std::move(source));\n                if (hopid == p->terminal_hopid())\n                {\n                    log::warning(logcat, \"Received client session path switch, hopid collides with receiving path.\");\n                    return;\n                }\n                static_cast<InboundClientSession*>(this)->handle_path_switch(std::move(hopid), std::move(p));\n            }\n        }\n    }\n\n    void InboundClientSession::handle_path_switch(HopID pivot, std::shared_ptr<path::Path> path)\n    {\n        log::debug(\n            logcat,\n            \"Session with {} switching to path {} with pivot hopid {}\",\n            _remote.router_id(),\n            *path,\n            pivot.to_view());\n        _current_path = std::move(path);\n        _dead_path = !_current_path;\n        _remote_pivot_txid = std::move(pivot);\n    }\n\n    void InboundRelaySession::handle_path_switch(HopID pivot, std::shared_ptr<path::TransitHop> thop)\n    {\n        log::debug(logcat, \"Session with {} switching to transit hop with pivot hopid {}\", _remote.router_id(), pivot);\n        _current_thop = std::move(thop);\n        _dead_path = !_current_thop;\n        _remote_pivot_txid = std::move(pivot);\n    }\n\n    void Session::send_session_data_message(std::span<const std::byte> data, net::IPProtocol proto)\n    {\n        uint8_t type;\n        if (proto == net::IPProtocol::UDP)\n            type = traffic_type::UDP;\n        else if (proto == net::IPProtocol::TCP)\n            type = traffic_type::TCP;\n        else\n            type = traffic_type::RAW;\n\n        return send_session_data_message(data, type);\n    }\n\n    // TODO FIXME: we could make this take a vector&& as input, and then provide a\n    // SESSION_DATA_MESSAGE constant that callers can use to reserve the needed extra storage before\n    // moving the vector into here.\n    std::optional<std::pair<std::vector<std::byte>, SymmNonce>> Session::make_session_data_message(\n        std::span<const std::byte> data, uint8_t type, bool control, bool init, SymmNonce nonce)\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        if (!_is_established and !init)\n        {\n            log::debug(logcat, \"Session not yet established: queuing packet for delayed delivery\");\n            queue_data_message(data, type);\n            return std::nullopt;\n        }\n\n        if (_dead_path)\n        {\n            log::warning(logcat, \"Dropping session data message: session has no current path\");\n            return std::nullopt;\n        }\n\n        // We use a single nonce for session + path encryption, but noting that path mutations\n        // are applied to reduce traceability.\n        //\n        // So, for instance, if we send the data message M from client A to client B\n        // via pivot P along these aligned paths:\n        //\n        // A -> X -> Y -> Z -> P <- W <- B\n        //\n        // then we generate a random nonce N, and encrypt the session message with it.  We then\n        // onion the X-Y-Z-P path message, starting with the original N for the pivot, using nonce\n        // (N ^ Pm) for hop Z, (N ^ Pm ^ Zm) for Y, and (N ^ Pm ^ Zm ^ Ym) for X.  The nonce that we\n        // *send* to X is thus N ^ Pm ^ Zm ^ Ym ^ Xm, so that each hop mutates the nonce with its\n        // own xor_nonce to get the decryption nonce it should use.\n        //\n        // (All of this nonce mutation is not for cryptographic security, but rather simply\n        // obfuscates packets somewhat, making it harder to link packets across different hops).\n        //\n        // The pivot (assuming this is a client-to-client data message) reuses this nonce again\n        // down the aligned path, each one mutating it with the xor nonce; the final client\n        // then undoes all of the far-side nonce mutations to arrive back at N, which it then\n        // uses to also decrypt the *session* level encryption.\n\n        // As this packet is what carries IP data, we want to make it as small as possible, and thus\n        // don't use bt-encoding here.  We also build this \"backwards\" by putting the parts\n        // eliminated first at the end, so that a receiver can drop them off the back of a vector\n        // without needing to shift the data.\n        //\n        // Thus we encode values packed together like this:\n        //\n        // 1. Encrypted(PAYLOAD + TYPE BYTE)\n        // 2. Session tag\n        // 3. Pivot ID (sometimes omitted)\n        //\n        // The PivotID instructs the pivot which aligned path to forward it down, and also\n        // identifies inbound relay session data messages: pivot ID == incoming hop ID means the\n        // message is a relay session data message.  The pivot ID is omitted for session data that\n        // ends at a client (i.e. post-pivot path data, and relay->client data on a relay session).\n        //\n        // All of the above make up the path's data message payload; when we deliver this down the\n        // path to the pivot, it will get encrypted repeatedly for each relay on the path (starting\n        // at the pivot), and a path nonce will be generated.  Thus in terms of data down a path we\n        // get:\n        //\n        // [{N}-onion([1][2][3])][N_mutated_nonce][hopid][0x01]\n        //\n        // where each hop down the path reads the hopid and uses this to get the TransitHop (which\n        // it has stored locally during path building), indicating the next hop, shared secret, and\n        // nonce mutator.  It uses this to mutate the nonce, and then mutates the payload to decrypt\n        // one layer of onion encryption.\n        //\n        // If that is an intermediate hop (which will be identifiable via the TransitHop info) then\n        // it sends a payload of the exact same size to the next hop, but where the onioned data has\n        // one onion layer removed, the nonce is replaced with the mutated nonce, and the hopid is\n        // replaced with the next hopid as specified during the path build.\n        //\n        // [{N-1}-onion([1][2][3])][N-1_mutated_nonce][next_hopid][0x01]\n        //\n        // (Note the 0x01 suffix byte above is essentially a datagram versioning byte indicating\n        // that this is a data message, and currently is always 0x01; future versions reserve other\n        // values for other potential future uses of the quic datagram channel).\n        //\n        // If, on the other hand, this payload arrives at the path terminus then the mutated payload\n        // will have removed the final layer of onioning and so the payload will have been mutated\n        // back to the plaintext [1-3] values listed above.  The pivot then reads the pivot ID,\n        // which it uses to look up the aligned path that the the data should be sent along (or to\n        // itself, if equal to the incoming hopid).\n        //\n        // If the pivot id indicates an aligned path (i.e. indicates that this is a pivot) then it\n        // uses the pivot id to determine the aligned path's next hop, discards the pivotid from the\n        // inner payload, onions it and then feeds the data down the aligned path:\n        //\n        // [{1}-onion([1][2])][nonce][hopid][0x01]\n        //\n        // each hop *adding* a layer of onion encryption to the payload and mutating the nonce by\n        // its xor_nonce.  (\"Decryption\" and \"encryption\" here are more or less just conceptual, as\n        // both are really just referring to one application of xchacha20).\n        //\n        // Finally this arrives at the remote client after the M hops, as:\n        //\n        // [{M}-onion([1][2])][M_mutated_nonce][final_hopid][0x01]\n        //\n        // The client then decrypts the *path* data message by forward-applying nonce mutation and\n        // onioning for the M nodes in the path, which when leaves a fully decrypted path payload\n        // consisting of the original items 1-2 described above ([3] was thrown away at the pivot).\n        // [0x01] at this point is also discarded (0x01 has done its job).\n        //\n        // The client then looks up the session by the session tag [2] (discarding if not found).\n        // This then allows it to apply session decryption by reusing the path nonce as the session\n        // nonce, and using the shared secret from the looked up session.  Once decrypted, it also\n        // drops the session tag off the end of the payload.\n        //\n        // This then leaves it with a decrypted PAYLOAD + TYPE BYTE, and this can then be dealt with\n        // as an IP packet.\n        //\n        // For a session message *to a relay* the first half of the above is similar, but the pivot\n        // ID == incoming hopid allows the terminal to realize that it is a session target rather\n        // than a pivot.  Thus instead of starting the encryption down an aligned path, it instead\n        // uses the session tag to look up the InboundRelaySession and then decrypts the session\n        // message just like a client would (again reusing the path nonce).\n        //\n        // Session messages *from a relay* to a client are similar, the main difference being that\n        // there is never a PivotID [3]: payload omits it from the beginning, and so ends up more\n        // closely resembling path data down the \"back\" path of two aligned paths.\n        const bool relay_session_return = !is_outbound && is_relay_session;\n\n        auto tag = init ? 0 : _outbound_tag;\n\n        // control messages do not append a \"type\" byte like datagrams\n        // session init messages are already encrypted and encoded, so no mac here\n        size_t chacha_size_with_mac = data.size() + (control ? 0 : 1) + (init ? 0 : crypto::MAC_SIZE);\n        std::vector<std::byte> everything;\n        auto target_size = chacha_size_with_mac + sizeof(tag) + (relay_session_return ? 0 : _remote_pivot_txid.size());\n        everything.reserve(target_size + path::Path::ENCRYPT_PATH_MESSAGE_OVERHEAD);\n        everything.resize(target_size);\n        auto [ciphertext, tag_span, pivot] = split_span(everything, chacha_size_with_mac, sizeof(tag));\n        assert(pivot.size() == (relay_session_return ? 0 : _remote_pivot_txid.size()));\n\n        std::memcpy(ciphertext.data(), data.data(), data.size());\n        if (!control)\n            ciphertext[data.size()] = static_cast<std::byte>(type);\n        oxenc::write_host_as_big(tag, tag_span.data());\n        if (!relay_session_return)\n            std::memcpy(pivot.data(), _remote_pivot_txid.data(), pivot.size());\n\n        if (!init)\n            crypto::xchacha20_poly1305_encrypt(ciphertext, _shared_secret, nonce);\n        return std::make_pair(std::move(everything), std::move(nonce));\n    }\n\n    void Session::send_session_data_message(std::span<const std::byte> data, uint8_t type, bool control, bool init)\n    {\n        auto maybe_message = make_session_data_message(data, type, control, init);\n        if (!maybe_message)\n            return;\n\n        auto& everything = (*maybe_message).first;\n        auto& nonce = (*maybe_message).second;\n        if (control)\n            send_path_control_message(std::move(everything), std::move(nonce), false);\n        else\n            send_path_data_message(std::move(everything), std::move(nonce));\n    }\n\n    void OutboundSession::queue_data_message(std::span<const std::byte> data, uint8_t type)\n    {\n        update_active();\n        if (!pre_establish_data_queue)\n            pre_establish_data_queue.emplace();\n        else\n            while (pre_establish_data_queue->size() >= MAX_QUEUED_PACKETS)\n                pre_establish_data_queue->pop_front();\n\n        auto& item = pre_establish_data_queue->emplace_back();\n        item.resize(data.size() + 1);\n        std::memcpy(item.data(), data.data(), data.size());\n        item.back() = static_cast<std::byte>(type);\n    }\n\n    void Session::recv_session_data_message(std::vector<std::byte> data, const SymmNonce& nonce)\n    {\n        last_inbound_activity = llarp::time_now_ms();\n        update_active();\n        if (data.empty())\n        {\n            log::error(logcat, \"received empty session data message!\");\n            return;\n        }\n\n        // FIXME: maybe this decrypt should return the size of the now-cleartext part of the\n        //        buffer instead of a span?\n        auto dspan = crypto::xchacha20_poly1305_decrypt(data, _shared_secret, nonce);\n        data.resize(dspan.size());\n\n        uint8_t dgram_type = std::to_integer<uint8_t>(data.back());\n        data.pop_back();\n        if (!traffic_type::is_valid(dgram_type))\n        {\n            log::warning(logcat, \"dropping session data message with unknown traffic type {}\", dgram_type);\n            return;\n        }\n\n        bool is_udp = dgram_type == traffic_type::UDP;\n        bool is_tunneled = dgram_type == traffic_type::TUNNELED_QUIC;\n\n        if (_r.embedded())\n        {\n            if (is_udp)\n            {\n                handle_udp_from_remote(IPPacket{std::move(data)});\n            }\n            else if (!is_tunneled)\n            {\n                log::warning(logcat, \"Received non-UDP, non-tunneled datagram on embedded client, dropping!\");\n            }\n            else\n                tcp_tunnel->quic_ep->manually_receive_packet(\n                    oxen::quic::Packet{tcp_tunnel->FAKE_QUIC_PATH, std::move(data)});\n            return;\n        }\n\n        // Otherwise we're not embedded; if the other side also isn't then this is just a raw IP\n        // packet to handle via the tun endpoint, and the same for UDP packets from embedded\n        // remotes (which also send raw UDP packets):\n        if (dgram_type == traffic_type::TUNNELED_QUIC)\n            tcp_tunnel->quic_ep->manually_receive_packet(\n                oxen::quic::Packet{tcp_tunnel->FAKE_QUIC_PATH, std::move(data)});\n        else\n            _r.tun_endpoint()->handle_inbound_packet(IPPacket{std::move(data)}, dgram_type, _remote);\n    }\n\n    void Session::publish_client_contact(const EncryptedClientContact& ecc)\n    {\n        auto payload_sv = ecc.bt_payload();\n        auto payload{oxen::quic::reinterpret_span<const std::byte>(payload_sv)};\n        send_session_control_message(\"publish_cc\", payload);\n    }\n\n    void Session::handle_client_contact(std::span<const std::byte>)\n    {\n        log::warning(logcat, \"Received client contact, but not OutboundClientSession.\");\n    }\n\n    void OutboundClientSession::handle_client_contact(std::span<const std::byte> payload)\n    {\n        auto ecc = EncryptedClientContact{payload};\n        if (auto cc = ecc.decrypt(_remote.router_id()); cc)\n        {\n            log::debug(logcat, \"Session with {} received valid new client contact, updating.\", _remote.router_id());\n            _intro_update_processed = false;\n            update_intros(*cc);\n        }\n        else\n            log::warning(logcat, \"Session with {} received invalid new client contact!\", _remote.router_id());\n    }\n\n    void Session::handle_udp_from_remote(IPPacket&& pkt)\n    {\n        if (!pkt.is_ip() || pkt.protocol() != net::IPProtocol::UDP)\n        {\n            log::debug(logcat, \"Dropping unsupported non-IPv4/v6 UDP packet\");\n            return;\n        }\n        auto source_port = pkt.source_port();\n        if (!source_port)\n        {\n            log::debug(logcat, \"Dropping malformed UDP packet: {}\", pkt.info_line());\n            return;\n        }\n        log::trace(logcat, \"incoming udp packet from remote port {}\", *source_port);\n        auto itr = udp_handles.find(*source_port);\n        if (itr == udp_handles.end())\n        {\n            log::debug(logcat, \"Received udp datagram from unknown source port {}\", *source_port);\n            return;\n        }\n        auto& socket = itr->second;\n        auto dest_port = *pkt.dest_port();\n        log::trace(logcat, \"incoming udp packet for pseudo port {}\", dest_port);\n        if (!udp_remote_ports.contains(dest_port))\n        {\n            log::warning(logcat, \"Received UDP packet destined for an unmapped port ({})\", dest_port);\n            return;\n        }\n        dest_port = udp_remote_ports[dest_port];\n        log::trace(logcat, \"pseudo port maps to client port {}\", dest_port);\n\n        auto payload = pkt.udp_data();\n        if (payload.empty())\n        {\n            log::warning(logcat, \"Received invalid udp datagram\");\n            return;\n        }\n        quic::Address dest = socket->address();\n        dest.set_port(dest_port);\n        const size_t bufsize = payload.size();\n        uint8_t ecn = 0;  // FIXME: do we have any way to obtain this?\n        size_t n_pkts = 1;\n        auto [ior, sent] = socket->send(quic::Path{socket->address(), dest}, payload.data(), &bufsize, ecn, n_pkts);\n        log::trace(\n            logcat, \"UDP from remote -> socket send to local returned {} (ec={})\", ior.success(), ior.error_code);\n    }\n\n    uint16_t Session::setup_udp_mapping(uint16_t dest_port)\n    {\n        if (auto itr = udp_handles.find(dest_port); itr != udp_handles.end())\n        {\n            auto mapped_port = itr->second->address().port();\n            log::debug(logcat, \"Returning existing mapped port ({}) for dest port {}\", mapped_port, dest_port);\n            return mapped_port;\n        }\n        quic::Address src{\"127.0.0.1\"s, 0};\n        quic::Address dest{\"127.0.0.1\"s, dest_port};\n        auto udp_handle = std::make_unique<quic::UDPSocket>(\n            _r.loop.get_event_base(), src, /*gso=*/false, [this, dest = std::move(dest)](quic::Packet&& pkt) {\n                auto client_port = pkt.path.remote.port();\n                if (!udp_client_ports.contains(client_port))\n                {\n                    log::debug(logcat, \"Adding client port {} to mapping for remote port {}\", client_port, dest.port());\n                    uint16_t new_port = next_udp_client_port;\n                    auto start_port = new_port;\n                    while (udp_remote_ports.contains(new_port))\n                    {\n                        new_port++;\n                        if (new_port < 1024)\n                            new_port = 1024;\n                        if (start_port == new_port)\n                            throw std::runtime_error{\"Ran out of pseudo-udp ports to use\"};\n                    }\n                    udp_client_ports[client_port] = new_port;\n                    udp_remote_ports[new_port] = client_port;\n                    log::trace(logcat, \"pseudo client port {} for real client port {}\", new_port, client_port);\n                    client_port = new_port;\n                }\n                else\n                    client_port = udp_client_ports[client_port];\n\n                // ip doesn't matter here, but give remote the source port so we receive responses\n                // as destined for that port and know where to send them\n                auto src = pkt.path.remote;\n                src.set_port(client_port);\n                auto payload = pkt.data();\n                auto packet = IPPacket::make_udp_packet(src, dest, payload);\n                send_session_data_message(packet, traffic_type::UDP);\n            });\n        auto bound_port = udp_handle->address().port();\n        udp_handles[dest_port] = std::move(udp_handle);\n\n        return bound_port;\n    }\n\n    uint16_t Session::map_tcp_remote_port(uint16_t dest_port) { return tcp_tunnel->map_tcp_remote_port(dest_port); }\n\n    bool Session::is_established() const { return _is_established && !_is_closed; }\n\n    void Session::close(bool send_close)\n    {\n        if (_is_closed)\n            return;\n\n        _is_closed = true;\n        log::debug(logcat, \"Session to remote ({}) closed!\", _remote);\n        if (send_close)\n        {\n            log::debug(logcat, \"Dispatching close session message...\");\n            send_session_control_message(\"session_close\", {});\n        }\n    }\n\n    void Session::recv_close() { _parent.close_session(_inbound_tag, false); }\n\n    void OutboundRelaySession::recv_close()\n    {\n        log::debug(logcat, \"OutboundRelaySession received close, manually dropping all paths.\");\n        invalidate_paths();\n    }\n\n    void OutboundClientSession::recv_close()\n    {\n        invalidate_paths();\n        cc_ok = false;\n    }\n\n    bool Session::is_expired(std::chrono::milliseconds now) const { return now - last_activity > SESSION_TIMEOUT; }\n\n    std::string OutboundSession::to_string() const\n    {\n        return \"OSession:[{}{} | {}]\"_format(\n            _is_closed            ? \"closing\"\n                : _is_established ? \"active\"\n                                  : \"pending\",\n            is_exit_capable ? \",exit-capable\" : \"\",\n            (_current_path && !_current_path->is_dead) ? fmt::to_string(*_current_path) : \"<NO-PATH>\");\n    }\n    std::string InboundClientSession::to_string() const\n    {\n        return \"ISession:[{}{} | {}]\"_format(\n            _is_closed            ? \"closing\"\n                : _is_established ? \"active\"\n                                  : \"pending\",\n            is_exit_capable ? \",exit-capable\" : \"\",\n            (_current_path && !_current_path->is_dead) ? fmt::to_string(*_current_path) : \"<NO-PATH>\");\n    }\n    std::string InboundRelaySession::to_string() const\n    {\n        return \"ISession:[{}{} | {}]\"_format(\n            _is_closed            ? \"closing\"\n                : _is_established ? \"active\"\n                                  : \"pending\",\n            is_exit_capable ? \",exit-capable\" : \"\",\n            (_current_thop && !_current_thop->is_dead) ? fmt::to_string(*_current_thop) : \"<NO-T-HOP>\");\n    }\n\n    OutboundSession::OutboundSession(\n        const NetworkAddress& remote,\n        handlers::SessionEndpoint& parent,\n        int num_hops,\n        session_tag inbound_tag,\n        std::function<void(OutboundSession& session)> on_est,\n        std::optional<std::chrono::milliseconds> est_timeout)\n        : PathHandler{parent.router, parent.router.config().paths.outbound_paths, num_hops},\n          Session{router, parent, remote, inbound_tag}\n    {\n        if (on_est)\n            on_established(std::move(on_est), est_timeout);\n        std::tie(_shared_secret, dh_pk, dh_nonce) = crypto::dh_client_gen(_remote.router_id());\n        // TODO: kick off path builds immediately\n    }\n\n    void OutboundSession::fire_waiting(std::chrono::milliseconds now)\n    {\n        // If we're established then we can immediately fire everything in the queue, otherwise we\n        // fire callbacks that have reached their timer (to signal a non-established timeout).\n        const bool est = is_established();\n        while (!_on_established.empty() && (est || _on_established.top().first <= now))\n        {\n            _on_established.top().second(*this);\n            _on_established.pop();\n        }\n    }\n\n    void OutboundSession::on_established(\n        std::function<void(OutboundSession&)> callback, std::optional<std::chrono::milliseconds> timeout)\n    {\n        _on_established.emplace(\n            llarp::time_now_ms() + timeout.value_or(_r.config().paths.build_timeout), std::move(callback));\n    }\n\n    void OutboundSession::tick(std::chrono::milliseconds now)\n    {\n        if (_is_closed)\n            return;\n        if (is_expired(now))\n        {\n            close(false);  // don't send close message -- if we expired they already did for sure\n            for (auto& p : active_paths())\n                drop_path(p);\n            return;\n        }\n        close_old_paths(now);\n        path::PathHandler::tick(now);\n        fire_waiting(now);\n    }\n\n    void OutboundClientSession::tick(std::chrono::milliseconds now)\n    {\n        if ((now - last_cc_update > 10min) || (now - last_inbound_activity > 30s))\n        {\n            log::critical(\n                logcat,\n                \"It has been > 5min since last cc update, or > 10s since last inbound activity; attempting to fetch a \"\n                \"new intro set for session to {}\",\n                _remote);\n            refresh_intros();\n        }\n    }\n\n    template <typename T>\n    bool check_dead(std::shared_ptr<T>& path_like, Session& s)\n    {\n        if (!path_like || path_like->is_dead)\n        {\n            s._dead_path = true;\n            if (path_like)\n                path_like.reset();\n            return true;\n        }\n        return false;\n    }\n\n    static void send_path_data_impl(\n        std::shared_ptr<path::Path>& path, Session& s, std::vector<std::byte>&& data, SymmNonce&& nonce)\n    {\n        if (check_dead(path, s))\n        {\n            log::debug(logcat, \"Unable to send session data message: no current path\");\n            return;\n        }\n        if (!path->is_established())\n        {\n            // TODO FIXME: queue traffic?  (Perhaps only if `is_outbound` and we have no path?)\n            log::debug(logcat, \"Unable to send session data message: our current path is not yet established\");\n            return;\n        }\n\n        path->send_path_data_message(std::move(data), std::move(nonce));\n    }\n\n    static void send_path_control_impl(\n        std::shared_ptr<path::Path>& path,\n        Session& s,\n        std::vector<std::byte>&& data,\n        SymmNonce&& nonce,\n        bool path_switch)\n    {\n        if (check_dead(path, s))\n        {\n            log::debug(logcat, \"Unable to send session control message: no current path\");\n            return;\n        }\n        if (!path->is_established())\n        {\n            log::debug(logcat, \"Unable to send session control message: our current path is not yet established\");\n            return;\n        }\n\n        path->send_session_control_message(\n            std::move(data),\n            std::move(nonce),\n            path_switch ? path::Path::PATH_SWITCH_MESSAGE_TYPE : path::Path::CONTROL_MESSAGE_TYPE);\n    }\n\n    void OutboundSession::send_path_data_message(std::vector<std::byte>&& data, SymmNonce&& nonce)\n    {\n        update_active();\n        send_path_data_impl(_current_path, *this, std::move(data), std::move(nonce));\n    }\n    void InboundClientSession::send_path_data_message(std::vector<std::byte>&& data, SymmNonce&& nonce)\n    {\n        update_active();\n        send_path_data_impl(_current_path, *this, std::move(data), std::move(nonce));\n    }\n\n    void OutboundSession::send_path_control_message(std::vector<std::byte>&& data, SymmNonce&& nonce, bool path_switch)\n    {\n        update_active();\n        send_path_control_impl(_current_path, *this, std::move(data), std::move(nonce), path_switch);\n    }\n    void InboundClientSession::send_path_control_message(\n        std::vector<std::byte>&& data, SymmNonce&& nonce, bool path_switch)\n    {\n        update_active();\n        send_path_control_impl(_current_path, *this, std::move(data), std::move(nonce), path_switch);\n    }\n\n    void OutboundSession::close_old_paths(std::chrono::milliseconds now)\n    {\n        // cf. select_new_current\n        //\n        // When selecting a new path, we select from all active paths that meet the\n        // [paths]:acceptable-expiry threshold, but if we don't find any, select from any that\n        // satisfy [paths]:min-expiry, and so we replicate that logic here so that we don't close\n        // any paths that select_new_current could choose if it was called right now.\n\n        std::vector<std::pair<path::Path*, std::chrono::milliseconds>> close_me, maybe_close;\n        bool found_acceptable_exp = false;\n        for (auto& path : active_paths())\n        {\n            if (_current_path.get() == &path)\n                continue;  // never close the current path\n\n            auto expires_in = path.expires_in(now);\n            if (expires_in >= router.config().paths.acceptable_expiry)\n                found_acceptable_exp = true;\n\n            else if (expires_in < router.config().paths.min_expiry)\n                close_me.emplace_back(&path, expires_in);\n\n            // Otherwise this is between min and acceptable, and so is a \"maybe\": we'll close it\n            // only if there are some paths above acceptable, but not if all paths are in-between.\n            else if (not found_acceptable_exp)\n                maybe_close.emplace_back(&path, expires_in);\n        }\n\n        int drops = 0;\n        for (auto [path, in] : close_me)\n        {\n            log::debug(logcat, \"Dropping {} unused path {} with imminent expiry (in {})\", *this, *path, in);\n            drop_path(*path);\n            drops++;\n        }\n\n        if (found_acceptable_exp)\n        {\n            for (auto [path, in] : maybe_close)\n            {\n                log::debug(\n                    logcat,\n                    \"Dropping {} unused path {} because it expires relative soon (in {})\"\n                    \" and we have preferable newer paths\",\n                    *this,\n                    *path,\n                    in);\n                drop_path(*path);\n                drops++;\n            }\n        }\n        if (drops)\n            log::debug(logcat, \"{} dropped {} paths; have {} remaining\", *this, drops, num_paths(now));\n        else\n            log::trace(logcat, \"{} found no close-to-expiry paths to drop\", *this);\n    }\n\n    OutboundRelaySession::OutboundRelaySession(\n        const NetworkAddress& remote,\n        handlers::SessionEndpoint& parent,\n        session_tag inbound_tag,\n        std::function<void(OutboundSession& session)> on_est,\n        std::optional<std::chrono::milliseconds> on_est_timeout)\n        : OutboundSession{\n              remote, parent, parent.router.config().paths.relay_hops(), inbound_tag, std::move(on_est), on_est_timeout}\n    {\n        _parent.lookup_relay_contact(_remote.router_id(), [this](std::optional<llarp::RelayContact> rc) mutable {\n            if (rc)\n            {\n                log::debug(logcat, \"Relay contact for {} found: {}\", _remote, *rc);\n                // Tick ourself to start building paths without waiting for the next scheduled tick\n                tick(llarp::time_now_ms());\n            }\n            else\n            {\n                log::debug(logcat, \"RC lookup failed for {}\", _remote);\n                // TODO FIXME: should we close the session?  Retry the lookup?  Start responding\n                // with ICMP unreachables?\n            }\n        });\n    }\n\n    void OutboundSession::select_new_current_impl(\n        std::vector<std::pair<path::Path*, HopID>>&& good, std::vector<std::pair<path::Path*, HopID>>&& fallback)\n    {\n        if (good.empty())\n            good = std::move(fallback);\n\n        if (good.empty())\n        {\n            log::warning(logcat, \"Unable to select new path to {}: no acceptable active paths\", _remote);\n            _current_path.reset();\n            _dead_path = true;\n            return;\n        }\n\n        auto& [chosen, hopid] = good[std::uniform_int_distribution<size_t>{0, good.size() - 1}(llarp::csrng)];\n        switch_path(*chosen, hopid);\n    }\n\n    void OutboundRelaySession::select_new_current()\n    {\n        // New path selection:\n        //\n        // Go look at all our current active paths that expire at least [paths]:acceptable-expiry\n        // from now, and choose one of them at random.\n        //\n        // If we can't find any suitable one, fallback to a random selection from any paths within\n        // [paths]:min-expiry.\n        //\n        // If we don't have any of those, either, then we fail and disable the current path; when a\n        // path build succeeds it will see that we have no current path and switch to it.\n\n        auto now = llarp::time_now_ms();\n        std::vector<std::pair<path::Path*, HopID>> good, fallback;\n        for (auto& path : active_paths())\n        {\n            auto expires_in = path.expires_in(now);\n            if (expires_in < router.config().paths.min_expiry)\n                continue;\n\n            auto& container = expires_in >= router.config().paths.acceptable_expiry ? good : fallback;\n            // path-to-relay: the \"pivot\" hopid equal to the incoming path id indicates path to\n            // relay, i.e. no pivoting:\n            container.emplace_back(&path, path.terminal_hopid());\n        }\n\n        select_new_current_impl(std::move(good), std::move(fallback));\n    }\n\n    void OutboundRelaySession::update_paths(std::chrono::milliseconds /*now*/)\n    {\n        int needed = _target_paths - num_paths();\n        if (needed <= 0)\n            return;\n        log::debug(\n            logcat,\n            \"OutboundRelaySession building {} paths to remote {} to reach target path count {}\",\n            needed,\n            _remote,\n            _target_paths);\n\n        int count = 0;\n        while (count < needed && build_path_to_remote(_remote.router_id()))\n            count++;\n\n        if (count == needed)\n            log::debug(logcat, \"SessionEndpoint successfully initiated {} path-builds\", needed);\n        else\n            log::warning(logcat, \"SessionEndpoint only initiated {} path-builds (wanted {})\", count, needed);\n    }\n\n    OutboundClientSession::OutboundClientSession(\n        const NetworkAddress& remote,\n        handlers::SessionEndpoint& parent,\n        session_tag inbound_tag,\n        std::function<void(OutboundSession& session)> on_est,\n        std::optional<std::chrono::milliseconds> timeout)\n        : OutboundSession{\n              remote, parent, parent.router.config().paths.client_hops, inbound_tag, std::move(on_est), timeout}\n    {\n        assert(!is_relay_session);\n\n        refresh_intros();\n\n        log::debug(logcat, \"Outbound session to {} initiated\", _remote);\n    }\n\n    void OutboundClientSession::refresh_intros()\n    {\n        if (updating_intros)\n            return;\n        updating_intros = true;\n        log::debug(logcat, \"Initiating intro lookup for {}\", _remote);\n        _parent.lookup_client_intro(\n            _remote.router_id(), [this, alive = canary()](std::optional<ClientContact> cc) mutable {\n                if (!alive.lock())\n                {\n                    log::debug(\n                        logcat,\n                        \"OutboundClientSession::refresh_intros lookup_client_intro callback returning early; \"\n                        \"session-alive canary is dead\");\n                    return;\n                }\n                updating_intros = false;\n                if (cc)\n                {\n                    log::debug(logcat, \"Session initiation returned client contact: {}\", *cc);\n                    cc_ok = true;\n                    _intro_update_processed = false;\n                    update_intros(*cc);\n                }\n                else\n                    log::warning(logcat, \"Failed to lookup intros for {}\", _remote);\n            });\n    }\n\n    void OutboundClientSession::update_intros(const ClientContact& cc)\n    {\n        log::debug(logcat, \"Update session {} intros from client contact: {}\", *this, cc);\n        last_cc_update = llarp::time_now_ms();\n        last_inbound_activity = last_cc_update;  // so we don't just fetch again right away\n        auto intros = cc.intros();\n        _intros.assign(intros.begin(), intros.end());\n        log::trace(logcat, \"New client intros: {}\", fmt::join(_intros, \", \"));\n        _pivots.clear();\n        for (auto& i : _intros)\n            _pivots.insert(i.relay);\n\n        update_paths(last_cc_update);\n    }\n\n    void OutboundClientSession::update_paths(std::chrono::milliseconds now)\n    {\n        // - If we have any current path to a pivot that is no longer in the client contact, kill\n        //   it.\n        // - If we killed our currently active path then switch to another.\n        // - If we end up with too few paths then start some builds.\n\n        if (!cc_ok)\n        {\n            log::debug(logcat, \"{} returning early, client contact empty or no longer usable\", __PRETTY_FUNCTION__);\n            return;\n        }\n\n        Lock_t l(paths_mutex);\n\n        if (!_intro_update_processed)\n        {\n            // Intros updated since we last updated, so we may have paths to pivots that are no\n            // longer valid and need to be dropped.  Checking that the pivot relay is still\n            // present is not sufficient, as the remote client may have built a new path to that\n            // relay with a different terminal HopID.\n            std::unordered_map<RouterID, std::vector<HopID>> pivots;\n            for (const auto& i : _intros)\n            {\n                auto& p = pivots[i.relay];\n                p.push_back(i.hop);\n            }\n            std::list<path::Path*> drop;  // Use a list because it isn't valid to drop while iterating\n            for (auto& p : paths())\n            {\n                const auto itr = pivots.find(p.terminal_rid());\n                bool keep = false;\n                if (itr != pivots.end())\n                {\n                    for (const auto& hopid : itr->second)\n                    {\n                        if (hopid == p.aligned_hopid)\n                        {\n                            keep = true;\n                            break;\n                        }\n                    }\n                }\n\n                if (!keep)\n                    drop.push_back(&p);\n            }\n\n            for (auto* drop : drop)\n                drop_path(*drop);\n\n            _intro_update_processed = true;\n        }\n\n        int n_paths = num_paths();\n\n        if (_current_path && _current_path->is_dead)\n        {\n            _current_path.reset();\n            _dead_path = true;\n        }\n\n        if (!_current_path && n_paths)\n            // We don't have a current path, possibly because we just dropped it in the above loop,\n            // so select a new one to make our current path\n            select_new_current();\n\n        const auto& pathconf = router.config().paths;\n        auto acceptable_ts = now + pathconf.acceptable_expiry;\n\n        // To figure out how many new paths we ought to build we only consider existing paths that\n        // are within our acceptable_paths window: anything older than that is due for replacement,\n        // and will be dropped (but only once a replacement path is built).\n        int needed = _target_paths - num_paths(acceptable_ts);\n        if (_current_path)\n        {\n            // If we are currently on a path between min and acceptable, however, then we *don't*\n            // need a replacement for it as we are sticking with it (until it reaches min expiry),\n            // but it will have been counted in `needed` above\n            if (auto curr_expires_in = _current_path->expires_in(now);\n                curr_expires_in > pathconf.min_expiry && curr_expires_in < pathconf.acceptable_expiry)\n                needed--;\n        }\n        if (needed <= 0)\n            return;\n\n        log::debug(\n            logcat,\n            \"OutboundClientSession building {} paths to remote {} to reach target path count {}\",\n            needed,\n            _remote,\n            _target_paths);\n\n        int count = 0;\n        while (count < needed)\n        {\n            auto p = select_pivot();\n            if (p)\n            {\n                if (auto* path_ptr = build_path_to_remote(p->first, p->second.first); path_ptr)\n                {\n                    path_ptr->aligned_hopid = p->second.second;\n                    count++;\n                }\n            }\n            else\n                break;\n        }\n\n        log::debug(logcat, \"Initiated {} path builds for {}\", count, _remote);\n    }\n\n    std::string OutboundSession::make_session_init(path::Path& path)\n    {\n        oxenc::bt_dict_producer inner_btdp;\n\n        inner_btdp.append(\"i\"sv, _r.id().span());\n        inner_btdp.append(\"p\"sv, path.terminal_hopid().span());\n        inner_btdp.append(\"t\"sv, _inbound_tag);\n\n        inner_btdp.append_signature(\"~\", [this](std::span<const std::byte> to_sign) {\n            std::array<std::byte, SIGSIZE> sig;\n            _r.secret_key().sign(sig, to_sign);\n            return sig;\n        });\n\n        auto inner_payload = std::move(inner_btdp).str();\n        inner_payload.resize(inner_payload.size() + crypto::MAC_SIZE);\n        crypto::xchacha20_poly1305_encrypt(inner_payload, _shared_secret, dh_nonce);\n\n        oxenc::bt_dict_producer btdp;\n\n        btdp.append(\"k\", dh_pk.span());\n        btdp.append(\"n\", dh_nonce.span());\n        btdp.append(\"x\", inner_payload);\n\n        return std::move(btdp).str();\n    }\n\n    void OutboundSession::switch_path(path::Path& path, const HopID& pivot_hopid)\n    {\n        log::debug(logcat, \"{} switching path to {} (hopid={})\", *this, path, pivot_hopid);\n\n        _current_path = path.shared_from_this();\n        _dead_path = !_current_path;\n        _remote_pivot_txid = path.aligned_hopid ? *path.aligned_hopid : path.terminal_hopid();\n\n        if (!_is_established)\n        {\n            log::debug(\n                logcat,\n                \"{} remote ({}) established, initiating session\",\n                _remote.client() ? \"Aligned path for\" : \"Path to\",\n                _remote);\n\n            auto msg = make_session_init(path);\n            send_session_data_message(as_bspan(msg), 0, true, true);\n        }\n        else\n        {\n            log::debug(\n                logcat,\n                \"Dispatching path-switch request to remote {} to use hopid {} (for path {})\",\n                _remote,\n                _remote_pivot_txid,\n                path);\n\n            auto session_init_msg = make_session_init(path);\n\n            auto switch_nonce = dh_nonce ^ switch_xor_factor;\n            oxenc::bt_dict_producer btdp;\n            btdp.append(\"p\"sv, path.terminal_hopid().span());\n            auto maybe_path_switch_msg = make_session_data_message(\n                PATH::CONTROL::serialize(\"path_switch\"sv, btdp.span<std::byte>()), 0, true, false, switch_nonce);\n            if (!maybe_path_switch_msg)\n            {\n                log::warning(logcat, \"Failed to create path switch message\");\n                return;\n            }\n            auto& m = maybe_path_switch_msg->first;\n            m.resize(m.size() - (sizeof(session_tag) + HopID::SIZE));  // these go on outer message here\n\n            oxenc::bt_list_producer btlp;\n            btlp.append(std::move((*maybe_path_switch_msg).first));\n            btlp.append(std::move(session_init_msg));\n            auto list_span = btlp.span<std::byte>();\n            std::vector<std::byte> payload{list_span.begin(), list_span.end()};\n            auto old_size = payload.size();\n            payload.resize(payload.size() + sizeof(_outbound_tag) + HopID::SIZE);  // see make_session_data_message\n            auto [payload_span, tag_span, pivot_span] = split_span(payload, old_size, sizeof(_outbound_tag));\n            oxenc::write_host_as_big(_outbound_tag, tag_span.data());\n            std::memcpy(pivot_span.data(), _remote_pivot_txid.data(), _remote_pivot_txid.size());\n            send_path_control_message(std::move(payload), SymmNonce{dh_nonce}, /*path_switch=*/true);\n        }\n    }\n\n    void OutboundClientSession::select_new_current()\n    {\n        log::trace(logcat, \"{} called\", __PRETTY_FUNCTION__);\n\n        // New path selection:\n        //\n        // Go look at all our current possible aligned paths to all introsets where both path and\n        // introset expiry are at least [paths]:acceptable-expiry from now, and choose one of them\n        // at random.\n        //\n        // If we can't find any suitable one, fallback to a random selection from any path+intro\n        // within [paths]:min-expiry.\n        //\n        // If we don't have any of those, either, then we fail and disable the current path; when a\n        // path build succeeds it will see that we have no current path and call this again to\n        // select one.\n\n        const auto min_exp = router.config().paths.min_expiry;\n        const auto acceptable_exp = router.config().paths.acceptable_expiry;\n\n        auto now = llarp::time_now_ms();\n        std::vector<std::pair<path::Path*, HopID>> good, fallback;\n        for (auto& path : active_paths())\n        {\n            auto path_expires_in = path.expires_in(now);\n            if (path_expires_in < min_exp)\n                continue;\n\n            for (auto& intro : _intros)\n            {\n                if (intro.relay != path.terminal_rid())\n                    continue;\n                auto intro_expires_in = intro.expires_in(now);\n                if (intro_expires_in < min_exp)\n                    continue;\n\n                auto expires_in = std::min(path_expires_in, intro_expires_in);\n                auto& container = expires_in >= acceptable_exp ? good : fallback;\n\n                container.emplace_back(&path, intro.hop);\n            }\n        }\n\n        select_new_current_impl(std::move(good), std::move(fallback));\n    }\n\n    nlohmann::json OutboundClientSession::ExtractStatus() const\n    {\n        auto obj = path::PathHandler::ExtractStatus();\n        // obj[\"lastExitUse\"] = to_json(_last_use);\n        //  auto pub = _auth->session_key().to_pubkey();\n        //  obj[\"exitIdentity\"] = pub.to_string();\n        obj[\"endpoint\"] = _remote.to_string();\n        return obj;\n    }\n\n    std::optional<std::pair<RouterID, std::pair<std::chrono::seconds, HopID>>> OutboundClientSession::select_pivot()\n    {\n        // We've been asked to select a new pivot to build a path to.  We select using various\n        // criteria:\n        //\n        // - start with all pivots listed in the intro sets\n        // - eliminate any that expire less than [paths]:acceptable-expiry from now\n        // - find the pivot(s) with the fewer number of existing (already built or building) paths\n        // - select randomly from those.\n        //\n        // Thus we spread out our pivots along all pivots in a CC not close to expiry, and only\n        // start doubling up on a pivot if we need to maintain more paths than there are pivots.\n\n        auto now = llarp::time_now_ms();\n        std::unordered_map<RouterID, std::pair<std::chrono::seconds, HopID>> select_from;\n        int min_path_count = std::numeric_limits<int>::max();\n        auto acceptable_cutoff =\n            std::chrono::sys_time<std::chrono::milliseconds>{now + router.config().paths.acceptable_expiry};\n        for (auto& intro : _intros)\n        {\n            if (intro.expiry < acceptable_cutoff)\n                continue;\n\n            const int existing_count = static_cast<int>(std::ranges::count_if(\n                paths(), [&intro](const path::Path& p) { return p.terminal_rid() == intro.relay; }));\n\n            if (existing_count > min_path_count)\n                // We already found a pivot with fewer paths, so we don't want this one\n                continue;\n            if (existing_count < min_path_count)\n            {\n                // This is better than anything we've seen before so discard everything and start\n                // over with this one\n                select_from.clear();\n                min_path_count = existing_count;\n            }\n            // In the case of duplicate router ids, replace with the newer (longer?) expiry\n            // and that Intro's HopID\n            auto exp = std::min<std::chrono::seconds>(\n                std::chrono::floor<std::chrono::seconds>(intro.expires_in(now)), path::MAX_LIFETIME);\n            if (auto [it, inserted] = select_from.emplace(intro.relay, std::make_pair(exp, intro.hop));\n                not inserted and it->second.first < exp)\n            {\n                it->second.first = exp;\n                it->second.second = intro.hop;\n            }\n        }\n\n        if (select_from.empty())\n            return std::nullopt;\n\n        return *std::next(\n            select_from.begin(),\n            std::uniform_int_distribution<int>{0, static_cast<int>(select_from.size()) - 1}(llarp::csrng));\n    }\n\n    void OutboundSession::on_path_build_success(int64_t /*build_id*/, path::Path& p)\n    {\n        log::debug(logcat, \"{} path {} built successfully\", _remote, p);\n        assert(router.loop.inside());\n\n        // If we don't have a current path then immediately switch to this built in.\n        //\n        // TODO FIXME: this will end up slightly biasing the first path that gets used towards\n        // closer edges and shorter hops; perhaps we should add a slight delay before choosing an\n        // initial path so that other paths we have a chance to finish building before we select a\n        // new one?\n        if (!_current_path || _current_path->is_dead)\n            select_new_current();\n    }\n\n    void OutboundSession::on_path_build_failure(int64_t /*build_id*/, path::Path* /*p*/, bool timeout)\n    {\n        log::warning(\n            logcat,\n            \"{} aligned path build failed: {}\",\n            _remote,\n            timeout ? \"build request timed out\" : \"path construction failed\");\n    }\n\n    InboundSession::InboundSession(handlers::SessionEndpoint& parent) : Session{parent.router, parent} {}\n\n    void InboundSession::session_init_accept()\n    {\n        oxenc::bt_dict_producer btdp;\n        btdp.append(\"t\"sv, _inbound_tag);\n        send_session_control_message(\"session_accept\"sv, btdp.span<std::byte>());\n    }\n\n    void Session::handle_session_accept(std::span<const std::byte>)\n    {\n        log::warning(logcat, \"Received session accept message, but not an outbound session.\");\n    }\n\n    void OutboundSession::handle_session_accept(std::span<const std::byte> params)\n    {\n        bool was_established = _is_established;\n        if (was_established)\n        {\n            log::debug(\n                logcat,\n                \"Received session accept message for established session, likely a path switch failed because the \"\n                \"remote restarted, so it accepted our backup session init.\");\n        }\n        _is_established = false;  // become unestablished if this parsing fails to trigger a new session init\n        oxenc::bt_dict_consumer btdc{params};\n        _outbound_tag = btdc.require<session_tag>(\"t\"sv);\n\n        log::debug(logcat, \"Remote provided session tag: {}\", _outbound_tag);\n\n        log::trace(\n            logcat, \"Outbound session to {} successfully {}established.\", remote(), was_established ? \"re-\"sv : \"\"sv);\n        _is_established = true;\n\n        if (pre_establish_data_queue)\n        {\n            for (const auto& d : *pre_establish_data_queue)\n                send_session_data_message(std::span{d.data(), d.size() - 1}, static_cast<uint8_t>(d.back()));\n            pre_establish_data_queue.reset();\n        }\n\n        fire_waiting(llarp::time_now_ms());\n    }\n\n    InboundClientSession::InboundClientSession(\n        handlers::SessionEndpoint& parent, std::shared_ptr<path::Path> p, std::vector<std::byte>&& request)\n        : InboundSession{parent}, _current_path{std::move(p)}\n    {\n        _dead_path = !_current_path;\n        init(std::move(request));\n    }\n\n    InboundRelaySession::InboundRelaySession(\n        handlers::SessionEndpoint& parent, std::shared_ptr<path::TransitHop> thop, std::vector<std::byte>&& request)\n        : InboundSession{parent}, _current_thop{std::move(thop)}\n    {\n        _dead_path = !_current_thop;\n        init(std::move(request));\n    }\n\n    void InboundRelaySession::encrypt_path_message(std::vector<std::byte>& data, SymmNonce&& nonce, std::byte type)\n    {\n        // This is similar to Path encrypt, except that we are operating at the far end of a\n        // transithop and starting the encrypt backwards (and so don't see the whole path, just our\n        // end of it) which means:\n        // - we only do our first single layer of the required onioning\n        // - we apply the xor_nonce *before* xchacha20 rather than after (because we are applying\n        //   the operations in reverse).\n        auto orig_size = data.size();\n        data.resize(orig_size + path::Path::ENCRYPT_PATH_MESSAGE_OVERHEAD);\n        static_assert(path::Path::ENCRYPT_PATH_MESSAGE_OVERHEAD == SymmNonce::SIZE + HopID::SIZE + 1);\n        auto [inner_payload, bnonce, bhop, msgtype] = split_span_tail<SymmNonce::SIZE, HopID::SIZE, 1>(data);\n        assert(inner_payload.size() == orig_size);\n\n        nonce ^= _current_thop->xor_nonce;\n        crypto::xchacha20(inner_payload, _current_thop->shared_secret, nonce);\n        nonce.copy_to(bnonce);\n        _current_thop->rxid.copy_to(bhop);\n        msgtype[0] = type;\n    }\n\n    void InboundRelaySession::send_path_data_message(std::vector<std::byte>&& data, SymmNonce&& nonce)\n    {\n        update_active();\n        if (check_dead(_current_thop, *this))\n        {\n            log::debug(logcat, \"Unable to send return relay session data message: no current transit hop\");\n            return;\n        }\n\n        encrypt_path_message(data, std::move(nonce), path::Path::DATA_MESSAGE_TYPE);\n        _parent.router.link_endpoint().send_datagram(_current_thop->downstream, std::move(data));\n    }\n\n    void InboundRelaySession::send_path_control_message(\n        std::vector<std::byte>&& data, SymmNonce&& nonce, bool path_switch)\n    {\n        update_active();\n        if (check_dead(_current_thop, *this))\n        {\n            log::debug(logcat, \"Unable to send return relay session control message: no current transit hop\");\n            return;\n        }\n\n        encrypt_path_message(data, std::move(nonce), path::Path::CONTROL_MESSAGE_TYPE);\n        _parent.router.link_endpoint().send_command(\n            _current_thop->downstream, \"session_control\"s, std::move(data), nullptr);\n    }\n}  // namespace llarp::session\n"
  },
  {
    "path": "llarp/session/session.hpp",
    "content": "#pragma once\n\n#include <llarp/address/address.hpp>\n#include <llarp/constants/path.hpp>\n#include <llarp/ev/tcp.hpp>\n#include <llarp/ev/udp.hpp>\n#include <llarp/net/ip_packet.hpp>\n#include <llarp/path/path.hpp>\n#include <llarp/path/path_handler.hpp>\n#include <llarp/path/transit_hop.hpp>\n#include <llarp/util/bspan.hpp>\n\n#include <oxen/quic/btstream.hpp>\n#include <oxen/quic/connection.hpp>\n#include <oxen/quic/endpoint.hpp>\n\n#include <chrono>\n#include <queue>\n\nnamespace llarp\n{\n    namespace quic = oxen::quic;\n\n    using recv_session_dgram_cb = std::function<void(std::span<std::byte>)>;\n\n    inline constexpr auto SESSION_PATH_BUILD_ATTEMPTS{3};\n\n    namespace link\n    {\n        class TunnelManager;\n    }  //  namespace link\n\n    namespace handlers\n    {\n        class SessionEndpoint;\n    }  // namespace handlers\n\n    /** Snode vs Client Session\n        - client to client: shared secret (symmetric key) is negotiated\n        - client to relay:\n          - the traffic to the pivot is encrypted\n          - the pivot is the terminus, so data doesn't need to be encrypted\n    */\n\n    namespace session\n    {\n        using session_tag = uint32_t;\n\n        struct TCPTunnel;\n\n        // We must not use the same nonce for path switch and session init, as they can be in the\n        // same message using the same shared secret.  As such, the path switch message will use\n        // dh_nonce ^ this xor factor.\n        inline const SymmNonce switch_xor_factor = SymmNonce::filled<SymmNonce>(0x42);\n\n        class Session\n        {\n            // TODO FIXME: how long since last use should is_expired() return true?\n            static constexpr std::chrono::milliseconds SESSION_TIMEOUT = 30s;\n\n            friend struct TCPTunnel;\n            template <typename T>\n            friend bool check_dead(std::shared_ptr<T>& path_like, Session& s);\n\n          protected:\n            Router& _r;\n            handlers::SessionEndpoint& _parent;\n\n            // The session tags.  Each side of the session decides its own inbound tag, meaning\n            // no worries about collision *and* shorter tags on each packet.\n            session_tag _inbound_tag;\n            session_tag _outbound_tag;\n\n            NetworkAddress _remote;\n\n            PubKey dh_pk;\n            SymmNonce dh_nonce;\n            SharedSecret _shared_secret;\n\n            // used for bridging data messages across aligned paths\n            HopID _remote_pivot_txid;\n\n            // Will be set to true when an outbound session is established; will always be true for\n            // inbound sessions.\n            bool _is_established{false};\n\n            // Will be set to true if this session has been closed (i.e. via a call to\n            // SessionEndpoint::close_session).  Closing is terminal (i.e. a closed Session instance\n            // will never become non-closed; reestablishing a closed Session requires replacing it).\n            bool _is_closed{false};\n\n            // Set to true if our current path is definitely dead, to short-circuit things like\n            // send_session_data_message encryption if we know we can't deliver it anywhere.  This\n            // is roughly equivalent to `!path || path->is_dead`, except that the base class doesn't\n            // know about `path` and so this allows subclasses the provide the information back to\n            // the base class without needing an extra virtual method call on every packet.\n            //\n            // Base classes should reset this to false as soon as they switch to a new path.\n            bool _dead_path{true};\n\n            std::unique_ptr<TCPTunnel> tcp_tunnel{nullptr};\n\n            // for tunneled clients, maps remote dest port to udp socket\n            // for return traffic, dest port will be the client's udp socket port\n            std::unordered_map<uint16_t, std::unique_ptr<quic::UDPSocket>> udp_handles;\n\n            // bidirectional map, obfuscating the randomized source port from the user and\n            // mapping that obfuscated port back to that obfuscated port for return traffic.\n            // This is both to track used ports so we don't accept traffic to an unmapped\n            // one, as well as in case port selection is fingerprintable.\n            // udp_client_ports maps client source port -> pseudo source port\n            // udp_remote_ports maps pseudo dest port -> client dest port\n            std::unordered_map<uint16_t, uint16_t> udp_client_ports;\n            std::unordered_map<uint16_t, uint16_t> udp_remote_ports;\n            uint16_t next_udp_client_port{1024};\n            std::chrono::milliseconds last_activity = llarp::time_now_ms();\n\n            // only currently useful for outbound client sessions, but more convenient here\n            // than an overload on all inbound traffic functions for that one case\n            std::chrono::milliseconds last_inbound_activity = llarp::time_now_ms();\n\n            void update_active();\n\n            // We capture a weak_ptr to this shared_ptr to avoid needing to use shared_from_this\n            // when we need to assure we are still alive in lambdas given to external objects.  I.e.\n            // this allows: `[alive=canary(), this] { if (!alive.lock()) return; ... }`\n            std::shared_ptr<bool> _destructor_canary{std::make_shared<bool>(true)};\n            std::weak_ptr<bool> canary() { return _destructor_canary; }\n\n            Session(\n                Router& r, handlers::SessionEndpoint& parent, const NetworkAddress& remote, session_tag inbound_tag);\n\n            Session(Router& r, handlers::SessionEndpoint& parent);\n\n            virtual void handle_client_contact(std::span<const std::byte> payload);\n\n            virtual ~Session();\n\n          public:\n            // Non-movable, non-copyable:\n            Session(Session&&) = delete;\n            Session(const Session&) = delete;\n            Session& operator=(Session&&) = delete;\n            Session& operator=(const Session&) = delete;\n\n            // True if this is an OutboundSession-derived instance.\n            const bool is_outbound;\n\n            // True if this is a session instance between a client and relay (i.e. either\n            // InboundRelaySession- or OutboundRelaySession-derived).\n            const bool is_relay_session;\n\n            // TODO FIXME: make this do something.  When the session establishes we should get some\n            // capabilities metadata, such as whether it supports exit.\n            const bool is_exit_capable{false};\n\n            const NetworkAddress& remote() const { return _remote; }\n\n            std::string encode_session_control_message(\n                std::string_view method,\n                std::span<const std::byte> body,\n                const SymmNonce& nonce,\n                std::optional<HopID> pivot_id);\n\n            // Attempts to send a session control message down the current path.  Returns false\n            // (without calling `func`) if there is no current path, otherwise returns true\n            bool send_session_control_message(std::string_view method, std::span<const std::byte> body);\n\n            void recv_session_control_message(\n                std::vector<std::byte>&& message,\n                const SymmNonce& nonce,\n                std::variant<std::shared_ptr<path::TransitHop>, std::shared_ptr<path::Path>> source);\n\n            virtual void handle_session_accept(std::span<const std::byte> params);\n\n            void send_session_data_message(std::span<const std::byte> data, net::IPProtocol proto);\n            std::optional<std::pair<std::vector<std::byte>, SymmNonce>> make_session_data_message(\n                std::span<const std::byte> data,\n                uint8_t type,\n                bool control = false,\n                bool init = false,\n                SymmNonce nonce = SymmNonce::make_random());\n            void send_session_data_message(\n                std::span<const std::byte> data, uint8_t type, bool control = false, bool init = false);\n\n            virtual void send_path_data_message(std::vector<std::byte>&& data, SymmNonce&& nonce) = 0;\n            virtual void send_path_control_message(\n                std::vector<std::byte>&& data, SymmNonce&& nonce, bool path_switch) = 0;\n\n            // Called by send_session_data_message if trying to send a data message on a\n            // not-yet-established connection (which, by definition, can only be an outbound\n            // session).  The default does nothing, but OutboundSession overrides to queue them (up\n            // to a limit) so that initially sent packets on an initializing session get delivered\n            // as soon as the session establishes.  This allows, for example, pings to get delivered\n            // rather than having the first couple getting dropped before establishing.\n            virtual void queue_data_message(std::span<const std::byte> /*data*/, uint8_t /*type*/) {}\n\n            void recv_session_data_message(std::vector<std::byte> data, const SymmNonce& nonce);\n\n            void publish_client_contact(const EncryptedClientContact& ecc);\n\n            void handle_udp_from_remote(IPPacket&& pkt);\n\n            uint16_t setup_udp_mapping(uint16_t dest_port);\n\n            uint16_t map_tcp_remote_port(uint16_t dest_port);\n\n            // Returns true if this session is established, and has not been explicitly closed.\n            // Inbound sessions are instantly established; outbound sessions are established once\n            // the session init response arrives from the remote.\n            bool is_established() const;\n\n            session_tag inbound_tag() const { return _inbound_tag; }\n            session_tag outbound_tag() const { return _outbound_tag; }\n\n            // Returns true if this session has been closed, i.e. it is in the middle of shutting\n            // down.\n            bool is_closed() const { return _is_closed; }\n\n            // Called to close this session.  If the bool is true then the session will attempt to\n            // send a session_close control message down the active path.\n            void close(bool send_close);\n\n            virtual void recv_close();\n\n            bool is_expired(std::chrono::milliseconds now) const;\n\n            virtual std::string to_string() const = 0;\n\n            static constexpr bool to_string_formattable = true;\n\n            // Called periodically (somewhere under Router::tick) to handle anything needed on the\n            // session, but also sometimes called in other places (e.g. if we need new paths ASAP\n            // rather than waiting for the next tick)\n            virtual void tick([[maybe_unused]] std::chrono::milliseconds now) {}\n        };\n\n        class OutboundSession : public path::PathHandler, public Session\n        {\n          protected:\n            std::shared_ptr<path::Path> _current_path;\n\n            OutboundSession(\n                const NetworkAddress& remote,\n                handlers::SessionEndpoint& parent,\n                int num_hops,\n                session_tag inbound_tag,\n                std::function<void(OutboundSession& session)> on_established,\n                std::optional<std::chrono::milliseconds> establish_timeout = std::nullopt);\n\n            ~OutboundSession() override = default;\n\n            void select_new_current_impl(\n                std::vector<std::pair<path::Path*, HopID>>&& good,\n                std::vector<std::pair<path::Path*, HopID>>&& fallback);\n\n            void tick(std::chrono::milliseconds now) override;\n\n            virtual void select_new_current() = 0;\n\n            // Closes non-active paths that are close to expiry, i.e. any paths that we would not\n            // select if we need to switch paths.\n            void close_old_paths(std::chrono::milliseconds now);\n\n            void send_path_data_message(std::vector<std::byte>&& data, SymmNonce&& nonce) override;\n            void send_path_control_message(std::vector<std::byte>&& data, SymmNonce&& nonce, bool path_switch) override;\n\n            void queue_data_message(std::span<const std::byte>, uint8_t type) override;\n\n            // We stash the `type` as the last byte of the vector\n            std::optional<std::deque<std::vector<std::byte>>> pre_establish_data_queue;\n\n          private:\n            // Switches to (or starts using) the given path.\n            void switch_path(path::Path& p, const HopID& new_pivot_txid);\n\n            std::string make_session_init(path::Path& path);\n\n            void fire_waiting(std::chrono::milliseconds now);\n\n            using active_item = std::pair<std::chrono::milliseconds, std::function<void(OutboundSession& session)>>;\n            struct on_established_sorter\n            {\n                bool operator()(const active_item& a, const active_item& b) const { return a.first > b.first; }\n            };\n            // Callbacks that we fire once we achieve active status (i.e. at least one established\n            // path for this session), or time out.  The key is the `llarp::time_now_ms()` expiry\n            // time after which we should give up and fire the callback anyway.  The callback can\n            // figure out which case this was by checking `session.is_active()`.\n            std::priority_queue<active_item, std::vector<active_item>, on_established_sorter> _on_established;\n\n            void on_path_build_success(int64_t build_id, path::Path& p) override;\n\n            void on_path_build_failure(int64_t build_id, path::Path* p, bool timeout) override;\n\n            void handle_session_accept(std::span<const std::byte> params) override;\n\n          public:\n            // void stop_session() override;\n\n            // Calls the given callback with the session when it becomes established, or after\n            // timing out.  (The callback can check which case occured via `.is_established()` on\n            // the argument).  If the session is already established when this is called then it is\n            // fired immediately (before returning).\n            //\n            // If the timeout is omitted then it defaults to the config\n            // [network]path-alignment-timeout setting.\n            //\n            // Multiple callbacks waiting on the same session are permitted.\n            void on_established(\n                std::function<void(OutboundSession&)> callback,\n                std::optional<std::chrono::milliseconds> timeout = std::nullopt);\n\n            std::string to_string() const override;\n\n            inline static constexpr int MAX_QUEUED_PACKETS = 30;\n        };\n\n        // Outbound Session to Remote Relay\n        class OutboundRelaySession final : public OutboundSession\n        {\n          public:\n            OutboundRelaySession(\n                const NetworkAddress& remote,\n                handlers::SessionEndpoint& parent,\n                session_tag inbound_tag,\n                std::function<void(OutboundSession& session)> on_established,\n                std::optional<std::chrono::milliseconds> establish_timeout = std::nullopt);\n\n            void update_paths(std::chrono::milliseconds now) override;\n\n            void recv_close() override;\n\n            // void stop(bool send_close = false) override;\n\n          private:\n            void select_new_current() override;\n        };\n\n        // Outbound Session to Remote Client\n        class OutboundClientSession final : public OutboundSession\n        {\n          public:\n            OutboundClientSession(\n                const NetworkAddress& remote,\n                handlers::SessionEndpoint& parent,\n                session_tag inbound_tag,\n                std::function<void(OutboundSession& session)> on_established,\n                std::optional<std::chrono::milliseconds> establish_timeout = std::nullopt);\n\n          private:\n            std::vector<ClientIntro> _intros;\n            std::unordered_set<RouterID> _pivots;\n            bool _intro_update_processed = false;\n            bool updating_intros = false;\n\n            std::chrono::milliseconds last_cc_update = 0s;\n            bool cc_ok = false;\n\n            // Chooses the next router id to pivot to, based on introset and current paths.  Returns\n            // nullopt if no pivot is available right now, otherwise the router id and the lifetime\n            // of paths to that pivot (so that we avoid creating paths that will become stale paths\n            // living beyond the expiry of the pivot).\n            std::optional<std::pair<RouterID, std::pair<std::chrono::seconds, HopID>>> select_pivot();\n\n            void select_new_current() override;\n\n          protected:\n            void handle_client_contact(std::span<const std::byte> payload) override;\n\n          public:\n            // Initiates a client intro lookup via the session endpoint.  This can be called even if\n            // there already is intros, to refresh/replace them.\n            void refresh_intros();\n\n            void tick(std::chrono::milliseconds now) override;\n\n            // Called with a client contact to replace the current set of client intros used by this\n            // session with the ones in the given client contact.  This is called by\n            // `refresh_intros()` upon a success fetch, but can also be called externally (such as\n            // when receiving intro updates through an existing session).\n            void update_intros(const ClientContact& cc);\n\n            void update_paths(std::chrono::milliseconds now) override;\n\n            void recv_close() override;\n\n            nlohmann::json ExtractStatus() const;\n\n            const RouterID& remote_endpoint() const { return _remote.router_id(); }\n        };\n\n        class InboundSession : public Session\n        {\n          protected:\n            InboundSession(handlers::SessionEndpoint& parent);\n\n            ~InboundSession() override = default;\n\n            void init(std::vector<std::byte>&& request);\n\n          public:\n            void session_init_accept();\n        };\n\n        // Inbound Session *to* client from client (we are the target client)\n        class InboundClientSession final : public InboundSession\n        {\n            std::shared_ptr<path::Path> _current_path;\n\n            void send_path_data_message(std::vector<std::byte>&& data, SymmNonce&& nonce) override;\n            void send_path_control_message(std::vector<std::byte>&& data, SymmNonce&& nonce, bool path_switch) override;\n\n          public:\n            InboundClientSession(\n                handlers::SessionEndpoint& parent, std::shared_ptr<path::Path> p, std::vector<std::byte>&& request);\n\n            void handle_path_switch(HopID pivot, std::shared_ptr<path::Path> path);\n\n            std::string to_string() const override;\n        };\n\n        // Inbound Session *to* relay from client (we are the target relay)\n        class InboundRelaySession final : public InboundSession\n        {\n            std::shared_ptr<path::TransitHop> _current_thop;\n\n            void encrypt_path_message(std::vector<std::byte>& data, SymmNonce&& nonce, std::byte type);\n\n            void send_path_data_message(std::vector<std::byte>&& data, SymmNonce&& nonce) override;\n\n            void send_path_control_message(std::vector<std::byte>&& data, SymmNonce&& nonce, bool path_switch) override;\n\n          public:\n            InboundRelaySession(\n                handlers::SessionEndpoint& parent,\n                std::shared_ptr<path::TransitHop> thop,\n                std::vector<std::byte>&& request);\n\n            void handle_path_switch(HopID pivot, std::shared_ptr<path::TransitHop> thop);\n\n            std::string to_string() const override;\n        };\n\n    }  // namespace session\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/simulation/sim_context.cpp",
    "content": "#include \"sim_context.hpp\"\n\n#include <llarp.hpp>\n\nnamespace llarp\n{\n    namespace simulate\n    {\n        Simulation::Simulation() : m_CryptoManager(new sodium::CryptoLibSodium()) {}\n\n        void Simulation::NodeUp(llarp::Context*) {}\n\n        Node_ptr Simulation::AddNode(const std::string& name)\n        {\n            auto itr = m_Nodes.find(name);\n            if (itr == m_Nodes.end())\n            {\n                itr = m_Nodes.emplace(name, std::make_shared<llarp::Context>(shared_from_this())).first;\n            }\n            return itr->second;\n        }\n\n        void Simulation::DelNode(const std::string& name) { m_Nodes.erase(name); }\n    }  // namespace simulate\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/simulation/sim_context.hpp",
    "content": "#pragma once\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/ev/ev.hpp>\n\nnamespace llarp\n{\n    // forward declair\n    struct Context;\n    using Node_ptr = std::shared_ptr<llarp::Context>;\n\n    namespace simulate\n    {\n        struct Simulation : public std::enable_shared_from_this<Simulation>\n        {\n            Simulation();\n\n            llarp::CryptoManager m_CryptoManager;\n            // std::shared_ptr<quic::Loop> m_NetLoop;\n\n            std::unordered_map<std::string, Node_ptr> m_Nodes;\n\n            void NodeUp(llarp::Context* node);\n\n            Node_ptr AddNode(const std::string& name);\n\n            void DelNode(const std::string& name);\n        };\n\n        using Sim_ptr = std::shared_ptr<Simulation>;\n    }  // namespace simulate\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/util/aligned.hpp",
    "content": "#pragma once\n\n#include <oxenc/base32z.h>\n#include <oxenc/bt.h>\n#include <oxenc/bt_serialize.h>\n#include <oxenc/hex.h>\n\n#include <algorithm>\n#include <array>\n#include <cstddef>\n#include <span>\n\nnamespace llarp\n{\n    /// aligned buffer that is sz bytes long and aligns to the nearest Alignment\n    template <size_t sz>\n    struct alignas(8) AlignedBuffer\n    {\n        static_assert(sz % 8 == 0, \"AlignedBuffer cannot be used with buffers that aren't a multiple of 8\");\n\n        static constexpr size_t SIZE = sz;\n\n        AlignedBuffer() { zero(); }\n\n        explicit AlignedBuffer(std::span<const uint8_t, SIZE> buf) { *this = buf; }\n        explicit AlignedBuffer(std::span<const std::byte, SIZE> buf) { *this = buf; }\n\n        AlignedBuffer& operator=(std::span<const uint8_t, SIZE> buf)\n        {\n            assign(buf);\n            return *this;\n        }\n        AlignedBuffer& operator=(std::span<const std::byte, SIZE> buf)\n        {\n            assign(buf);\n            return *this;\n        }\n        // Assigns to the aligned buffer contents from a spannable input of the same size\n        void assign(std::span<const uint8_t, SIZE> buf) { std::memcpy(_data.data(), buf.data(), SIZE); }\n        void assign(std::span<const std::byte, SIZE> buf) { std::memcpy(_data.data(), buf.data(), SIZE); }\n\n        // Copies the aligned buffer contents into a writeable span of the same size\n        void copy_to(std::span<std::byte, SIZE> buf) const { std::memcpy(buf.data(), _data.data(), SIZE); }\n        void copy_to(std::span<uint8_t, SIZE> buf) const { std::memcpy(buf.data(), _data.data(), SIZE); }\n\n        /// bitwise NOT\n        AlignedBuffer<sz> operator~() const\n        {\n            AlignedBuffer<sz> ret;\n            std::transform(begin(), end(), ret.begin(), [](uint8_t a) { return ~a; });\n\n            return ret;\n        }\n\n        auto operator<=>(const AlignedBuffer& other) const = default;\n        bool operator==(const AlignedBuffer& other) const = default;\n\n        AlignedBuffer operator^(const AlignedBuffer& other) const\n        {\n            AlignedBuffer<sz> ret;\n            std::transform(begin(), end(), other.begin(), ret.begin(), std::bit_xor<>());\n            return ret;\n        }\n\n        AlignedBuffer& operator^=(const AlignedBuffer& other)\n        {\n            // Mutate in place instead.\n            for (size_t i = 0; i < sz; ++i)\n            {\n                _data[i] ^= other._data[i];\n            }\n            return *this;\n        }\n\n        uint8_t& operator[](size_t idx)\n        {\n            assert(idx < SIZE);\n            return _data[idx];\n        }\n\n        const uint8_t& operator[](size_t idx) const\n        {\n            assert(idx < SIZE);\n            return _data[idx];\n        }\n\n        static constexpr size_t size() { return sz; }\n\n        void Fill(uint8_t f) { _data.fill(f); }\n\n        std::array<uint8_t, SIZE>& as_array() { return _data; }\n\n        const std::array<uint8_t, SIZE>& as_array() const { return _data; }\n\n        uint8_t* data() { return _data.data(); }\n\n        const uint8_t* data() const { return _data.data(); }\n\n        std::span<std::byte, SIZE> span()\n        {\n            return std::span<std::byte, SIZE>{reinterpret_cast<std::byte*>(_data.data()), SIZE};\n        }\n        std::span<const std::byte, SIZE> span() const\n        {\n            return std::span<const std::byte, SIZE>{reinterpret_cast<const std::byte*>(_data.data()), SIZE};\n        }\n\n        // Implicit conversion to span\n        operator std::span<std::byte, SIZE>() { return span(); }\n        operator std::span<std::byte>() { return span(); }\n        operator std::span<const std::byte, SIZE>() const { return span(); }\n        operator std::span<const std::byte>() const { return span(); }\n\n        // Shortcut for .span().first/last:\n        std::span<std::byte> first(size_t n) { return span().first(n); }\n        template <size_t N>\n            requires(N <= SIZE)\n        std::span<std::byte, N> first()\n        {\n            return span().template first<N>();\n        }\n        std::span<std::byte> last(size_t n) { return span().last(n); }\n        template <size_t N>\n            requires(N <= SIZE)\n        std::span<std::byte, N> last()\n        {\n            return span().template last<N>();\n        }\n\n        bool is_zero() const\n        {\n            const auto* ptr = reinterpret_cast<const uint64_t*>(data());\n            for (size_t idx = 0; idx < SIZE / sizeof(uint64_t); idx++)\n            {\n                if (ptr[idx])\n                    return false;\n            }\n            return true;\n        }\n\n        void zero() { _data.fill(0); }\n\n        typename std::array<uint8_t, SIZE>::iterator begin() { return _data.begin(); }\n\n        typename std::array<uint8_t, SIZE>::iterator end() { return _data.end(); }\n\n        typename std::array<uint8_t, SIZE>::const_iterator begin() const { return _data.cbegin(); }\n\n        typename std::array<uint8_t, SIZE>::const_iterator end() const { return _data.cend(); }\n\n        bool from_base32z(std::string_view b32z)\n        {\n            if (b32z.size() != oxenc::to_base32z_size(sz) || !oxenc::is_base32z(b32z))\n                return false;\n            oxenc::from_base32z(b32z.begin(), b32z.end(), _data.begin());\n            return true;\n        }\n\n        std::string bt_encode() const { return oxenc::bt_serialize(_data); }\n\n        bool bt_decode(std::string buf)\n        {\n            oxenc::bt_deserialize(buf, _data);\n            return true;\n        }\n\n        std::string_view to_view() const { return {reinterpret_cast<const char*>(data()), sz}; }\n\n        std::string ToHex() const { return oxenc::to_hex(begin(), end()); }\n\n        bool FromHex(std::string_view str)\n        {\n            if (str.size() != 2 * size() || !oxenc::is_hex(str))\n                return false;\n            oxenc::from_hex(str.begin(), str.end(), begin());\n            return true;\n        }\n\n        std::string to_string() const { return ToHex(); }\n        static constexpr bool to_string_formattable = true;\n\n        // Deferred conversion object meant for log statements to be able to log a shortened b32z\n        // value without needing to do the conversion when the log statement is skipped.\n        struct short_log_printer\n        {\n            const AlignedBuffer<sz>& buf;\n            std::string to_string() const { return oxenc::to_base32z(buf.begin(), buf.begin() + 5); }\n            static constexpr bool to_string_formattable = true;\n        };\n        // Used in log statements to log the value as its first 8 base32z characters:\n        short_log_printer short_string() const { return {*this}; }\n\n        template <typename T>\n            requires(std::derived_from<T, AlignedBuffer<T::SIZE>>)\n        static T filled(uint8_t f)\n        {\n            T ret;\n            ret.Fill(f);\n            return ret;\n        }\n\n      private:\n        std::array<uint8_t, SIZE> _data;\n    };\n\n    static_assert(sizeof(AlignedBuffer<32>) == 32, \"AlignedBuffer should have no overhead\");\n    static_assert(sizeof(AlignedBuffer<24>) == 24, \"AlignedBuffer should have no overhead\");\n    static_assert(sizeof(AlignedBuffer<8>) == 8, \"AlignedBuffer should have no overhead\");\n\n    struct AlignedHasher\n    {\n        // Hashing implementation that uses the raw data value held in an AlignedBuffer-derived\n        // class as the hash value.  This is only suitable for values that come from hashes or\n        // pubkeys where values are unlikely to be correlated.\n        template <typename T>\n            requires std::is_base_of_v<AlignedBuffer<sizeof(T)>, T>\n        std::size_t operator()(const T& buf) const noexcept\n        {\n            if constexpr (alignof(T) >= sizeof(size_t))\n                return *reinterpret_cast<const size_t*>(buf.data());\n            else\n            {\n                std::size_t h;\n                static_assert(T::SIZE >= sizeof(h));\n                std::memcpy(&h, buf.data(), sizeof(h));\n                return h;\n            }\n        }\n    };\n\n}  // namespace llarp\n\nnamespace std\n{\n    template <size_t sz>\n    struct hash<llarp::AlignedBuffer<sz>> : llarp::AlignedHasher\n    {};\n}  // namespace std\n"
  },
  {
    "path": "llarp/util/bspan.hpp",
    "content": "#pragma once\n\n#include <concepts>\n#include <cstddef>\n#include <cstdint>\n#include <span>\n#include <string>\n#include <string_view>\n\nnamespace llarp\n{\n\n    // Helper shims to convert a uint8_t span into a std::byte span of the same size/extent, and\n    // vice versa.\n\n    template <std::size_t Extent = std::dynamic_extent>\n    std::span<std::byte, Extent> as_bspan(std::span<uint8_t, Extent> s)\n    {\n        return std::span<std::byte, Extent>{reinterpret_cast<std::byte*>(s.data()), s.size()};\n    }\n    template <std::size_t Extent = std::dynamic_extent>\n    std::span<const std::byte, Extent> as_bspan(std::span<const uint8_t, Extent> s)\n    {\n        return std::span<const std::byte, Extent>{reinterpret_cast<const std::byte*>(s.data()), s.size()};\n    }\n\n    template <std::size_t Extent = std::dynamic_extent>\n    std::span<std::uint8_t, Extent> as_uspan(std::span<std::byte, Extent> s)\n    {\n        return std::span<std::uint8_t, Extent>{reinterpret_cast<std::uint8_t*>(s.data()), s.size()};\n    }\n    template <std::size_t Extent = std::dynamic_extent>\n    std::span<const std::uint8_t, Extent> as_uspan(std::span<const std::byte, Extent> s)\n    {\n        return std::span<const std::uint8_t, Extent>{reinterpret_cast<const std::uint8_t*>(s.data()), s.size()};\n    }\n\n    // Convert string_view into const byte span:\n    inline std::span<const std::byte> as_bspan(std::string_view s)\n    {\n        return {reinterpret_cast<const std::byte*>(s.data()), s.size()};\n    }\n\n    // Convert string into byte span:\n    inline std::span<std::byte> as_bspan(std::string& s) { return {reinterpret_cast<std::byte*>(s.data()), s.size()}; }\n\n    namespace detail\n    {\n        inline constexpr std::span<std::byte> split_span(std::byte*& data, size_t n)\n        {\n            std::span<std::byte> ret{data, n};\n            data += n;\n            return ret;\n        }\n        template <size_t Len>\n        constexpr std::span<std::byte, Len> split_span(std::byte*& data)\n        {\n            std::span<std::byte, Len> ret{data, Len};\n            data += Len;\n            return ret;\n        }\n    }  // namespace detail\n\n    // Split a span into subspans.  Takes n length values and returns an array of n+1 spans (the\n    // last contains everything beyond the last specified size).  Does *NOT* check length, you must\n    // ensure the input is no shorter than the sum of the input sizes!\n    //\n    // e.g. split_span(x, 1, 2) with x set to \"abcdef\" would give [\"a\", \"bc\", \"def\"].\n    template <std::convertible_to<size_t>... Lengths>\n        requires(sizeof...(Lengths) > 0)\n    constexpr std::array<std::span<std::byte>, sizeof...(Lengths) + 1> split_span(\n        std::span<std::byte> input, Lengths... n)\n    {\n        auto* data = input.data();\n        return {detail::split_span(data, n)..., {data, (input.size() - ... - n)}};\n    }\n\n    // Similar to split_span, except that it takes the 2nd through Nth sizes, and leaves the *first*\n    // size as dynamic rather than that last.  e.g. split_span_tail(x, 1, 2) with x set to\n    // \"abcdef\" would give [\"abc\", \"d\", \"ef\"].\n    template <std::convertible_to<size_t>... Lengths>\n    constexpr std::array<std::span<std::byte>, sizeof...(Lengths) + 1> split_span_tail(\n        std::span<std::byte> input, Lengths... n)\n    {\n        auto* data = input.data() + (input.size() - ... - n);\n        return {input.first((input.size() - ... - n)), detail::split_span(data, n)...};\n    }\n\n    // Similar to the above, but takes sizes as template arguments returning fixed-size spans\n    // (except for the tail piece).  The length of the input span is not checked and must be at\n    // least as large as the sum of the input lengths!\n    template <size_t... Lengths>\n        requires(sizeof...(Lengths) > 0)\n    constexpr std::tuple<std::span<std::byte, Lengths>..., std::span<std::byte>> split_span(std::span<std::byte> input)\n    {\n        auto* data = input.data();\n        return {detail::split_span<Lengths>(data)..., {data, (input.size() - ... - Lengths)}};\n    }\n\n    // Same as split_span_tail, except that it takes the 2nd through Nth sizes as template arguments\n    // and those elements in the returned tuple are fixed size spans of the requested sizes.  Input\n    // must be long enough to satisfy the requested sizes (this is not checked).\n    template <size_t... Lengths>\n        requires(sizeof...(Lengths) > 0)\n    constexpr std::tuple<std::span<std::byte>, std::span<std::byte, Lengths>...> split_span_tail(\n        std::span<std::byte> input)\n    {\n        auto* data = input.data() + (input.size() - ... - Lengths);\n        return {input.first((input.size() - ... - Lengths)), detail::split_span<Lengths>(data)...};\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/util/buffer.cpp",
    "content": "#include \"buffer.hpp\"\n\n#include <oxenc/endian.h>\n\n#include <cstdarg>\n#include <cstdio>\n\nnamespace\n{\n    template <typename UInt>\n    bool put(llarp_buffer_t& buf, UInt i)\n    {\n        if (buf.size_left() < sizeof(UInt))\n            return false;\n        oxenc::write_host_as_big(i, buf.cur);\n        buf.cur += sizeof(UInt);\n        return true;\n    }\n\n    template <typename UInt>\n    bool read(llarp_buffer_t& buf, UInt& i)\n    {\n        if (buf.size_left() < sizeof(UInt))\n            return false;\n        i = oxenc::load_big_to_host<UInt>(buf.cur);\n        buf.cur += sizeof(UInt);\n        return true;\n    }\n\n}  // namespace\n\nbool llarp_buffer_t::put_uint16(uint16_t i) { return put(*this, i); }\n\nbool llarp_buffer_t::put_uint64(uint64_t i) { return put(*this, i); }\n\nbool llarp_buffer_t::put_uint32(uint32_t i) { return put(*this, i); }\n\nbool llarp_buffer_t::read_uint16(uint16_t& i) { return read(*this, i); }\n\nbool llarp_buffer_t::read_uint32(uint32_t& i) { return read(*this, i); }\n\nbool llarp_buffer_t::read_uint64(uint64_t& i) { return read(*this, i); }\n\nstd::vector<uint8_t> llarp_buffer_t::copy() const\n{\n    std::vector<uint8_t> copy;\n    copy.resize(sz);\n    std::copy_n(base, sz, copy.data());\n\n    return copy;\n}\n"
  },
  {
    "path": "llarp/util/buffer.hpp",
    "content": "#pragma once\n\n#include <oxenc/common.h>\n\n#include <algorithm>\n#include <cassert>\n#include <cstdint>\n#include <cstdio>\n#include <cstdlib>\n#include <cstring>\n#include <iterator>\n#include <string>\n#include <type_traits>\n#include <utility>\n#include <vector>\n\n/// TODO: replace usage of these with std::span (via a backport until we move to C++20).  That's a\n/// fairly big job, though, as llarp_buffer_t is currently used a bit differently (i.e. maintains\n/// both start and current position, plus has some value reading/writing methods).\nstruct /* [[deprecated(\"this type is stupid, use something else\")]] */ llarp_buffer_t\n{\n    /// starting memory address\n    uint8_t* base{nullptr};\n    /// memory address of stream position\n    uint8_t* cur{nullptr};\n    /// max size of buffer\n    size_t sz{0};\n\n    uint8_t operator[](size_t x) { return *(this->base + x); }\n\n    llarp_buffer_t() = default;\n\n    llarp_buffer_t(uint8_t* b, uint8_t* c, size_t s) : base(b), cur(c), sz(s) {}\n\n    template <typename Byte>\n    static constexpr bool is_basic_byte = sizeof(Byte) == 1 and std::is_trivially_copyable_v<Byte>;\n\n    /// Construct referencing some 1-byte, trivially copyable (e.g. char, unsigned char, uint8_t)\n    /// pointer type and a buffer size.\n    template <typename Byte, typename = std::enable_if_t<not std::is_const_v<Byte> && is_basic_byte<Byte>>>\n    llarp_buffer_t(Byte* buf, size_t sz) : base{reinterpret_cast<uint8_t*>(buf)}, cur{base}, sz{sz}\n    {}\n\n    /// initialize llarp_buffer_t from vector or array of byte-like values\n    template <typename Byte, typename = std::enable_if_t<not std::is_const_v<Byte> && is_basic_byte<Byte>>>\n    llarp_buffer_t(std::vector<Byte>& b) : llarp_buffer_t{b.data(), b.size()}\n    {}\n\n    template <typename Byte, size_t N, typename = std::enable_if_t<not std::is_const_v<Byte> && is_basic_byte<Byte>>>\n    llarp_buffer_t(std::array<Byte, N>& b) : llarp_buffer_t{b.data(), b.size()}\n    {}\n\n    // These overloads, const_casting away the const, are not just gross but downright dangerous:\n    template <typename Byte, typename = std::enable_if_t<is_basic_byte<Byte>>>\n    llarp_buffer_t(const Byte* buf, size_t sz) : llarp_buffer_t{const_cast<Byte*>(buf), sz}\n    {}\n\n    template <typename Byte, typename = std::enable_if_t<is_basic_byte<Byte>>>\n    llarp_buffer_t(const std::vector<Byte>& b) : llarp_buffer_t{const_cast<Byte*>(b.data()), b.size()}\n    {}\n\n    template <typename Byte, size_t N, typename = std::enable_if_t<is_basic_byte<Byte>>>\n    llarp_buffer_t(const std::array<Byte, N>& b) : llarp_buffer_t{const_cast<Byte*>(b.data()), b.size()}\n    {}\n\n    /// Explicitly construct a llarp_buffer_t from anything with a `.data()` and a `.size()`.\n    /// Cursed.\n    template <typename T, typename = std::void_t<decltype(std::declval<T>().data() + std::declval<T>().size())>>\n    explicit llarp_buffer_t(T&& t) : llarp_buffer_t{t.data(), t.size()}\n    {}\n\n    std::string to_string() const { return {reinterpret_cast<const char*>(base), sz}; }\n\n    uint8_t* begin() { return base; }\n    const uint8_t* begin() const { return base; }\n    uint8_t* end() { return base + sz; }\n    const uint8_t* end() const { return base + sz; }\n\n    size_t size_left() const\n    {\n        size_t diff = cur - base;\n        assert(diff <= sz);\n        if (diff > sz)\n            return 0;\n        return sz - diff;\n    }\n\n    template <typename InputIt>\n    bool write(InputIt begin, InputIt end);\n\n    bool put_uint16(uint16_t i);\n    bool put_uint32(uint32_t i);\n\n    bool put_uint64(uint64_t i);\n\n    bool read_uint16(uint16_t& i);\n    bool read_uint32(uint32_t& i);\n\n    bool read_uint64(uint64_t& i);\n\n    /// make a copy of this buffer\n    std::vector<uint8_t> copy() const;\n\n  private:\n    llarp_buffer_t(const llarp_buffer_t&) = default;\n    llarp_buffer_t(llarp_buffer_t&&) = default;\n};\n\ntemplate <typename InputIt>\nbool llarp_buffer_t::write(InputIt begin, InputIt end)\n{\n    auto dist = std::distance(begin, end);\n    if (static_cast<decltype(dist)>(size_left()) >= dist)\n    {\n        cur = std::copy(begin, end, cur);\n        return true;\n    }\n    return false;\n}\n"
  },
  {
    "path": "llarp/util/common.hpp",
    "content": "#pragma once\n#ifdef __STRICT_ANSI__\n#define INLINE __inline__\n#else\n#define INLINE inline\n#endif\n\n#include <cstdint>\n#include <cstdlib>\n\n// clang-format off\n#if defined(__GNUC__) || defined(__clang__)\n    #define DO_PRAGMA(X)                    _Pragma(#X)\n    #define DISABLE_WARNING_PUSH            DO_PRAGMA(GCC diagnostics push)\n    #define DISABLE_WARNING_POP             DO_PRAGMA(GCC diagnostic pop)\n    #define DISABLE_WARNING(wname)          DO_PRAGMA(GCC diagnostic ignored #wname)\n#elif defined(_MSC_VER_)\n    #define DISABLE_WARNING_PUSH            __pragma(warning(push))\n    #define DISABLE_WARNING_POP             __pragma(warning(pop))\n#define DISABLE_WARNING(wnumber)            __pragma(warning(disable : wnumber))\n#else\n    // unknown compiler, ignore suppression directives\n    #define DISABLE_WARNING_PUSH\n    #define DISABLE_WARNING_POP\n#endif\n\n#if defined(__GNUC__) || defined(__clang__)\n    #define DISABLE_MAYBE_UNINITIALIZED     DISABLE_WARNING(-Wmaybe-uninitialized)\n#elif defined(_MSC_VER_)\n    #define DISABLE_MAYBE_UNINITIALIZED     DISABLE_WARNING(C4701)\n#else\n    #define DISABLE_MAYBE_UNINITIALIZED\n#endif\n// clang-format on\n"
  },
  {
    "path": "llarp/util/compare_ptr.hpp",
    "content": "#pragma once\n#include <functional>\n#include <memory>\n\nnamespace llarp\n{\n    /// type for comparing smart pointer's managed values\n    template <typename Ptr_t, typename Compare = std::less<>>\n    struct ComparePtr\n    {\n        bool operator()(const Ptr_t& left, const Ptr_t& right) const\n        {\n            if (left && right)\n                return Compare()(*left, *right);\n\n            return Compare()(left, right);\n        }\n    };\n\n    /// type for comparing weak_ptr by value\n    template <typename Type_t, typename Compare = std::less<>>\n    struct CompareWeakPtr\n    {\n        bool operator()(const std::weak_ptr<Type_t>& left, const std::weak_ptr<Type_t>& right) const\n        {\n            return ComparePtr<std::shared_ptr<Type_t>, Compare>{}(left.lock(), right.lock());\n        }\n    };\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/util/decaying_hashset.hpp",
    "content": "#pragma once\n\n#include \"time.hpp\"\n\n#include <unordered_map>\n\nnamespace llarp::util\n{\n    template <typename Val_t, typename Hash_t = std::hash<Val_t>>\n    struct DecayingHashSet\n    {\n        DecayingHashSet(std::chrono::milliseconds cacheInterval = 1s) : m_CacheInterval(cacheInterval) {}\n\n        size_t Size() const { return m_Values.size(); }\n\n        /// determine if we have v contained in our decaying hashset\n        bool Contains(const Val_t& v) const { return m_Values.count(v) != 0; }\n\n        /// return true if inserted\n        /// return false if not inserted\n        bool Insert(const Val_t& v, std::chrono::milliseconds now = 0s)\n        {\n            if (now == 0s)\n                now = llarp::time_now_ms();\n            return m_Values.try_emplace(v, now).second;\n        }\n\n        /// upsert will insert or update a value with time as now\n        void Upsert(const Val_t& v) { m_Values[v] = llarp::time_now_ms(); }\n\n        /// decay hashset entries\n        void Decay(std::chrono::milliseconds now = 0s)\n        {\n            if (now == 0s)\n                now = llarp::time_now_ms();\n            EraseIf([&](const auto& item) { return (m_CacheInterval + item.second) <= now; });\n        }\n\n        std::chrono::milliseconds DecayInterval() const { return m_CacheInterval; }\n\n        bool Empty() const { return m_Values.empty(); }\n\n        void DecayInterval(std::chrono::milliseconds interval) { m_CacheInterval = interval; }\n\n        void Remove(const Val_t& val) { m_Values.erase(val); }\n\n      private:\n        template <typename Predicate_t>\n        void EraseIf(Predicate_t pred)\n        {\n            for (auto i = m_Values.begin(), last = m_Values.end(); i != last;)\n            {\n                if (pred(*i))\n                {\n                    i = m_Values.erase(i);\n                }\n                else\n                {\n                    ++i;\n                }\n            }\n        }\n\n        std::chrono::milliseconds m_CacheInterval;\n        std::unordered_map<Val_t, std::chrono::milliseconds, Hash_t> m_Values;\n    };\n}  // namespace llarp::util\n"
  },
  {
    "path": "llarp/util/decaying_hashtable.hpp",
    "content": "#pragma once\n\n#include \"time.hpp\"\n\n#include <optional>\n#include <unordered_map>\n\nnamespace llarp::util\n{\n    template <typename Key_t, typename Value_t, typename Hash_t = std::hash<Key_t>>\n    struct DecayingHashTable\n    {\n        DecayingHashTable(std::chrono::milliseconds cacheInterval = 1h) : m_CacheInterval(cacheInterval) {}\n\n        void Decay(std::chrono::milliseconds now)\n        {\n            EraseIf([&](const auto& item) { return item.second.second + m_CacheInterval <= now; });\n        }\n\n        /// return if we have this value by key\n        bool Has(const Key_t& k) const { return m_Values.find(k) != m_Values.end(); }\n\n        /// return true if inserted\n        /// return false if not inserted\n        bool Put(Key_t key, Value_t value, std::chrono::milliseconds now = 0s)\n        {\n            if (now == 0s)\n                now = llarp::time_now_ms();\n            return m_Values.try_emplace(std::move(key), std::make_pair(std::move(value), now)).second;\n        }\n\n        /// get value by key\n        std::optional<Value_t> Get(Key_t k) const\n        {\n            const auto itr = m_Values.find(k);\n            if (itr == m_Values.end())\n                return std::nullopt;\n            return itr->second.first;\n        }\n\n        /// explicit remove an item from the cache by key\n        void Remove(const Key_t& key) { m_Values.erase(key); }\n\n      private:\n        template <typename Predicate_t>\n        void EraseIf(Predicate_t pred)\n        {\n            for (auto i = m_Values.begin(), last = m_Values.end(); i != last;)\n            {\n                if (pred(*i))\n                {\n                    i = m_Values.erase(i);\n                }\n                else\n                {\n                    ++i;\n                }\n            }\n        }\n\n        std::chrono::milliseconds m_CacheInterval;\n        std::unordered_map<Key_t, std::pair<Value_t, std::chrono::milliseconds>, Hash_t> m_Values;\n    };\n}  // namespace llarp::util\n"
  },
  {
    "path": "llarp/util/exceptions.hpp",
    "content": "#pragma once\n\nnamespace llarp::util\n{\n    class bind_socket_error : public std::runtime_error\n    {\n      public:\n        using std::runtime_error::runtime_error;\n    };\n}  // namespace llarp::util\n"
  },
  {
    "path": "llarp/util/file.cpp",
    "content": "#include \"file.hpp\"\n\n#include \"formattable.hpp\"\n#include \"logging.hpp\"\n\n#include <fcntl.h>\n\n#include <fstream>\n#include <ios>\n#include <stdexcept>\n#include <system_error>\n\n#ifdef WIN32\n#include <io.h>\n#else\n#include <unistd.h>\n#endif\n\nnamespace llarp::util\n{\n    static auto logcat = log::Cat(\"util.file\");\n\n    std::string file_to_string(const std::filesystem::path& filename, size_t max_size)\n    {\n        std::ifstream in;\n        std::string contents;\n\n        in.exceptions(std::ifstream::failbit | std::ifstream::badbit);\n        in.open(filename, std::ios::binary | std::ios::in);\n        in.seekg(0, std::ios::end);\n        auto size = in.tellg();\n        in.seekg(0, std::ios::beg);\n\n        if (auto sz = static_cast<size_t>(size); sz > max_size)\n            throw std::length_error{\n                \"Cannot load {}: file size {} exceeds max allowed size {}\"_format(filename, sz, max_size)};\n\n        contents.resize(size);\n        in.read(contents.data(), size);\n        return contents;\n    }\n\n    void buffer_to_file(const std::filesystem::path& filename, std::string_view contents)\n    {\n        std::ofstream out;\n        out.exceptions(std::ifstream::failbit | std::ifstream::badbit);\n        out.open(filename, std::ios::binary | std::ios::out | std::ios::trunc);\n        out.write(contents.data(), static_cast<std::streamsize>(contents.size()));\n    }\n\n    static std::error_code errno_error()\n    {\n        int e = errno;\n        errno = 0;\n        return std::make_error_code(static_cast<std::errc>(e));\n    }\n\n    error_code_t EnsurePrivateFile(const std::filesystem::path& pathname)\n    {\n        errno = 0;\n        error_code_t ec = errno_error();\n        const auto str = pathname.string();\n        if (exists(pathname, ec))  // file exists\n        {\n            permissions(pathname, std::filesystem::perms::owner_read | std::filesystem::perms::owner_write, ec);\n            if (ec)\n                log::error(logcat, \"failed to set permissions on {}\", pathname);\n        }\n        else  // file is not there\n        {\n            errno = 0;\n            int fd = ::open(str.c_str(), O_RDWR | O_CREAT, 0600);\n            ec = errno_error();\n            if (fd != -1)\n            {\n                ::close(fd);\n            }\n        }\n\n#ifndef WIN32\n        if (ec)\n            log::error(logcat, \"failed to ensure {}: {}\", str, ec.message());\n        return ec;\n#else\n        return {};\n#endif\n    }\n\n}  // namespace llarp::util\n"
  },
  {
    "path": "llarp/util/file.hpp",
    "content": "#pragma once\n\n#include <filesystem>\n#include <limits>\n#include <optional>\n#include <string>\n#include <string_view>\n\n#ifndef _MSC_VER\n#include <dirent.h>\n#endif\n\n#include <oxenc/common.h>\n\nnamespace llarp::util\n{\n    /// Reads a binary file from disk into a string.  Throws on error.\n    std::string file_to_string(\n        const std::filesystem::path& filename, size_t max_size = std::numeric_limits<size_t>::max());\n\n    /// Reads a binary file from disk directly into a buffer.  Throws a std::length_error if the\n    /// file is bigger than the buffer.  Returns the bytes copied on success.\n    size_t file_to_buffer(const std::filesystem::path& filename, char* buffer, size_t buffer_size);\n\n    /// Dumps binary string contents to disk. The file is overwritten if it already exists.  Throws\n    /// on error.\n    void buffer_to_file(const std::filesystem::path& filename, std::string_view contents);\n\n    struct FileHash\n    {\n        size_t operator()(const std::filesystem::path& f) const\n        {\n            std::hash<std::string> h;\n            return h(f.string());\n        }\n    };\n\n    using error_code_t = std::error_code;\n\n    /// Ensure that a file exists and has correct permissions\n    /// return any error code or success\n    error_code_t EnsurePrivateFile(const std::filesystem::path& pathname);\n\n    /// open a stream to a file and ensure it exists before open\n    /// sets any permissions on creation\n    template <typename T>\n    std::optional<T> OpenFileStream(const std::filesystem::path& pathname, std::ios::openmode mode)\n    {\n        if (EnsurePrivateFile(pathname))\n            return {};\n        return std::make_optional<T>(pathname, mode);\n    }\n\n}  // namespace llarp::util\n"
  },
  {
    "path": "llarp/util/formattable.hpp",
    "content": "#pragma once\n\n#include <fmt/ranges.h>\n#include <fmt/std.h>\n#include <oxen/log/format.hpp>\n#include <oxen/quic/format.hpp>\n#include <oxen/quic/formattable.hpp>\n\nnamespace llarp\n{\n    using namespace std::literals;\n    using namespace oxen::log::literals;\n\n}  // namespace llarp\n\nnamespace fmt\n{\n    // Make sure that fmt doesn't interpret our custom formattable types as range formattable, which\n    // results in ambiguous overloads:\n    template <oxen::quic::ToStringFormattable T>\n    struct is_range<T, char>\n    {\n        static constexpr bool value = false;\n    };\n}  // namespace fmt\n\n// fmt added optional support in version 10.0.0\n#if FMT_VERSION < 100000\n\n#include <optional>\n\nnamespace fmt\n{\n    template <typename T, typename Char>\n    struct formatter<std::optional<T>, Char, std::enable_if_t<is_formattable<T, Char>::value>>\n    {\n      private:\n        formatter<T, Char> underlying_;\n        static constexpr basic_string_view<Char> optional =\n            detail::string_literal<Char, 'o', 'p', 't', 'i', 'o', 'n', 'a', 'l', '('>{};\n        static constexpr basic_string_view<Char> none = detail::string_literal<Char, 'n', 'o', 'n', 'e'>{};\n\n        template <class U>\n        FMT_CONSTEXPR static auto maybe_set_debug_format(U& u, bool set) -> decltype(u.set_debug_format(set))\n        {\n            u.set_debug_format(set);\n        }\n\n        template <class U>\n        FMT_CONSTEXPR static void maybe_set_debug_format(U&, ...)\n        {}\n\n      public:\n        template <typename ParseContext>\n        FMT_CONSTEXPR auto parse(ParseContext& ctx)\n        {\n            maybe_set_debug_format(underlying_, true);\n            return underlying_.parse(ctx);\n        }\n\n        template <typename FormatContext>\n        auto format(const std::optional<T>& opt, FormatContext& ctx) const -> decltype(ctx.out())\n        {\n            if (!opt)\n                return detail::write<Char>(ctx.out(), none);\n\n            auto out = ctx.out();\n            out = detail::write<Char>(out, optional);\n            ctx.advance_to(out);\n            out = underlying_.format(*opt, ctx);\n            return detail::write(out, ')');\n        }\n    };\n}  //  namespace fmt\n\n#endif\n"
  },
  {
    "path": "llarp/util/logging/buffer.hpp",
    "content": "#pragma once\n\n#include <oxen/quic/format.hpp>\n\nnamespace llarp\n{\n    using oxen::quic::buffer_printer;\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/util/logging/callback_sink.hpp",
    "content": "#include <lokinet/lokinet_misc.h>\n#include <spdlog/details/null_mutex.h>\n#include <spdlog/sinks/base_sink.h>\n\n#include <mutex>\n\nnamespace llarp::logging\n{\n    // Logger that calls a C function with the formatted log message\n    template <typename Mutex>\n    class CallbackSink : public spdlog::sinks::base_sink<Mutex>\n    {\n      private:\n        lokinet_logger_func log_;\n        lokinet_logger_sync sync_;\n        void* ctx_;\n\n      public:\n        explicit CallbackSink(lokinet_logger_func log, lokinet_logger_sync sync = nullptr, void* context = nullptr)\n            : log_{log}, sync_{sync}, ctx_{context}\n        {}\n\n      protected:\n        void sink_it_(const spdlog::details::log_msg& msg) override\n        {\n            if (!log_)\n                return;\n            spdlog::memory_buf_t formatted;\n            spdlog::sinks::base_sink<Mutex>::formatter_->format(msg, formatted);\n            log_(fmt::to_string(formatted).c_str(), ctx_);\n        }\n\n        void flush_() override\n        {\n            if (sync_)\n                sync_(ctx_);\n        }\n    };\n\n    // Convenience aliases with or without thread safety\n    using CallbackSink_mt = CallbackSink<std::mutex>;\n    using CallbackSink_st = CallbackSink<spdlog::details::null_mutex>;\n\n}  // namespace llarp::logging\n"
  },
  {
    "path": "llarp/util/logging.cpp",
    "content": "#include \"logging.hpp\"\n\n#include <oxen/log/catlogger.hpp>\n#include <oxen/log/ring_buffer_sink.hpp>\n\nnamespace llarp\n{\n\n    log::CategoryLogger log_global = log::Cat(\"lokinet\");\n\n    std::shared_ptr<log::RingBufferSink> logRingBuffer{};\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/util/logging.hpp",
    "content": "#pragma once\n\n// Header for making actual log statements such as llarp::log::Info and so on work.\n\n#include <oxen/log.hpp>\n#include <oxen/log/catlogger.hpp>\n#include <oxen/log/ring_buffer_sink.hpp>\n\nnamespace llarp\n{\n    namespace log = oxen::log;\n\n    // Special \"global\" category that defaults to info level logging if not specifically overridden\n    // (i.e. even if using a warning or higher global log level).\n    extern log::CategoryLogger log_global;\n\n    extern std::shared_ptr<log::RingBufferSink> logRingBuffer;\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/util/lokinet_init.h",
    "content": "#pragma once\n\n#ifdef __cplusplus\nextern \"C\"\n{\n#endif\n\n    int Lokinet_INIT(void);\n\n#ifdef __cplusplus\n}\n#endif\n"
  },
  {
    "path": "llarp/util/mem.cpp",
    "content": "#define NO_JEMALLOC\n#include \"mem.h\"\n\n#include <cstdlib>\n\nnamespace llarp\n{\n    void Zero(void* ptr, size_t sz)\n    {\n        auto* p = (uint8_t*)ptr;\n        while (sz--)\n        {\n            *p = 0;\n            ++p;\n        }\n    }\n}  // namespace llarp\n\nvoid llarp_mem_slab(struct llarp_alloc* /*mem*/, uint32_t* /*buf*/, size_t /*sz*/)\n{\n    // not implemented\n    abort();\n}\n\nbool llarp_eq(const void* a, const void* b, size_t sz)\n{\n    bool result = true;\n    const auto* a_ptr = (const uint8_t*)a;\n    const auto* b_ptr = (const uint8_t*)b;\n    while (sz--)\n    {\n        result &= a_ptr[sz] == b_ptr[sz];\n    }\n    return result;\n}\n"
  },
  {
    "path": "llarp/util/mem.h",
    "content": "#pragma once\n\n#include <cstdint>\n#include <cstdlib>\n\n/** constant time memcmp */\nbool llarp_eq(const void* a, const void* b, size_t sz);\n"
  },
  {
    "path": "llarp/util/mem.hpp",
    "content": "#pragma once\n\n#include \"buffer.hpp\"\n#include \"mem.h\"\n\n#include <cctype>\n#include <cstdio>\n#include <memory>\n\nnamespace llarp\n{\n    void Zero(void* ptr, size_t sz);\n\n    template <typename T>\n    void dumphex(const uint8_t* t)\n    {\n        size_t idx = 0;\n        while (idx < sizeof(T))\n        {\n            printf(\"%.2x \", t[idx++]);\n            if (idx % 8 == 0)\n                printf(\"\\n\");\n        }\n    }\n\n    template <typename T, size_t align = 128>\n    void DumpBufferHex(const T& buff)\n    {\n        size_t idx = 0;\n        printf(\"buffer of size %zu\\n\", buff.sz);\n        while (idx < buff.sz)\n        {\n            if (buff.base + idx == buff.cur)\n            {\n#ifndef _WIN32\n                printf(\"%c[1;31m\", 27);\n#endif\n            }\n            printf(\"%.2x\", buff.base[idx]);\n            if (buff.base + idx == buff.cur)\n            {\n#ifndef _WIN32\n                printf(\"%c[0;0m\", 27);\n#endif\n            }\n            ++idx;\n            if (idx % align == 0)\n                printf(\"\\n\");\n        }\n        printf(\"\\n\");\n        fflush(stdout);\n    }\n\n    template <typename T, size_t align = 128>\n    void DumpBuffer(const T& buff)\n    {\n        size_t idx = 0;\n        printf(\"buffer of size %zu\\n\", buff.sz);\n        while (idx < buff.sz)\n        {\n            if (buff.base + idx == buff.cur)\n            {\n#ifndef _WIN32\n                printf(\"%c[1;31m\", 27);\n#endif\n            }\n            if (std::isprint(buff.base[idx]))\n            {\n                printf(\"%c\", buff.base[idx]);\n            }\n            else\n            {\n                printf(\".\");\n            }\n            if (buff.base + idx == buff.cur)\n            {\n#ifndef _WIN32\n                printf(\"%c[0;0m\", 27);\n#endif\n            }\n            ++idx;\n            if (idx % align == 0)\n                printf(\"\\n\");\n        }\n        printf(\"\\n\");\n        fflush(stdout);\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/util/nop_service_manager.cpp",
    "content": "#include \"service_manager.hpp\"\n\nnamespace llarp::sys\n{\n    NOP_SystemLayerHandler _manager{};\n    I_SystemLayerManager* const service_manager = &_manager;\n}  // namespace llarp::sys\n"
  },
  {
    "path": "llarp/util/random.hpp",
    "content": "#pragma once\n\n#include <sodium/randombytes.h>\n\n#include <limits>\n#include <span>\n\nnamespace llarp\n{\n    /// RNG type that produces cryptographically secure random values.\n    struct CSRNG\n    {\n        using result_type = uint64_t;\n\n        static constexpr uint64_t min() { return std::numeric_limits<uint64_t>::min(); }\n\n        static constexpr uint64_t max() { return std::numeric_limits<uint64_t>::max(); }\n\n        uint64_t operator()()\n        {\n            uint64_t i;\n            randombytes_buf(&i, sizeof(i));\n            return i;\n        }\n    };\n\n    extern CSRNG csrng;\n\n    inline void random_fill(std::span<std::byte> s) { randombytes_buf(s.data(), s.size()); }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/util/service_manager.hpp",
    "content": "#pragma once\n\nnamespace llarp\n{\n    struct Context;\n}\n\nnamespace llarp::sys\n{\n\n    // what state lokinet will report we are in to the system layer\n    enum class ServiceState\n    {\n        Initial,\n        Starting,\n        Running,\n        Stopping,\n        Stopped,\n        HardStop,\n        Failed,\n    };\n\n    /// interface type for interacting with the os dependant system layer\n    class I_SystemLayerManager\n    {\n      protected:\n        bool m_disable{false};\n        llarp::Context* m_Context{nullptr};\n\n        /// change our state and report it to the system layer\n        virtual void we_changed_our_state(ServiceState st) = 0;\n\n      public:\n        virtual ~I_SystemLayerManager() = default;\n\n        /// disable all reporting to system layer\n        inline void disable() { m_disable = true; }\n\n        /// give our current lokinet context to the system layer manager\n        inline void give_context(llarp::Context* ctx) { m_Context = ctx; }\n\n        /// system told us to enter this state\n        virtual void system_changed_our_state(ServiceState st) = 0;\n\n        /// report our current state to the system layer\n        virtual void report_changed_state() = 0;\n\n        /// report our stats on each timer tick\n        virtual void report_periodic_stats() {};\n\n        void starting()\n        {\n            if (m_disable)\n                return;\n            we_changed_our_state(ServiceState::Starting);\n        }\n\n        void ready()\n        {\n            if (m_disable)\n                return;\n            we_changed_our_state(ServiceState::Running);\n        }\n\n        void stopping()\n        {\n            if (m_disable)\n                return;\n            we_changed_our_state(ServiceState::Stopping);\n        }\n\n        void stopped()\n        {\n            if (m_disable)\n                return;\n            we_changed_our_state(ServiceState::Stopped);\n        }\n\n        void failed()\n        {\n            if (m_disable)\n                return;\n            we_changed_our_state(ServiceState::Failed);\n        }\n    };\n\n    extern I_SystemLayerManager* const service_manager;\n\n    class NOP_SystemLayerHandler : public I_SystemLayerManager\n    {\n      protected:\n        void we_changed_our_state(ServiceState) override {}\n\n      public:\n        void report_changed_state() override {};\n        void system_changed_our_state(ServiceState) override{};\n    };\n}  // namespace llarp::sys\n"
  },
  {
    "path": "llarp/util/str.cpp",
    "content": "#include \"str.hpp\"\n\n#include <cstring>\n#include <string>\n\n#ifdef _WIN32\n#include <llarp/win32/exception.hpp>\n\n#include <windows.h>\n\n#include <stringapiset.h>\n#endif\n\nnamespace llarp\n{\n    static constexpr char whitespace[] = \" \\t\\n\\r\\f\\v\";\n\n    std::string_view TrimWhitespace(std::string_view str)\n    {\n        size_t begin = str.find_first_not_of(whitespace);\n        if (begin == std::string_view::npos)\n        {\n            str.remove_prefix(str.size());\n            return str;\n        }\n        str.remove_prefix(begin);\n\n        size_t end = str.find_last_not_of(whitespace);\n        if (end != std::string_view::npos)\n            str.remove_suffix(str.size() - end - 1);\n\n        return str;\n    }\n\n    std::vector<std::string_view> split(std::string_view str, const std::string_view delim, bool trim)\n    {\n        std::vector<std::string_view> results;\n        // Special case for empty delimiter: splits on each character boundary:\n        if (delim.empty())\n        {\n            results.reserve(str.size());\n            for (size_t i = 0; i < str.size(); i++)\n                results.emplace_back(str.data() + i, 1);\n            return results;\n        }\n\n        for (size_t pos = str.find(delim); pos != std::string_view::npos; pos = str.find(delim))\n        {\n            if (!trim || !results.empty() || pos > 0)\n                results.push_back(str.substr(0, pos));\n            str.remove_prefix(pos + delim.size());\n        }\n        if (!trim || str.size())\n            results.push_back(str);\n        else\n            while (!results.empty() && results.back().empty())\n                results.pop_back();\n        return results;\n    }\n\n    std::vector<std::string_view> split_any(std::string_view str, const std::string_view delims, bool trim)\n    {\n        if (delims.empty())\n            return split(str, delims, trim);\n        std::vector<std::string_view> results;\n        for (size_t pos = str.find_first_of(delims); pos != std::string_view::npos; pos = str.find_first_of(delims))\n        {\n            if (!trim || !results.empty() || pos > 0)\n                results.push_back(str.substr(0, pos));\n            size_t until = str.find_first_not_of(delims, pos + 1);\n            if (until == std::string_view::npos)\n                str.remove_prefix(str.size());\n            else\n                str.remove_prefix(until);\n        }\n        if (!trim || str.size())\n            results.push_back(str);\n        else\n            while (!results.empty() && results.back().empty())\n                results.pop_back();\n        return results;\n    }\n\n    std::string lowercase_ascii_string(std::string src)\n    {\n        for (char& ch : src)\n            if (ch >= 'A' && ch <= 'Z')\n                ch = ch + ('a' - 'A');\n        return src;\n    }\n\n    std::wstring to_wide(std::string data)\n    {\n        std::wstring buf;\n        buf.resize(data.size());\n#ifdef _WIN32\n        // win32 specific codepath because balmer made windows so that man may suffer\n        if (MultiByteToWideChar(CP_UTF8, 0, data.c_str(), data.size(), buf.data(), buf.size()) == 0)\n            throw win32::error{GetLastError(), \"failed to convert string to wide string\"};\n\n#else\n        // this dumb but probably works (i guess?)\n        std::transform(data.begin(), data.end(), buf.begin(), [](const auto& ch) -> wchar_t { return ch; });\n#endif\n        return buf;\n    }\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/util/str.hpp",
    "content": "#pragma once\n\n#include <fmt/format.h>\n#include <fmt/ranges.h>\n\n#include <algorithm>\n#include <charconv>\n#include <chrono>\n#include <iterator>\n#include <string_view>\n#include <vector>\n\nnamespace llarp\n{\n    /// Returns true if the first argument begins with the second argument\n    inline constexpr bool starts_with(std::string_view str, std::string_view prefix)\n    {\n        return str.substr(0, prefix.size()) == prefix;\n    }\n\n    /// Returns true if the first argument ends with the second argument\n    inline constexpr bool ends_with(std::string_view str, std::string_view suffix)\n    {\n        return str.size() >= suffix.size() && str.substr(str.size() - suffix.size()) == suffix;\n    }\n\n    /// removes a prefix from a string if it exists\n    inline constexpr std::string_view strip_prefix(std::string_view str, std::string_view prefix)\n    {\n        if (starts_with(str, prefix))\n            return str.substr(prefix.size());\n        return str;\n    }\n\n    /// Splits a string on some delimiter string and returns a vector of string_view's pointing into\n    /// the pieces of the original string.  The pieces are valid only as long as the original string\n    /// remains valid.  Leading and trailing empty substrings are not removed.  If delim is empty\n    /// you get back a vector of string_views each viewing one character.  If `trim` is true then\n    /// leading and trailing empty values will be suppressed.\n    ///\n    ///     auto v = split(\"ab--c----de\", \"--\"); // v is {\"ab\", \"c\", \"\", \"de\"}\n    ///     auto v = split(\"abc\", \"\"); // v is {\"a\", \"b\", \"c\"}\n    ///     auto v = split(\"abc\", \"c\"); // v is {\"ab\", \"\"}\n    ///     auto v = split(\"abc\", \"c\", true); // v is {\"ab\"}\n    ///     auto v = split(\"-a--b--\", \"-\"); // v is {\"\", \"a\", \"\", \"b\", \"\", \"\"}\n    ///     auto v = split(\"-a--b--\", \"-\", true); // v is {\"a\", \"\", \"b\"}\n    ///\n    std::vector<std::string_view> split(std::string_view str, std::string_view delim, bool trim = false);\n\n    /// Splits a string on any 1 or more of the given delimiter characters and returns a vector of\n    /// string_view's pointing into the pieces of the original string.  If delims is empty this\n    /// works the same as split().  `trim` works like split (suppresses leading and trailing empty\n    /// string pieces).\n    ///\n    ///     auto v = split_any(\"abcdedf\", \"dcx\"); // v is {\"ab\", \"e\", \"f\"}\n    std::vector<std::string_view> split_any(std::string_view str, std::string_view delims, bool trim = false);\n\n    /// Joins [begin, end) with a delimiter and returns the resulting string.  Elements can be\n    /// anything that is fmt formattable.\n    template <typename It>\n    std::string join(std::string_view delimiter, It begin, It end)\n    {\n        return fmt::format(\"{}\", fmt::join(delimiter, begin, end));\n    }\n\n    /// Wrapper around the above that takes a container and passes c.begin(), c.end() to the above.\n    template <typename Container>\n    std::string join(std::string_view delimiter, const Container& c)\n    {\n        return join(delimiter, c.begin(), c.end());\n    }\n\n    /// Parses an integer of some sort from a string, requiring that the entire string be consumed\n    /// during parsing.  Return false if parsing failed, sets `value` and returns true if the entire\n    /// string was consumed.\n    template <typename T>\n    bool parse_int(const std::string_view str, T& value, int base = 10)\n    {\n        T tmp;\n        auto* strend = str.data() + str.size();\n        auto [p, ec] = std::from_chars(str.data(), strend, tmp, base);\n        if (ec != std::errc() || p != strend)\n            return false;\n        value = tmp;\n        return true;\n    }\n\n    std::string lowercase_ascii_string(std::string src);\n\n    std::string_view TrimWhitespace(std::string_view str);\n\n    /// convert a \"normal\" string into a wide string\n    std::wstring to_wide(std::string data);\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/util/thread/barrier.hpp",
    "content": "#pragma once\n#include <condition_variable>\n#include <mutex>\n\nnamespace llarp\n{\n    namespace util\n    {\n        /// Barrier class that blocks all threads until the high water mark of\n        /// threads (set during construction) is reached, then releases them all.\n        class Barrier\n        {\n            std::mutex mutex;\n            std::condition_variable cv;\n            unsigned pending;\n\n          public:\n            Barrier(unsigned threads) : pending{threads} {}\n\n            /// Returns true if *this* Block call is the one that releases all of\n            /// them; returns false (i.e. after unblocking) if some other thread\n            /// triggered the released.\n            bool Block()\n            {\n                std::unique_lock lock{mutex};\n                if (pending == 1)\n                {\n                    pending = 0;\n                    lock.unlock();\n                    cv.notify_all();\n                    return true;\n                }\n                else if (pending > 1)\n                {\n                    pending--;\n                }\n                cv.wait(lock, [this] { return !pending; });\n                return false;\n            }\n        };\n\n    }  // namespace util\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/util/thread/queue.hpp",
    "content": "#pragma once\n\n#include \"queue_manager.hpp\"\n#include \"threading.hpp\"\n\n#include <atomic>\n#include <optional>\n#include <tuple>\n\nnamespace llarp::thread\n{\n    template <typename Type>\n    class QueuePushGuard;\n    template <typename Type>\n    class QueuePopGuard;\n\n    template <typename Type>\n    class Queue\n    {\n        // This class provides a thread-safe, lock-free, fixed-size queue.\n      public:\n        static constexpr size_t Alignment{64};\n\n      private:\n        Type* m_data;\n        const char m_dataPadding[Alignment - sizeof(Type*)];\n\n        QueueManager m_manager;\n\n        std::atomic<std::uint32_t> m_waitingPoppers;\n        util::Semaphore m_popSemaphore;\n        const char m_popSemaphorePadding[(2u * Alignment) - sizeof(util::Semaphore)];\n\n        std::atomic<std::uint32_t> m_waitingPushers;\n        util::Semaphore m_pushSemaphore;\n        const char m_pushSemaphorePadding[(2u * Alignment) - sizeof(util::Semaphore)];\n\n        friend QueuePopGuard<Type>;\n        friend QueuePushGuard<Type>;\n\n      public:\n        explicit Queue(size_t capacity);\n\n        ~Queue();\n\n        Queue(const Queue&) = delete;\n        Queue& operator=(const Queue&) = delete;\n\n        // Push back to the queue, blocking until space is available (if\n        // required). Will fail if the queue is disabled (or becomes disabled\n        // while waiting for space on the queue).\n        QueueReturn pushBack(const Type& value);\n\n        QueueReturn pushBack(Type&& value);\n\n        // Try to push back to the queue. Return false if the queue is full or\n        // disabled.\n        QueueReturn tryPushBack(const Type& value);\n\n        QueueReturn tryPushBack(Type&& value);\n\n        // Remove an element from the queue. Block until an element is available\n        Type popFront();\n\n        // Remove an element from the queue. Block until an element is available\n        // or until <timeout> microseconds have elapsed\n        std::optional<Type> popFrontWithTimeout(std::chrono::microseconds timeout);\n\n        std::optional<Type> tryPopFront();\n\n        // Remove all elements from the queue. Note this is not atomic, and if\n        // other threads `pushBack` onto the queue during this call, the `size` of\n        // the queue is not guaranteed to be 0.\n        void removeAll();\n\n        // Disable the queue. All push operations will fail \"fast\" (including\n        // blocked operations). Calling this method on a disabled queue has no\n        // effect.\n        void disable();\n\n        // Enable the queue. Calling this method on a disabled queue has no\n        // effect.\n        void enable();\n\n        size_t capacity() const;\n\n        size_t size() const;\n\n        bool enabled() const;\n\n        bool full() const;\n\n        bool empty() const;\n    };\n\n    // Provide a guard class to provide exception safety for pushing to a queue.\n    // On destruction, unless the `release` method has been called, will remove\n    // and destroy all elements from the queue, putting the queue into an empty\n    // state.\n    template <typename Type>\n    class QueuePushGuard\n    {\n      private:\n        Queue<Type>* m_queue;\n        uint32_t m_generation;\n        uint32_t m_index;\n\n      public:\n        QueuePushGuard(Queue<Type>& queue, uint32_t generation, uint32_t index)\n            : m_queue(&queue), m_generation(generation), m_index(index)\n        {}\n\n        ~QueuePushGuard();\n\n        void release();\n    };\n\n    // Provide a guard class to provide exception safety for popping from a\n    // queue. On destruction, this will pop the the given element from the\n    // queue.\n    template <typename Type>\n    class QueuePopGuard\n    {\n      private:\n        Queue<Type>& m_queue;\n        uint32_t m_generation;\n        uint32_t m_index;\n\n      public:\n        QueuePopGuard(Queue<Type>& queue, uint32_t generation, uint32_t index)\n            : m_queue(queue), m_generation(generation), m_index(index)\n        {}\n\n        ~QueuePopGuard();\n    };\n\n    template <typename Type>\n    Queue<Type>::Queue(size_t capacity)\n        : m_data(nullptr),\n          m_dataPadding(),\n          m_manager(capacity),\n          m_waitingPoppers(0),\n          m_popSemaphore(0),\n          m_popSemaphorePadding(),\n          m_waitingPushers(0),\n          m_pushSemaphore(0),\n          m_pushSemaphorePadding()\n    {\n        m_data = static_cast<Type*>(::operator new(capacity * sizeof(Type)));\n    }\n\n    template <typename Type>\n    Queue<Type>::~Queue()\n    {\n        removeAll();\n\n        // We have already deleted the queue members above, free as (void *)\n        ::operator delete(static_cast<void*>(m_data));\n    }\n\n    template <typename Type>\n    QueueReturn Queue<Type>::tryPushBack(const Type& value)\n    {\n        uint32_t generation = 0;\n        uint32_t index = 0;\n\n        // Sync point A\n        //\n        // The next call writes with full sequential consistency to the push\n        // index, which guarantees that the relaxed read to the waiting poppers\n        // count sees any waiting poppers from Sync point B.\n\n        QueueReturn retVal = m_manager.reservePushIndex(generation, index);\n\n        if (retVal != QueueReturn::Success)\n        {\n            return retVal;\n        }\n\n        // Copy into the array. If the copy constructor throws, the pushGuard will\n        // roll the reserve back.\n\n        QueuePushGuard<Type> pushGuard(*this, generation, index);\n\n        // Construct in place.\n        ::new (&m_data[index]) Type(value);\n\n        pushGuard.release();\n\n        m_manager.commitPushIndex(generation, index);\n\n        if (m_waitingPoppers > 0)\n        {\n            m_popSemaphore.notify();\n        }\n\n        return QueueReturn::Success;\n    }\n\n    template <typename Type>\n    QueueReturn Queue<Type>::tryPushBack(Type&& value)\n    {\n        uint32_t generation = 0;\n        uint32_t index = 0;\n\n        // Sync point A\n        //\n        // The next call writes with full sequential consistency to the push\n        // index, which guarantees that the relaxed read to the waiting poppers\n        // count sees any waiting poppers from Sync point B.\n\n        QueueReturn retVal = m_manager.reservePushIndex(generation, index);\n\n        if (retVal != QueueReturn::Success)\n        {\n            return retVal;\n        }\n\n        // Copy into the array. If the copy constructor throws, the pushGuard will\n        // roll the reserve back.\n\n        QueuePushGuard<Type> pushGuard(*this, generation, index);\n\n        Type& dummy = value;\n\n        // Construct in place.\n        ::new (&m_data[index]) Type(std::move(dummy));\n\n        pushGuard.release();\n\n        m_manager.commitPushIndex(generation, index);\n\n        if (m_waitingPoppers > 0)\n        {\n            m_popSemaphore.notify();\n        }\n\n        return QueueReturn::Success;\n    }\n\n    template <typename Type>\n    std::optional<Type> Queue<Type>::tryPopFront()\n    {\n        uint32_t generation;\n        uint32_t index;\n\n        // Sync Point C.\n        //\n        // The call to reservePopIndex writes with full *sequential* consistency,\n        // which guarantees the relaxed read to waiting poppers is synchronized\n        // with Sync Point D.\n\n        QueueReturn retVal = m_manager.reservePopIndex(generation, index);\n\n        if (retVal != QueueReturn::Success)\n        {\n            return {};\n        }\n\n        // Pop guard will (even if the move/copy constructor throws)\n        // - destroy the original object\n        // - update the queue\n        // - notify any waiting pushers\n\n        QueuePopGuard<Type> popGuard(*this, generation, index);\n        return std::optional<Type>(std::move(m_data[index]));\n    }\n\n    template <typename Type>\n    QueueReturn Queue<Type>::pushBack(const Type& value)\n    {\n        for (;;)\n        {\n            QueueReturn retVal = tryPushBack(value);\n\n            switch (retVal)\n            {\n                // Queue disabled.\n                case QueueReturn::QueueDisabled:\n                // We pushed the value back\n                case QueueReturn::Success:\n                    return retVal;\n                default:\n                    // continue on.\n                    break;\n            }\n\n            m_waitingPushers.fetch_add(1, std::memory_order_relaxed);\n\n            // Sync Point B.\n            //\n            // The call to `full` below loads the push index with full *sequential*\n            // consistency, which gives visibility of the change above to\n            // waiting pushers in Synchronisation Point B.\n\n            if (full() && enabled())\n            {\n                m_pushSemaphore.wait();\n            }\n\n            m_waitingPushers.fetch_add(-1, std::memory_order_relaxed);\n        }\n    }\n\n    template <typename Type>\n    QueueReturn Queue<Type>::pushBack(Type&& value)\n    {\n        for (;;)\n        {\n            QueueReturn retVal = tryPushBack(std::move(value));\n\n            switch (retVal)\n            {\n                // Queue disabled.\n                case QueueReturn::QueueDisabled:\n                // We pushed the value back\n                case QueueReturn::Success:\n                    return retVal;\n                default:\n                    // continue on.\n                    break;\n            }\n\n            m_waitingPushers.fetch_add(1, std::memory_order_relaxed);\n\n            // Sync Point B.\n            //\n            // The call to `full` below loads the push index with full *sequential*\n            // consistency, which gives visibility of the change above to\n            // waiting pushers in Synchronisation Point C.\n\n            if (full() && enabled())\n            {\n                m_pushSemaphore.wait();\n            }\n\n            m_waitingPushers.fetch_add(-1, std::memory_order_relaxed);\n        }\n    }\n\n    template <typename Type>\n    Type Queue<Type>::popFront()\n    {\n        uint32_t generation = 0;\n        uint32_t index = 0;\n        while (m_manager.reservePopIndex(generation, index) != QueueReturn::Success)\n        {\n            m_waitingPoppers.fetch_add(1, std::memory_order_relaxed);\n\n            if (empty())\n            {\n                m_popSemaphore.wait();\n            }\n\n            m_waitingPoppers.fetch_sub(1, std::memory_order_relaxed);\n        }\n\n        QueuePopGuard<Type> popGuard(*this, generation, index);\n        return Type(std::move(m_data[index]));\n    }\n\n    template <typename Type>\n    std::optional<Type> Queue<Type>::popFrontWithTimeout(std::chrono::microseconds timeout)\n    {\n        uint32_t generation = 0;\n        uint32_t index = 0;\n        bool secondTry = false;\n        bool success = false;\n        for (;;)\n        {\n            success = m_manager.reservePopIndex(generation, index) == QueueReturn::Success;\n\n            if (secondTry || success)\n                break;\n\n            m_waitingPoppers.fetch_add(1, std::memory_order_relaxed);\n\n            if (empty())\n            {\n                m_popSemaphore.waitFor(timeout);\n                secondTry = true;\n            }\n\n            m_waitingPoppers.fetch_sub(1, std::memory_order_relaxed);\n        }\n\n        if (success)\n        {\n            QueuePopGuard<Type> popGuard(*this, generation, index);\n            return Type(std::move(m_data[index]));\n        }\n\n        return {};\n    }\n\n    template <typename Type>\n    void Queue<Type>::removeAll()\n    {\n        size_t elemCount = size();\n\n        uint32_t poppedItems = 0;\n\n        while (poppedItems++ < elemCount)\n        {\n            uint32_t generation = 0;\n            uint32_t index = 0;\n\n            if (m_manager.reservePopIndex(generation, index) != QueueReturn::Success)\n            {\n                break;\n            }\n\n            m_data[index].~Type();\n            m_manager.commitPopIndex(generation, index);\n        }\n\n        size_t wakeups = std::min(poppedItems, m_waitingPushers.load());\n\n        while (wakeups--)\n        {\n            m_pushSemaphore.notify();\n        }\n    }\n\n    template <typename Type>\n    void Queue<Type>::disable()\n    {\n        m_manager.disable();\n\n        uint32_t numWaiting = m_waitingPushers;\n\n        while (numWaiting--)\n        {\n            m_pushSemaphore.notify();\n        }\n    }\n\n    template <typename Type>\n    void Queue<Type>::enable()\n    {\n        m_manager.enable();\n    }\n\n    template <typename Type>\n    size_t Queue<Type>::capacity() const\n    {\n        return m_manager.capacity();\n    }\n\n    template <typename Type>\n    size_t Queue<Type>::size() const\n    {\n        return m_manager.size();\n    }\n\n    template <typename Type>\n    bool Queue<Type>::enabled() const\n    {\n        return m_manager.enabled();\n    }\n\n    template <typename Type>\n    bool Queue<Type>::full() const\n    {\n        return (capacity() <= size());\n    }\n\n    template <typename Type>\n    bool Queue<Type>::empty() const\n    {\n        return (0 >= size());\n    }\n\n    template <typename Type>\n    QueuePushGuard<Type>::~QueuePushGuard()\n    {\n        if (m_queue)\n        {\n            // Thread currently has the cell at index/generation. Dispose of it.\n\n            uint32_t generation = 0;\n            uint32_t index = 0;\n\n            // We should always have at least one item to pop.\n            size_t poppedItems = 1;\n\n            while (m_queue->m_manager.reservePopForClear(generation, index, m_generation, m_index))\n            {\n                m_queue->m_data[index].~Type();\n\n                poppedItems++;\n\n                m_queue->m_manager.commitPopIndex(generation, index);\n            }\n\n            // And release\n\n            m_queue->m_manager.abortPushIndexReservation(m_generation, m_index);\n\n            while (poppedItems--)\n            {\n                m_queue->m_pushSemaphore.notify();\n            }\n        }\n    }\n\n    template <typename Type>\n    void QueuePushGuard<Type>::release()\n    {\n        m_queue = nullptr;\n    }\n\n    template <typename Type>\n    QueuePopGuard<Type>::~QueuePopGuard()\n    {\n        m_queue.m_data[m_index].~Type();\n        m_queue.m_manager.commitPopIndex(m_generation, m_index);\n\n        // Notify a pusher\n        if (m_queue.m_waitingPushers > 0)\n        {\n            m_queue.m_pushSemaphore.notify();\n        }\n    }\n\n}  // namespace llarp::thread\n"
  },
  {
    "path": "llarp/util/thread/queue_manager.cpp",
    "content": "#include \"queue_manager.hpp\"\n\n#include <thread>\n\nnamespace llarp::thread\n{\n#if __cplusplus >= 201703L\n    // Turn an enum into its underlying value.\n    template <typename E>\n    constexpr auto to_underlying(E e) noexcept\n    {\n        return static_cast<std::underlying_type_t<E>>(e);\n    }\n#else\n    template <typename E>\n    constexpr uint32_t to_underlying(E e) noexcept\n    {\n        return static_cast<uint32_t>(e);\n    }\n#endif\n    static constexpr uint32_t GENERATION_COUNT_SHIFT = 0x2;\n\n    // Max number of generations which can be held in an uint32_t.\n    static constexpr size_t NUM_ELEMENT_GENERATIONS = 1 << ((sizeof(uint32_t) * 8) - 2);\n\n    // mask for holding the element state from an element\n    static constexpr uint32_t ELEMENT_STATE_MASK = 0x3;\n\n    // mask for holding the disabled bit in the index.\n    static constexpr uint32_t DISABLED_STATE_MASK = 1 << ((sizeof(uint32_t) * 8) - 1);\n\n    // Max number of combinations of index and generations.\n    static constexpr uint32_t NUM_COMBINED_INDEXES = DISABLED_STATE_MASK;\n\n    bool isDisabledFlagSet(uint32_t encodedIndex) { return (encodedIndex & DISABLED_STATE_MASK); }\n\n    uint32_t discardDisabledFlag(uint32_t encodedIndex) { return (encodedIndex & ~DISABLED_STATE_MASK); }\n\n    uint32_t encodeElement(uint32_t generation, ElementState state)\n    {\n        return (generation << GENERATION_COUNT_SHIFT) | to_underlying(state);\n    }\n\n    uint32_t decodeGenerationFromElementState(uint32_t state) { return state >> GENERATION_COUNT_SHIFT; }\n\n    ElementState decodeStateFromElementState(uint32_t state) { return ElementState(state & ELEMENT_STATE_MASK); }\n\n    QueueManager::AtomicIndex& QueueManager::pushIndex() { return m_pushIndex; }\n\n    QueueManager::AtomicIndex& QueueManager::popIndex() { return m_popIndex; }\n\n    const QueueManager::AtomicIndex& QueueManager::pushIndex() const { return m_pushIndex; }\n\n    const QueueManager::AtomicIndex& QueueManager::popIndex() const { return m_popIndex; }\n\n    uint32_t QueueManager::nextCombinedIndex(uint32_t index) const\n    {\n        if (m_maxCombinedIndex == index)\n        {\n            return 0;\n        }\n\n        return index + 1;\n    }\n\n    uint32_t QueueManager::nextGeneration(uint32_t generation) const\n    {\n        if (m_maxGeneration == generation)\n        {\n            return 0;\n        }\n\n        return generation + 1;\n    }\n\n    size_t QueueManager::capacity() const { return m_capacity; }\n\n    int32_t QueueManager::circularDifference(uint32_t startingValue, uint32_t subtractValue, uint32_t modulo)\n    {\n        assert(modulo <= (static_cast<uint32_t>(std::numeric_limits<int32_t>::max()) + 1));\n        assert(startingValue < modulo);\n        assert(subtractValue < modulo);\n\n        int32_t difference = startingValue - subtractValue;\n        if (difference > static_cast<int32_t>(modulo / 2))\n        {\n            return difference - modulo;\n        }\n        if (difference < -static_cast<int32_t>(modulo / 2))\n        {\n            return difference + modulo;\n        }\n\n        return difference;\n    }\n\n    uint32_t QueueManager::numGenerations(size_t capacity)\n    {\n        assert(capacity != 0);\n\n        return static_cast<uint32_t>(std::min(NUM_COMBINED_INDEXES / capacity, NUM_ELEMENT_GENERATIONS));\n    }\n\n    QueueManager::QueueManager(size_t capacity)\n        : m_pushIndex(0),\n          m_popIndex(0),\n          m_capacity(capacity),\n          m_maxGeneration(numGenerations(capacity) - 1),\n          m_maxCombinedIndex(numGenerations(capacity) * static_cast<uint32_t>(capacity) - 1)\n    {\n        assert(0 < capacity);\n        assert(capacity <= MAX_CAPACITY);\n        (void)m_pushPadding;\n        (void)m_popPadding;\n\n        m_states = new std::atomic<std::uint32_t>[capacity];\n\n        for (size_t i = 0; i < capacity; ++i)\n        {\n            m_states[i] = 0;\n        }\n    }\n\n    QueueManager::~QueueManager() { delete[] m_states; }\n\n    QueueReturn QueueManager::reservePushIndex(uint32_t& generation, uint32_t& index)\n    {\n        uint32_t loadedPushIndex = pushIndex().load(std::memory_order_relaxed);\n\n        uint32_t savedPushIndex = -1;\n\n        uint32_t combinedIndex = 0;\n        uint32_t currIdx = 0;\n        uint32_t currGen = 0;\n\n        // Use savedPushIndex to make us acquire an index at least twice before\n        // returning QueueFull.\n        // This prevents us from massive contention when we have a queue of size 1\n\n        for (;;)\n        {\n            if (isDisabledFlagSet(loadedPushIndex))\n            {\n                return QueueReturn::QueueDisabled;\n            }\n\n            combinedIndex = discardDisabledFlag(loadedPushIndex);\n\n            currGen = static_cast<uint32_t>(combinedIndex / m_capacity);\n            currIdx = static_cast<uint32_t>(combinedIndex % m_capacity);\n\n            uint32_t compare = encodeElement(currGen, ElementState::Empty);\n            const uint32_t swap = encodeElement(currGen, ElementState::Writing);\n\n            if (m_states[currIdx].compare_exchange_strong(compare, swap))\n            {\n                // We changed the state.\n                generation = currGen;\n                index = currIdx;\n                break;\n            }\n\n            // We failed to reserve the index. Use the result from cmp n swap to\n            // determine if the queue was full or not. Either:\n            // 1. The cell is from a previous generation (so the queue is full)\n            // 2. Another cell has reserved this cell for writing, but not commited\n            // yet\n            // 3. The push index has been changed between the load and the cmp.\n\n            uint32_t elemGen = decodeGenerationFromElementState(compare);\n\n            auto difference = static_cast<int32_t>(currGen - elemGen);\n\n            if (difference == 1 || (difference == -static_cast<int32_t>(m_maxGeneration)))\n            {\n                // Queue is full.\n\n                assert(1 == circularDifference(currGen, elemGen, m_maxGeneration + 1));\n\n                ElementState state = decodeStateFromElementState(compare);\n\n                if (state == ElementState::Reading)\n                {\n                    // Another thread is reading. Yield this thread\n                    std::this_thread::yield();\n                    loadedPushIndex = pushIndex().load(std::memory_order_relaxed);\n                    continue;\n                }\n\n                assert(state != ElementState::Empty);\n\n                if (savedPushIndex != loadedPushIndex)\n                {\n                    // Make another attempt to check the queue is full before failing\n                    std::this_thread::yield();\n                    savedPushIndex = loadedPushIndex;\n                    loadedPushIndex = pushIndex().load(std::memory_order_relaxed);\n                    continue;\n                }\n\n                return QueueReturn::QueueFull;\n            }\n\n            // Another thread has already acquired this cell, try to increment the\n            // push index and go again.\n\n            assert(0 >= circularDifference(currGen, elemGen, m_maxGeneration + 1));\n\n            const uint32_t next = nextCombinedIndex(combinedIndex);\n            pushIndex().compare_exchange_strong(combinedIndex, next);\n            loadedPushIndex = combinedIndex;\n        }\n\n        // We got the cell, increment the push index\n        const uint32_t next = nextCombinedIndex(combinedIndex);\n        pushIndex().compare_exchange_strong(combinedIndex, next);\n\n        return QueueReturn::Success;\n    }\n\n    void QueueManager::commitPushIndex(uint32_t generation, uint32_t index)\n    {\n        assert(generation <= m_maxGeneration);\n        assert(index < m_capacity);\n        assert(ElementState::Writing == decodeStateFromElementState(m_states[index]));\n        assert(generation == decodeGenerationFromElementState(m_states[index]));\n\n        m_states[index] = encodeElement(generation, ElementState::Full);\n    }\n\n    QueueReturn QueueManager::reservePopIndex(uint32_t& generation, uint32_t& index)\n    {\n        uint32_t loadedPopIndex = popIndex().load();\n        uint32_t savedPopIndex = -1;\n\n        uint32_t currIdx = 0;\n        uint32_t currGen = 0;\n\n        for (;;)\n        {\n            currGen = static_cast<uint32_t>(loadedPopIndex / m_capacity);\n            currIdx = static_cast<uint32_t>(loadedPopIndex % m_capacity);\n\n            // Try to swap this state from full to reading.\n\n            uint32_t compare = encodeElement(currGen, ElementState::Full);\n            const uint32_t swap = encodeElement(currGen, ElementState::Reading);\n\n            if (m_states[currIdx].compare_exchange_strong(compare, swap))\n            {\n                generation = currGen;\n                index = currIdx;\n                break;\n            }\n\n            // We failed to reserve the index. Use the result from cmp n swap to\n            // determine if the queue was full or not. Either:\n            // 1. The cell is from a previous generation (so the queue is empty)\n            // 2. The cell is from the current generation and empty (so the queue is\n            // empty)\n            // 3. The queue is being written to\n            // 4. The pop index has been changed between the load and the cmp.\n\n            uint32_t elemGen = decodeGenerationFromElementState(compare);\n            ElementState state = decodeStateFromElementState(compare);\n\n            auto difference = static_cast<int32_t>(currGen - elemGen);\n\n            if (difference == 1 || (difference == -static_cast<int32_t>(m_maxGeneration)))\n            {\n                // Queue is full.\n                assert(state == ElementState::Reading);\n                assert(1 == (circularDifference(currGen, elemGen, m_maxGeneration + 1)));\n\n                return QueueReturn::QueueEmpty;\n            }\n\n            if (difference == 0 && state == ElementState::Empty)\n            {\n                // The cell is empty in the current generation, so the queue is empty\n\n                if (savedPopIndex != loadedPopIndex)\n                {\n                    std::this_thread::yield();\n                    savedPopIndex = loadedPopIndex;\n                    loadedPopIndex = popIndex().load(std::memory_order_relaxed);\n                    continue;\n                }\n\n                return QueueReturn::QueueEmpty;\n            }\n\n            if (difference != 0 || state == ElementState::Writing)\n            {\n                // The cell is currently being written to or the index is outdated)\n                // Yield and try again.\n                std::this_thread::yield();\n                loadedPopIndex = popIndex().load(std::memory_order_relaxed);\n                continue;\n            }\n\n            popIndex().compare_exchange_strong(loadedPopIndex, nextCombinedIndex(loadedPopIndex));\n        }\n\n        popIndex().compare_exchange_strong(loadedPopIndex, nextCombinedIndex(loadedPopIndex));\n\n        return QueueReturn::Success;\n    }\n\n    void QueueManager::commitPopIndex(uint32_t generation, uint32_t index)\n    {\n        assert(generation <= m_maxGeneration);\n        assert(index < m_capacity);\n        assert(decodeStateFromElementState(m_states[index]) == ElementState::Reading);\n        assert(generation == decodeGenerationFromElementState(m_states[index]));\n\n        m_states[index] = encodeElement(nextGeneration(generation), ElementState::Empty);\n    }\n\n    void QueueManager::disable()\n    {\n        // Loop until we set the disabled bit\n        for (;;)\n        {\n            uint32_t index = pushIndex();\n\n            if (isDisabledFlagSet(index))\n            {\n                // Queue is already disabled(?!)\n                return;\n            }\n\n            if (pushIndex().compare_exchange_strong(index, index | DISABLED_STATE_MASK))\n            {\n                // queue has been disabled\n                return;\n            }\n        }\n    }\n\n    void QueueManager::enable()\n    {\n        for (;;)\n        {\n            uint32_t index = pushIndex();\n\n            if (!isDisabledFlagSet(index))\n            {\n                // queue is already enabled.\n                return;\n            }\n\n            if (pushIndex().compare_exchange_strong(index, index & ~DISABLED_STATE_MASK))\n            {\n                // queue has been enabled\n                return;\n            }\n        }\n    }\n\n    bool QueueManager::reservePopForClear(\n        uint32_t& generation, uint32_t& index, uint32_t endGeneration, uint32_t endIndex)\n    {\n        assert(endGeneration <= m_maxGeneration);\n        assert(endIndex < m_capacity);\n\n        uint32_t loadedCombinedIndex = popIndex().load(std::memory_order_relaxed);\n\n        for (;;)\n        {\n            uint32_t endCombinedIndex = (endGeneration * static_cast<uint32_t>(m_capacity)) + endIndex;\n\n            if (circularDifference(endCombinedIndex, loadedCombinedIndex, m_maxCombinedIndex + 1) == 0)\n            {\n                return false;\n            }\n\n            assert(0 < circularDifference(endCombinedIndex, loadedCombinedIndex, m_maxCombinedIndex + 1));\n\n            auto currIdx = static_cast<uint32_t>(loadedCombinedIndex % m_capacity);\n            auto currGen = static_cast<uint32_t>(loadedCombinedIndex / m_capacity);\n\n            // Try to swap this cell from Full to Reading.\n            // We only set this to Empty after trying to increment popIndex, so we\n            // don't race against another thread.\n\n            uint32_t compare = encodeElement(currGen, ElementState::Full);\n            const uint32_t swap = encodeElement(currGen, ElementState::Reading);\n\n            if (m_states[currIdx].compare_exchange_strong(compare, swap))\n            {\n                // We've dropped this index.\n\n                generation = currGen;\n                index = currIdx;\n                break;\n            }\n\n            ElementState state = decodeStateFromElementState(compare);\n\n            if (state == ElementState::Writing || state == ElementState::Full)\n            {\n                // Another thread is writing to this cell, or this thread has slept\n                // for too long.\n                std::this_thread::yield();\n                loadedCombinedIndex = popIndex().load(std::memory_order_relaxed);\n                continue;\n            }\n\n            const uint32_t next = nextCombinedIndex(loadedCombinedIndex);\n            popIndex().compare_exchange_strong(loadedCombinedIndex, next);\n        }\n\n        // Attempt to increment the index.\n        const uint32_t next = nextCombinedIndex(loadedCombinedIndex);\n        popIndex().compare_exchange_strong(loadedCombinedIndex, next);\n\n        return true;\n    }\n\n    void QueueManager::abortPushIndexReservation(uint32_t generation, uint32_t index)\n    {\n        assert(generation <= m_maxGeneration);\n        assert(index < m_capacity);\n        assert(static_cast<uint32_t>((generation * m_capacity) + index) == popIndex().load(std::memory_order_relaxed));\n        assert(decodeStateFromElementState(m_states[index]) == ElementState::Writing);\n        assert(generation == decodeGenerationFromElementState(m_states[index]));\n\n        uint32_t loadedPopIndex = popIndex().load(std::memory_order_relaxed);\n\n        assert(generation == loadedPopIndex / m_capacity);\n        assert(index == loadedPopIndex % m_capacity);\n\n        m_states[index] = encodeElement(generation, ElementState::Reading);\n\n        const uint32_t nextIndex = nextCombinedIndex(loadedPopIndex);\n        popIndex().compare_exchange_strong(loadedPopIndex, nextIndex);\n\n        m_states[index] = encodeElement(nextGeneration(generation), ElementState::Empty);\n    }\n\n    size_t QueueManager::size() const\n    {\n        // Note that we rely on these loads being sequentially consistent.\n\n        uint32_t combinedPushIndex = discardDisabledFlag(pushIndex());\n        uint32_t combinedPopIndex = popIndex();\n\n        int32_t difference = combinedPushIndex - combinedPopIndex;\n\n        if (difference >= 0)\n        {\n            if (difference > static_cast<int32_t>(m_capacity))\n            {\n                // We've raced between getting push and pop indexes, in this case, it\n                // means the queue is empty.\n                assert(0 > circularDifference(combinedPushIndex, combinedPopIndex, m_maxCombinedIndex + 1));\n\n                return 0;\n            }\n\n            return static_cast<size_t>(difference);\n        }\n\n        if (difference < -static_cast<int32_t>(m_maxCombinedIndex / 2))\n        {\n            assert(0 < circularDifference(combinedPushIndex, combinedPopIndex, m_maxCombinedIndex + 1));\n\n            difference += m_maxCombinedIndex + 1;\n            return std::min(static_cast<size_t>(difference), m_capacity);\n        }\n\n        return 0;\n    }\n\n    bool QueueManager::enabled() const { return !isDisabledFlagSet(pushIndex().load()); }\n}  // namespace llarp::thread\n"
  },
  {
    "path": "llarp/util/thread/queue_manager.hpp",
    "content": "#pragma once\n#include <algorithm>\n#include <atomic>\n#include <cassert>\n#include <cstdint>\n#include <iostream>\n#include <limits>\n#include <string>\n#include <type_traits>\n\nnamespace llarp::thread\n{\n    using namespace std::literals;\n\n    enum class ElementState : uint32_t\n    {\n        Empty = 0,\n        Writing = 1,\n        Full = 2,\n        Reading = 3\n    };\n\n    enum class QueueReturn\n    {\n        Success,\n        QueueDisabled,\n        QueueEmpty,\n        QueueFull\n    };\n\n    constexpr std::string_view to_string(QueueReturn val)\n    {\n        switch (val)\n        {\n            case QueueReturn::Success:\n                return \"Success\"sv;\n            case QueueReturn::QueueDisabled:\n                return \"QueueDisabled\"sv;\n            case QueueReturn::QueueEmpty:\n                return \"QueueEmpty\"sv;\n            case QueueReturn::QueueFull:\n                return \"QueueFull\"sv;\n        }\n        return \"(queue-return-unknown)\"sv;\n    }\n\n    class QueueManager\n    {\n        // This class provides thread-safe state management for a queue.\n\n        // Common terminology in this class:\n        // - \"Combined Index\": the combination of an index into the circular\n        //   buffer and the generation count. Precisely:\n        //\n        //     Combined Index = (Generation * Capacity) + Element Index\n        //\n        //   The combined index has the useful property where incrementing the\n        //   index when the element index is at the end of the buffer does two\n        //   things:\n        //     1. Sets the element index back to 0\n        //     2. Increments the generation\n\n      public:\n        static constexpr size_t Alignment = 64;\n\n        using AtomicIndex = std::atomic<std::uint32_t>;\n\n      private:\n        AtomicIndex m_pushIndex;  // Index in the buffer that the next\n                                  // element will be added to.\n\n        char m_pushPadding[Alignment - sizeof(AtomicIndex)];\n\n        AtomicIndex m_popIndex;  // Index in the buffer that the next\n                                 // element will be removed from.\n\n        char m_popPadding[Alignment - sizeof(AtomicIndex)];\n\n        const size_t m_capacity;  // max size of the manager.\n\n        const uint32_t m_maxGeneration;  // Maximum generation for this object.\n\n        const uint32_t m_maxCombinedIndex;  // Maximum combined value of index and\n                                            // generation for this object.\n\n        std::atomic<std::uint32_t>* m_states;  // Array of index states.\n\n        AtomicIndex& pushIndex();\n\n        AtomicIndex& popIndex();\n\n        const AtomicIndex& pushIndex() const;\n\n        const AtomicIndex& popIndex() const;\n\n        // Return the next combined index\n        uint32_t nextCombinedIndex(uint32_t index) const;\n\n        // Return the next generation\n        uint32_t nextGeneration(uint32_t generation) const;\n\n      public:\n        // Return the difference between the startingValue and the subtractValue\n        // around a particular modulo.\n        static int32_t circularDifference(uint32_t startingValue, uint32_t subtractValue, uint32_t modulo);\n\n        // Return the number of possible generations a circular buffer can hold.\n        static uint32_t numGenerations(size_t capacity);\n\n        // The max capacity of the queue manager.\n        // 2 bits are used for holding the disabled status and the number of\n        // generations is at least 2.\n        static constexpr size_t MAX_CAPACITY = 1 << ((sizeof(uint32_t) * 8) - 2);\n\n        explicit QueueManager(size_t capacity);\n\n        ~QueueManager();\n\n        // Push operations\n\n        // Reserve the next available index to enqueue an element at. On success:\n        // - Load `index` with the next available index\n        // - Load `generation` with the current generation\n        //\n        // If this call succeeds, other threads may spin until `commitPushIndex`\n        // is called.\n        QueueReturn reservePushIndex(uint32_t& generation, uint32_t& index);\n\n        // Mark the `index` in the given `generation` as in-use. This unblocks\n        // any other threads which were waiting on the index state.\n        void commitPushIndex(uint32_t generation, uint32_t index);\n\n        // Pop operations\n\n        // Reserve the next available index to remove an element from. On success:\n        // - Load `index` with the next available index\n        // - Load `generation` with the current generation\n        //\n        // If this call succeeds, other threads may spin until `commitPopIndex`\n        // is called.\n        QueueReturn reservePopIndex(uint32_t& generation, uint32_t& index);\n\n        // Mark the `index` in the given `generation` as available. This unblocks\n        // any other threads which were waiting on the index state.\n        void commitPopIndex(uint32_t generation, uint32_t index);\n\n        // Disable the queue\n        void disable();\n\n        // Enable the queue\n        void enable();\n\n        // Exception safety\n\n        // If the next available index an element can be popped from is before\n        // the `endGeneration` and the `endIndex`, reserve that index into `index`\n        // and `generation`.\n        //\n        // Return true if an index was reserved and false otherwise.\n        //\n        // Behaviour is undefined if `endGeneration` and `endIndex` have not been\n        // acquired for writing.\n        //\n        // The intended usage of this method is to help remove all elements if an\n        // exception is thrown between reserving and committing an index.\n        // Workflow:\n        // 1. call reservePopForClear\n        // 2. call commitPopIndex, emptying all cells up to the reserved index\n        // 3. call abortPushIndexReservation on the index.\n        bool reservePopForClear(uint32_t& generation, uint32_t& index, uint32_t endGeneration, uint32_t endIndex);\n\n        void abortPushIndexReservation(uint32_t generation, uint32_t index);\n\n        // Accessors\n\n        bool enabled() const;\n\n        size_t size() const;\n\n        size_t capacity() const;\n    };\n}  // namespace llarp::thread\n"
  },
  {
    "path": "llarp/util/thread/threading.cpp",
    "content": "#include \"threading.hpp\"\n\n#include <llarp/util/logging.hpp>\n\n#include <cstring>\n\n#ifdef POSIX\n#include <pthread.h>\n#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)\n#include <pthread_np.h>\n#endif\n#endif\n\n#ifdef _MSC_VER\n#include <windows.h>\nextern \"C\" void SetThreadName(DWORD dwThreadID, LPCSTR szThreadName);\n#endif\n\nnamespace llarp::util\n{\n    static auto logcat = log::Cat(\"util.threading\");\n\n    void SetThreadName(const std::string& name)\n    {\n#if defined(POSIX) || __MINGW32__\n#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)\n        /* on bsd this function has void return type */\n        pthread_set_name_np(pthread_self(), name.c_str());\n#else\n#if defined(__MACH__)\n        const int rc = pthread_setname_np(name.c_str());\n// API present upstream since v2.11.3 and imported downstream\n// in CR 8158 <https://www.illumos.org/issues/8158>\n// We only use the native function on Microsoft C++ builds\n#elif defined(__linux__) || defined(__sun) || __MINGW32__\n        const int rc = pthread_setname_np(pthread_self(), name.c_str());\n#else\n#error \"unsupported platform\"\n#endif\n        if (rc)\n        {\n            log::error(logcat, \"Failed to set thread name to {} errno = {} errstr = {}\", name, rc, ::strerror(rc));\n        }\n#endif\n#elif _MSC_VER\n        ::SetThreadName(::GetCurrentThreadId(), name.c_str());\n#else\n        log::info(logcat, \"Thread name setting not supported on this platform\");\n        (void)name;\n#endif\n    }\n}  // namespace llarp::util\n"
  },
  {
    "path": "llarp/util/thread/threading.hpp",
    "content": "#pragma once\n\n#include <condition_variable>\n#include <iostream>\n#include <mutex>\n#include <optional>\n#include <shared_mutex>\n#include <thread>\n\n#if defined(WIN32) && !defined(__GNUC__)\n#include <process.h>\n\nusing pid_t = int;\n#else\n#include <sys/types.h>\n#include <unistd.h>\n#endif\n\nnamespace llarp::util\n{\n    /// a mutex that does nothing\n    ///\n    /// this exists to convert mutexes that were initially in use (but may no\n    /// longer be necessary) into no-op placeholders (except in debug mode\n    /// where they complain loudly when they are actually accessed across\n    /// different threads; see below).\n    ///\n    /// the idea is to \"turn off\" the mutexes and see where they are actually\n    /// needed.\n    struct NullMutex\n    {\n#ifndef NDEBUG\n        /// in debug mode, we implement lock() to enforce that any lock is only\n        /// used from a single thread. the point of this is to identify locks that\n        /// are actually needed by dying a painful death when used across threads\n        mutable std::optional<std::thread::id> m_id;\n        void lock() const\n        {\n            if (!m_id)\n            {\n                m_id = std::this_thread::get_id();\n            }\n            else if (*m_id != std::this_thread::get_id())\n            {\n                std::cerr << \"NullMutex \" << this << \" was used across threads: locked by \"\n                          << std::this_thread::get_id() << \" and was previously locked by \" << *m_id << \"\\n\";\n                // if you're encountering this abort() call, you may have discovered a\n                // case where a NullMutex should be reverted to a \"real mutex\"\n                std::abort();\n            }\n        }\n#else\n        void lock() const {}\n#endif\n        // Does nothing; once locked the mutex belongs to that thread forever\n        void unlock() const {}\n    };\n\n    /// a lock that does nothing\n    struct NullLock\n    {\n        NullLock(NullMutex& mtx) { mtx.lock(); }\n\n        ~NullLock()\n        {\n            (void)this;  // trick clang-tidy\n        }\n    };\n\n    /// Default mutex type, supporting shared and exclusive locks.\n    using Mutex = std::shared_timed_mutex;\n\n    /// Basic RAII lock type for the default mutex type.\n    using Lock = std::lock_guard<Mutex>;\n\n    class Semaphore\n    {\n      private:\n        std::mutex m_mutex;  // protects m_count\n        size_t m_count;\n        std::condition_variable m_cv;\n\n      public:\n        Semaphore(size_t count) : m_count(count) {}\n\n        void notify()\n        {\n            {\n                std::lock_guard<std::mutex> lock(m_mutex);\n                m_count++;\n            }\n            m_cv.notify_one();\n        }\n\n        void wait()\n        {\n            std::unique_lock lock{m_mutex};\n            m_cv.wait(lock, [this] { return m_count > 0; });\n            m_count--;\n        }\n\n        bool waitFor(std::chrono::microseconds timeout)\n        {\n            std::unique_lock lock{m_mutex};\n            if (!m_cv.wait_for(lock, timeout, [this] { return m_count > 0; }))\n                return false;\n\n            m_count--;\n            return true;\n        }\n    };\n\n    void SetThreadName(const std::string& name);\n\n    inline pid_t GetPid()\n    {\n#ifdef WIN32\n        return _getpid();\n#else\n        return ::getpid();\n#endif\n    }\n\n    // type for detecting contention on a resource\n    struct ContentionKiller\n    {\n        template <typename F>\n        void TryAccess(F visit) const\n        {\n#ifndef NDEBUG\n            NullLock lock(_access);\n#endif\n            visit();\n        }\n#ifndef NDEBUG\n      private:\n        mutable NullMutex _access;\n#endif\n    };\n}  // namespace llarp::util\n"
  },
  {
    "path": "llarp/util/time.cpp",
    "content": "#include \"time.hpp\"\n\n#include <llarp/util/formattable.hpp>\n\n#include <nlohmann/json.hpp>\n\nnamespace llarp\n{\n    namespace\n    {\n        template <typename Res, typename Clock>\n        static std::chrono::milliseconds time_since_epoch(std::chrono::time_point<Clock> point)\n        {\n            return std::chrono::duration_cast<Res>(point.time_since_epoch());\n        }\n\n        static const auto started_at_system = std::chrono::system_clock::now();\n\n        static const auto started_at_steady = std::chrono::steady_clock::now();\n    }  // namespace\n\n    std::chrono::steady_clock::time_point get_time() { return std::chrono::steady_clock::now(); }\n\n    std::chrono::nanoseconds get_timestamp() { return std::chrono::steady_clock::now().time_since_epoch(); }\n\n    uint64_t to_milliseconds(std::chrono::milliseconds ms) { return ms.count(); }\n\n    /// get our uptime in ms\n    std::chrono::milliseconds uptime()\n    {\n        return std::chrono::duration_cast<std::chrono::milliseconds>(\n            std::chrono::steady_clock::now() - started_at_steady);\n    }\n\n    std::chrono::milliseconds time_now_ms()\n    {\n        return uptime() + time_since_epoch<std::chrono::milliseconds, std::chrono::system_clock>(started_at_system);\n    }\n\n    nlohmann::json to_json(const std::chrono::milliseconds& t) { return to_milliseconds(t); }\n\n    static auto extract_h_m_s_ms(const std::chrono::milliseconds& dur)\n    {\n        return std::make_tuple(\n            std::chrono::duration_cast<std::chrono::hours>(dur).count(),\n            (std::chrono::duration_cast<std::chrono::minutes>(dur) % 1h).count(),\n            (std::chrono::duration_cast<std::chrono::seconds>(dur) % 1min).count(),\n            (std::chrono::duration_cast<std::chrono::milliseconds>(dur) % 1s).count());\n    }\n\n    std::string short_time_from_now(\n        const std::chrono::system_clock::time_point& t, const std::chrono::milliseconds& now_threshold)\n    {\n        auto delta = std::chrono::duration_cast<std::chrono::milliseconds>(\n            std::chrono::system_clock::time_point::clock::now() - t);\n\n        bool future = delta < 0s;\n        if (future)\n            delta = -delta;\n\n        auto [hours, mins, secs, ms] = extract_h_m_s_ms(delta);\n\n        auto in = future ? \"in \"sv : \"\"sv;\n        auto ago = future ? \"\"sv : \" ago\"sv;\n        return delta < now_threshold ? \"now\"s\n            : delta < 10s            ? \"{}{}.{:03d}s{}\"_format(in, secs, ms, ago)\n            : delta < 1h             ? \"{}{}m{:02d}s{}\"_format(in, mins, secs, ago)\n                                     : \"{}{}h{:02d}m{}\"_format(in, hours, mins, ago);\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/util/time.hpp",
    "content": "#pragma once\n\n#include <nlohmann/json_fwd.hpp>\n\n#include <chrono>\n#include <random>\n#include <type_traits>\n\nusing namespace std::literals;\n\nnamespace llarp\n{\n    // Libevent uses µs precision\n    using loop_time = std::chrono::microseconds;\n\n    /// get time right now as milliseconds, this is monotonic\n    std::chrono::milliseconds time_now_ms();\n\n    /// get the uptime of the process\n    std::chrono::milliseconds uptime();\n\n    /// convert to milliseconds\n    uint64_t to_milliseconds(std::chrono::milliseconds duration);\n\n    nlohmann::json to_json(const std::chrono::milliseconds& t);\n\n    // Returns a string such as \"27m13s ago\" or \"in 1h12m\" or \"now\".  You get precision of minutes\n    // (for >=1h), seconds (>=10s), or milliseconds.  The `now_threshold` argument controls how\n    // close to current time (default 1s) the time has to be to get the \"now\" argument.\n    std::string short_time_from_now(\n        const std::chrono::system_clock::time_point& t, const std::chrono::milliseconds& now_threshold = 1s);\n\n    inline timeval loop_time_to_timeval(loop_time t)\n    {\n        return timeval{\n            .tv_sec = static_cast<decltype(timeval::tv_sec)>(t / 1s),\n            .tv_usec = static_cast<decltype(timeval::tv_usec)>((t % 1s) / 1us)};\n    }\n\n    std::chrono::nanoseconds get_timestamp();\n\n    template <typename unit_t>\n    auto get_timestamp()\n    {\n        return std::chrono::duration_cast<unit_t>(get_timestamp());\n    }\n\n    /** Returns a duration uniformly distributed between `a` and `b`.  E.g.\n     *\n     *     auto t = llarp::uniform_duration_distribution{5min, 8min}(llarp);\n     *\n     * yields a duration uniformly distributed in [5min, 8min], with `Time` precision.\n     *\n     * Time defaults to at least milliseconds (if given less precise types, such as minutes), but\n     * will be more precise if constructed with more precise duration types.\n     */\n    template <typename Time>\n    struct uniform_duration_distribution\n    {\n        using underlying_rep = Time::rep;\n        std::conditional_t<\n            std::is_floating_point_v<underlying_rep>,\n            std::uniform_real_distribution<underlying_rep>,\n            std::uniform_int_distribution<underlying_rep>>\n            underlying_dist;\n\n        using result_type = Time;\n\n        constexpr uniform_duration_distribution(Time a, Time b) : underlying_dist{a.count(), b.count()} {}\n\n        template <class Generator>\n        Time operator()(Generator& g)\n        {\n            return Time{underlying_dist(g)};\n        }\n    };\n\n    template <typename TimeA, typename TimeB>\n    uniform_duration_distribution(TimeA a, TimeB b)\n        -> uniform_duration_distribution<std::common_type_t<TimeA, TimeB, std::chrono::milliseconds>>;\n\n}  // namespace llarp\n"
  },
  {
    "path": "llarp/util/zstd.cpp",
    "content": "#include \"zstd.hpp\"\n\n#include \"formattable.hpp\"\n\n#include <zstd.h>\n\n#include <cassert>\n#include <stdexcept>\n\nnamespace llarp::zstd\n{\n\n    static ZSTD_CCtx* cctx(void* c) { return static_cast<ZSTD_CCtx*>(c); }\n    static ZSTD_DCtx* dctx(void* d) { return static_cast<ZSTD_DCtx*>(d); }\n\n    compressor::compressor() : _context{ZSTD_createCCtx()} {}\n    compressor::~compressor() { ZSTD_freeCCtx(cctx(_context)); }\n\n    decompressor::decompressor() : _context{ZSTD_createDCtx()} {}\n    decompressor::~decompressor() { ZSTD_freeDCtx(dctx(_context)); }\n\n    std::vector<std::byte> compressor::compress(\n        std::span<const std::byte> data, int level, std::span<const std::byte> prefix)\n    {\n        auto* ctx = cctx(_context);\n        std::vector<std::byte> compressed;\n        compressed.resize(prefix.size() + ZSTD_compressBound(data.size()));\n        if (!prefix.empty())\n            std::memcpy(compressed.data(), prefix.data(), prefix.size());\n        auto size = ZSTD_compressCCtx(\n            ctx, compressed.data() + prefix.size(), compressed.size() - prefix.size(), data.data(), data.size(), level);\n        if (ZSTD_isError(size))\n            throw std::runtime_error{\"Compression failed: {}\"_format(ZSTD_getErrorName(size))};\n        compressed.resize(prefix.size() + size);\n        return compressed;\n    }\n    std::vector<std::byte> compressor::compress(std::string_view data, int level, std::string_view prefix)\n    {\n        return compress(\n            {reinterpret_cast<const std::byte*>(data.data()), data.size()},\n            level,\n            {reinterpret_cast<const std::byte*>(prefix.data()), prefix.size()});\n    }\n\n    template <typename B>\n    static std::vector<std::byte> compress_piecewise(\n        ZSTD_CCtx* ctx, std::span<const B> buffers, int level, const B prefix)\n    {\n        ZSTD_CCtx_reset(ctx, ZSTD_reset_session_and_parameters);\n        size_t in_size = 0;\n        for (auto& b : buffers)\n            in_size += b.size();\n        ZSTD_CCtx_setParameter(ctx, ZSTD_c_compressionLevel, level);\n        ZSTD_CCtx_setPledgedSrcSize(ctx, in_size);\n        std::vector<std::byte> compressed;\n        compressed.resize(prefix.size() + ZSTD_compressBound(in_size));\n        if (!prefix.empty())\n            std::memcpy(compressed.data(), prefix.data(), prefix.size());\n        ZSTD_outBuffer output{\n            .dst = compressed.data() + prefix.size(), .size = compressed.size() - prefix.size(), .pos = 0};\n        for (const auto& buf : buffers)\n        {\n            const auto last = &buf == &buffers.back();\n            const auto mode = last ? ZSTD_e_end : ZSTD_e_continue;\n            ZSTD_inBuffer input{.src = buf.data(), .size = buf.size(), .pos = 0};\n            bool finished;\n            do\n            {\n                const auto remaining = ZSTD_compressStream2(ctx, &output, &input, mode);\n                if (ZSTD_isError(remaining))\n                    throw std::runtime_error{\"Compression failed: {}\"_format(ZSTD_getErrorName(remaining))};\n                finished = last ? remaining == 0 : input.pos == input.size;\n            } while (not finished);\n            assert(input.pos == input.size);\n        }\n        assert(prefix.size() + output.pos <= compressed.size());\n        compressed.resize(prefix.size() + output.pos);\n        return compressed;\n    }\n\n    std::vector<std::byte> compressor::compress(\n        std::span<const std::string_view> buffers, int level, std::string_view prefix)\n    {\n        return compress_piecewise(cctx(_context), buffers, level, prefix);\n    }\n    std::vector<std::byte> compressor::compress(\n        std::span<const std::span<const std::byte>> buffers, int level, const std::span<const std::byte> prefix)\n    {\n        return compress_piecewise(cctx(_context), buffers, level, prefix);\n    }\n\n    /// Attempts to decompress `data`.  Returns nullopt if decompression fails.  If max_size is\n    /// non-zero then this returns nullopt if the decompressed data would expand to more than\n    /// max_size bytes.\n    std::optional<std::vector<std::byte>> decompressor::decompress(\n        std::span<const std::byte> compressed, size_t max_size)\n    {\n        auto* ctx = dctx(_context);\n        ZSTD_initDStream(ctx);\n        ZSTD_inBuffer input{.src = compressed.data(), .size = compressed.size(), .pos = 0};\n\n        std::array<std::byte, 16384> out_buf;\n        ZSTD_outBuffer output{.dst = out_buf.data(), .size = out_buf.size(), .pos = 0};\n\n        std::vector<std::byte> decompressed;\n\n        size_t ret;\n        do\n        {\n            output.pos = 0;\n            ret = ZSTD_decompressStream(ctx, &output, &input);\n            if (ZSTD_isError(ret) or (max_size > 0 && decompressed.size() + output.pos > max_size))\n                return std::nullopt;\n\n            decompressed.insert(decompressed.end(), out_buf.begin(), out_buf.begin() + output.pos);\n        } while (ret > 0 or input.pos < input.size);\n\n        return decompressed;\n    }\n\n}  // namespace llarp::zstd\n"
  },
  {
    "path": "llarp/util/zstd.hpp",
    "content": "#pragma once\n#include <memory>\n#include <optional>\n#include <span>\n#include <vector>\n\nnamespace llarp::zstd\n{\n    // Compression class that can be held to be used repeatedly, saving some system resources for\n    // repeated compressions.  NOT thread-safe (i.e. the same instance must not be used from\n    // different threads at the same time).\n    class compressor\n    {\n      private:\n        void* _context;\n\n      public:\n        static constexpr int DEFAULT_LEVEL = 3;\n\n        compressor();\n\n        compressor(compressor&&) = delete;\n        compressor(const compressor&) = delete;\n        compressor& operator=(const compressor&) = delete;\n        compressor& operator=(compressor&&) = delete;\n\n        // Compress a single buffer.  Throws on serious error.  If non-empty, `prefix` will be\n        // prepended to the returned vector.\n        std::vector<std::byte> compress(\n            std::span<const std::byte> data, int level = DEFAULT_LEVEL, std::span<const std::byte> prefix = {});\n        std::vector<std::byte> compress(std::string_view data, int level = DEFAULT_LEVEL, std::string_view prefix = {});\n\n        // Compress the concatenation of multiple buffers without requiring pre-concatenation.\n        // Throws on serious error.\n        std::vector<std::byte> compress(\n            std::span<const std::span<const std::byte>> buffers,\n            int level = DEFAULT_LEVEL,\n            std::span<const std::byte> prefix = {});\n        std::vector<std::byte> compress(\n            std::span<const std::string_view> buffers, int level = DEFAULT_LEVEL, std::string_view prefix = {});\n\n        ~compressor();\n    };\n\n    // Decompression class that can be stored to save initialization if used repeatedly.  NOT\n    // thread-safe.\n    class decompressor\n    {\n      private:\n        void* _context;\n\n      public:\n        decompressor();\n\n        decompressor(decompressor&&) = delete;\n        decompressor(const decompressor&) = delete;\n        decompressor& operator=(const decompressor&) = delete;\n        decompressor& operator=(decompressor&&) = delete;\n\n        // Attempts to decompress a single buffer with maximum allowed decompressed size of\n        // `max_size` (if non-zero).  Returns nullptr if compression failed (or if the `max_size`\n        // limit is hit).\n        std::optional<std::vector<std::byte>> decompress(std::span<const std::byte> compressed, size_t max_size = 0);\n\n        ~decompressor();\n    };\n\n}  // namespace llarp::zstd\n"
  },
  {
    "path": "llarp/vpn/android.hpp",
    "content": "#pragma once\n\n#include \"common.hpp\"\n#include \"platform.hpp\"\n\n#include <llarp.hpp>\n\n#include <unistd.h>\n\n#include <cstdio>\n\nnamespace llarp::vpn\n{\n    class AndroidInterface : public NetworkInterface\n    {\n        const int m_fd;\n\n      public:\n        AndroidInterface(InterfaceInfo info, int fd) : NetworkInterface{std::move(info)}, m_fd{fd}\n        {\n            if (m_fd == -1)\n                throw std::runtime_error(\"Error opening AndroidVPN layer FD: \" + std::string{strerror(errno)});\n        }\n\n        virtual ~AndroidInterface()\n        {\n            if (m_fd != -1)\n                ::close(m_fd);\n        }\n\n        int PollFD() const override { return m_fd; }\n\n        IPPacket read_next_packet() override\n        {\n            std::vector<uint8_t> pkt;\n            pkt.reserve(MAX_PACKET_SIZE);\n            const auto n = read(m_fd, pkt.data(), pkt.capacity());\n            pkt.resize(std::min(std::max(ssize_t{}, n), static_cast<ssize_t>(pkt.capacity())));\n            return IPPacket{std::move(pkt)};\n        }\n\n        bool write_packet(IPPacket pkt) override\n        {\n            const auto sz = write(m_fd, pkt.data(), pkt.size());\n            if (sz <= 0)\n                return false;\n            return sz == static_cast<ssize_t>(pkt.size());\n        }\n    };\n\n    class AndroidRouteManager : public AbstractRouteManager\n    {\n        void add_route(quic::Address, quic::Address) override{};\n\n        void delete_route(quic::Address, quic::Address) override{};\n\n        void add_default_route_via_interface(NetworkInterface&) override {};\n\n        void delete_default_route_via_interface(NetworkInterface&) override {};\n\n        void add_route_via_interface(NetworkInterface&, IPRange) override {};\n\n        void delete_route_via_interface(NetworkInterface&, IPRange) override {};\n\n        std::vector<quic::Address> get_non_interface_gateways(NetworkInterface&) override\n        {\n            return std::vector<quic::Address>{};\n        };\n    };\n\n    class AndroidPlatform : public Platform\n    {\n        const int fd;\n        AndroidRouteManager _route_manager{};\n\n      public:\n        AndroidPlatform(llarp::Context* ctx) : fd{ctx->androidFD} {}\n\n        std::shared_ptr<NetworkInterface> obtain_interface(InterfaceInfo info, Router*) override\n        {\n            return std::make_shared<AndroidInterface>(std::move(info), fd);\n        }\n        AbstractRouteManager& RouteManager() override { return _route_manager; }\n    };\n\n}  // namespace llarp::vpn\n"
  },
  {
    "path": "llarp/vpn/common.hpp",
    "content": "#pragma once\n\n#include <netinet/in.h>\n#include <sys/ioctl.h>\n#include <sys/socket.h>\n#include <sys/types.h>\n#include <unistd.h>\n\n#include <cerrno>\n#include <cstring>\n#include <stdexcept>\n\nnamespace llarp::vpn\n{\n    class permission_error : public std::runtime_error\n    {\n      public:\n        using std::runtime_error::runtime_error;\n    };\n\n    struct IOCTL\n    {\n        const int _fd;\n\n        explicit IOCTL(int af) : _fd{::socket(af, SOCK_DGRAM, IPPROTO_IP)}\n        {\n            if (_fd == -1)\n                throw std::invalid_argument{strerror(errno)};\n        }\n\n        ~IOCTL() { ::close(_fd); }\n\n        template <typename Command, typename... Args>\n        void ioctl(Command cmd, Args&&... args)\n        {\n            if (::ioctl(_fd, cmd, std::forward<Args>(args)...) == -1)\n            {\n                if (errno == EACCES)\n                {\n                    throw permission_error{\"we are not allowed to call this ioctl\"};\n                }\n                else\n                    throw std::runtime_error(\"ioctl failed: \" + std::string{strerror(errno)});\n            }\n        }\n    };\n}  // namespace llarp::vpn\n"
  },
  {
    "path": "llarp/vpn/egres_packet_router.cpp",
    "content": "#include \"egres_packet_router.hpp\"\n\nnamespace llarp::vpn\n{\n    struct EgresUDPPacketHandler : public EgresLayer4Handler\n    {\n        EgresPacketHandlerFunc _handler;\n        std::unordered_map<uint16_t, EgresPacketHandlerFunc> _ports;\n\n        explicit EgresUDPPacketHandler(EgresPacketHandlerFunc baseHandler) : _handler{std::move(baseHandler)} {}\n\n        void AddSubHandler(uint16_t localport, EgresPacketHandlerFunc handler) override\n        {\n            _ports.emplace(std::move(localport), std::move(handler));\n        }\n\n        void RemoveSubHandler(uint16_t localport) override { _ports.erase(localport); }\n\n        void HandleIPPacketFrom(NetworkAddress from, IPPacket pkt) override\n        {\n            (void)from;\n            (void)pkt;\n            // TOFIX:\n            // auto ip_pkt = IPPacket::from_udp(std::move(pkt));\n\n            // if (auto dstPort = pkt.path.remote.port())\n            // {\n            //     if (auto itr = _ports.find(dstPort); itr != _ports.end())\n            //     {\n            //         itr->second(std::move(from), std::move(ip_pkt));\n            //         return;\n            //     }\n            // }\n            // _handler(std::move(from), std::move(ip_pkt));\n        }\n    };\n\n    struct EgresGenericLayer4Handler : public EgresLayer4Handler\n    {\n        EgresPacketHandlerFunc _handler;\n\n        explicit EgresGenericLayer4Handler(EgresPacketHandlerFunc baseHandler) : _handler{std::move(baseHandler)} {}\n\n        void HandleIPPacketFrom(NetworkAddress from, IPPacket pkt) override\n        {\n            // TOFIX: this\n            (void)from;\n            (void)pkt;\n            // _handler(std::move(from), IPPacket::from_udp(std::move(pkt)));\n        }\n    };\n\n    EgresPacketRouter::EgresPacketRouter(EgresPacketHandlerFunc baseHandler) : _handler{std::move(baseHandler)} {}\n\n    void EgresPacketRouter::HandleIPPacketFrom(NetworkAddress from, IPPacket pkt)\n    {\n        (void)from;\n        (void)pkt;\n        // const auto proto = pkt.Header()->protocol;\n        // if (const auto itr = _proto_handlers.find(proto); itr != _proto_handlers.end())\n        // {\n        //     itr->second->HandleIPPacketFrom(std::move(from), std::move(pkt));\n        // }\n        // else\n        // TOFIX:\n        // _handler(std::move(from), IPPacket::from_udp(std::move(pkt)));\n    }\n\n    namespace\n    {\n        constexpr uint8_t udp_proto = 0x11;\n    }\n\n    void EgresPacketRouter::AddUDPHandler(uint16_t localport, EgresPacketHandlerFunc func)\n    {\n        if (_proto_handlers.find(udp_proto) == _proto_handlers.end())\n        {\n            _proto_handlers.emplace(udp_proto, std::make_unique<EgresUDPPacketHandler>(_handler));\n        }\n        _proto_handlers[udp_proto]->AddSubHandler(localport, std::move(func));\n    }\n\n    void EgresPacketRouter::AddIProtoHandler(uint8_t proto, EgresPacketHandlerFunc func)\n    {\n        _proto_handlers[proto] = std::make_unique<EgresGenericLayer4Handler>(std::move(func));\n    }\n\n    void EgresPacketRouter::RemoveUDPHandler(uint16_t localport)\n    {\n        if (auto itr = _proto_handlers.find(udp_proto); itr != _proto_handlers.end())\n        {\n            itr->second->RemoveSubHandler(localport);\n        }\n    }\n\n}  // namespace llarp::vpn\n"
  },
  {
    "path": "llarp/vpn/egres_packet_router.hpp",
    "content": "#pragma once\n\n#include <llarp/address/address.hpp>\n#include <llarp/net/ip_packet.hpp>\n\n#include <functional>\n#include <unordered_map>\n\nnamespace llarp::vpn\n{\n    using EgresPacketHandlerFunc = std::function<void(NetworkAddress, IPPacket)>;\n\n    struct EgresLayer4Handler\n    {\n        virtual ~EgresLayer4Handler() = default;\n\n        virtual void HandleIPPacketFrom(NetworkAddress from, IPPacket pkt) = 0;\n\n        virtual void AddSubHandler(uint16_t, EgresPacketHandlerFunc) {};\n        virtual void RemoveSubHandler(uint16_t) {};\n    };\n\n    class EgresPacketRouter\n    {\n        EgresPacketHandlerFunc _handler;\n        std::unordered_map<uint8_t, std::unique_ptr<EgresLayer4Handler>> _proto_handlers;\n\n      public:\n        /// baseHandler will be called if no other handlers matches a packet\n        explicit EgresPacketRouter(EgresPacketHandlerFunc baseHandler);\n\n        /// feed in an ip packet for handling\n        void HandleIPPacketFrom(NetworkAddress, IPPacket pkt);\n\n        /// add a non udp packet handler using ip protocol proto\n        void AddIProtoHandler(uint8_t proto, EgresPacketHandlerFunc func);\n\n        /// helper that adds a udp packet handler for UDP destinted for localport\n        void AddUDPHandler(uint16_t localport, EgresPacketHandlerFunc func);\n\n        /// remove a udp handler that is already set up by bound port\n        void RemoveUDPHandler(uint16_t localport);\n    };\n}  // namespace llarp::vpn\n"
  },
  {
    "path": "llarp/vpn/linux.hpp",
    "content": "#pragma once\n\n#include \"common.hpp\"\n#include \"platform.hpp\"\n\n#include <llarp.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/util/str.hpp>\n\n#include <arpa/inet.h>\n#include <fcntl.h>\n#include <linux/if_tun.h>\n#include <linux/rtnetlink.h>\n#include <net/if.h>\n#include <oxenc/endian.h>\n#include <sys/socket.h>\n#include <sys/types.h>\n#include <unistd.h>\n\n#include <cstring>\n#include <stdexcept>\n\nnamespace llarp::vpn\n{\n    static auto logcat = log::Cat(\"vpn.linux\");\n\n    inline constexpr std::array<ipv6, 4> if_ipv6_addrs{ipv6{}, ipv6{0x4000}, ipv6{0x8000}, ipv6{0xc000}};\n\n    struct in6_ifreq\n    {\n        in6_addr addr;\n        uint32_t prefixlen;\n        unsigned int ifindex;\n    };\n\n    struct call_on_destroy\n    {\n        std::function<void()> f;\n        ~call_on_destroy()\n        {\n            if (f)\n                f();\n        }\n        void disarm() { f = nullptr; }\n    };\n\n    // Send a netlink request and wait for the response.  Returns an error string on error, nullopt\n    // on success.  Must have set NLM_F_ACK on the request header flags!\n    template <typename NLRequestT>\n    std::optional<std::string> nl_submit(int nlfd, const NLRequestT& req)\n    {\n        assert(req.header.nlmsg_flags & NLM_F_REQUEST);\n        log::trace(logcat, \"submitting netlink request to fd {}\", nlfd);\n        if (-1 == send(nlfd, &req, req.header.nlmsg_len, 0))\n            return strerror(errno);\n\n        char resp_buf[4096];\n        log::trace(logcat, \"waiting for netlink response\");\n        auto resp_len = recv(nlfd, resp_buf, sizeof(resp_buf), 0);\n        log::trace(logcat, \"got netlink response\");\n        auto* resp = reinterpret_cast<nlmsghdr*>(resp_buf);\n        if (!NLMSG_OK(resp, resp_len) || resp->nlmsg_type != NLMSG_ERROR)\n            return \"Invalid netlink response\"s;\n        if (auto* nlerr = reinterpret_cast<nlmsgerr*>(NLMSG_DATA(resp_buf)); nlerr->error < 0)\n            return strerror(-nlerr->error);\n        return std::nullopt;\n    }\n\n    class LinuxInterface : public NetworkInterface\n    {\n        const int _fd;\n\n      public:\n        LinuxInterface(InterfaceInfo info) : NetworkInterface{std::move(info)}, _fd{::open(\"/dev/net/tun\", O_RDWR)}\n        {\n            if (_fd == -1)\n                throw std::runtime_error(\"cannot open /dev/net/tun {}\"_format(strerror(errno)));\n\n            call_on_destroy fd_abort{[fd = _fd] { close(fd); }};\n\n            if (fcntl(_fd, F_SETFL, O_NONBLOCK) == -1)\n                throw std::runtime_error{\n                    \"Failed to set `O_NONBLOCK` on Linux interface FD: {}\"_format(strerror(errno))};\n\n            ifreq ifr{};\n            ifr.ifr_flags = IFF_TUN | IFF_NO_PI;\n            std::memcpy(ifr.ifr_name, _info.ifname.c_str(), std::min<size_t>(_info.ifname.size(), IFNAMSIZ - 1));\n\n            log::debug(logcat, \"Setting interface name to '{}'\", _info.ifname);\n            if (::ioctl(_fd, TUNSETIFF, &ifr) == -1)\n                throw std::runtime_error{\"Cannot set TUN interface name: {}\"_format(strerror(errno))};\n\n            // The ioctl above could have changed the tun device on us:\n            _info.ifname = ifr.ifr_name;\n            _info.index = if_nametoindex(_info.ifname.c_str());\n\n            log::debug(logcat, \"Set interface name to '{}'.  Adding adresses\", _info.ifname);\n\n            int nlfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);\n            if (nlfd == -1)\n                throw std::runtime_error{\"netlink failed: {}\"_format(strerror(errno))};\n            call_on_destroy nlfd_cleanup([nlfd] { close(nlfd); });\n\n            if (_info.addrs.empty())\n                throw std::runtime_error{\"Cannot set up a TUN interface with no addresses!\"};\n\n            std::list<std::string> addr_strings;\n            // Add addresses to the tun interface:\n            for (const auto& ifaddr : _info.addrs)\n            {\n                log::debug(logcat, \"Adding address {} to {}\", ifaddr, _info.ifname);\n                struct\n                {\n                    nlmsghdr header;\n                    ifaddrmsg content;\n                    char buf[256];\n                } request{};\n                size_t buf_avail = sizeof(request.buf);\n\n                request.header.nlmsg_len = NLMSG_LENGTH(sizeof request.content);\n                request.header.nlmsg_flags = NLM_F_REQUEST | NLM_F_EXCL | NLM_F_CREATE | NLM_F_ACK;\n                request.header.nlmsg_type = RTM_NEWADDR;\n                request.content.ifa_index = _info.index;\n\n                if (auto* n4 = std::get_if<ipv4_net>(&ifaddr))\n                {\n                    addr_strings.push_back(n4->to_string());\n                    request.content.ifa_family = AF_INET;\n                    request.content.ifa_prefixlen = n4->mask;\n                    auto* req_attr = IFA_RTA(&request.content);\n                    req_attr->rta_type = IFA_LOCAL;\n                    req_attr->rta_len = RTA_LENGTH(sizeof(in_addr));\n                    request.header.nlmsg_len += req_attr->rta_len;\n                    oxenc::write_host_as_big(n4->ip.addr, RTA_DATA(req_attr));\n\n                    req_attr = RTA_NEXT(req_attr, buf_avail);\n                    req_attr->rta_type = IFA_ADDRESS;\n                    req_attr->rta_len = RTA_LENGTH(sizeof(in_addr));\n                    request.header.nlmsg_len += req_attr->rta_len;\n                    oxenc::write_host_as_big(n4->ip.addr, RTA_DATA(req_attr));\n                }\n                else\n                {\n                    auto& n6 = std::get<ipv6_net>(ifaddr);\n                    addr_strings.push_back(n6.to_string());\n                    request.content.ifa_family = AF_INET6;\n                    request.content.ifa_prefixlen = n6.mask;\n                    auto* req_attr = IFA_RTA(&request.content);\n                    req_attr->rta_type = IFA_LOCAL;\n                    req_attr->rta_len = RTA_LENGTH(sizeof(in6_addr));\n                    request.header.nlmsg_len += req_attr->rta_len;\n                    char* addr_data = static_cast<char*>(RTA_DATA(req_attr));\n                    oxenc::write_host_as_big(n6.ip.hi, addr_data);\n                    oxenc::write_host_as_big(n6.ip.lo, addr_data + 8);\n\n                    req_attr = RTA_NEXT(req_attr, buf_avail);\n                    req_attr->rta_type = IFA_ADDRESS;\n                    req_attr->rta_len = RTA_LENGTH(sizeof(in_addr));\n                    request.header.nlmsg_len += req_attr->rta_len;\n                    addr_data = static_cast<char*>(RTA_DATA(req_attr));\n                    oxenc::write_host_as_big(n6.ip.hi, addr_data);\n                    oxenc::write_host_as_big(n6.ip.lo, addr_data + 8);\n                }\n\n                if (auto err = nl_submit(nlfd, request))\n                    throw std::runtime_error{\n                        \"Failed to add address {} to {}: {}\"_format(addr_strings.back(), _info.ifname, *err)};\n            }\n\n            // Bring up the tun device:\n            {\n                struct\n                {\n                    nlmsghdr header;\n                    ifinfomsg content;\n                } request{};\n                request.header.nlmsg_len = NLMSG_LENGTH(sizeof request.content);\n                request.header.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK;\n                request.header.nlmsg_type = RTM_NEWLINK;\n                request.content.ifi_index = static_cast<int>(_info.index);\n                request.content.ifi_flags = IFF_UP;\n                request.content.ifi_change = 1;\n\n                if (auto err = nl_submit(nlfd, request))\n                    throw std::runtime_error{\"Failed to bring up tun device {}: {}\"_format(_info.ifname, *err)};\n            }\n\n            fd_abort.disarm();\n            log::info(logcat, \"TUN device {} now up with address(es): {}\", _info.ifname, fmt::join(addr_strings, \", \"));\n        }\n\n        ~LinuxInterface() override { ::close(_fd); }\n\n        int PollFD() const override { return _fd; }\n\n        IPPacket read_next_packet() override\n        {\n            std::vector<std::byte> buf;\n            buf.resize(MAX_PACKET_SIZE);\n            const auto sz = read(_fd, buf.data(), buf.capacity());\n            // log::trace(logcat, \"{} bytes read from fd {} (err?:{})\", sz, _fd, strerror(errno));\n            if (sz < 0)\n            {\n                if (errno == EAGAIN or errno == EWOULDBLOCK)\n                {\n                    errno = 0;\n                    return IPPacket{};\n                }\n                throw std::error_code{errno, std::system_category()};\n            }\n\n            buf.resize(sz);\n            return IPPacket{std::move(buf)};\n        }\n\n        bool write_packet(IPPacket pkt) override\n        {\n            const auto sz = write(_fd, pkt.data(), pkt.size());\n            // log::trace(logcat, \"{} bytes written to fd {} (err?:{})\", sz, _fd, strerror(errno));\n            if (sz <= 0)\n                return false;\n            return sz == static_cast<ssize_t>(pkt.size());\n        }\n    };\n\n    class LinuxRouteManager : public AbstractRouteManager\n    {\n        const int fd;\n\n        enum class GatewayMode\n        {\n            FirstHop,  // Selector for a first-hop route, i.e. for route poking\n            Default,   // Selector for default routing, i.e. for global exit traffic routing\n        };\n\n        struct NLRequest\n        {\n            nlmsghdr n;\n            rtmsg r;\n            char buf[4096];\n\n            void AddData(int type, const void* data, int alen)\n            {\n#define NLMSG_TAIL(nmsg) ((struct rtattr*)(((intptr_t)(nmsg)) + NLMSG_ALIGN((nmsg)->nlmsg_len)))\n\n                int len = RTA_LENGTH(alen);\n                rtattr* rta;\n                if (NLMSG_ALIGN(n.nlmsg_len) + RTA_ALIGN(len) > sizeof(*this))\n                {\n                    throw std::length_error{\"nlrequest add data overflow\"};\n                }\n                rta = NLMSG_TAIL(&n);\n                rta->rta_type = type;\n                rta->rta_len = len;\n                if (alen)\n                {\n                    memcpy(RTA_DATA(rta), data, alen);\n                }\n                n.nlmsg_len = NLMSG_ALIGN(n.nlmsg_len) + RTA_ALIGN(len);\n#undef NLMSG_TAIL\n            }\n        };\n\n        void make_blackhole(int cmd, int flags, int af)\n        {\n            NLRequest nl_request{};\n            /* Initialize request structure */\n            nl_request.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));\n            nl_request.n.nlmsg_flags = NLM_F_REQUEST | flags;\n            nl_request.n.nlmsg_type = cmd;\n            nl_request.n.nlmsg_pid = getpid();\n            nl_request.r.rtm_family = af;\n            nl_request.r.rtm_table = RT_TABLE_LOCAL;\n            nl_request.r.rtm_type = RTN_BLACKHOLE;\n            nl_request.r.rtm_scope = RT_SCOPE_UNIVERSE;\n            if (af == AF_INET)\n            {\n                ipv4 addr{};\n                nl_request.AddData(RTA_DST, &addr.addr, sizeof(addr.addr));\n            }\n            else\n            {\n                std::array<uint64_t, 2> addr{};\n                nl_request.AddData(RTA_DST, &addr, sizeof(addr));\n            }\n            send(fd, &nl_request, sizeof(nl_request), 0);\n        }\n\n        NLRequest init_route_cmd(int cmd, int flags)\n        {\n            NLRequest nl_request{};\n            /* Initialize request structure */\n            nl_request.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));\n            nl_request.n.nlmsg_flags = NLM_F_REQUEST | flags;\n            nl_request.n.nlmsg_type = cmd;\n            nl_request.n.nlmsg_pid = getpid();\n            nl_request.r.rtm_table = RT_TABLE_MAIN;\n            nl_request.r.rtm_scope = RT_SCOPE_NOWHERE;\n\n            /* Set additional flags if NOT deleting route */\n            if (cmd != RTM_DELROUTE)\n            {\n                nl_request.r.rtm_protocol = RTPROT_BOOT;\n                nl_request.r.rtm_type = RTN_UNICAST;\n            }\n\n            return nl_request;\n        }\n\n        void route_cmd(int cmd, int flags, const ipv4_range& dst, const ipv4& gw, GatewayMode mode, int if_idx)\n        {\n            auto nl_request = init_route_cmd(cmd, flags);\n\n            nl_request.r.rtm_family = AF_INET;\n            if (if_idx)\n                nl_request.r.rtm_scope = RT_SCOPE_LINK;\n            nl_request.r.rtm_dst_len = dst.mask;\n\n            char gwbuf[4], ipbuf[4];\n            oxenc::write_host_as_big(gw.addr, gwbuf);\n            oxenc::write_host_as_big(dst.ip.addr, ipbuf);\n            nl_request.AddData(RTA_GATEWAY, gwbuf, sizeof(gwbuf));\n            nl_request.AddData(RTA_DST, ipbuf, sizeof(ipbuf));\n\n            if (mode == GatewayMode::FirstHop)\n            {\n                /* Set interface */\n                nl_request.AddData(RTA_OIF, &if_idx, sizeof(if_idx));\n            }\n            /* Send message to the netlink */\n            send(fd, &nl_request, sizeof(nl_request), 0);\n        }\n\n        void route_cmd(int cmd, int flags, const ipv6_range& dst, const ipv6& gw, GatewayMode mode, int if_idx)\n        {\n            auto nl_request = init_route_cmd(cmd, flags);\n            nl_request.r.rtm_family = AF_INET6;\n            if (if_idx)\n                nl_request.r.rtm_scope = RT_SCOPE_UNIVERSE;\n            nl_request.r.rtm_dst_len = dst.mask;\n\n            char gwbuf[16], ipbuf[16];\n            oxenc::write_host_as_big(gw.hi, gwbuf);\n            oxenc::write_host_as_big(gw.lo, gwbuf + 8);\n            oxenc::write_host_as_big(dst.ip.hi, ipbuf);\n            oxenc::write_host_as_big(dst.ip.lo, ipbuf + 8);\n            nl_request.AddData(RTA_GATEWAY, gwbuf, sizeof(gwbuf));\n            nl_request.AddData(RTA_DST, ipbuf, sizeof(ipbuf));\n\n            if (mode == GatewayMode::FirstHop)\n            {\n                /* Set interface */\n                nl_request.AddData(RTA_OIF, &if_idx, sizeof(if_idx));\n            }\n            /* Send message to the netlink */\n            send(fd, &nl_request, sizeof(nl_request), 0);\n        }\n\n        void route_all_via_interface(NetworkInterface& vpn, int cmd, int flags)\n        {\n            const auto& info = vpn.interface_info();\n\n            const auto maybe = Net().get_interface_ipv4(info.ifname);\n            if (not maybe)\n                throw std::runtime_error{\"we dont have our own network interface?\"};\n            auto& tun_ip = *maybe;\n\n            for (const auto& range : {ipv4{0, 0, 0, 0} / 1, ipv4{128, 0, 0, 0} / 1})\n                route_cmd(cmd, flags, range, tun_ip, GatewayMode::Default, info.index);\n\n            if (const auto ip6 = Net().get_interface_ipv6(info.ifname))\n                for (uint16_t nibble : {0x0000, 0x4000, 0x8000, 0xc000})\n                    route_cmd(cmd, flags, ipv6{nibble} / 2, *ip6, GatewayMode::Default, info.index);\n        }\n\n        void route_range_via_interface(int cmd, int flags, NetworkInterface& vpn, ipv4_range range)\n        {\n            const auto& info = vpn.interface_info();\n            const auto maybe = Net().get_interface_ipv4(info.ifname);\n            if (not maybe)\n                throw std::runtime_error{\"Unable to add routed IPv4 range: interface has no IPv4 address\"};\n\n            route_cmd(cmd, flags, range, *maybe, GatewayMode::Default, info.index);\n        }\n        void route_range_via_interface(int cmd, int flags, NetworkInterface& vpn, ipv6_range range)\n        {\n            const auto& info = vpn.interface_info();\n            const auto maybe = Net().get_interface_ipv6(info.ifname);\n            if (not maybe)\n                throw std::runtime_error{\"Unable to add routed IPv6 range: interface has no IPv6 address\"};\n\n            route_cmd(cmd, flags, range, *maybe, GatewayMode::Default, info.index);\n        }\n\n        void route_via_gateway(int cmd, int flags, ipv4_range range, ipv4 gateway)\n        {\n            route_cmd(cmd, flags, range, gateway, GatewayMode::FirstHop, 0);\n        }\n        void route_via_gateway(int cmd, int flags, ipv4 dest, ipv4 gateway)\n        {\n            return route_via_gateway(cmd, flags, dest / 32, gateway);\n        }\n        void route_via_gateway(int cmd, int flags, ipv6_range range, ipv6 gateway)\n        {\n            route_cmd(cmd, flags, range, gateway, GatewayMode::FirstHop, 0);\n        }\n        void route_via_gateway(int cmd, int flags, ipv6 dest, ipv6 gateway)\n        {\n            return route_via_gateway(cmd, flags, dest / 128, gateway);\n        }\n\n      public:\n        LinuxRouteManager() : fd{socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE)}\n        {\n            if (fd == -1)\n                throw std::runtime_error{\"netlink failed: {}\"_format(strerror(errno))};\n        }\n\n        ~LinuxRouteManager() override { close(fd); }\n\n        void add_route(ipv4 ip, ipv4 gateway) override\n        {\n            route_via_gateway(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_EXCL, ip, gateway);\n        }\n        void add_route(ipv6 ip, ipv6 gateway) override\n        {\n            route_via_gateway(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_EXCL, ip, gateway);\n        }\n\n        void delete_route(ipv4 ip, ipv4 gateway) override { route_via_gateway(RTM_DELROUTE, 0, ip, gateway); }\n        void delete_route(ipv6 ip, ipv6 gateway) override { route_via_gateway(RTM_DELROUTE, 0, ip, gateway); }\n\n        void add_default_route_via_interface(NetworkInterface& vpn) override\n        {\n            route_all_via_interface(vpn, RTM_NEWROUTE, NLM_F_CREATE | NLM_F_EXCL);\n        }\n\n        void delete_default_route_via_interface(NetworkInterface& vpn) override\n        {\n            route_all_via_interface(vpn, RTM_DELROUTE, 0);\n        }\n\n        void add_route_via_interface(NetworkInterface& vpn, ipv4_range range) override\n        {\n            route_range_via_interface(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_EXCL, vpn, range);\n        }\n        void add_route_via_interface(NetworkInterface& vpn, ipv6_range range) override\n        {\n            route_range_via_interface(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_EXCL, vpn, range);\n        }\n\n        void delete_route_via_interface(NetworkInterface& vpn, ipv4_range range) override\n        {\n            route_range_via_interface(RTM_DELROUTE, 0, vpn, range);\n        }\n\n        void delete_route_via_interface(NetworkInterface& vpn, ipv6_range range) override\n        {\n            route_range_via_interface(RTM_DELROUTE, 0, vpn, range);\n        }\n\n        std::vector<quic::Address> get_non_interface_gateways(NetworkInterface& vpn) override\n        {\n            const auto& ifname = vpn.interface_info().ifname;\n            std::vector<quic::Address> gateways{};\n\n            std::ifstream inf{\"/proc/net/route\"};\n            for (std::string line; std::getline(inf, line);)\n            {\n                const auto parts = split(line, \"\\t\");\n                if (parts[1].find_first_not_of('0') == std::string::npos and parts[0] != ifname)\n                {\n                    const auto& ip = parts[2];\n                    if ((ip.size() == sizeof(uint32_t) * 2) and oxenc::is_hex(ip))\n                    {\n                        std::string buf;\n                        oxenc::from_hex(ip.begin(), ip.end(), buf.data());\n                        quic::Address addr{buf, 0};\n                        gateways.push_back(std::move(addr));\n                    }\n                }\n            }\n            return gateways;\n        }\n\n        void add_blackhole() override\n        {\n            make_blackhole(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_EXCL, AF_INET);\n            make_blackhole(RTM_NEWROUTE, NLM_F_CREATE | NLM_F_EXCL, AF_INET6);\n        }\n\n        void delete_blackhole() override\n        {\n            make_blackhole(RTM_DELROUTE, 0, AF_INET);\n            make_blackhole(RTM_DELROUTE, 0, AF_INET6);\n        }\n    };\n\n    class LinuxPlatform : public Platform\n    {\n        LinuxRouteManager _routeManager{};\n\n      public:\n        std::shared_ptr<NetworkInterface> obtain_interface(InterfaceInfo info, Router*) override\n        {\n            return std::make_shared<LinuxInterface>(std::move(info));\n        };\n\n        AbstractRouteManager& RouteManager() override { return _routeManager; }\n    };\n\n}  // namespace llarp::vpn\n"
  },
  {
    "path": "llarp/vpn/packet_intercept.hpp",
    "content": "#pragma once\n\n#include <functional>\n#include <vector>\n\nnamespace llarp::vpn\n{\n    using PacketSendFunc_t = std::function<void(std::vector<uint8_t>)>;\n    using PacketInterceptFunc_t = std::function<void(std::vector<uint8_t>, PacketSendFunc_t)>;\n\n    class I_PacketInterceptor\n    {\n      public:\n        virtual ~I_PacketInterceptor() = default;\n\n        /// start intercepting packets and call a callback for each one we get\n        /// the callback passes in an ip packet and a function that we can use to send an ip packet\n        /// to its origin\n        virtual void start(PacketInterceptFunc_t f) = 0;\n\n        /// stop intercepting packets\n        virtual void stop() = 0;\n    };\n\n}  // namespace llarp::vpn\n"
  },
  {
    "path": "llarp/vpn/packet_io.hpp",
    "content": "#pragma once\n\n#include <llarp/net/ip_packet.hpp>\n\n#include <functional>\n\nnamespace llarp::vpn\n{\n    class PacketIO\n    {\n      public:\n        virtual ~PacketIO() = default;\n\n        /// start any platform specific operations before running\n        virtual void Start() {};\n\n        /// stop operation and tear down anything that Start() set up.\n        virtual void Stop() {};\n\n        /// read next ip packet, return an empty packet if there are none ready.\n        virtual IPPacket read_next_packet() = 0;\n\n        /// write a packet to the interface\n        /// returns false if we dropped it\n        virtual bool write_packet(IPPacket pkt) = 0;\n\n        /// get pollable fd for reading\n        virtual int PollFD() const = 0;\n    };\n\n}  // namespace llarp::vpn\n"
  },
  {
    "path": "llarp/vpn/packet_router.cpp",
    "content": "#include \"packet_router.hpp\"\n\n#include <llarp/util/logging.hpp>\n\nnamespace llarp::vpn\n{\n    static auto logcat = log::Cat(\"packet_router\");\n\n    struct UDPPacketHandler : public Layer4Handler\n    {\n        ip_pkt_hook _base_handler;\n        std::unordered_map<uint16_t, ip_pkt_hook> _port_mapped_handlers;\n\n        explicit UDPPacketHandler(ip_pkt_hook baseHandler) : _base_handler{std::move(baseHandler)} {}\n\n        void add_sub_handler(uint16_t localport, ip_pkt_hook handler) override\n        {\n            _port_mapped_handlers.emplace(localport, std::move(handler));\n            log::debug(logcat, \"UDP packet sub-handler registered for local port {}\", localport);\n        }\n\n        void handle_ip_packet(IPPacket pkt) override\n        {\n            log::trace(logcat, \"{}\", pkt.info_line());\n            auto dstport = pkt.dest_port();\n\n            if (not dstport)\n            {\n                // TOFIX:\n                // _base_handler(IPPacket::from_udp(std::move(pkt)));\n                return;\n            }\n\n            if (auto itr = _port_mapped_handlers.find(*dstport); itr != _port_mapped_handlers.end())\n                itr->second(std::move(pkt));\n            // else\n            //     _base_handler(IPPacket::from_udp(std::move(pkt)));\n        }\n    };\n\n    struct GenericLayer4Handler : public Layer4Handler\n    {\n        ip_pkt_hook _base_handler;\n\n        explicit GenericLayer4Handler(ip_pkt_hook baseHandler) : _base_handler{std::move(baseHandler)} {}\n\n        void handle_ip_packet(IPPacket pkt) override\n        {\n            log::critical(logcat, \"(FIXME) l4 pkt: {}\", pkt.info_line());\n            // TOFIX:\n            // _base_handler(IPPacket::from_udp(std::move(pkt)));\n        }\n    };\n\n    PacketRouter::PacketRouter(ip_pkt_hook baseHandler) : _handler{std::move(baseHandler)} {}\n\n    void PacketRouter::handle_ip_packet(IPPacket pkt) const\n    {\n        log::trace(logcat, \"{}\", pkt.info_line());\n        auto dest_port = pkt.dest_port();\n        if (not dest_port)\n            return _handler(std::move(pkt));\n\n        auto proto = pkt.protocol();\n        if (auto itr = _ip_proto_handler.find(proto); itr != _ip_proto_handler.end())\n            itr->second->handle_ip_packet(std::move(pkt));\n        else\n            _handler(std::move(pkt));\n    }\n\n    void PacketRouter::add_udp_handler(uint16_t localport, ip_pkt_hook func)\n    {\n        auto [it, b] = _ip_proto_handler.try_emplace(net::IPProtocol::UDP, nullptr);\n\n        if (b)\n            it->second = std::make_unique<UDPPacketHandler>(_handler);\n        else\n            // FIXME: this should probably throw\n            log::info(logcat, \"Packet router already holds registered UDP packet handler!\");\n\n        it->second->add_sub_handler(localport, std::move(func));\n    }\n\n    void PacketRouter::add_ip_proto_handler(net::IPProtocol proto, ip_pkt_hook func)\n    {\n        _ip_proto_handler[proto] = std::make_unique<GenericLayer4Handler>(std::move(func));\n    }\n\n}  // namespace llarp::vpn\n"
  },
  {
    "path": "llarp/vpn/packet_router.hpp",
    "content": "#pragma once\n\n#include <llarp/net/ip_packet.hpp>\n\n#include <functional>\n#include <unordered_map>\n\nnamespace llarp::vpn\n{\n    struct Layer4Handler;\n\n    class PacketRouter\n    {\n        ip_pkt_hook _handler;\n        // std::unordered_map<uint8_t, std::unique_ptr<Layer4Handler>> _ip_proto_handler;\n        std::unordered_map<net::IPProtocol, std::unique_ptr<Layer4Handler>> _ip_proto_handler;\n\n      public:\n        /// baseHandler will be called if no other handlers matches a packet\n        explicit PacketRouter(ip_pkt_hook baseHandler);\n\n        /// feed in an ip packet for handling\n        void handle_ip_packet(IPPacket pkt) const;\n\n        /// add a non udp packet handler using ip protocol proto\n        void add_ip_proto_handler(net::IPProtocol proto, ip_pkt_hook func);\n\n        /// helper that adds a udp packet handler for UDP destined for localport\n        void add_udp_handler(uint16_t port, ip_pkt_hook func);\n\n        /// remove a udp handler that is already set up by bound port\n        void remove_udp_handler(uint16_t port);\n    };\n\n    struct Layer4Handler\n    {\n        virtual ~Layer4Handler() = default;\n\n        virtual void handle_ip_packet(IPPacket pkt) = 0;\n\n        virtual void add_sub_handler(uint16_t, ip_pkt_hook) {};\n    };\n\n}  // namespace llarp::vpn\n"
  },
  {
    "path": "llarp/vpn/platform.cpp",
    "content": "\n#include \"platform.hpp\"\n\n#ifdef _WIN32\n#include \"win32.hpp\"\n#endif\n#ifdef __linux__\n#ifdef ANDROID\n#include \"android.hpp\"\n#else\n#include \"linux.hpp\"\n#endif\n#endif\n\nnamespace llarp::vpn\n{\n    const llarp::net::Platform* AbstractRouteManager::net_ptr() const { return llarp::net::Platform::Default_ptr(); }\n\n    std::shared_ptr<Platform> MakeNativePlatform(llarp::Context* ctx)\n    {\n        (void)ctx;\n        std::shared_ptr<Platform> plat;\n#ifdef _WIN32\n        plat = std::make_shared<llarp::win32::VPNPlatform>(ctx);\n#endif\n#ifdef __linux__\n#ifdef ANDROID\n        plat = std::make_shared<vpn::AndroidPlatform>(ctx);\n#else\n        plat = std::make_shared<vpn::LinuxPlatform>();\n#endif\n#endif\n#ifdef __APPLE__\n        throw std::runtime_error{\"not supported\"};\n#endif\n        return plat;\n    }\n\n}  // namespace llarp::vpn\n"
  },
  {
    "path": "llarp/vpn/platform.hpp",
    "content": "#pragma once\n\n#include \"packet_io.hpp\"\n\n#include <llarp/address/ip_range.hpp>\n#include <llarp/net/ip_packet.hpp>\n#include <llarp/net/platform.hpp>\n\n#include <oxen/quic/address.hpp>\n\n#include <variant>\n\nnamespace llarp\n{\n    struct Context;\n    class Router;\n}  // namespace llarp\n\nnamespace llarp::vpn\n{\n    struct InterfaceInfo\n    {\n        unsigned int index;\n        std::string ifname;\n        std::vector<std::variant<ipv4_net, ipv6_net>> addrs;\n    };\n\n    /// a vpn network interface\n    class NetworkInterface : public PacketIO\n    {\n      protected:\n        InterfaceInfo _info;\n\n      public:\n        NetworkInterface() = default;\n        NetworkInterface(InterfaceInfo info) : _info{std::move(info)} {}\n        NetworkInterface(const NetworkInterface&) = delete;\n        NetworkInterface(NetworkInterface&&) = delete;\n\n        const InterfaceInfo& interface_info() const { return _info; }\n\n        /// idempotently wake up the upper layers as needed (platform dependant)\n        virtual void MaybeWakeUpperLayers() const {};\n    };\n\n    class AbstractRouteManager\n    {\n      public:\n        AbstractRouteManager() = default;\n        AbstractRouteManager(const AbstractRouteManager&) = delete;\n        AbstractRouteManager(AbstractRouteManager&&) = delete;\n        virtual ~AbstractRouteManager() = default;\n\n        virtual const llarp::net::Platform* net_ptr() const;\n\n        inline const llarp::net::Platform& Net() const { return *net_ptr(); }\n\n        virtual void add_route(ipv4 ip, ipv4 gateway) = 0;\n        virtual void add_route(ipv6 ip, ipv6 gateway) = 0;\n\n        virtual void delete_route(ipv4 ip, ipv4 gateway) = 0;\n        virtual void delete_route(ipv6 ip, ipv6 gateway) = 0;\n\n        virtual void add_default_route_via_interface(NetworkInterface& vpn) = 0;\n        virtual void delete_default_route_via_interface(NetworkInterface& vpn) = 0;\n\n        virtual void add_route_via_interface(NetworkInterface& vpn, ipv4_range range) = 0;\n        virtual void add_route_via_interface(NetworkInterface& vpn, ipv6_range range) = 0;\n\n        virtual void delete_route_via_interface(NetworkInterface& vpn, ipv4_range range) = 0;\n        virtual void delete_route_via_interface(NetworkInterface& vpn, ipv6_range range) = 0;\n\n        virtual std::vector<quic::Address> get_non_interface_gateways(NetworkInterface& vpn) = 0;\n\n        virtual void add_blackhole() {}\n\n        virtual void delete_blackhole() {}\n    };\n\n    /// a vpn platform\n    /// responsible for obtaining vpn interfaces\n    class Platform\n    {\n      protected:\n        /// get a new network interface fully configured given the interface info\n        /// blocks until ready, throws on error\n        virtual std::shared_ptr<NetworkInterface> obtain_interface(InterfaceInfo info, Router* router) = 0;\n\n      public:\n        Platform() = default;\n        Platform(const Platform&) = delete;\n        Platform(Platform&&) = delete;\n        virtual ~Platform() = default;\n\n        /// create and start a network interface\n        std::shared_ptr<NetworkInterface> create_interface(InterfaceInfo info, Router* router)\n        {\n            if (auto netif = obtain_interface(std::move(info), router))\n            {\n                netif->Start();\n                return netif;\n            }\n            return nullptr;\n        }\n\n        /// get owned ip route manager for managing routing table\n        virtual AbstractRouteManager& RouteManager() = 0;\n\n        /// create a packet io that will read (and optionally write) packets on a network interface\n        /// the lokinet process does not own\n        /// @param index the interface index of the network interface to use or 0 for all\n        /// interfaces on the system\n        virtual std::shared_ptr<PacketIO> create_packet_io(\n            [[maybe_unused]] unsigned int ifindex,\n            [[maybe_unused]] const std::optional<quic::Address>& dns_upstream_src)\n        {\n            throw std::runtime_error{\"raw packet io is unimplemented\"};\n        }\n    };\n\n    /// create native vpn platform\n    std::shared_ptr<Platform> MakeNativePlatform(llarp::Context* ctx);\n\n}  // namespace llarp::vpn\n"
  },
  {
    "path": "llarp/vpn/win32.cpp",
    "content": "#include \"win32.hpp\"\n\n#include <llarp/win32/adapters.hpp>\n#include <llarp/win32/windivert.hpp>\n#include <llarp/win32/wintun.hpp>\n\n#include <fmt/core.h>\n\nnamespace llarp::win32\n{\n    namespace\n    {\n        template <typename T>\n        std::string ip_to_string(T ip)\n        {\n            return var::visit([](auto&& ip) { return ip.to_string(); }, ip);\n        }\n    }  // namespace\n\n    void VPNPlatform::make_route(std::string ip, std::string gw, std::string cmd)\n    {\n        llarp::win32::Exec(\"route.exe\", fmt::format(\"{} {} MASK 255.255.255.255 {} METRIC {}\", cmd, ip, gw, m_Metric));\n    }\n\n    void VPNPlatform::default_route_via_interface(NetworkInterface& vpn, std::string cmd)\n    {\n        // route hole for loopback bacause god is dead on windows\n        llarp::win32::Exec(\"route.exe\", fmt::format(\"{} 127.0.0.0 MASK 255.0.0.0 0.0.0.0\", cmd));\n        // set up ipv4 routes\n        route_via_interface(vpn, \"0.0.0.0\", \"128.0.0.0\", cmd);\n        route_via_interface(vpn, \"128.0.0.0\", \"128.0.0.0\", cmd);\n    }\n\n    void VPNPlatform::route_via_interface(NetworkInterface& vpn, std::string addr, std::string mask, std::string cmd)\n    {\n        const auto& info = vpn.interface_info();\n        auto ifaddr = ip_to_string(info[0]);\n        // this changes the last 1 to a 0 so that it routes over the interface\n        // this is required because windows is idiotic af\n        ifaddr.back()--;\n        llarp::win32::Exec(\"route.exe\", fmt::format(\"{} {} MASK {} {} METRIC {}\", cmd, addr, mask, ifaddr, m_Metric));\n    }\n\n    void VPNPlatform::add_route(quic::Address ip, quic::Address gateway)\n    {\n        make_route(ip.to_string(), gateway.to_string(), \"ADD\");\n    }\n\n    void VPNPlatform::delete_route(quic::Address ip, quic::Address gateway)\n    {\n        make_route(ip.to_string(), gateway.to_string(), \"DELETE\");\n    }\n\n    void VPNPlatform::add_route_via_interface(NetworkInterface& vpn, IPRange range)\n    {\n        route_via_interface(vpn, range.BaseAddressString(), range.NetmaskString(), \"ADD\");\n    }\n\n    void VPNPlatform::delete_route_via_interface(NetworkInterface& vpn, IPRange range)\n    {\n        route_via_interface(vpn, range.BaseAddressString(), range.NetmaskString(), \"DELETE\");\n    }\n\n    std::vector<quic::Address> VPNPlatform::get_non_interface_gateways(NetworkInterface& vpn)\n    {\n        std::set<quic::Address> gateways;\n\n        // FIXME: This code is probably broken.  The idea here:\n        // - iterate through all system interfaces\n        // - if the interface has no gateway, skip it.\n        // - if any of those interfaces have a network that contains our interface network(s) (that\n        //   is: those in the `vpn` input), then skip it.\n        // - else collect the gateway address\n        //\n        win32::iter_adapters([&if_info = vpn.interface_info(), &gateways](auto* a) {\n            auto* igw = a->FirstGatewayAddress;\n            if (!igw)\n                return;\n            quic::Address gw{igw->Address.lpSockaddr, igw->Address.iSockaddrLength};\n\n            bool accept = true;\n            for (auto* addr = a->FirstUnicastAddress; accept and addr; addr = addr->Next)\n            {\n                if (addr->Address.lpSockaddr->sa_family != AF_INET && addr->Address.lpSockaddr->sa_family != AF_INET6)\n                    continue;\n                quic::Address adapter_addr{addr->Address.lpSockaddr, addr->Address.iSockaddrLength};\n                auto netmask_bits = ipaddr_netmask_bits(addr->OnLinkPrefixLength, addr->Address.lpSockaddr->sa_family);\n                std::variant<ipv4_range, ipv6_range> adapter_range;\n                if (adapter_addr.is_ipv4())\n                    adapter_range = adapter_addr.to_ipv4() / netmask_bits;\n                else\n                    adapter_range = adapter_addr.to_ipv6() / netmask_bits;\n\n                for (auto& a : if_info.addrs)\n                {\n                    auto contains = std::visit(\n                        [&adapter_range]<typename Net, typename Range>(const Net& a, const Range& b) {\n                            if constexpr (\n                                (std::same_as<Net, ipv4_net> && std::same_as<Range, ipv4_range>)\n                                || (std::same_as<Net, ipv6_net> && std::same_as<Range, ipv6_range>))\n                                return b.contains(a.ip);\n                            return false;\n                        },\n                        a,\n                        adapter_range);\n                    if (contains)\n                    {\n                        accept = false;\n                        break;\n                    }\n                }\n            }\n\n            if (accept)\n                gateways.insert(std::move(gw));\n        });\n\n        return {gateways.begin(), gateways.end()};\n    }\n\n    void VPNPlatform::add_default_route_via_interface(NetworkInterface& vpn)\n    {\n        // kill ipv6\n        llarp::win32::Exec(\n            \"WindowsPowerShell\\\\v1.0\\\\powershell.exe\",\n            \"-Command (Disable-NetAdapterBinding -Name \\\"* \\\" -ComponentID ms_tcpip6)\");\n\n        default_route_via_interface(vpn, \"ADD\");\n    }\n\n    void VPNPlatform::delete_default_route_via_interface(NetworkInterface& vpn)\n    {\n        // restore ipv6\n        llarp::win32::Exec(\n            \"WindowsPowerShell\\\\v1.0\\\\powershell.exe\",\n            \"-Command (Enable-NetAdapterBinding -Name \\\"* \\\" -ComponentID ms_tcpip6)\");\n\n        default_route_via_interface(vpn, \"DELETE\");\n    }\n\n    std::shared_ptr<NetworkInterface> VPNPlatform::obtain_interface(InterfaceInfo info, Router* router)\n    {\n        return wintun::make_interface(std::move(info), router);\n    }\n\n    std::shared_ptr<PacketIO> VPNPlatform::create_packet_io(\n        unsigned int ifindex, const std::optional<quic::Address>& dns_upstream_src)\n    {\n        // we only want do this on all interfaes with windivert\n        if (ifindex)\n            throw std::invalid_argument{\n                \"cannot create packet io on explicitly specified interface, not currently \"\n                \"supported on \"\n                \"windows (yet)\"};\n\n        uint16_t upstream_src_port = dns_upstream_src ? dns_upstream_src->port() : 0;\n        std::string udp_filter = upstream_src_port != 0\n            ? fmt::format(\"( udp.DstPort == 53 and udp.SrcPort != {} )\", upstream_src_port)\n            : \"udp.DstPort == 53\";\n\n        auto filter = \"outbound and ( \" + udp_filter + \" or tcp.DstPort == 53 )\";\n\n        // TESTNET:\n        return WinDivert::make_interceptor(filter, [router = _ctx->router] { /* router->TriggerPump(); */ });\n    }\n}  // namespace llarp::win32\n"
  },
  {
    "path": "llarp/vpn/win32.hpp",
    "content": "#pragma once\n\n#include \"platform.hpp\"\n\n#include <llarp.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/win32/exec.hpp>\n\n#include <winsock2.h>\n\n#include <windows.h>\n\n#include <iphlpapi.h>\n\nnamespace llarp::win32\n{\n    using namespace llarp::vpn;\n    class VPNPlatform : public Platform, public AbstractRouteManager\n    {\n        llarp::Context* const _ctx;\n        const int m_Metric{2};\n\n        const auto& Net() const { return _ctx->router->net(); }\n\n        void make_route(std::string ip, std::string gw, std::string cmd);\n\n        void default_route_via_interface(NetworkInterface& vpn, std::string cmd);\n\n        void route_via_interface(NetworkInterface& vpn, std::string addr, std::string mask, std::string cmd);\n\n      public:\n        VPNPlatform(const VPNPlatform&) = delete;\n        VPNPlatform(VPNPlatform&&) = delete;\n\n        VPNPlatform(llarp::Context* ctx) : Platform{}, _ctx{ctx} {}\n\n        ~VPNPlatform() override = default;\n\n        void add_route(quic::Address ip, quic::Address gateway) override;\n\n        void delete_route(quic::Address ip, quic::Address gateway) override;\n\n        void add_route_via_interface(NetworkInterface& vpn, IPRange range) override;\n\n        void delete_route_via_interface(NetworkInterface& vpn, IPRange range) override;\n\n        std::vector<quic::Address> get_non_interface_gateways(NetworkInterface& vpn) override;\n\n        void add_default_route_via_interface(NetworkInterface& vpn) override;\n\n        void delete_default_route_via_interface(NetworkInterface& vpn) override;\n\n        std::shared_ptr<NetworkInterface> obtain_interface(InterfaceInfo info, Router* router) override;\n\n        std::shared_ptr<PacketIO> create_packet_io(\n            unsigned int ifindex, const std::optional<quic::Address>& dns_upstream_src) override;\n\n        AbstractRouteManager& RouteManager() override { return *this; }\n    };\n\n}  // namespace llarp::win32\n"
  },
  {
    "path": "llarp/win32/adapters.hpp",
    "content": "#pragma once\n#include <llarp/win32/exception.hpp>\n\n#include <iphlpapi.h>\n\n#include <concepts>\n\nnamespace llarp::win32\n{\n\n    /// visit all adapters (not addresses). windows serves net info per adapter unlink posix\n    /// which gives a list of all distinct addresses.\n    template <std::invocable<PIP_ADAPTER_ADDRESSES> Visitor>\n    void iter_adapters(Visitor&& visit, int af = AF_UNSPEC)\n    {\n        ULONG err;\n        ULONG sz = 15000;  // MS-recommended so that it \"never fails\", but often fails with a\n                           // too large error.\n        std::unique_ptr<uint8_t[]> ptr;\n        PIP_ADAPTER_ADDRESSES addr;\n        int tries = 0;\n        do\n        {\n            ptr = std::make_unique<uint8_t[]>(sz);\n            addr = reinterpret_cast<PIP_ADAPTER_ADDRESSES>(ptr.get());\n            err = GetAdaptersAddresses(af, GAA_FLAG_INCLUDE_GATEWAYS | GAA_FLAG_INCLUDE_PREFIX, nullptr, addr, &sz);\n        } while (err == ERROR_BUFFER_OVERFLOW and ++tries < 4);\n\n        if (err != ERROR_SUCCESS)\n            throw llarp::win32::error{err, \"GetAdaptersAddresses()\"};\n\n        for (; addr; addr = addr->Next)\n            visit(addr);\n    }\n}  // namespace llarp::win32\n"
  },
  {
    "path": "llarp/win32/dll.cpp",
    "content": "#include \"dll.hpp\"\n\n#include <llarp/util/logging.hpp>\n#include <llarp/util/str.hpp>\n\nnamespace llarp::win32\n{\n    namespace\n    {\n        auto cat = log::Cat(\"win32-dll\");\n    }\n\n    namespace detail\n    {\n        HMODULE\n        load_dll(const std::string& dll)\n        {\n            auto handle = LoadLibraryExA(dll.c_str(), NULL, LOAD_LIBRARY_SEARCH_APPLICATION_DIR);\n            if (not handle)\n                throw win32::error{fmt::format(\"failed to load '{}'\", dll)};\n            log::info(cat, \"loaded '{}'\", dll);\n            return handle;\n        }\n    }  // namespace detail\n}  // namespace llarp::win32\n"
  },
  {
    "path": "llarp/win32/dll.hpp",
    "content": "#pragma once\n#include \"exception.hpp\"\n\n#include <llarp/util/str.hpp>\n\n#include <windows.h>\n\nnamespace llarp::win32\n{\n    namespace detail\n    {\n        HMODULE\n        load_dll(const std::string& dll);\n\n        template <typename Func, typename... More>\n        void load_funcs(HMODULE handle, const std::string& name, Func*& f, More&&... more)\n        {\n            if (auto ptr = GetProcAddress(handle, name.c_str()))\n                f = reinterpret_cast<Func*>(ptr);\n            else\n                throw win32::error{fmt::format(\"function '{}' not found\", name)};\n            if constexpr (sizeof...(More) > 0)\n                load_funcs(handle, std::forward<More>(more)...);\n        }\n    }  // namespace detail\n\n    // Loads a DLL and extracts function pointers from it.  Takes the dll name and pairs of\n    // name/function pointer arguments.  Throws on failure.\n    template <typename Func, typename... More>\n    void load_dll_functions(const std::string& dll, const std::string& fname, Func*& f, More&&... funcs)\n    {\n        detail::load_funcs(detail::load_dll(dll), fname, f, std::forward<More>(funcs)...);\n    }\n}  // namespace llarp::win32\n"
  },
  {
    "path": "llarp/win32/exception.cpp",
    "content": "#include \"exception.hpp\"\n\n#include \"windows.h\"\n\n#include <llarp/util/str.hpp>\n\n#include <array>\n\nnamespace llarp::win32\n\n{\n    error::error(std::string msg) : error{GetLastError(), std::move(msg)} {}\n    error::error(DWORD err, std::string msg)\n        : std::runtime_error{fmt::format(\"{}: {} (code={})\", msg, error_to_string(err), err)}\n    {}\n    std::string error_to_string(DWORD err)\n    {\n        // mostly yoinked from https://stackoverflow.com/a/45565001\n        LPTSTR psz{nullptr};\n        const DWORD cchMsg = FormatMessage(\n            FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_ALLOCATE_BUFFER,\n            nullptr,\n            err,\n            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),\n            reinterpret_cast<LPTSTR>(&psz),\n            0,\n            nullptr);\n\n        if (cchMsg <= 0)\n        {\n            // cannot get message for error, reset the last error here so it does not propagate\n            ::SetLastError(0);\n            return \"unknown error\";\n        }\n\n        auto deleter = [](void* p) { ::LocalFree(p); };\n        std::unique_ptr<TCHAR, decltype(deleter)> ptrBuffer{psz, deleter};\n        return std::string{ptrBuffer.get(), cchMsg};\n    }\n}  // namespace llarp::win32\n"
  },
  {
    "path": "llarp/win32/exception.hpp",
    "content": "#pragma once\n#include <windows.h>\n\n#include <stdexcept>\n#include <string>\n\nnamespace llarp::win32\n{\n    std::string error_to_string(DWORD err);\n\n    class error : public std::runtime_error\n    {\n      public:\n        explicit error(std::string msg);\n        virtual ~error() = default;\n        explicit error(DWORD err, std::string msg);\n    };\n}  // namespace llarp::win32\n"
  },
  {
    "path": "llarp/win32/exec.cpp",
    "content": "#include \"exec.hpp\"\n\n#include \"exception.hpp\"\n\n#include <llarp/util/logging.hpp>\n\n#include <array>\n\nnamespace llarp::win32\n{\n    namespace\n    {\n        static auto logcat = log::Cat(\"win32:exec\");\n\n        /// get the directory for system32 which contains all the executables we use\n        std::string SystemExeDir()\n        {\n            std::array<char, MAX_PATH + 1> path{};\n\n            if (GetSystemDirectoryA(path.data(), path.size()))\n                return path.data();\n\n            return \"C:\\\\Windows\\\\system32\";\n        }\n\n    }  // namespace\n\n    void Exec(std::string exe, std::string args) { OneShotExec{exe, args}; }\n\n    OneShotExec::OneShotExec(std::string cmd, std::chrono::milliseconds timeout)\n        : _si{}, _pi{}, _timeout{static_cast<DWORD>(timeout.count())}\n    {\n        log::info(logcat, \"exec: {}\", cmd);\n        if (not CreateProcessA(nullptr, cmd.data(), nullptr, nullptr, false, 0, nullptr, nullptr, &_si, &_pi))\n            throw win32::error(GetLastError(), \"failed to execute subprocess\");\n    }\n\n    OneShotExec::~OneShotExec()\n    {\n        WaitForSingleObject(_pi.hProcess, _timeout);\n        CloseHandle(_pi.hProcess);\n        CloseHandle(_pi.hThread);\n    }\n\n    OneShotExec::OneShotExec(std::string cmd, std::string args, std::chrono::milliseconds timeout)\n        : OneShotExec{fmt::format(\"{}\\\\{} {}\", SystemExeDir(), cmd, args), timeout}\n    {}\n\n    DeferExec::~DeferExec() { OneShotExec{std::move(_exe), std::move(_args), std::move(_timeout)}; }\n}  // namespace llarp::win32\n"
  },
  {
    "path": "llarp/win32/exec.hpp",
    "content": "#pragma once\n\n#include <llarp/util/time.hpp>\n\n#include <windows.h>\n\n#include <string>\n\nnamespace llarp::win32\n{\n    /// RAII wrapper for calling a subprocess in win32 for parallel executuion in the same scope\n    /// destructor blocks until timeout has been hit of the execution of the subprocses finished\n    class OneShotExec\n    {\n        STARTUPINFO _si;\n        PROCESS_INFORMATION _pi;\n        const DWORD _timeout;\n\n        OneShotExec(std::string cmd, std::chrono::milliseconds timeout);\n\n      public:\n        /// construct a call to an exe in system32 with args, will resolve the full path of the exe\n        /// to prevent path injection\n        explicit OneShotExec(std::string exe, std::string args, std::chrono::milliseconds timeout = 5s);\n\n        ~OneShotExec();\n    };\n\n    /// a wrapper for OneShotExec that calls the thing we want on destruction\n    class DeferExec\n    {\n        std::string _exe;\n        std::string _args;\n        std::chrono::milliseconds _timeout;\n\n      public:\n        explicit DeferExec(std::string exe, std::string args, std::chrono::milliseconds timeout = 5s)\n            : _exe{std::move(exe)}, _args{std::move(args)}, _timeout{std::move(timeout)}\n        {}\n\n        ~DeferExec();\n    };\n\n    /// simple wrapper to run a single command in a blocking way\n    void Exec(std::string exe, std::string args);\n\n}  // namespace llarp::win32\n"
  },
  {
    "path": "llarp/win32/guid.hpp",
    "content": "#pragma once\n\n#include <llarp/crypto/crypto.hpp>\n\n#include <windows.h>\n\nnamespace llarp::win32\n{\n    /// @brief given a container of data hash it and make it into a GUID so we have a way to\n    /// deterministically generate GUIDs\n    template <typename Data>\n    inline GUID MakeDeterministicGUID(Data data)\n    {\n        auto h = crypto::shorthash(std::span{reinterpret_cast<const std::byte*>(data.data()), data.size()});\n        static_assert(sizeof(GUID) <= decltype(h)::SIZE);\n        GUID guid{};\n        std::memcpy(&guid, h.data(), sizeof(guid));\n        return guid;\n    }\n}  // namespace llarp::win32\n"
  },
  {
    "path": "llarp/win32/handle.hpp",
    "content": "#pragma once\n\n#include \"exception.hpp\"\n\nnamespace llarp::win32\n{\n    inline void ensure_handle_is_valid(HANDLE h)\n    {\n        BY_HANDLE_FILE_INFORMATION info{};\n        if (GetFileInformationByHandle(h, &info))\n            return;\n        if (auto err = GetLastError())\n        {\n            SetLastError(0);\n            throw llarp::win32::error{err, \"handle validity check failed\"};\n        }\n    }\n}  // namespace llarp::win32\n"
  },
  {
    "path": "llarp/win32/service_manager.cpp",
    "content": "#include \"service_manager.hpp\"\n\n#include <llarp.hpp>\n#include <llarp/util/logging.hpp>\n\n#include <cassert>\n#include <chrono>\n#include <csignal>\n#include <optional>\n\nnamespace llarp::sys\n{\n\n    static auto logcat = log::Cat(\"svc\");\n\n    namespace\n    {\n        std::optional<DWORD> to_win32_state(ServiceState st)\n        {\n            switch (st)\n            {\n                case ServiceState::Starting:\n                    return SERVICE_START_PENDING;\n                case ServiceState::Running:\n                    return SERVICE_RUNNING;\n                case ServiceState::Stopping:\n                    return SERVICE_STOP_PENDING;\n                case ServiceState::Stopped:\n                    return SERVICE_STOPPED;\n                default:\n                    return std::nullopt;\n            }\n        }\n    }  // namespace\n\n    SVC_Manager::SVC_Manager() { _status.dwServiceType = SERVICE_WIN32_OWN_PROCESS; }\n\n    void SVC_Manager::system_changed_our_state(ServiceState st)\n    {\n        if (m_disable)\n            return;\n        if (st == ServiceState::Stopping)\n        {\n            we_changed_our_state(st);\n        }\n    }\n\n    void SVC_Manager::report_changed_state()\n    {\n        if (m_disable)\n            return;\n\n        log::debug(\n            logcat,\n            \"Reporting Windows service status '{}', exit code {}, wait hint {}, dwCP {}, dwCA {}\",\n            _status.dwCurrentState == SERVICE_START_PENDING      ? \"start pending\"\n                : _status.dwCurrentState == SERVICE_RUNNING      ? \"running\"\n                : _status.dwCurrentState == SERVICE_STOPPED      ? \"stopped\"\n                : _status.dwCurrentState == SERVICE_STOP_PENDING ? \"stop pending\"\n                                                                 : fmt::format(\"unknown: {}\", _status.dwCurrentState),\n            _status.dwWin32ExitCode,\n            _status.dwWaitHint,\n            _status.dwCheckPoint,\n            _status.dwControlsAccepted);\n\n        SetServiceStatus(handle, &_status);\n    }\n\n    void SVC_Manager::we_changed_our_state(ServiceState st)\n    {\n        if (st == ServiceState::Failed)\n        {\n            _status.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;\n            _status.dwServiceSpecificExitCode = 2;  // TODO: propagate more info ?\n            report_changed_state();\n        }\n        else if (auto maybe_state = to_win32_state(st))\n        {\n            auto new_state = *maybe_state;\n            _status.dwWin32ExitCode = NO_ERROR;\n            _status.dwCurrentState = new_state;\n            _status.dwControlsAccepted = st == ServiceState::Running ? SERVICE_ACCEPT_STOP : 0;\n            _status.dwWaitHint =\n                std::chrono::milliseconds{\n                    st == ServiceState::Starting       ? StartupTimeout\n                        : st == ServiceState::Stopping ? StopTimeout\n                                                       : 0s}\n                    .count();\n            // dwCheckPoint gets incremented during a start/stop to tell windows \"we're still\n            // starting/stopping\" and to reset its must-be-hung timer.  We increment it here so that\n            // this can be called multiple times to tells Windows something is happening.\n            if (st == ServiceState::Starting or st == ServiceState::Stopping)\n                _status.dwCheckPoint++;\n            else\n                _status.dwCheckPoint = 0;\n\n            report_changed_state();\n        }\n    }\n\n    SVC_Manager _manager{};\n    I_SystemLayerManager* const service_manager = &_manager;\n}  // namespace llarp::sys\n"
  },
  {
    "path": "llarp/win32/service_manager.hpp",
    "content": "#pragma once\n#include <llarp/util/service_manager.hpp>\n\n#include <windows.h>\n\n#include <dbghelp.h>\n\n#include <chrono>\n\nnamespace llarp::sys\n{\n\n    class SVC_Manager : public I_SystemLayerManager\n    {\n        SERVICE_STATUS _status;\n\n      public:\n        SERVICE_STATUS_HANDLE handle;\n\n        // How long we tell Windows to give us to startup before assuming we have stalled/hung.  The\n        // biggest potential time here is wintun, which if it is going to fail appears to take\n        // around 15s before doing so.\n        static constexpr auto StartupTimeout = 17s;\n\n        // How long we tell Windows to give us to fully stop before killing us.\n        static constexpr auto StopTimeout = 5s;\n\n        SVC_Manager();\n\n        void system_changed_our_state(ServiceState st) override;\n\n        void report_changed_state() override;\n\n        void we_changed_our_state(ServiceState st) override;\n    };\n}  // namespace llarp::sys\n"
  },
  {
    "path": "llarp/win32/version.rc.in",
    "content": "// WARNING: for the love of all that is good and holy\n// please DO NOT convert this file to UTF-8, much less\n// UTF-16 - the UNIX cross-rc does not understand UTF-16,\n// and UTF-8 chews up the copyright symbols.\n// -rick\n//\n// Microsoft Visual C++ generated resource script.\n//\n\n// clang-format off\n#define lokinet_VERSION @lokinet_VERSION_MAJOR@, @lokinet_VERSION_MINOR@, @lokinet_VERSION_PATCH@, 0\n\n#ifdef __GNUC__ // make windows rc accept this\n#include <winresrc.h>\n#endif\n/////////////////////////////////////////////////////////////////////////////\n// English (United States) resources\n\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\n#ifdef _WIN32\nLANGUAGE 1033,1\n#pragma code_page(1252)\n#endif //_WIN32\n\n#ifdef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// TEXTINCLUDE\n//\n\n1 TEXTINCLUDE\nBEGIN\n    \"resource.h\\0\"\nEND\n\n2 TEXTINCLUDE\nBEGIN\n    \"\\0\"\nEND\n\n3 TEXTINCLUDE\nBEGIN\n    \"\\r\\n\"\n    \"\\0\"\nEND\n\n#endif    // APSTUDIO_INVOKED\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Version\n//\n\nVS_VERSION_INFO VERSIONINFO\n FILEVERSION lokinet_VERSION\n PRODUCTVERSION lokinet_VERSION\n FILEFLAGSMASK 0x17L\n#ifdef _DEBUG\n FILEFLAGS 0x3L\n#else\n FILEFLAGS 0x2L\n#endif\n FILEOS 0x40004L\n FILETYPE 0x1L\n FILESUBTYPE 0x0L\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904b0\"\n        BEGIN\n            VALUE \"Comments\", \"This comment has invoked its 5th ammendment constitutional right to remain silent\"\n            VALUE \"CompanyName\", \"OPTF\"\n            VALUE \"FileDescription\", \"LokiNET daemon for Windows\"\n            VALUE \"FileVersion\", \"@lokinet_VERSION@\"\n            VALUE \"InternalName\", \"lokinet\"\n            VALUE \"LegalCopyright\", \"Copyright (c) 2018-2022 Jeff Becker, Rick V for the OPTF. This software is provided under the terms of the GPL3; see the file LICENSE for details.\"\n            VALUE \"OriginalFilename\", \"@lokinet_EXE_NAME@\"\n            VALUE \"ProductName\", \"LokiNET for Windows\"\n            VALUE \"ProductVersion\", \"@lokinet_VERSION@\"\n        END\n    END\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        VALUE \"Translation\", 0x409, 1200\n    END\nEND\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// RT_MANIFEST\n//\n\n// Uncomment if your toolchain does not provide a default-manifest.o\n//3                       RT_MANIFEST             \"app.xml\"\n\n#endif    // English (United States) resources\n/////////////////////////////////////////////////////////////////////////////\n\n\n\n#ifndef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 3 resource.\n//\n\n\n/////////////////////////////////////////////////////////////////////////////\n#endif    // not APSTUDIO_INVOKED\n"
  },
  {
    "path": "llarp/win32/win32_inet.c",
    "content": "#if defined(__MINGW32__) && !defined(_WIN64)\n/*\n * Contains routines missing from WS2_32.DLL until 2006, if yer using\n * Microsoft C/C++, then this code is irrelevant, as the official\n * Platform SDK already links against these routines in the correct\n * libraries.\n *\n * -despair86 30/07/18\n */\n\n// these need to be in a specific order\n#include <llarp/net/net.h>\n\n#include <windows.h>\n\n#include <assert.h>\n#include <iphlpapi.h>\n#include <stdbool.h>\n\nconst char* inet_ntop(int af, const void* src, char* dst, size_t size)\n{\n    int address_length;\n    DWORD string_length = size;\n    struct sockaddr_storage sa;\n    struct sockaddr_in* sin = (struct sockaddr_in*)&sa;\n    struct sockaddr_in6* sin6 = (struct sockaddr_in6*)&sa;\n\n    memset(&sa, 0, sizeof(sa));\n    switch (af)\n    {\n        case AF_INET:\n            address_length = sizeof(struct sockaddr_in);\n            sin->sin_family = af;\n            memcpy(&sin->sin_addr, src, sizeof(struct in_addr));\n            break;\n\n        case AF_INET6:\n            address_length = sizeof(struct sockaddr_in6);\n            sin6->sin6_family = af;\n            memcpy(&sin6->sin6_addr, src, sizeof(struct in6_addr));\n            break;\n\n        default:\n            return NULL;\n    }\n\n    if (WSAAddressto_string((LPSOCKADDR)&sa, address_length, NULL, dst, &string_length) == 0)\n    {\n        return dst;\n    }\n\n    return NULL;\n}\n\nint inet_pton(int af, const char* src, void* dst)\n{\n    int address_length;\n    struct sockaddr_storage sa;\n    struct sockaddr_in* sin = (struct sockaddr_in*)&sa;\n    struct sockaddr_in6* sin6 = (struct sockaddr_in6*)&sa;\n\n    switch (af)\n    {\n        case AF_INET:\n            address_length = sizeof(struct sockaddr_in);\n            break;\n\n        case AF_INET6:\n            address_length = sizeof(struct sockaddr_in6);\n            break;\n\n        default:\n            return -1;\n    }\n\n    if (WSAStringToAddress((LPTSTR)src, af, NULL, (LPSOCKADDR)&sa, &address_length) == 0)\n    {\n        switch (af)\n        {\n            case AF_INET:\n                memcpy(dst, &sin->sin_addr, sizeof(struct in_addr));\n                break;\n\n            case AF_INET6:\n                memcpy(dst, &sin6->sin6_addr, sizeof(struct in6_addr));\n                break;\n        }\n        return 1;\n    }\n\n    return 0;\n}\n\n#endif\n"
  },
  {
    "path": "llarp/win32/win32_intrnl.c",
    "content": "// there's probably an use case for a _newer_ implementation\n// of pthread_setname_np(3), in fact, I may just merge _this_\n// upstream...\n#ifdef _MSC_VER\n#include <windows.h>\n\ntypedef HRESULT(FAR PASCAL* p_SetThreadDescription)(void*, const wchar_t*);\n#define EXCEPTION_SET_THREAD_NAME ((DWORD)0x406D1388)\n\ntypedef struct _THREADNAME_INFO\n{\n    DWORD dwType;     /* must be 0x1000 */\n    LPCSTR szName;    /* pointer to name (in user addr space) */\n    DWORD dwThreadID; /* thread ID (-1=caller thread) */\n    DWORD dwFlags;    /* reserved for future use, must be zero */\n} THREADNAME_INFO;\n\nvoid SetThreadName(DWORD dwThreadID, LPCSTR szThreadName)\n{\n    THREADNAME_INFO info;\n    DWORD infosize;\n    HANDLE hThread;\n    /* because loonix is SHIT and limits thread names to 16 bytes */\n    wchar_t thr_name_w[16];\n    p_SetThreadDescription _SetThreadDescription;\n\n    /* current win10 flights now have a new named-thread API, let's try to use\n     * that first! */\n    /* first, dlsym(2) the new call from system library */\n    hThread = NULL;\n    _SetThreadDescription = (p_SetThreadDescription)GetProcAddress(GetModuleHandle(\"kernel32\"), \"SetThreadDescription\");\n    if (_SetThreadDescription)\n    {\n        /* grab another reference to the thread */\n        hThread = OpenThread(THREAD_SET_LIMITED_INFORMATION, FALSE, dwThreadID);\n        /* windows takes unicode, our input is utf-8 or plain ascii */\n        MultiByteToWideChar(CP_ACP, 0, szThreadName, -1, thr_name_w, 16);\n        if (hThread)\n            _SetThreadDescription(hThread, thr_name_w);\n        else\n            goto old; /* for whatever reason, we couldn't get a handle to the thread.\n                         Just use the old method. */\n    }\n    else\n    {\n    old:\n        info.dwType = 0x1000;\n        info.szName = szThreadName;\n        info.dwThreadID = dwThreadID;\n        info.dwFlags = 0;\n\n        infosize = sizeof(info) / sizeof(DWORD);\n\n        __try\n        {\n            RaiseException(EXCEPTION_SET_THREAD_NAME, 0, infosize, (DWORD*)&info);\n        }\n        __except (EXCEPTION_EXECUTE_HANDLER)\n        {}\n    }\n    /* clean up */\n    if (hThread)\n        CloseHandle(hThread);\n}\n#endif\n\n#ifdef _WIN32\n#if 0\n// Generate a core dump if we crash. Finally.\n// Unix-style, we just leave a file named \"core\" in\n// the user's working directory. Gets overwritten if\n// a new crash occurs.\n#include <dbghelp.h>\n#ifdef _MSC_VER\n#pragma comment(lib, \"dbghelp.lib\")\n#endif\n\nHRESULT\nGenerateCrashDump(MINIDUMP_TYPE flags, EXCEPTION_POINTERS *seh)\n{\n  HRESULT error                         = S_OK;\n  MINIDUMP_USER_STREAM_INFORMATION info = {0};\n  MINIDUMP_USER_STREAM stream           = {0};\n\n  // get the time\n  SYSTEMTIME sysTime = {0};\n  GetSystemTime(&sysTime);\n\n  // get the computer name\n  char compName[MAX_COMPUTERNAME_LENGTH + 1] = {0};\n  DWORD compNameLen                          = ARRAYSIZE(compName);\n  GetComputerNameA(compName, &compNameLen);\n\n  // This information is written to a core dump user stream\n  char extra_info[1024] = {0};\n  snprintf(extra_info, 1024,\n           \"hostname=%s;datetime=%02u-%02u-%02u_%02u-%02u-%02u\", compName,\n           sysTime.wYear, sysTime.wMonth, sysTime.wDay, sysTime.wHour,\n           sysTime.wMinute, sysTime.wSecond);\n\n  // open the file\n  HANDLE hFile =\n      CreateFileA(\"core\", GENERIC_READ | GENERIC_WRITE,\n                  FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,\n                  CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);\n\n  if(hFile == INVALID_HANDLE_VALUE)\n  {\n    error = GetLastError();\n    error = HRESULT_FROM_WIN32(error);\n    return error;\n  }\n\n  // get the process information\n  HANDLE hProc = GetCurrentProcess();\n  DWORD procID = GetCurrentProcessId();\n\n  // if we have SEH info, package it up\n  MINIDUMP_EXCEPTION_INFORMATION sehInfo = {0};\n  MINIDUMP_EXCEPTION_INFORMATION *sehPtr = NULL;\n\n  // Collect hostname and time\n  info.UserStreamCount = 1;\n  info.UserStreamArray = &stream;\n  stream.Type          = CommentStreamA;\n  stream.BufferSize    = strlen(extra_info) + 1;\n  stream.Buffer        = extra_info;\n\n  if(seh)\n  {\n    sehInfo.ThreadId          = GetCurrentThreadId();\n    sehInfo.ExceptionPointers = seh;\n    sehInfo.ClientPointers    = FALSE;\n    sehPtr                    = &sehInfo;\n  }\n\n  // generate the crash dump\n  BOOL result =\n      MiniDumpWriteDump(hProc, procID, hFile, flags, sehPtr, &info, NULL);\n  if(!result)\n  {\n    error = (HRESULT)GetLastError();  // already an HRESULT\n  }\n\n  // close the file\n  CloseHandle(hFile);\n  return error;\n}\n\n// ok try a UNIX-style signal handler\nLONG FAR PASCAL win32_signal_handler(EXCEPTION_POINTERS *e)\n{\n  MessageBox(NULL,\n             \"A fatal error has occurred. A core dump was generated and \"\n             \"dropped in the daemon's working directory. Please create an \"\n             \"issue at https://github.com/loki-network/loki-project, and \"\n             \"attach the core dump for further assistance.\",\n             \"Fatal Error\", MB_ICONHAND);\n  GenerateCrashDump(\n      MiniDumpWithFullMemory | MiniDumpWithHandleData | MiniDumpWithThreadInfo\n          | MiniDumpWithProcessThreadData | MiniDumpWithFullMemoryInfo\n          | MiniDumpWithUnloadedModules | MiniDumpWithFullAuxiliaryState\n          | MiniDumpIgnoreInaccessibleMemory | MiniDumpWithTokenInformation,\n      e);\n  exit(127);\n  return 0;\n}\n#endif\n#endif\n"
  },
  {
    "path": "llarp/win32/windivert.cpp",
    "content": "#include \"windivert.hpp\"\n\n#include \"dll.hpp\"\n#include \"handle.hpp\"\n\n#include <llarp/util/formattable.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/logging/buffer.hpp>\n#include <llarp/util/thread/queue.hpp>\n\n#include <winsock2.h>\n\n#include <windows.h>\n\n#include <thread>\nextern \"C\"\n{\n#include <windivert.h>\n}\n\nnamespace\n{\n    std::string windivert_addr_to_string(const WINDIVERT_ADDRESS& addr)\n    {\n        std::string layer_str{};\n        std::string ifidx_str{};\n        switch (addr.Layer)\n        {\n            case WINDIVERT_LAYER_NETWORK:\n                layer_str = \"WINDIVERT_LAYER_NETWORK\";\n                ifidx_str = \"Network: [IfIdx: {}, SubIfIdx: {}]\"_format(addr.Network.IfIdx, addr.Network.SubIfIdx);\n                break;\n            case WINDIVERT_LAYER_NETWORK_FORWARD:\n                layer_str = \"WINDIVERT_LAYER_NETWORK_FORWARD\";\n                break;\n            case WINDIVERT_LAYER_FLOW:\n                layer_str = \"WINDIVERT_LAYER_FLOW\";\n                break;\n            case WINDIVERT_LAYER_SOCKET:\n                layer_str = \"WINDIVERT_LAYER_SOCKET\";\n                break;\n            case WINDIVERT_LAYER_REFLECT:\n                layer_str = \"WINDIVERT_LAYER_REFLECT\";\n                break;\n            default:\n                layer_str = \"unknown\";\n        }\n\n        std::string event_str{};\n        switch (addr.Event)\n        {\n            case WINDIVERT_EVENT_NETWORK_PACKET:\n                event_str = \"WINDIVERT_EVENT_NETWORK_PACKET\";\n                break;\n            case WINDIVERT_EVENT_FLOW_ESTABLISHED:\n                event_str = \"WINDIVERT_EVENT_FLOW_ESTABLISHED\";\n                break;\n            case WINDIVERT_EVENT_FLOW_DELETED:\n                event_str = \"WINDIVERT_EVENT_FLOW_DELETED\";\n                break;\n            case WINDIVERT_EVENT_SOCKET_BIND:\n                event_str = \"WINDIVERT_EVENT_SOCKET_BIND\";\n                break;\n            case WINDIVERT_EVENT_SOCKET_CONNECT:\n                event_str = \"WINDIVERT_EVENT_SOCKET_CONNECT\";\n                break;\n            case WINDIVERT_EVENT_SOCKET_LISTEN:\n                event_str = \"WINDIVERT_EVENT_SOCKET_LISTEN\";\n                break;\n            case WINDIVERT_EVENT_SOCKET_ACCEPT:\n                event_str = \"WINDIVERT_EVENT_SOCKET_ACCEPT\";\n                break;\n            case WINDIVERT_EVENT_SOCKET_CLOSE:\n                event_str = \"WINDIVERT_EVENT_SOCKET_CLOSE\";\n                break;\n            case WINDIVERT_EVENT_REFLECT_OPEN:\n                event_str = \"WINDIVERT_EVENT_REFLECT_OPEN\";\n                break;\n            case WINDIVERT_EVENT_REFLECT_CLOSE:\n                event_str = \"WINDIVERT_EVENT_REFLECT_CLOSE\";\n                break;\n            default:\n                event_str = \"unknown\";\n        }\n\n        return fmt::format(\n            \"Windivert WINDIVERT_ADDRESS -- Timestamp: {}, Layer: {}, Event: {}, Sniffed: {}, \"\n            \"Outbound: {}, Loopback: {}, Imposter: {}, IPv6: {}, IPChecksum: {}, TCPChecksum: {}, \"\n            \"UDPChecksum: {}, {}\",\n            addr.Timestamp,\n            layer_str,\n            event_str,\n            addr.Sniffed ? \"true\" : \"false\",\n            addr.Outbound ? \"true\" : \"false\",\n            addr.Loopback ? \"true\" : \"false\",\n            addr.Impostor ? \"true\" : \"false\",\n            addr.IPv6 ? \"true\" : \"false\",\n            addr.IPChecksum ? \"true\" : \"false\",\n            addr.TCPChecksum ? \"true\" : \"false\",\n            addr.UDPChecksum ? \"true\" : \"false\",\n            ifidx_str);\n    }\n}  // namespace\n\nnamespace llarp::win32\n{\n    static auto logcat = log::Cat(\"windivert\");\n\n    namespace wd\n    {\n        namespace\n        {\n            decltype(::WinDivertOpen)* open = nullptr;\n            decltype(::WinDivertClose)* close = nullptr;\n            decltype(::WinDivertShutdown)* shutdown = nullptr;\n            decltype(::WinDivertHelperCalcChecksums)* calc_checksum = nullptr;\n            decltype(::WinDivertSend)* send = nullptr;\n            decltype(::WinDivertRecv)* recv = nullptr;\n            decltype(::WinDivertHelperFormatIPv4Address)* format_ip4 = nullptr;\n            decltype(::WinDivertHelperFormatIPv6Address)* format_ip6 = nullptr;\n\n            void Initialize()\n            {\n                if (wd::open)\n                    return;\n\n                // clang-format off\n      load_dll_functions(\n          \"WinDivert.dll\",\n\n          \"WinDivertOpen\",                    open,\n          \"WinDivertClose\",                   close,\n          \"WinDivertShutdown\",                shutdown,\n          \"WinDivertHelperCalcChecksums\",     calc_checksum,\n          \"WinDivertSend\",                    send,\n          \"WinDivertRecv\",                    recv,\n          \"WinDivertHelperFormatIPv4Address\", format_ip4,\n          \"WinDivertHelperFormatIPv6Address\", format_ip6);\n                // clang-format on\n            }\n        }  // namespace\n\n        struct Packet\n        {\n            std::vector<uint8_t> pkt;\n            WINDIVERT_ADDRESS addr;\n        };\n\n        class IO : public llarp::vpn::PacketIO\n        {\n            std::function<void(void)> m_Wake;\n\n            HANDLE m_Handle;\n            std::thread m_Runner;\n            std::atomic<bool> m_Shutdown{false};\n            thread::Queue<Packet> m_RecvQueue;\n            // dns packet queue size\n            static constexpr size_t recv_queue_size = 64;\n\n          public:\n            IO(const std::string& filter_spec, std::function<void(void)> wake)\n                : m_Wake{wake}, m_RecvQueue{recv_queue_size}\n            {\n                wd::Initialize();\n                log::info(logcat, \"load windivert with filterspec: '{}'\", filter_spec);\n\n                m_Handle = wd::open(filter_spec.c_str(), WINDIVERT_LAYER_NETWORK, 0, 0);\n                if (auto err = GetLastError())\n                    throw win32::error{err, \"cannot open windivert handle\"};\n            }\n\n            ~IO() { wd::close(m_Handle); }\n\n            std::optional<Packet> recv_packet() const\n            {\n                WINDIVERT_ADDRESS addr{};\n                std::vector<uint8_t> pkt;\n                pkt.resize(1500);  // net::IPPacket::MaxSize\n                UINT sz{};\n                if (not wd::recv(m_Handle, pkt.data(), pkt.size(), &sz, &addr))\n                {\n                    auto err = GetLastError();\n                    if (err == ERROR_NO_DATA)\n                        // The handle is shut down and the packet queue is empty\n                        return std::nullopt;\n                    if (err == ERROR_BROKEN_PIPE)\n                    {\n                        SetLastError(0);\n                        return std::nullopt;\n                    }\n\n                    log::critical(logcat, \"error receiving packet: {}\", err);\n                    throw win32::error{err, fmt::format(\"failed to receive packet from windivert (code={})\", err)};\n                }\n                pkt.resize(sz);\n\n                log::trace(logcat, \"got packet of size {}B\", sz);\n                log::trace(logcat, \"{}\", windivert_addr_to_string(addr));\n                return Packet{std::move(pkt), std::move(addr)};\n            }\n\n            void send_packet(Packet w_pkt) const\n            {\n                auto& pkt = w_pkt.pkt;\n                auto* addr = &w_pkt.addr;\n\n                addr->Outbound = !addr->Outbound;  // re-used from recv, so invert direction\n\n                log::trace(logcat, \"send dns packet of size {}B\", pkt.size());\n                log::trace(logcat, \"{}\", windivert_addr_to_string(w_pkt.addr));\n\n                UINT sz{};\n                // recalc IP packet checksum in case it needs it\n                wd::calc_checksum(pkt.data(), pkt.size(), addr, 0);\n\n                if (!wd::send(m_Handle, pkt.data(), pkt.size(), &sz, addr))\n                    throw win32::error{\"windivert send failed\"};\n            }\n\n            virtual int PollFD() const { return -1; }\n\n            bool write_packet(net::IPPacket) override { return false; }\n\n            net::IPPacket read_next_packet() override\n            {\n                auto w_pkt = m_RecvQueue.tryPopFront();\n                if (not w_pkt)\n                    return net::IPPacket{};\n                net::IPPacket pkt{std::move(w_pkt->pkt)};\n                pkt.reply = [this, addr = std::move(w_pkt->addr)](auto pkt) {\n                    if (!m_Shutdown)\n                        send_packet(Packet{pkt.steal(), addr});\n                };\n                return pkt;\n            }\n\n            void Start() override\n            {\n                log::info(logcat, \"starting windivert\");\n                if (m_Runner.joinable())\n                    throw std::runtime_error{\"windivert thread is already running\"};\n\n                auto read_loop = [this]() {\n                    log::debug(logcat, \"windivert read loop start\");\n                    while (true)\n                    {\n                        // in the read loop, read packets until they stop coming in\n                        // each packet is sent off\n                        if (auto maybe_pkt = recv_packet())\n                        {\n                            m_RecvQueue.pushBack(std::move(*maybe_pkt));\n                            // wake up event loop\n                            m_Wake();\n                        }\n                        else  // leave loop on read fail\n                            break;\n                    }\n                    log::debug(logcat, \"windivert read loop end\");\n                };\n\n                m_Runner = std::thread{std::move(read_loop)};\n            }\n\n            void Stop() override\n            {\n                log::info(logcat, \"stopping windivert\");\n                m_Shutdown = true;\n                wd::shutdown(m_Handle, WINDIVERT_SHUTDOWN_BOTH);\n                m_Runner.join();\n            }\n        };\n\n    }  // namespace wd\n\n    namespace WinDivert\n    {\n        std::string format_ip(uint32_t ip)\n        {\n            std::array<char, 128> buf;\n            wd::format_ip4(ip, buf.data(), buf.size());\n            return buf.data();\n        }\n\n        std::shared_ptr<llarp::vpn::PacketIO> make_interceptor(\n            const std::string& filter_spec, std::function<void(void)> wake)\n        {\n            return std::make_shared<wd::IO>(filter_spec, wake);\n        }\n    }  // namespace WinDivert\n\n}  // namespace llarp::win32\n"
  },
  {
    "path": "llarp/win32/windivert.hpp",
    "content": "#pragma once\n#include <llarp/vpn/packet_io.hpp>\n\n#include <windows.h>\n\n#include <memory>\n#include <string>\n\nnamespace llarp::win32::WinDivert\n{\n    /// format an ipv4 in host order to string such that a windivert filter spec can understand it\n    std::string format_ip(uint32_t ip);\n\n    /// create a packet interceptor that uses windivert.\n    /// filter_spec describes the kind of traffic we wish to intercept.\n    /// pass in a callable that wakes up the main event loop.\n    /// we hide all implementation details from other compilation units to prevent issues with\n    /// linkage that may arrise.\n    std::shared_ptr<llarp::vpn::PacketIO> make_interceptor(\n        const std::string& filter_spec, std::function<void(void)> wakeup);\n\n}  // namespace llarp::win32::WinDivert\n"
  },
  {
    "path": "llarp/win32/wintun.cpp",
    "content": "extern \"C\"\n{\n#include <wintun.h>\n}\n\n#include \"dll.hpp\"\n#include \"exception.hpp\"\n#include \"guid.hpp\"\n#include \"wintun.hpp\"\n\n#include <llarp/router/router.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/str.hpp>\n#include <llarp/util/thread/queue.hpp>\n#include <llarp/vpn/platform.hpp>\n\n#include <iphlpapi.h>\n\n#include <map>\n#include <unordered_set>\n\nnamespace llarp::win32\n{\n    namespace\n    {\n        static auto logcat = log::Cat(\"wintun\");\n        constexpr auto PoolName = \"lokinet\";\n\n        WINTUN_CREATE_ADAPTER_FUNC* create_adapter = nullptr;\n        WINTUN_CLOSE_ADAPTER_FUNC* close_adapter = nullptr;\n        WINTUN_OPEN_ADAPTER_FUNC* open_adapter = nullptr;\n        WINTUN_GET_ADAPTER_LUID_FUNC* get_adapter_LUID = nullptr;\n        WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC* get_version = nullptr;\n        WINTUN_DELETE_DRIVER_FUNC* delete_driver = nullptr;\n        WINTUN_SET_LOGGER_FUNC* set_logger = nullptr;\n        WINTUN_START_SESSION_FUNC* start_session = nullptr;\n        WINTUN_END_SESSION_FUNC* end_session = nullptr;\n        WINTUN_GET_READ_WAIT_EVENT_FUNC* get_adapter_handle = nullptr;\n        WINTUN_RECEIVE_PACKET_FUNC* read_packet = nullptr;\n        WINTUN_RELEASE_RECEIVE_PACKET_FUNC* release_read = nullptr;\n        WINTUN_ALLOCATE_SEND_PACKET_FUNC* alloc_write = nullptr;\n        WINTUN_SEND_PACKET_FUNC* send_packet = nullptr;\n\n        void WintunInitialize()\n        {\n            if (create_adapter)\n                return;\n\n            // clang-format off\n      load_dll_functions(\n          \"wintun.dll\",\n\n          \"WintunCreateAdapter\",            create_adapter,\n          \"WintunCloseAdapter\",             close_adapter,\n          \"WintunOpenAdapter\",              open_adapter,\n          \"WintunGetAdapterLUID\",           get_adapter_LUID,\n          \"WintunGetRunningDriverVersion\",  get_version,\n          \"WintunDeleteDriver\",             delete_driver,\n          \"WintunSetLogger\",                set_logger,\n          \"WintunStartSession\",             start_session,\n          \"WintunEndSession\",               end_session,\n          \"WintunGetReadWaitEvent\",         get_adapter_handle,\n          \"WintunReceivePacket\",            read_packet,\n          \"WintunReleaseReceivePacket\",     release_read,\n          \"WintunAllocateSendPacket\",       alloc_write,\n          \"WintunSendPacket\",               send_packet);\n            // clang-format on\n        }\n\n        using Adapter_ptr = std::shared_ptr<_WINTUN_ADAPTER>;\n\n        struct PacketWrapper\n        {\n            BYTE* data;\n            DWORD size;\n            WINTUN_SESSION_HANDLE session;\n            /// copy our data into an ip packet struct\n            net::IPPacket copy() const\n            {\n                net::IPPacket pkt{size};\n                std::copy_n(data, size, pkt.data());\n                return pkt;\n            }\n\n            ~PacketWrapper() { release_read(session, data); }\n        };\n\n        /// autovivify a wintun adapter handle\n        [[nodiscard]] auto make_adapter(std::string adapter_name, std::string tunnel_name)\n        {\n            auto adapter_name_wide = to_wide(adapter_name);\n            if (auto _impl = open_adapter(adapter_name_wide.c_str()))\n            {\n                log::info(logcat, \"opened existing adapter: '{}'\", adapter_name);\n                return _impl;\n            }\n            if (auto err = GetLastError())\n            {\n                log::info(logcat, \"did not open existing adapter '{}': {}\", adapter_name, error_to_string(err));\n                SetLastError(0);\n            }\n            const auto guid = llarp::win32::MakeDeterministicGUID(fmt::format(\"{}|{}\", adapter_name, tunnel_name));\n            log::info(logcat, \"creating adapter: '{}' on pool '{}'\", adapter_name, tunnel_name);\n            auto tunnel_name_wide = to_wide(tunnel_name);\n            if (auto _impl = create_adapter(adapter_name_wide.c_str(), tunnel_name_wide.c_str(), &guid))\n            {\n                if (auto v = get_version())\n                    log::info(logcat, \"created adapter (wintun v{}.{})\", (v >> 16) & 0xff, v & 0xff);\n                else\n                    log::warning(logcat, \"failed to query wintun driver version: {}!\", error_to_string(GetLastError()));\n                return _impl;\n            }\n\n            throw win32::error{\"failed to create wintun adapter\"};\n        }\n\n        class WintunAdapter\n        {\n            WINTUN_ADAPTER_HANDLE _handle;\n\n            [[nodiscard]] auto GetAdapterLUID() const\n            {\n                NET_LUID _uid{};\n                get_adapter_LUID(_handle, &_uid);\n                return _uid;\n            }\n\n          public:\n            explicit WintunAdapter(std::string name)\n            {\n                _handle = make_adapter(std::move(name), PoolName);\n                if (_handle == nullptr)\n                    throw std::runtime_error{\"failed to create wintun adapter\"};\n            }\n\n            /// put adapter up\n            void Up(const vpn::InterfaceInfo& info) const\n            {\n                const auto luid = GetAdapterLUID();\n                for (const auto& addr : info.addrs)\n                {\n                    // TODO: implement ipv6\n                    if (addr.fam != AF_INET)\n                        continue;\n                    MIB_UNICASTIPADDRESS_ROW AddressRow;\n                    InitializeUnicastIpAddressEntry(&AddressRow);\n                    AddressRow.InterfaceLuid = luid;\n\n                    AddressRow.Address.Ipv4.sin_family = AF_INET;\n                    AddressRow.Address.Ipv4.sin_addr.S_un.S_addr = ToNet(net::TruncateV6(addr.range.addr)).n;\n                    AddressRow.OnLinkPrefixLength = addr.range.HostmaskBits();\n                    AddressRow.DadState = IpDadStatePreferred;\n\n                    if (auto err = CreateUnicastIpAddressEntry(&AddressRow); err != ERROR_SUCCESS)\n                        throw win32::error{err, fmt::format(\"cannot set address '{}'\", addr.range)};\n\n                    log::debug(logcat, \"Added address: {}\", addr.range);\n                }\n            }\n\n            /// put adapter down and close it\n            void Down() const { close_adapter(_handle); }\n\n            /// auto vivify a wintun session handle and read handle off of our adapter\n            [[nodiscard]] std::pair<WINTUN_SESSION_HANDLE, HANDLE> session() const\n            {\n                if (auto wintun_ver = get_version())\n                    log::info(\n                        logcat,\n                        fmt::format(\"wintun version {}.{} loaded\", (wintun_ver >> 16) & 0xff, wintun_ver & 0xff));\n                else\n                    throw win32::error{\"Failed to load wintun\"};\n                if (auto impl = start_session(_handle, WINTUN_MAX_RING_CAPACITY))\n                {\n                    if (auto handle = get_adapter_handle(impl))\n                        return {impl, handle};\n                    end_session(impl);\n                }\n                return {nullptr, nullptr};\n            }\n        };\n\n        class WintunSession\n        {\n            WINTUN_SESSION_HANDLE _impl;\n            HANDLE _handle;\n            std::atomic<bool> ended{false};\n            static_assert(std::atomic<bool>::is_always_lock_free);\n\n          public:\n            WintunSession() : _impl{nullptr}, _handle{nullptr} {}\n\n            void Start(const std::shared_ptr<WintunAdapter>& adapter)\n            {\n                if (auto [impl, handle] = adapter->session(); impl and handle)\n                {\n                    _impl = impl;\n                    _handle = handle;\n                    return;\n                }\n                throw error{GetLastError(), \"could not create wintun session\"};\n            }\n\n            void Stop()\n            {\n                ended = true;\n                end_session(_impl);\n            }\n\n            void WaitFor(std::chrono::milliseconds dur) { WaitForSingleObject(_handle, dur.count()); }\n\n            /// read a unique pointer holding a packet read from wintun, returns the packet if we\n            /// read one and a bool, set to true if our adapter is now closed\n            [[nodiscard]] std::pair<std::unique_ptr<PacketWrapper>, bool> ReadPacket() const\n            {\n                if (ended)\n                    return {nullptr, true};\n                DWORD sz;\n                if (auto* ptr = read_packet(_impl, &sz))\n                    return {std::unique_ptr<PacketWrapper>{new PacketWrapper{ptr, sz, _impl}}, false};\n                const auto err = GetLastError();\n                if (err == ERROR_NO_MORE_ITEMS or err == ERROR_HANDLE_EOF)\n                {\n                    SetLastError(0);\n                    return {nullptr, err == ERROR_HANDLE_EOF};\n                }\n                throw error{err, \"failed to read packet\"};\n            }\n\n            /// write an ip packet to the interface, return 2 bools, first is did we write the\n            /// packet, second if we are terminating\n            std::pair<bool, bool> WritePacket(net::IPPacket pkt) const\n            {\n                if (auto* buf = alloc_write(_impl, pkt.size()))\n                {\n                    std::copy_n(pkt.data(), pkt.size(), buf);\n                    send_packet(_impl, buf);\n                    return {true, false};\n                }\n                const auto err = GetLastError();\n                if (err == ERROR_BUFFER_OVERFLOW or err == ERROR_HANDLE_EOF)\n                {\n                    SetLastError(0);\n                    return {err != ERROR_BUFFER_OVERFLOW, err == ERROR_HANDLE_EOF};\n                }\n                throw error{err, \"failed to write packet\"};\n            }\n        };\n\n        class WintunInterface : public vpn::NetworkInterface\n        {\n            Router* const _router;\n            std::shared_ptr<WintunAdapter> _adapter;\n            std::shared_ptr<WintunSession> _session;\n            thread::Queue<net::IPPacket> _recv_queue;\n            thread::Queue<net::IPPacket> _send_queue;\n            std::thread _recv_thread;\n            std::thread _send_thread;\n\n            inline static constexpr size_t packet_queue_length = 1024;\n\n          public:\n            WintunInterface(vpn::InterfaceInfo info, Router* router)\n                : vpn::NetworkInterface{std::move(info)},\n                  _router{router},\n                  _adapter{std::make_shared<WintunAdapter>(m_Info.ifname)},\n                  _session{std::make_shared<WintunSession>()},\n                  _recv_queue{packet_queue_length},\n                  _send_queue{packet_queue_length}\n            {}\n\n            void Start() override\n            {\n                m_Info.index = 0;\n                // put the adapter and set addresses\n                _adapter->Up(m_Info);\n                // start up io session\n                _session->Start(_adapter);\n\n                // start read packet loop\n                _recv_thread = std::thread{[session = _session, this]() {\n                    do\n                    {\n                        // read all our packets this iteration\n                        bool more{true};\n                        do\n                        {\n                            auto [pkt, done] = session->ReadPacket();\n                            // bail if we are closing\n                            if (done)\n                                return;\n                            if (pkt)\n                                _recv_queue.pushBack(pkt->copy());\n                            else\n                                more = false;\n                        } while (more);\n                        // wait for more packets\n                        session->WaitFor(5s);\n                    } while (true);\n                }};\n                // start write packet loop\n                _send_thread = std::thread{[this, session = _session]() {\n                    do\n                    {\n                        if (auto maybe = _send_queue.popFrontWithTimeout(100ms))\n                        {\n                            auto [written, done] = session->WritePacket(std::move(*maybe));\n                            if (done)\n                                return;\n                        }\n                    } while (_send_queue.enabled());\n                }};\n            }\n\n            void Stop() override\n            {\n                // end writing packets\n                _send_queue.disable();\n                _send_thread.join();\n                // end reading packets\n                _session->Stop();\n                _recv_thread.join();\n                // close session\n                _session.reset();\n                // put adapter down\n                _adapter->Down();\n                _adapter.reset();\n            }\n\n            net::IPPacket read_next_packet() override\n            {\n                net::IPPacket pkt{};\n                if (auto maybe_pkt = _recv_queue.tryPopFront())\n                    pkt = std::move(*maybe_pkt);\n                return pkt;\n            }\n\n            bool WritePacket(net::IPPacket pkt) override\n            {\n                return _send_queue.tryPushBack(std::move(pkt)) == thread::QueueReturn::Success;\n            }\n\n            int PollFD() const override { return -1; }\n\n            void MaybeWakeUpperLayers() const override { _router->TriggerPump(); }\n        };\n    }  // namespace\n\n    namespace wintun\n    {\n        std::shared_ptr<vpn::NetworkInterface> make_interface(const llarp::vpn::InterfaceInfo& info, llarp::Router* r)\n        {\n            WintunInitialize();\n            return std::static_pointer_cast<vpn::NetworkInterface>(std::make_shared<WintunInterface>(info, r));\n        }\n    }  // namespace wintun\n\n}  // namespace llarp::win32\n"
  },
  {
    "path": "llarp/win32/wintun.hpp",
    "content": "#pragma once\n\n#include <memory>\n\nnamespace llarp\n{\n    class Router;\n}\n\nnamespace llarp::vpn\n{\n    struct InterfaceInfo;\n    class NetworkInterface;\n}  // namespace llarp::vpn\n\nnamespace llarp::win32::wintun\n{\n    /// makes a new vpn interface with a wintun context given info and a router pointer\n    std::shared_ptr<vpn::NetworkInterface> make_interface(const vpn::InterfaceInfo& info, Router* router);\n\n}  // namespace llarp::win32::wintun\n"
  },
  {
    "path": "pybind/CMakeLists.txt",
    "content": "pybind11_add_module(pyllarp MODULE\n  module.cpp\n  llarp/context.cpp\n  llarp/router.cpp\n  llarp/router_id.cpp\n  llarp/router_contact.cpp\n  llarp/crypto/types.cpp\n  llarp/config.cpp\n  llarp/logger.cpp\n  llarp/peerstats.cpp\n  llarp/dht/dht_types.cpp\n  llarp/path/path_types.cpp\n  llarp/path/path_hop_config.cpp\n  llarp/handlers/pyhandler.cpp\n  llarp/tooling/router_hive.cpp\n  llarp/tooling/router_event.cpp\n  llarp/service/address.cpp\n)\ntarget_link_libraries(pyllarp PUBLIC lokinet-amalgum)\ntarget_include_directories(pyllarp PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})\n"
  },
  {
    "path": "pybind/common.hpp",
    "content": "#pragma once\n#include <llarp/util/fs.hpp>\n\n#include <pybind11/functional.h>\n#include <pybind11/pybind11.h>\n#include <pybind11/stl.h>\n\n#include <unordered_map>\n\nnamespace py = pybind11;\n\nnamespace llarp\n{\n    void Logger_Init(py::module& mod);\n\n    void Context_Init(py::module& mod);\n\n    void CryptoTypes_Init(py::module& mod);\n\n    void Router_Init(py::module& mod);\n\n    void RouterID_Init(py::module& mod);\n\n    void RelayContact_Init(py::module& mod);\n\n    void Config_Init(py::module& mod);\n\n    void PathTypes_Init(py::module& mod);\n\n    void PeerDb_Init(py::module& mod);\n\n    void PeerStats_Init(py::module& mod);\n\n    namespace dht\n    {\n        void DHTTypes_Init(py::module& mod);\n    }\n\n    namespace path\n    {\n        void PathHopConfig_Init(py::module& mod);\n    }\n\n    namespace handlers\n    {\n        void PyHandler_Init(py::module& mod);\n    }\n\n    namespace service\n    {\n        void Address_Init(py::module& mod);\n    }\n\n}  // namespace llarp\n\nnamespace tooling\n{\n    void RouterHive_Init(py::module& mod);\n\n    void RouterEvent_Init(py::module& mod);\n\n    void HiveContext_Init(py::module& mod);\n\n    void HiveRouter_Init(py::module& mod);\n}  // namespace tooling\n"
  },
  {
    "path": "pybind/llarp/config.cpp",
    "content": "#include <llarp/config/config.hpp>\n\n#include <common.hpp>\n#include <netinet/in.h>\n\nnamespace llarp\n{\n    void in_addr_set(in_addr* addr, const char* str) { inet_aton(str, addr); }\n\n    void Config_Init(py::module& mod)\n    {\n        using Config_ptr = std::shared_ptr<Config>;\n        py::class_<Config, Config_ptr>(mod, \"Config\")\n            .def(py::init<std::string>())\n            .def_readwrite(\"router\", &Config::router)\n            .def_readwrite(\"network\", &Config::network)\n            .def_readwrite(\"connect\", &Config::connect)\n            .def_readwrite(\"links\", &Config::links)\n            .def_readwrite(\"api\", &Config::api)\n            .def_readwrite(\"lokid\", &Config::lokid)\n            .def_readwrite(\"bootstrap\", &Config::bootstrap)\n            .def_readwrite(\"logging\", &Config::logging)\n            .def_readwrite(\"paths\", &Config::paths)\n            .def(\"Load\", &Config::Load);\n\n        py::class_<RouterConfig>(mod, \"RouterConfig\")\n            .def(py::init<>())\n            .def_readwrite(\"minConnectedRouters\", &RouterConfig::m_minConnectedRouters)\n            .def_readwrite(\"maxConnectedRouters\", &RouterConfig::m_maxConnectedRouters)\n            .def_readwrite(\"netid\", &RouterConfig::m_netId)\n            .def_readwrite(\"nickname\", &RouterConfig::m_nickname)\n            .def_property(\n                \"dataDir\",\n                [](RouterConfig& self) { return self.m_dataDir.c_str(); },\n                [](RouterConfig& self, std::string dir) { self.m_dataDir = dir; })\n            .def_readwrite(\"blockBogons\", &RouterConfig::m_blockBogons)\n            .def_readwrite(\"workerThreads\", &RouterConfig::m_workerThreads)\n            .def_readwrite(\"numNetThreads\", &RouterConfig::m_numNetThreads)\n            .def_readwrite(\"JobQueueSize\", &RouterConfig::m_JobQueueSize);\n\n        py::class_<PeerSelectionConfig>(mod, \"PeerSelectionConfig\")\n            .def(py::init<>())\n            .def_readwrite(\"netmask\", &PeerSelectionConfig::m_UniqueHopsNetmaskSize);\n\n        py::class_<NetworkConfig>(mod, \"NetworkConfig\")\n            .def(py::init<>())\n            .def_readwrite(\"enableProfiling\", &NetworkConfig::m_enableProfiling)\n            .def_readwrite(\"endpointType\", &NetworkConfig::m_endpointType)\n            .def_readwrite(\"keyfile\", &NetworkConfig::m_keyfile)\n            .def_readwrite(\"endpointType\", &NetworkConfig::m_endpointType)\n            .def_readwrite(\"reachable\", &NetworkConfig::m_reachable)\n            .def_readwrite(\"hops\", &NetworkConfig::m_Hops)\n            .def_readwrite(\"paths\", &NetworkConfig::m_Paths)\n            .def_readwrite(\"snodeBlacklist\", &NetworkConfig::m_snodeBlacklist)\n            .def_readwrite(\"mapAddr\", &NetworkConfig::m_mapAddrs)\n            .def_readwrite(\"strictConnect\", &NetworkConfig::m_strictConnect);\n\n        py::class_<ConnectConfig>(mod, \"ConnectConfig\")\n            .def(py::init<>())\n            .def_readwrite(\"routers\", &ConnectConfig::routers);\n\n        py::class_<DnsConfig>(mod, \"DnsConfig\").def(py::init<>());\n\n        py::class_<LinksConfig>(mod, \"LinksConfig\")\n            .def(py::init<>())\n            .def(\n                \"addOutboundLink\",\n                [](LinksConfig& self, std::string _addr) { self.OutboundLinks.emplace_back(std::move(_addr)); })\n            .def(\"addInboundLink\", [](LinksConfig& self, std::string _addr) {\n                self.InboundListenAddrs.emplace_back(std::move(_addr));\n            });\n\n        py::class_<ApiConfig>(mod, \"ApiConfig\")\n            .def(py::init<>())\n            .def_readwrite(\"enableRPCServer\", &ApiConfig::m_enableRPCServer)\n            .def_readwrite(\"rpcBindAddresses\", &ApiConfig::m_rpcBindAddresses);\n\n        py::class_<LokidConfig>(mod, \"LokidConfig\")\n            .def(py::init<>())\n            .def_readwrite(\"whitelistRouters\", &LokidConfig::whitelistRouters)\n            .def_readwrite(\"ident_keyfile\", &LokidConfig::ident_keyfile)\n            .def_property(\n                \"lokidRPCAddr\",\n                [](LokidConfig& self) { return self.lokidRPCAddr.full_address().c_str(); },\n                [](LokidConfig& self, std::string arg) { self.lokidRPCAddr = oxenmq::address(arg); });\n\n        py::class_<BootstrapConfig>(mod, \"BootstrapConfig\")\n            .def(py::init<>())\n            .def_readwrite(\"seednode\", &BootstrapConfig::seednode)\n            .def_property(\n                \"routers\",\n                [](BootstrapConfig& self) {\n                    std::vector<std::string> args;\n                    for (const auto& rc : self.routers)\n                    {\n                        args.emplace_back(rc.pubkey.ToString());\n                    }\n                    return args;\n                },\n                [](BootstrapConfig& self, std::vector<std::string> args) {\n                    self.routers.clear();\n                    for (const auto& arg : args)\n                    {\n                        RelayContact rc{};\n                        if (rc.Read(arg))\n                            self.routers.emplace(std::move(rc));\n                        else\n                            throw std::invalid_argument{\"cannot read rc from \" + arg};\n                    }\n                });\n\n        py::class_<LoggingConfig>(mod, \"LoggingConfig\")\n            .def(py::init<>())\n            .def_readwrite(\"m_logType\", &LoggingConfig::m_logType)\n            .def_readwrite(\"m_logFile\", &LoggingConfig::m_logFile);\n\n        py::class_<sockaddr_in>(mod, \"sockaddr_in\")\n            .def_readwrite(\"sin_family\", &sockaddr_in::sin_family)\n            .def_readwrite(\"sin_port\", &sockaddr_in::sin_port)\n            .def_readwrite(\"sin_addr\", &sockaddr_in::sin_addr);\n\n        py::class_<in_addr>(mod, \"in_addr\").def(\"set\", &in_addr_set);\n    }\n}  // namespace llarp\n"
  },
  {
    "path": "pybind/llarp/context.cpp",
    "content": "#include <llarp.hpp>\n#include <llarp/handlers/pyhandler.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/service/protocol_type.hpp>\n#include <llarp/tooling/hive_context.hpp>\n\n#include <common.hpp>\n\nnamespace llarp\n{\n    void Context_Init(py::module& mod)\n    {\n        using Context_ptr = std::shared_ptr<Context>;\n        py::class_<Context, Context_ptr>(mod, \"Context\")\n            .def(\"Setup\", [](Context_ptr self, bool isRouter) { self->Setup({false, false, isRouter}); })\n            .def(\"Run\", [](Context_ptr self) -> int { return self->Run(RuntimeOptions{}); })\n            .def(\"Stop\", [](Context_ptr self) { self->CloseAsync(); })\n            .def(\"IsUp\", &Context::IsUp)\n            .def(\"IsRelay\", [](Context_ptr self) -> bool { return self->router->IsServiceNode(); })\n            .def(\"LooksAlive\", &Context::LooksAlive)\n            .def(\"Configure\", &Context::Configure)\n            .def(\n                \"TrySendPacket\",\n                [](Context_ptr self, std::string from, service::Address to, std::string pkt) {\n                    auto ep = self->router->hiddenServiceContext().GetEndpointByName(from);\n                    std::vector<byte_t> buf;\n                    buf.resize(pkt.size());\n                    std::copy_n(pkt.c_str(), pkt.size(), buf.data());\n                    return ep and ep->SendToOrQueue(to, std::move(buf), service::ProtocolType::Control);\n                })\n            .def(\n                \"AddEndpoint\",\n                [](Context_ptr self, handlers::PythonEndpoint_ptr ep) {\n                    self->router->hiddenServiceContext().InjectEndpoint(ep->OurName, ep);\n                })\n            .def(\"CallSafe\", &Context::CallSafe);\n    }\n\n}  // namespace llarp\n\nnamespace tooling\n{\n    void HiveContext_Init(py::module& mod)\n    {\n        using HiveContext_ptr = std::shared_ptr<HiveContext>;\n        py::class_<tooling::HiveContext, HiveContext_ptr, llarp::Context>(mod, \"HiveContext\")\n            .def(\n                \"getRouterAsHiveRouter\",\n                &tooling::HiveContext::getRouterAsHiveRouter,\n                py::return_value_policy::reference);\n    }\n}  // namespace tooling\n"
  },
  {
    "path": "pybind/llarp/crypto/types.cpp",
    "content": "#include <llarp/crypto/types.hpp>\n\n#include <common.hpp>\n\nnamespace llarp\n{\n    void CryptoTypes_Init(py::module& mod)\n    {\n        py::class_<PubKey>(mod, \"PubKey\")\n            .def(py::init<>())\n            .def(\"FromHex\", &PubKey::FromString)\n            .def(\"__repr__\", &PubKey::ToString);\n        py::class_<SecretKey>(mod, \"SecretKey\")\n            .def(py::init<>())\n            .def(\"LoadFile\", &SecretKey::LoadFromFile)\n            .def(\"SaveFile\", &SecretKey::SaveToFile)\n            .def(\"ToPublic\", &SecretKey::toPublic);\n        py::class_<Signature>(mod, \"Signature\").def(py::init<>()).def(\"__repr__\", [](const Signature& sig) {\n            return sig.ToHex();\n        });\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "pybind/llarp/dht/dht_types.cpp",
    "content": "#include <llarp/dht/key.hpp>\n\n#include <common.hpp>\n#include <pybind11/operators.h>\n\nnamespace llarp\n{\n    namespace dht\n    {\n        void DHTTypes_Init(py::module& mod)\n        {\n            py::class_<Key_t>(mod, \"DHTKey\")\n                .def(py::self == py::self)\n                .def(py::self < py::self)\n                .def(py::self ^ py::self)\n                .def(\"distance\", [](const Key_t* const lhs, const Key_t* const rhs) { return *lhs ^ *rhs; })\n                .def(\"ShortString\", [](const Key_t* const key) {\n                    return llarp::RouterID(key->as_array()).ShortString();\n                });\n        }\n\n    }  // namespace dht\n}  // namespace llarp\n"
  },
  {
    "path": "pybind/llarp/handlers/pyhandler.cpp",
    "content": "#include <llarp/handlers/pyhandler.hpp>\nnamespace llarp\n{\n    namespace handlers\n    {\n        void PyHandler_Init(py::module& mod)\n        {\n            py::class_<PythonEndpoint, PythonEndpoint_ptr>(mod, \"Endpoint\")\n                .def(py::init<std::string, Context_ptr>())\n                .def(\"SendTo\", &PythonEndpoint::SendPacket)\n                .def(\"OurAddress\", &PythonEndpoint::GetOurAddress)\n                .def_readwrite(\"GotPacket\", &PythonEndpoint::handlePacket);\n        }\n\n    }  // namespace handlers\n\n}  // namespace llarp\n"
  },
  {
    "path": "pybind/llarp/handlers/pyhandler.hpp",
    "content": "#pragma once\n#include <llarp.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/service/context.hpp>\n#include <llarp/service/endpoint.hpp>\n\n#include <common.hpp>\n\nnamespace llarp\n{\n    namespace handlers\n    {\n        using Context_ptr = std::shared_ptr<llarp::Context>;\n\n        struct PythonEndpoint final : public llarp::service::Endpoint,\n                                      public std::enable_shared_from_this<PythonEndpoint>\n        {\n            PythonEndpoint(std::string name, Context_ptr routerContext)\n                : llarp::service::Endpoint(routerContext->router.get(), &routerContext->router->hiddenServiceContext()),\n                  OurName(std::move(name))\n            {}\n            const std::string OurName;\n\n            bool HandleInboundPacket(\n                const service::ConvoTag tag,\n                const llarp_buffer_t& pktbuf,\n                service::ProtocolType proto,\n                uint64_t) override\n            {\n                if (handlePacket)\n                {\n                    service::Address addr{};\n                    if (auto maybe = GetEndpointWithConvoTag(tag))\n                    {\n                        if (auto ptr = std::get_if<service::Address>(&*maybe))\n                            addr = *ptr;\n                        else\n                            return false;\n                    }\n                    else\n                        return false;\n                    std::vector<byte_t> pkt;\n                    pkt.resize(pktbuf.sz);\n                    std::copy_n(pktbuf.base, pktbuf.sz, pkt.data());\n                    handlePacket(addr, std::move(pkt), proto);\n                }\n                return true;\n            }\n\n            std::shared_ptr<path::PathSet> GetSelf() override { return shared_from_this(); }\n\n            std::weak_ptr<path::PathSet> GetWeak() override { return weak_from_this(); }\n\n            bool SupportsV6() const override { return false; }\n\n            llarp::huint128_t ObtainIPForAddr(std::variant<service::Address, RouterID>) override { return {0}; }\n\n            std::optional<std::variant<service::Address, RouterID>> ObtainAddrForIP(huint128_t) const override\n            {\n                return std::nullopt;\n            }\n\n            std::string GetIfName() const override { return \"\"; }\n\n            using PacketHandler_t = std::function<void(service::Address, std::vector<byte_t>, service::ProtocolType)>;\n\n            PacketHandler_t handlePacket;\n\n            void SendPacket(service::Address remote, std::vector<byte_t> pkt, service::ProtocolType proto)\n            {\n                m_router->loop()->call([remote, pkt, proto, self = shared_from_this()]() {\n                    self->SendToOrQueue(remote, llarp_buffer_t(pkt), proto);\n                });\n            }\n\n            void SendPacketToRemote(const llarp_buffer_t&, service::ProtocolType) override {};\n\n            std::string GetOurAddress() const { return m_Identity.pub.Addr().ToString(); }\n        };\n\n        using PythonEndpoint_ptr = std::shared_ptr<PythonEndpoint>;\n    }  // namespace handlers\n}  // namespace llarp\n"
  },
  {
    "path": "pybind/llarp/logger.cpp",
    "content": "#include <llarp/util/logging.hpp>\n\n#include <common.hpp>\n\n#include <memory>\n\nnamespace llarp\n{\n    struct PyLogger\n    {\n        std::optional<llarp::log::Level> silenced;\n    };\n\n    void Logger_Init(py::module& mod)\n    {\n        py::class_<PyLogger>(mod, \"LogContext\")\n            .def(py::init<>())\n            .def_property(\n                \"shutup\",\n                [](PyLogger& self) { return self.silenced.has_value(); },\n                [](PyLogger& self, bool shutup) {\n                    if (shutup and not self.silenced)\n                        self.silenced = llarp::log::get_level_default();\n                    else if (not shutup and self.silenced)\n                    {\n                        llarp::log::reset_level(*self.silenced);\n                        self.silenced.reset();\n                    }\n                });\n    }\n}  // namespace llarp\n"
  },
  {
    "path": "pybind/llarp/path/path_hop_config.cpp",
    "content": "#include <llarp/path/path.hpp>\n\n#include <common.hpp>\n\nnamespace llarp\n{\n    namespace path\n    {\n        void PathHopConfig_Init(py::module& mod)\n        {\n            auto str_func = [](PathHopConfig* hop) {\n                std::string s = \"Hop: [\";\n                s += RouterID(hop->rc.pubkey).ShortString();\n                s += \"] -> [\";\n                s += hop->upstream.ShortString();\n                s += \"]\";\n                return s;\n            };\n            py::class_<PathHopConfig>(mod, \"PathHopConfig\")\n                .def_readonly(\"rc\", &PathHopConfig::rc)\n                .def_readonly(\"upstreamRouter\", &PathHopConfig::upstream)\n                .def_readonly(\"txid\", &PathHopConfig::txID)\n                .def_readonly(\"rxid\", &PathHopConfig::rxID)\n                .def(\"ToString\", str_func)\n                .def(\"__str__\", str_func)\n                .def(\"__repr__\", str_func);\n        }\n    }  // namespace path\n}  // namespace llarp\n"
  },
  {
    "path": "pybind/llarp/path/path_types.cpp",
    "content": "#include <llarp/path/path.hpp>\n\n#include <common.hpp>\n#include <pybind11/operators.h>\n\nnamespace llarp\n{\n    void PathTypes_Init(py::module& mod)\n    {\n        py::class_<PathID_t>(mod, \"PathID\")\n            .def(py::self == py::self)\n            .def(\"ShortHex\", &PathID_t::ShortHex)\n            .def(\"__str__\", &PathID_t::ShortHex)\n            .def(\"__repr__\", &PathID_t::ShortHex);\n    }\n\n}  // namespace llarp\n"
  },
  {
    "path": "pybind/llarp/peerstats.cpp",
    "content": "#include <llarp/config/config.hpp>\n#include <llarp/peerstats/peer_db.hpp>\n#include <llarp/peerstats/types.hpp>\n\n#include <common.hpp>\n#include <netinet/in.h>\n\nnamespace llarp\n{\n    void PeerDb_Init(py::module& mod)\n    {\n        using PeerDb_ptr = std::shared_ptr<PeerDb>;\n        py::class_<PeerDb, PeerDb_ptr>(mod, \"PeerDb\").def(\"getCurrentPeerStats\", &PeerDb::getCurrentPeerStats);\n    }\n\n    void PeerStats_Init(py::module& mod)\n    {\n        py::class_<PeerStats>(mod, \"PeerStats\")\n            .def_readwrite(\"routerId\", &PeerStats::routerId)\n            .def_readwrite(\"numConnectionAttempts\", &PeerStats::numConnectionAttempts)\n            .def_readwrite(\"numConnectionSuccesses\", &PeerStats::numConnectionSuccesses)\n            .def_readwrite(\"numConnectionRejections\", &PeerStats::numConnectionRejections)\n            .def_readwrite(\"numConnectionTimeouts\", &PeerStats::numConnectionTimeouts)\n            .def_readwrite(\"numPathBuilds\", &PeerStats::numPathBuilds)\n            .def_readwrite(\"numPacketsAttempted\", &PeerStats::numPacketsAttempted)\n            .def_readwrite(\"numPacketsSent\", &PeerStats::numPacketsSent)\n            .def_readwrite(\"numPacketsDropped\", &PeerStats::numPacketsDropped)\n            .def_readwrite(\"numPacketsResent\", &PeerStats::numPacketsResent)\n            .def_readwrite(\"numDistinctRCsReceived\", &PeerStats::numDistinctRCsReceived)\n            .def_readwrite(\"numLateRCs\", &PeerStats::numLateRCs)\n            .def_readwrite(\"peakBandwidthBytesPerSec\", &PeerStats::peakBandwidthBytesPerSec)\n            .def_readwrite(\"longestRCReceiveInterval\", &PeerStats::longestRCReceiveInterval)\n            .def_readwrite(\"leastRCRemainingLifetime\", &PeerStats::leastRCRemainingLifetime)\n            .def_readwrite(\"lastRCUpdated\", &PeerStats::lastRCUpdated)\n            .def_readwrite(\"stale\", &PeerStats::stale);\n    }\n}  // namespace llarp\n"
  },
  {
    "path": "pybind/llarp/router.cpp",
    "content": "#include \"common.hpp\"\n\n#include <llarp/router/router.hpp>\n#include <llarp/tooling/hive_router.hpp>\n\nnamespace llarp\n{\n    void Router_Init(py::module& mod)\n    {\n        py::class_<Router, std::shared_ptr<Router>>(mod, \"Router\")\n            .def(\"rc\", &Router::rc)\n            .def(\"Stop\", &Router::Stop)\n            .def(\"peerDb\", &Router::peerDb);\n    }\n\n}  // namespace llarp\n\nnamespace tooling\n{\n    void HiveRouter_Init(py::module& mod)\n    {\n        py::class_<HiveRouter, llarp::Router, std::shared_ptr<HiveRouter>>(mod, \"HiveRouter\")\n            .def(\"disableGossiping\", &HiveRouter::disableGossiping)\n            .def(\"enableGossiping\", &HiveRouter::enableGossiping);\n    }\n}  // namespace tooling\n"
  },
  {
    "path": "pybind/llarp/router_contact.cpp",
    "content": "#include \"common.hpp\"\n\n#include <llarp/dht/key.hpp>\n#include <llarp/relay_contact.hpp>\n#include <llarp/util/time.hpp>\n\nnamespace llarp\n{\n    void RelayContact_Init(py::module& mod)\n    {\n        py::class_<RelayContact>(mod, \"RelayContact\")\n            .def(py::init<>())\n            .def_property_readonly(\n                \"routerID\", [](const RelayContact* const rc) -> llarp::RouterID { return llarp::RouterID(rc->pubkey); })\n            .def_property_readonly(\n                \"AsDHTKey\",\n                [](const RelayContact* const rc) -> llarp::dht::Key_t {\n                    return llarp::dht::Key_t{rc->pubkey.as_array()};\n                })\n            .def(\"ReadFile\", &RelayContact::Read)\n            .def(\"WriteFile\", &RelayContact::Write)\n            .def(\"ToString\", &RelayContact::ToString)\n            .def(\"__str__\", &RelayContact::ToString)\n            .def(\"__repr__\", &RelayContact::ToString)\n            .def(\"Verify\", [](const RelayContact* const rc) -> bool {\n                const std::chrono::milliseconds now = llarp::time_now_ms();\n                return rc->Verify(now);\n            });\n    }\n}  // namespace llarp\n"
  },
  {
    "path": "pybind/llarp/router_id.cpp",
    "content": "#include <llarp/contact/router_id.hpp>\n\n#include <common.hpp>\n\nnamespace llarp\n{\n    void RouterID_Init(py::module& mod)\n    {\n        py::class_<RouterID>(mod, \"RouterID\")\n            .def(\n                \"FromHex\",\n                [](RouterID* r, const std::string& hex) {\n                    if (hex.size() != 2 * r->size() || !oxenc::is_hex(hex))\n                        throw std::runtime_error(\"FromHex requires a 64-digit hex string\");\n                    oxenc::from_hex(hex.begin(), hex.end(), r->data());\n                })\n            .def(\"__repr__\", &RouterID::ToString)\n            .def(\"__str__\", &RouterID::ToString)\n            .def(\"ShortString\", &RouterID::ShortString)\n            .def(\"__eq__\", [](const RouterID& lhs, const RouterID& rhs) { return lhs == rhs; });\n    }\n}  // namespace llarp\n"
  },
  {
    "path": "pybind/llarp/service/address.cpp",
    "content": "#include <llarp/service/address.hpp>\n\n#include <common.hpp>\n\nnamespace llarp\n{\n    namespace service\n    {\n        void Address_Init(py::module& mod)\n        {\n            py::class_<Address>(mod, \"ServiceAddress\")\n                .def(py::init<std::string>())\n                .def(\"__str__\", [](const Address& addr) -> std::string { return addr.ToString(); });\n        }\n    }  // namespace service\n}  // namespace llarp\n"
  },
  {
    "path": "pybind/llarp/tooling/router_event.cpp",
    "content": "#include <llarp/messages/relay_status.hpp>\n#include <llarp/path/path.hpp>\n#include <llarp/tooling/dht_event.hpp>\n#include <llarp/tooling/path_event.hpp>\n#include <llarp/tooling/peer_stats_event.hpp>\n#include <llarp/tooling/rc_event.hpp>\n#include <llarp/tooling/router_event.hpp>\n\n#include <common.hpp>\n#include <pybind11/stl.h>\n\nnamespace tooling\n{\n    void RouterEvent_Init(py::module& mod)\n    {\n        py::class_<RouterEvent>(mod, \"RouterEvent\")\n            .def(\"__repr__\", &RouterEvent::ToString)\n            .def(\"__str__\", &RouterEvent::ToString)\n            .def_readonly(\"routerID\", &RouterEvent::routerID)\n            .def_readonly(\"triggered\", &RouterEvent::triggered);\n\n        py::class_<PathAttemptEvent, RouterEvent>(mod, \"PathAttemptEvent\")\n            .def_readonly(\"hops\", &PathAttemptEvent::hops);\n\n        py::class_<PathRequestReceivedEvent, RouterEvent>(mod, \"PathRequestReceivedEvent\")\n            .def_readonly(\"prevHop\", &PathRequestReceivedEvent::prevHop)\n            .def_readonly(\"nextHop\", &PathRequestReceivedEvent::nextHop)\n            .def_readonly(\"txid\", &PathRequestReceivedEvent::txid)\n            .def_readonly(\"rxid\", &PathRequestReceivedEvent::rxid)\n            .def_readonly(\"isEndpoint\", &PathRequestReceivedEvent::isEndpoint);\n\n        py::class_<PathStatusReceivedEvent, RouterEvent>(mod, \"PathStatusReceivedEvent\")\n            .def_readonly(\"rxid\", &PathStatusReceivedEvent::rxid)\n            .def_readonly(\"status\", &PathStatusReceivedEvent::rxid)\n            .def_property_readonly(\"Successful\", [](const PathStatusReceivedEvent* const ev) {\n                return ev->status == llarp::LR_StatusRecord::SUCCESS;\n            });\n\n        py::class_<PubIntroSentEvent, RouterEvent>(mod, \"DhtPubIntroSentEvent\")\n            .def_readonly(\"introsetPubkey\", &PubIntroSentEvent::introsetPubkey)\n            .def_readonly(\"relay\", &PubIntroSentEvent::relay)\n            .def_readonly(\"relayIndex\", &PubIntroSentEvent::relayIndex);\n\n        py::class_<PubIntroReceivedEvent, RouterEvent>(mod, \"DhtPubIntroReceivedEvent\")\n            .def_readonly(\"from\", &PubIntroReceivedEvent::from)\n            .def_readonly(\"location\", &PubIntroReceivedEvent::location)\n            .def_readonly(\"relayOrder\", &PubIntroReceivedEvent::relayOrder)\n            .def_readonly(\"txid\", &PubIntroReceivedEvent::txid);\n\n        py::class_<GotIntroReceivedEvent, RouterEvent>(mod, \"DhtGotIntroReceivedEvent\")\n            .def_readonly(\"from\", &GotIntroReceivedEvent::From)\n            .def_readonly(\"location\", &GotIntroReceivedEvent::Introset)\n            .def_readonly(\"relayOrder\", &GotIntroReceivedEvent::RelayOrder)\n            .def_readonly(\"txid\", &GotIntroReceivedEvent::TxID);\n\n        py::class_<RCGossipReceivedEvent, RouterEvent>(mod, \"RCGossipReceivedEvent\")\n            .def_readonly(\"rc\", &RCGossipReceivedEvent::rc)\n            .def(\"LongString\", &RCGossipReceivedEvent::LongString);\n\n        py::class_<RCGossipSentEvent, RouterEvent>(mod, \"RCGossipSentEvent\")\n            .def_readonly(\"rc\", &RCGossipSentEvent::rc)\n            .def(\"LongString\", &RCGossipSentEvent::LongString);\n\n        py::class_<FindRouterEvent, RouterEvent>(mod, \"FindRouterEvent\")\n            .def_readonly(\"from\", &FindRouterEvent::from)\n            .def_readonly(\"iterative\", &FindRouterEvent::iterative)\n            .def_readonly(\"exploritory\", &FindRouterEvent::exploritory)\n            .def_readonly(\"txid\", &FindRouterEvent::txid)\n            .def_readonly(\"version\", &FindRouterEvent::version);\n\n        py::class_<FindRouterReceivedEvent, FindRouterEvent, RouterEvent>(mod, \"FindRouterReceivedEvent\");\n\n        py::class_<FindRouterSentEvent, FindRouterEvent, RouterEvent>(mod, \"FindRouterSentEvent\");\n\n        py::class_<LinkSessionEstablishedEvent, RouterEvent>(mod, \"LinkSessionEstablishedEvent\")\n            .def_readonly(\"remoteId\", &LinkSessionEstablishedEvent::remoteId)\n            .def_readonly(\"inbound\", &LinkSessionEstablishedEvent::inbound);\n\n        py::class_<ConnectionAttemptEvent, RouterEvent>(mod, \"ConnectionAttemptEvent\")\n            .def_readonly(\"remoteId\", &ConnectionAttemptEvent::remoteId);\n    }\n\n}  // namespace tooling\n"
  },
  {
    "path": "pybind/llarp/tooling/router_hive.cpp",
    "content": "#include <llarp.hpp>\n#include <llarp/router/router.hpp>\n#include <llarp/tooling/router_hive.hpp>\n\n#include <common.hpp>\n#include <pybind11/iostream.h>\n#include <pybind11/stl.h>\n\nnamespace tooling\n{\n    void RouterHive_Init(py::module& mod)\n    {\n        using RouterHive_ptr = std::shared_ptr<RouterHive>;\n        using Context_ptr = RouterHive::Context_ptr;\n        using ContextVisitor = std::function<void(Context_ptr)>;\n\n        py::class_<RouterHive, RouterHive_ptr>(mod, \"RouterHive\")\n            .def(py::init<>())\n            .def(\"AddRelay\", &RouterHive::AddRelay)\n            .def(\"AddClient\", &RouterHive::AddClient)\n            .def(\"StartRelays\", &RouterHive::StartRelays)\n            .def(\"StartClients\", &RouterHive::StartClients)\n            .def(\"StopAll\", &RouterHive::StopRouters)\n            .def(\n                \"ForEachRelay\",\n                [](RouterHive& hive, ContextVisitor visit) {\n                    hive.ForEachRelay([visit](Context_ptr ctx) {\n                        py::gil_scoped_acquire acquire;\n                        visit(std::move(ctx));\n                    });\n                })\n            .def(\n                \"ForEachClient\",\n                [](RouterHive& hive, ContextVisitor visit) {\n                    hive.ForEachClient([visit](Context_ptr ctx) {\n                        py::gil_scoped_acquire acquire;\n                        visit(std::move(ctx));\n                    });\n                })\n            .def(\n                \"ForEachRouter\",\n                [](RouterHive& hive, ContextVisitor visit) {\n                    hive.ForEachRouter([visit](Context_ptr ctx) {\n                        py::gil_scoped_acquire acquire;\n                        visit(std::move(ctx));\n                    });\n                })\n            .def(\"GetNextEvent\", &RouterHive::GetNextEvent)\n            .def(\"GetAllEvents\", &RouterHive::GetAllEvents)\n            .def(\"RelayConnectedRelays\", &RouterHive::RelayConnectedRelays)\n            .def(\"GetRelayRCs\", &RouterHive::GetRelayRCs)\n            .def(\"GetRelay\", &RouterHive::GetRelay);\n    }\n}  // namespace tooling\n"
  },
  {
    "path": "pybind/module.cpp",
    "content": "#include \"common.hpp\"\n\n#include <llarp/util/logging.hpp>\n\nPYBIND11_MODULE(pyllarp, m)\n{\n    tooling::RouterHive_Init(m);\n    tooling::RouterEvent_Init(m);\n    llarp::Router_Init(m);\n    tooling::HiveRouter_Init(m);\n    llarp::PeerDb_Init(m);\n    llarp::PeerStats_Init(m);\n    llarp::RouterID_Init(m);\n    llarp::RelayContact_Init(m);\n    llarp::CryptoTypes_Init(m);\n    llarp::Context_Init(m);\n    tooling::HiveContext_Init(m);\n    llarp::Config_Init(m);\n    llarp::dht::DHTTypes_Init(m);\n    llarp::PathTypes_Init(m);\n    llarp::path::PathHopConfig_Init(m);\n    llarp::handlers::PyHandler_Init(m);\n    llarp::service::Address_Init(m);\n    m.def(\"EnableDebug\", []() { llarp::log::reset_level(llarp::log::Level::debug); });\n    llarp::Logger_Init(m);\n}\n"
  },
  {
    "path": "pybind/readme.md",
    "content": "pybind lokinet module for integration tests and network simulation\n"
  },
  {
    "path": "readme.md",
    "content": "# Lokinet\n\n<!-- [Español](readme_es.md) [Русский](readme_ru.md) [Français](readme_fr.md) -->\n\nLokinet is the reference implementation of LLARP (low latency anonymous routing protocol), a layer 3 onion routing protocol.\n\n### Installation instructions can be found [here](docs/install.md).\n\n#### You can learn more about the high level, how to use it and the internals of the protocol [here](docs/readme.md)\n\n[![Build Status](https://ci.oxen.rocks/api/badges/oxen-io/lokinet/status.svg?ref=refs/heads/dev)](https://ci.oxen.rocks/oxen-io/lokinet)\n\n# License\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\n(at your option) any later version.\n\n```\nCopyright © 2018-2022 The Oxen Project\nCopyright © 2018-2022 Jeff Becker\nCopyright © 2018-2020 Rick V. (Historical Windows NT port and portions)\n```\n"
  },
  {
    "path": "readme_es.md",
    "content": "# Lokinet\n\n[Ingles](readme.md)\n\nLokinet es la implementación referente de LLARP (low latency anonymous routing protocol, protocolo de enrutado anónimo de baja latencia), un protocolo de enrutado onion de capa 3.\n\nPuede aprender a grandes razgos sobre el diseño de LLARP [aquí](docs/high-level.txt) , documento en idioma ingles.\n\nY puede leer las especificaciones del protocolo [aquí](docs/proto_v0.txt) , documento técnico en idioma ingles.\n\nPuede ver la documentación, en ingles, de como empezar [aqui](https://oxen-io.github.io/loki-docs/Lokinet/LokinetOverview/) .\n\n[![Build Status](https://ci.oxen.rocks/api/badges/oxen-io/lokinet/status.svg?ref=refs/heads/dev)](https://ci.oxen.rocks/oxen-io/lokinet)\n\n\n## Uso\n\nVea, en ingles, [documentación](https://oxen-io.github.io/loki-docs/Lokinet/LokinetOverview/) en como comenzar.\n\nTambién lea, en ingles, [La guia de pruebas publicas](https://lokidocs.com/Lokinet/Guides/PublicTestingGuide/#1-lokinet-installation) para la instalación y mas información util.\n\n## Corriendo en Linux\n\n**NO CORRER COMO ROOT**, correr como usuario normal. Esto requiere que el binario tenga asignado por `make install` los setcaps apropiados el binario.\n\npara ejecutar como cliente:\n\n    $ lokinet -g\n    $ lokinet-bootstrap\n    $ lokinet\n\npara correr un relay:\n\n    $ lokinet -r -g\n    $ lokinet-bootstrap\n    $ lokinet\n\n## Corriendo en macOS/UNIX/BSD\n\n**USTED TIENE QUE CORRER COMO ROOT**, correr usando sudo. Los privilegios elevados son necesarios para crear una interfaz de tunel virtual.\n\nEl instalador de macOS coloca los binarios normales (`lokinet` y `lokinet-bootstrap`) en `/usr/local/bin` los que pudieran estar en ruta, asi que usted puede usar los binarios facilmente desde su terminal. El instalador tambien truena sus configuraciones y llaves previas, y descarga la semilla de arranque mas actual.\n\npara correr como cliente:\n\n    $ lokinet -g\n    $ lokinet-bootstrap\n    $ sudo lokinet\n\npara correr como relay:\n\n    $ lokinet -r -g\n    $ lokinet-bootstrap\n    $ sudo lokinet\n\n\n## Corriendo en Windows:\n\n**NO CORRER COMO USUARIO ELEVADO**, correr como un usuario normal.\n\npara correr como usuario, correr el archivo en lote `run-lokinet.bat` como su usuario normal.\n\n\n## Compilando\n\nRequerimientos de compilación:\n\n* GNU Make\n* CMake\n* Compilador C++ que pueda usar C++ 17\n* gcovr (para generar la covertura de prueba en gcc)\n* libuv >= 1.27.0\n* libsodium >= 1.0.18\n* libunbound\n* libzmq\n* cppzmq\n* sqlite3\n\n### Linux\n\ncompilando:\n\n    $ sudo apt install build-essential cmake git libcap-dev curl libuv1-dev libsodium-dev pkg-config\n    $ git clone --recursive https://github.com/oxen-io/lokinet\n    $ cd lokinet\n    $ mkdir build\n    $ cd build\n    $ cmake .. -DBUILD_STATIC_DEPS=ON -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON\n    $ make\n\ninstalando:\n\n    $ sudo make install\n\n\ncomo alternativa hacer un paquete debian con:\n\n    $ debuild -uc -us -b\n\nesto coloca el paquete compilado en `../`\n\n### macOS\n\ncompilando:\n    este seguro que usted tiene cmake, libuv y las herramientas de terminal de xcode ya instaladas\n\n     $ git clone --recursive https://github.com/oxen-io/lokinet\n    $ cd lokinet\n    $ mkdir build\n    $ cd build\n    $ cmake .. -DBUILD_STATIC_DEPS=ON -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON\n    $ make -j$(sysctl -n hw.ncpu)\n\ninstalando:\n\n    $ sudo make install\n\n### Windows\n\ncompilar (donde `$ARCH` es su plataforma - `i686` or `x86_64`):\n\n    $ pacman -Sy base-devel mingw-w64-$ARCH-toolchain git libtool autoconf mingw-w64-$ARCH-cmake\n    $ git clone https://github.com/oxen-io/lokinet.git\n    $ cd lokinet\n    $ mkdir -p build; cd build\n    $ cmake .. -DCMAKE_BUILD_TYPE=[Debug|Release] -DSTATIC_LINK_RUNTIME=ON -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -G 'Unix Makefiles'\n\ninstalando (con priviligios elevados) en `$PROGRAMFILES/lokinet` o `$ProgramFiles(x86)/lokinet`:\n\n    $ make install\n\nsi usa compilacion cruzada (cross-compiling), instale mingw-w64 desde su administrador de paquetes, o [compile desde el codigo fuente](https://sourceforge.net/p/mingw-w64/wiki2/Cross%20Win32%20and%20Win64%20compiler/), then:\n\n    $ mkdir -p build; cd build\n    $ export COMPILER=clang # si esta usando clang para windows\n    $ cmake .. -DCMAKE_BUILD_TYPE=[Debug|Release] -DSTATIC_LINK_RUNTIME=ON -DCMAKE_CROSSCOMPILING=ON -DCMAKE_TOOLCHAIN_FILE=../contrib/cross/mingw[32].cmake\n\nesto crea un binario que puede ser instalado en cualquier parte, con ninguna otra dependencia aparte de libc (v6.1 minimo)\n\n### Solaris 2.10+\n\nNOTA: Los usuarios de Solaris de Oracle necesitan descargar/compilar el controlador TAP de http://www.whiteboard.ne.jp/~admin2/tuntap/\n\nLos binarios generados _podrían_ funcionar en Solaris 2.10 o anteriores, va por su cuenta. (Recomendable: `-static-libstdc++ -static-libgcc`, y el controlador TAP si aun no esta instalado en el sistema de destino.)\n\nCompilar en un sistema anterior a v2.10 en sistemas previos no esta soportado, e incluso puede que no funcione, lanzamientos recientes de GCC han estado desechando el soporte en lanzamientos mas viejos del sistema.\n\ncompilando:\n\n    $ sudo pkg install build-essential gcc8 wget tuntap cmake (opcional: ninja ccache - de los extra de omnios) (OmniOS CE)\n    $ sudo pkg install base-developer-utilities developer-gnu developer-studio-utilities gcc-7 wget cmake (Solaris de Oracle, ver notas)\n    $ sudo pkg install build-essential wget gcc-8 documentation/tuntap header-tun tun (opcional: ninja ccache) (todos los demas SunOS)\n    $ git clone https://github.com/oxen-io/lokinet\n    $ cd lokinet\n    $ gmake -j8\n\ninstalando:\n\n    $ sudo make install\n\n\n### NetBSD (y otras plataformas donde pkgsrc es _el_ mgr nativo de paquetes)\n\nPENDIENTE: agregar instrucciones para pkgsrc\n\n### OpenBSD (usa el administrador pkg legado de NetBSD)\n\ncompilando:\n\n    # pkg_add curl cmake git (opcional: ninja ccache)\n    $ git clone --recursive https://github.com/oxen-io/lokinet\n    $ cd lokinet\n    $ mkdir build\n    $ cd build\n    $ cmake .. -DBUILD_STATIC_DEPS=ON -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON\n    $ make\n\ninstalando (root):\n\n    # gmake install\n\n### FreeBSD\n\ncompilando:\n\n    $ pkg install cmake git curl libuv-1.27.0 libsodium\n    $ git clone --recursive https://github.com/oxen-io/lokinet\n    $ cd lokinet\n    $ mkdir build\n    $ cd build\n    $ cmake .. -DBUILD_STATIC_DEPS=ON -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON\n    $ make\n\ninstalando (root):\n\n    # gmake install\n"
  },
  {
    "path": "readme_fr.md",
    "content": "# Lokinet\n\n[Español](readme_es.md) [Русский](readme_ru.md) [Français](readme_fr.md)\n\nLokinet est l'implementation de référence du LLARP (Low Latency Anonymous Routing Protocol, protocole de routage anonyme à latence faible), un protocole de routage en oignon de couche 3.\n\nVous pouvez en savoir plus sur le haut niveau de conception du LLARP [ici](docs/)\n\n[![Build Status](https://ci.oxen.rocks/api/badges/oxen-io/lokinet/status.svg?ref=refs/heads/dev)](https://ci.oxen.rocks/oxen-io/lokinet)\n\n## Installer\n\nSi vous souhaitez simplement installer Lokinet sans avoir à le compiler vous-même, nous vous proposons plusieurs options de plates-formes d'exécution :\n\nTier 1:\n\n* [Linux](#linux-install)\n* [Android](#apk-install)\n\nTier 2:\n\n* [Windows](#windows-install)\n* [MacOS](#mac-install)\n* [FreeBSD](#freebsd-install)\n\nPlateformes actuellement non supportées : (des mainteneurs sont les bienvenus)\n\n* Apple iPhone \n* Homebrew\n* \\[Insérez ici le gestionnaire de paquets Windows à la mode ce mois-ci.\\]\n\n## Construction\n\nPackets necessaires pour construire:\n\n* Git\n* CMake\n* C++ 17 capable C++ compilateur\n* libuv >= 1.27.0\n* libsodium >= 1.0.18\n* libssl (pour lokinet-bootstrap)\n* libcurl (fpour lokinet-bootstrap)\n* libunbound\n* libzmq\n* cppzmq\n* sqlite3\n\n### Linux <span id=\"linux-install\" />\n\nVous n'avez pas besoin de construire les paquets à partir des sources si vous êtes sous debian ou ubuntu car nous avons des dépôts apt avec des paquets lokinet pré-construits sur `deb.oxen.io` ou `rpm.oxen.io`.\n\nVous pouvez installer les paquets debian en utilisant :\n\n    $ sudo curl -so /etc/apt/trusted.gpg.d/oxen.gpg https://deb.oxen.io/pub.gpg\n    $ echo \"deb https://deb.oxen.io $(lsb_release -sc) main\" | sudo tee /etc/apt/sources.list.d/oxen.list\n    $ sudo apt update\n    $ sudo apt install lokinet\n\nSi vous voulez construire lokinet à partir des sources :\n\n    $ sudo apt install build-essential cmake git libcap-dev pkg-config automake libtool libuv1-dev libsodium-dev libzmq3-dev libcurl4-openssl-dev libevent-dev nettle-dev libunbound-dev libsqlite3-dev libssl-dev nlohmann-json3-dev\n    $ git clone --recursive https://github.com/oxen-io/lokinet\n    $ cd lokinet\n    $ mkdir build\n    $ cd build\n    $ cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF\n    $ make -j$(nproc)\n    $ sudo make install\n\n#### Arch Linux <span id=\"mom-cancel-my-meetings-arch-linux-broke-again\" />\n\nEn raison de [circonstances indépendantes de notre volonté](https://github.com/oxen-io/lokinet/discussions/1823) un `PKGBUILD` fonctionnel peut être trouvé [ici](https://raw.githubusercontent.com/oxen-io/lokinet/makepkg/contrib/archlinux/PKGBUILD).\n\n#### Compilation croisée pour Linux <span id=\"linux-cross\" />\n\nles autres architectures actuellement supportées :\n\n* aarch64-linux-gnu\n* arm-linux-gnueabihf\n* mips-linux-gnu\n* mips64-linux-gnuabi64\n* mipsel-linux-gnu\n* powerpc64le-linux-gnu\n\ninstaller la chaîne d'outils (la suivante est pour `aarch64-linux-gnu`, vous pouvez fournir votre propre chaîne d'outils si vous voulez)\n\n    $ sudo apt install g{cc,++}-aarch64-linux-gnu\n\nconstruire pour une ou plusieurs architectures :\n\n    $ ./contrib/cross.sh arch_1 arch_2 ... arch_n\n\n### MacOS <span id=\"mac-install\" />\n\nLokinet ~~est~~ sera disponible sur l'App store d'Apple.\n\nLa compilation du code source de Lokinet par les utilisateurs finaux n'est pas supportée ou autorisée par apple sur leurs plateformes, voir [ceci](contrib/macos/README.txt) pour plus d'informations. Si vous trouvez cela désagréable, envisagez d'utiliser une plateforme qui permet la compilation à partir des sources.\n\n### Windows <span id=\"windows-install\" />\n\nVous pouvez obtenir la dernière version stable de Windows à l'adresse https://lokinet.org/ ou consulter la [page des versions sur github] (https://github.com/oxen-io/lokinet/releases).\n\n\nles compilation automatique de nuit pour les courageux ou les impatients peuvent être trouvées à partir de notre pipeline CI [ici](https://oxen.rocks/oxen-io/lokinet/)\n\n#### Construire les paquets sur Windows <span id=\"win32-cross\" />\n\nles compilations Windows sont des compilations croisées à partir de debian/ubuntu linux\n\nexigences de construction supplémentaires:\n\n* nsis\n* cpack\n\nconfiguration:\n\n    $ sudo apt install build-essential cmake git pkg-config mingw-w64 nsis cpack automake libtool\n    $ sudo update-alternatives --set x86_64-w64-mingw32-gcc /usr/bin/x86_64-w64-mingw32-gcc-posix\n    $ sudo update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix\n\nbuilding:\n\n    $ git clone --recursive https://github.com/oxen-io/lokinet\n    $ cd lokinet\n    $ ./contrib/windows.sh\n\n### FreeBSD <span id=\"freebsd-install\" />\n\nCurrently has no VPN Platform code, see #1513\n\nconstruction:\n\n    $ pkg install cmake git pkgconf\n    $ git clone --recursive https://github.com/oxen-io/lokinet\n    $ cd lokinet\n    $ mkdir build\n    $ cd build\n    $ cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON -DBUILD_STATIC_DEPS=ON ..\n    $ make\n\ninstallation (root):\n\n    # make install\n    \n### Android <span id=\"apk-install\" />\n\nNous avons un APK Android pour le VPN lokinet via l'API VPN android. \n\nA venir sur F-Droid quand cela arrivera. [[issue]](https://github.com/oxen-io/lokinet-flutter-app/issues/8)\n\n* [code source](https://github.com/oxen-io/lokinet-flutter-app)\n* [CI builds](https://oxen.rocks/oxen-io/lokinet/)\n\n## Usage\n\n### Debian / Ubuntu paquets <span id=\"systemd-linux-usage\" />\n\nLorsque vous installez le paquet debian, les étapes suivantes ne sont pas nécessaires car il est déjà en cours d'exécution et prêt à être utilisé.\nprêt à être utilisé.  Vous pouvez l'arrêter/démarrer/redémarrer en utilisant `systemctl start lokinet`, `systemctl stop\nlokinet`, etc.\n\n### Exécution sur Linux (sans debs) <span id=\"arcane-linux-usage\" />\n\n**NE PAS EXECUTER EN TANT QUE ROOT**, exécutez en tant qu'utilisateur normal.\n\nmettre en place les configurations initiales:\n\n    $ lokinet -g\n    $ lokinet-bootstrap\n\naprès avoir créé la configuration par défaut, exécutez-la:\n\n    $ lokinet\n\nCela nécessite que le binaire ait les capacités appropriées, ce qui est généralement défini par `make install` sur le binaire. Si vous avez des erreurs concernant les permissions d'ouvrir une nouvelle interface, cela peut être résolu en utilisant :\n\n    $ sudo setcap cap_net_admin,cap_net_bind_service=+eip /usr/local/bin/lokinet\n\n\n----\n\n# License\n\nCe programme est un logiciel libre : vous pouvez le redistribuer et/ou le modifier selon les termes de la Licence Publique Générale GNU telle que publiée par la Free Software Foundation, soit la version 3 de la Licence, soit (au choix) toute version ultérieure.\n\n```\nCopyright © 2018-2022 The Oxen Project\nCopyright © 2018-2022 Jeff Becker\nCopyright © 2018-2020 Rick V. (Historical Windows NT port and portions)\n"
  },
  {
    "path": "readme_ru.md",
    "content": "# Lokinet\n\n[English](readme.md) [Español](readme_es.md)\n\nLokinet - реализация LLARP (протокол анонимной маршрутизации с малой задержкой), протокола трёхуровневой луковой маршрутизации.\n\nПочитать о дизайне высокого уровня LLARP [здесь](docs/high-level.txt)\n\nПочитать спецификацию протокола LLARP [здесь](docs/proto_v0.txt)\n\nПочитать документацию о том, как начать работу [здесь](https://oxen-io.github.io/loki-docs/Lokinet/LokinetOverview/)\n\n[![Build Status](https://drone.lokinet.dev/api/badges/oxen-io/lokinet/status.svg?ref=refs/heads/master)](https://drone.lokinet.dev/oxen-io/lokinet)\n\n## Использование\n\nО том как начать работу см. [Документацию](https://oxen-io.github.io/loki-docs/Lokinet/LokinetOverview/)\n\nТакже прочтите [Public Testing Guide](https://lokidocs.com/Lokinet/Guides/PublicTestingGuide/#1-lokinet-installation) для установки и другой полезной информации.\n\n### Создание стандартной конфигурации\n\nнастроить как клиент:\n\n    $ lokinet -g\n    $ lokinet-bootstrap\n\nнастроить как транслятор:\n\n    $ lokinet -r -g\n    $ lokinet-bootstrap\n\n\n## Запуск в Linux\n\n** НЕ ЗАПУСКАЙТЕ С ПРАВАМИ СУПЕРПОЛЬЗОВАТЕЛЯ **, запускайте как обычный пользователь. Это требуется для того, чтобы в исполняемом файле были установлены правильные ограничения, установленные командой make install.\n\nдля запуска, после создания конфигурации:\n\n    $ lokinet\n\n## Запуск в macOS/UNIX/BSD\n\n** ВЫ ДОЛЖНЫ ЗАПУСКАТЬ С ПРАВАМИ СУПЕРПОЛЬЗОВАТЕЛЯ **, запускайте с помощью `sudo`. Для создания интерфейса виртуального туннеля необходимы повышенные привилегии.\n\nУстановщик macOS помещает исполняемые файлы (`lokinet` и` lokinet-bootstrap`) в `/usr/local/bin`, благодаря этому вы можете легко использовать исполняемые файлы в своем терминале. Установщик также уничтожает вашу предыдущую конфигурацию и ключи, устанавливает новую конфигурацию и загружает актуальную версию bootstrap.\n\nдля запуска, после создания конфигурации:\n\n    $ sudo lokinet\n\n## Запуск в Windows\n\n** НЕ ЗАПУСКАЙТЕ ОТ ПРИВИЛИГЕРОВАННОГО ПОЛЬЗОВАТЕЛЯ **, запускайте его от имени обычного пользователя.\n\n## Сборка\n\nТребования:\n\n* Git\n* CMake\n* C++ 17 capable C++ compiler\n* libuv >= 1.27.0\n* libsodium >= 1.0.18\n* libunbound\n* libzmq\n* cppzmq\n* sqlite3\n\n### Linux\n\nсборка:\n\n    $ sudo apt install build-essential cmake git libcap-dev curl libuv1-dev libsodium-dev pkg-config\n    $ git clone --recursive https://github.com/oxen-io/lokinet\n    $ cd lokinet\n    $ mkdir build\n    $ cd build\n    $ cmake .. -DBUILD_STATIC_DEPS=ON -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON\n    $ make -j$(nproc)\n\nустановка:\n\n    $ sudo make install\n\n### macOS\n\nсборка:\n    убедитесь, что у вас установлены инструменты командной строки cmake, libuv и xcode\n\n    $ git clone --recursive https://github.com/oxen-io/lokinet\n    $ cd lokinet\n    $ mkdir build\n    $ cd build\n    $ cmake .. -DBUILD_STATIC_DEPS=ON -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON\n    $ make -j$(sysctl -n hw.ncpu)\n\nустановка:\n\n    $ sudo make install\n\n### Windows\n\nсборки Windows кросс-скомпилированы из ubuntu linux\n\nдополнительные требования:\n\n* nsis\n* cpack\n\nнастроить:\n\n    $ sudo apt install build-essential cmake git pkg-config mingw-w64 nsis\n\nсборка:\n\n    $ git clone --recursive https://github.com/oxen-io/lokinet\n    $ cd lokinet\n    $ mkdir build-windows\n    $ cd build-windows\n    $ cmake -DBUILD_STATIC_DEPS=ON -DLOKINET_NATIVE_BUILD=OFF -DCMAKE_BUILD_TYPE=Release -DLOKINET_PACKAGE=ON -DCMAKE_TOOLCHAIN_FILE='../contrib/cross/mingw64.cmake' -DLOKINET_TESTS=OFF -DCMAKE_CROSSCOMPILING=ON ..\n    $ cpack -D CPACK_MONOLITHIC_INSTALL=1 -G NSIS ..\n\n### Solaris 2.10+\n\nПРИМЕЧАНИЕ. Пользователи Oracle Solaris должны загрузить/скомпилировать драйвер TAP с http://www.whiteboard.ne.jp/~admin2/tuntap/\n\nСгенерированные исполняемые файлы _могут_ работать в Solaris 2.10 или более ранней версии, но могут быть не стабильны. (Рекомендуется: `-static-libstdc ++ -static-libgcc` и драйвер TAP, если он еще не установлен в целевой системе.)\n\nСборка на системе v2.10 или более ранней версии не поддерживается и может даже не работать; в последних выпусках GCC постепенно прекращается поддержка старых выпусков системы.\n\nсборка:\n\n    $ sudo pkg install build-essential gcc8 wget tuntap cmake (optional: ninja ccache - from omnios extra) (OmniOS CE)\n    $ sudo pkg install base-developer-utilities developer-gnu developer-studio-utilities gcc-7 wget cmake (Oracle Solaris, see note)\n    $ sudo pkg install build-essential wget gcc-8 documentation/tuntap header-tun tun (optional: ninja ccache) (all other SunOS)\n    $ git clone --recursive https://github.com/oxen-io/lokinet\n    $ cd lokinet\n    $ mkdir build\n    $ cd build\n    $ cmake ..\n    $ make -j$(nproc)\n\nустановка:\n\n    $ sudo make install\n\n### FreeBSD\n\nсборка:\n\n    $ pkg install cmake git curl libuv libsodium pkgconf libunbound\n    $ git clone --recursive https://github.com/oxen-io/lokinet\n    $ cd lokinet\n    $ mkdir build\n    $ cmake -DCMAKE_BUILD_TYPE=Release ..\n    $ make\n\nустановка (root):\n\n    # make install\n"
  },
  {
    "path": "test/CMakeLists.txt",
    "content": "if (LOKINET_HIVE)\n  add_custom_target(hive_build DEPENDS lokinet-amalgum pyllarp)\n  add_custom_target(hive ${CMAKE_COMMAND} -E\n      env PYTHONPATH=\"$ENV{PYTHONPATH}:${CMAKE_BINARY_DIR}/pybind\"\n      ${PYTHON_EXECUTABLE} -m pytest\n      ${CMAKE_CURRENT_SOURCE_DIR}/hive\n      DEPENDS\n      hive_build)\nendif()\n\nif(NOT TARGET Catch2::Catch2)\n  add_subdirectory(Catch2)\nendif()\n\nadd_executable(testAll\n  # helpers\n  check_main.cpp\n  test_util.cpp\n  # actual test cases\n  config/test_llarp_config_definition.cpp\n  config/test_llarp_config_ini.cpp\n  config/test_llarp_config_output.cpp\n  config/test_llarp_config_values.cpp\n  crypto/test_llarp_crypto_types.cpp\n  crypto/test_llarp_crypto.cpp\n  crypto/test_llarp_key_manager.cpp\n  dns/test_llarp_dns_dns.cpp\n  net/test_ip_address.cpp\n  net/test_llarp_net.cpp\n  net/test_sock_addr.cpp\n  nodedb/test_nodedb.cpp\n  path/test_path.cpp\n  router/test_llarp_router_version.cpp\n  routing/test_llarp_routing_transfer_traffic.cpp\n  routing/test_llarp_routing_obtainexitmessage.cpp\n  service/test_llarp_service_address.cpp\n  service/test_llarp_service_identity.cpp\n  service/test_llarp_service_name.cpp\n  util/meta/test_llarp_util_memfn.cpp\n  util/thread/test_llarp_util_queue_manager.cpp\n  util/thread/test_llarp_util_queue.cpp\n  util/test_llarp_util_aligned.cpp\n  util/test_llarp_util_bencode.cpp\n  util/test_llarp_util_bits.cpp\n  util/test_llarp_util_decaying_hashset.cpp\n  util/test_llarp_util_log_level.cpp\n  util/test_llarp_util_str.cpp\n  test_llarp_encrypted_frame.cpp\n  test_llarp_router_contact.cpp)\n\n\nif(LOKINET_PEERSTATS)\n  target_sources(testAll PRIVATE\n    peerstats/test_peer_db.cpp\n    peerstats/test_peer_types.cpp)\nendif()\n\ntarget_link_libraries(testAll PUBLIC lokinet-amalgum Catch2::Catch2)\ntarget_include_directories(testAll PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})\n\nif(WIN32)\n    target_sources(testAll PUBLIC \"${CMAKE_CURRENT_SOURCE_DIR}/win32/test.rc\")\n    target_link_libraries(testAll PUBLIC ws2_32 iphlpapi shlwapi)\nendif()\n\nadd_custom_target(check COMMAND testAll)\n"
  },
  {
    "path": "test/check_main.cpp",
    "content": "#define CATCH_CONFIG_RUNNER\n#include <catch2/catch.hpp>\n\n#include <llarp/util/logging.hpp>\n#include <llarp/util/service_manager.hpp>\n\n#ifdef _WIN32\n#include <winsock2.h>\nint\nstartWinsock()\n{\n  WSADATA wsockd;\n  int err;\n  err = ::WSAStartup(MAKEWORD(2, 2), &wsockd);\n  if (err)\n  {\n    perror(\"Failed to start Windows Sockets\");\n    return err;\n  }\n  return 0;\n}\n#endif\n\nint\nmain(int argc, char* argv[])\n{\n  llarp::sys::service_manager->disable();\n  llarp::log::reset_level(llarp::log::Level::off);\n\n#ifdef _WIN32\n  if (startWinsock())\n    return -1;\n#endif\n\n  int result = Catch::Session().run(argc, argv);\n#ifdef _WIN32\n  WSACleanup();\n#endif\n  return result;\n}\n"
  },
  {
    "path": "test/config/test_llarp_config_definition.cpp",
    "content": "#include <llarp/config/definition.hpp>\n\n#include <catch2/catch.hpp>\n\nusing namespace llarp::config;\n\nTEST_CASE(\"OptionDefinition int parse test\", \"[config]\")\n{\n  llarp::OptionDefinition<int> def(\"foo\", \"bar\", Default{42});\n\n  CHECK(def.getValue() == 42);\n  CHECK(def.getNumberFound() == 0);\n  CHECK(def.defaultValuesAsString().size() == 1);\n  CHECK(def.defaultValuesAsString()[0] == \"42\");\n\n  CHECK_NOTHROW(def.parseValue(\"43\"));\n  CHECK(def.getValue() == 43);\n  CHECK(def.getNumberFound() == 1);\n\n  CHECK(def.defaultValuesAsString().size() == 1);\n  CHECK(def.defaultValuesAsString()[0] == \"42\");\n\n  constexpr Default sqrt_625{25};\n  llarp::OptionDefinition<int> def2(\"a\", \"b\", sqrt_625);\n  CHECK(def2.getValue() == 25);\n  CHECK(def2.defaultValuesAsString().size() == 1);\n  CHECK(def2.defaultValuesAsString()[0] == \"25\");\n  CHECK_NOTHROW(def2.parseValue(\"99\"));\n  CHECK(def2.getValue() == 99);\n  CHECK(def2.getNumberFound() == 1);\n}\n\nTEST_CASE(\"OptionDefinition string parse test\", \"[config]\")\n{\n  llarp::OptionDefinition<std::string> def(\"foo\", \"bar\", Default{\"test\"});\n\n  CHECK(def.getValue() == \"test\");\n  CHECK(not def.defaultValuesAsString().empty());\n  CHECK(def.defaultValuesAsString()[0] == \"test\");\n\n  CHECK_NOTHROW(def.parseValue(\"foo\"));\n  CHECK(def.getValue() == \"foo\");\n  CHECK(def.getNumberFound() == 1);\n}\n\nTEST_CASE(\"OptionDefinition multiple parses test\", \"[config]\")\n{\n  {\n    llarp::OptionDefinition<int> def(\"foo\", \"bar\", MultiValue, Default{8});\n\n    CHECK_NOTHROW(def.parseValue(\"9\"));\n    CHECK(def.getValue() == 9);\n    CHECK(def.getNumberFound() == 1);\n\n    // should allow since it is multi-value\n    CHECK_NOTHROW(def.parseValue(\"12\"));\n    CHECK(def.getValue() == 9); // getValue() should return first value\n    REQUIRE(def.getNumberFound() == 2);\n\n  }\n\n  {\n    llarp::OptionDefinition<int> def(\"foo\", \"baz\", Default{4});\n\n    CHECK_NOTHROW(def.parseValue(\"3\"));\n    CHECK(def.getValue() == 3);\n    CHECK(def.getNumberFound() == 1);\n\n    // shouldn't allow since not multi-value\n    CHECK_THROWS(def.parseValue(\"2\"));\n    CHECK(def.getNumberFound() == 1);\n  }\n\n}\n\nTEST_CASE(\"OptionDefinition acceptor test\", \"[config]\")\n{\n  int test = -1;\n  llarp::OptionDefinition<int> def(\"foo\", \"bar\", Default{42}, [&](int arg) {\n    test = arg;\n  });\n\n  CHECK_NOTHROW(def.tryAccept());\n  CHECK(def.getValue() == 42);\n  CHECK(not def.defaultValues.empty());\n  CHECK(def.defaultValues[0] == 42);\n  CHECK(test == 42);\n\n  def.parseValue(\"43\");\n  CHECK_NOTHROW(def.tryAccept());\n  CHECK(test == 43);\n}\n\nTEST_CASE(\"OptionDefinition acceptor throws test\", \"[config]\")\n{\n  llarp::OptionDefinition<int> def(\"foo\", \"bar\", Default{42}, [&](int arg) {\n    (void)arg;\n    throw std::runtime_error(\"FAIL\");\n  });\n\n  REQUIRE_THROWS_WITH(def.tryAccept(), \"FAIL\");\n}\n\nTEST_CASE(\"OptionDefinition tryAccept missing option test\", \"[config]\")\n{\n  int unset = -1;\n  llarp::OptionDefinition<int> def(\"foo\", \"bar\", Required, [&](int arg) {\n    (void)arg;\n    unset = 0; // should never be called\n  });\n\n  REQUIRE_THROWS_WITH(def.tryAccept(),\n      \"cannot call tryAccept() on [foo]:bar when required but no value available\");\n}\n\nTEST_CASE(\"ConfigDefinition basic add/get test\", \"[config]\")\n{\n  llarp::ConfigDefinition config{true};\n  config.defineOption(std::make_unique<llarp::OptionDefinition<int>>(\n            \"router\",\n            \"threads\",\n            Default{4}));\n\n  CHECK(config.getConfigValue<int>(\"router\", \"threads\") == 4);\n\n  CHECK_NOTHROW(config.addConfigValue(\n        \"router\",\n        \"threads\",\n        \"5\"));\n\n  CHECK(config.getConfigValue<int>(\"router\", \"threads\") == 5);\n}\n\nTEST_CASE(\"ConfigDefinition router/client-only tests\", \"[config]\")\n{\n  llarp::ConfigDefinition r_config{true}, c_config{false};\n  r_config.defineOption<int>(\"router\", \"abc\", Default{1}, RelayOnly);\n  r_config.defineOption<int>(\"router\", \"def\", Default{1}, ClientOnly);\n  r_config.defineOption<int>(\"router\", \"ghi\", Default{1});\n  c_config.defineOption<int>(\"router\", \"abc\", Default{1}, RelayOnly);\n  c_config.defineOption<int>(\"router\", \"def\", Default{1}, ClientOnly);\n  c_config.defineOption<int>(\"router\", \"ghi\", Default{1});\n\n  CHECK_NOTHROW(r_config.getConfigValue<int>(\"router\", \"abc\"));\n  CHECK_THROWS(r_config.getConfigValue<int>(\"router\", \"def\"));\n  CHECK_NOTHROW(r_config.getConfigValue<int>(\"router\", \"ghi\"));\n\n  CHECK_THROWS(c_config.getConfigValue<int>(\"router\", \"abc\"));\n  CHECK_NOTHROW(c_config.getConfigValue<int>(\"router\", \"def\"));\n  CHECK_NOTHROW(c_config.getConfigValue<int>(\"router\", \"ghi\"));\n}\n\nTEST_CASE(\"ConfigDefinition missing def test\", \"[config]\")\n{\n  llarp::ConfigDefinition config{true};\n  CHECK_THROWS(config.addConfigValue(\"foo\", \"bar\", \"5\"));\n  CHECK_THROWS(config.getConfigValue<int>(\"foo\", \"bar\") == 5);\n\n  config.defineOption(std::make_unique<llarp::OptionDefinition<int>>(\n            \"quux\",\n            \"bar\",\n            Default{4}));\n\n  CHECK_THROWS(config.addConfigValue(\"foo\", \"bar\", \"5\"));\n}\n\nTEST_CASE(\"ConfigDefinition required test\", \"[config]\")\n{\n  llarp::ConfigDefinition config{true};\n  config.defineOption(std::make_unique<llarp::OptionDefinition<int>>(\n            \"router\",\n            \"threads\",\n            Required));\n\n  CHECK_THROWS(config.validateRequiredFields());\n\n  config.addConfigValue(\"router\", \"threads\", \"12\");\n\n  CHECK_NOTHROW(config.validateRequiredFields());\n}\n\nTEST_CASE(\"ConfigDefinition section test\", \"[config]\")\n{\n  llarp::ConfigDefinition config{true};\n  config.defineOption(std::make_unique<llarp::OptionDefinition<int>>(\n            \"foo\",\n            \"bar\",\n            Required\n            ));\n  config.defineOption(std::make_unique<llarp::OptionDefinition<int>>(\n            \"goo\",\n            \"bar\",\n            Required\n            ));\n\n  CHECK_THROWS(config.validateRequiredFields());\n\n  config.addConfigValue(\"foo\", \"bar\", \"5\");\n  CHECK_THROWS(config.validateRequiredFields());\n\n  CHECK_NOTHROW(config.addConfigValue(\"goo\", \"bar\", \"6\"));\n  CHECK_NOTHROW(config.validateRequiredFields());\n\n  CHECK(config.getConfigValue<int>(\"foo\", \"bar\") == 5);\n  CHECK(config.getConfigValue<int>(\"goo\", \"bar\") == 6);\n}\n\nTEST_CASE(\"ConfigDefinition acceptAllOptions test\", \"[config]\")\n{\n  int fooBar = -1;\n  std::string fooBaz = \"\";\n\n  llarp::ConfigDefinition config{true};\n  config.defineOption(std::make_unique<llarp::OptionDefinition<int>>(\n      \"foo\", \"bar\", Default{1}, [&](int arg) {\n        fooBar = arg;\n      }));\n  config.defineOption(std::make_unique<llarp::OptionDefinition<std::string>>(\n      \"foo\", \"baz\", Default{\"no\"}, [&](std::string arg) {\n        fooBaz = arg;\n      }));\n\n  config.addConfigValue(\"foo\", \"baz\", \"yes\");\n\n  REQUIRE_NOTHROW(config.validateRequiredFields());\n  REQUIRE_NOTHROW(config.acceptAllOptions());\n  CHECK(fooBar == 1);\n  CHECK(fooBaz == \"yes\");\n}\n\nTEST_CASE(\"ConfigDefinition acceptAllOptions exception propagation test\", \"[config]\")\n{\n  llarp::ConfigDefinition config{true};\n  config.defineOption(std::make_unique<llarp::OptionDefinition<int>>(\n      \"foo\", \"bar\", Default{1}, [&](int arg) {\n        (void)arg;\n        throw std::runtime_error(\"FAIL\");\n      }));\n\n  REQUIRE_THROWS_WITH(config.acceptAllOptions(), \"FAIL\");\n}\n\nTEST_CASE(\"ConfigDefinition defineOptions passthrough test\", \"[config]\")\n{\n  llarp::ConfigDefinition config{true};\n  config.defineOption<int>(\"foo\", \"bar\", Default{1});\n  CHECK(config.getConfigValue<int>(\"foo\", \"bar\") == 1);\n}\n\nTEST_CASE(\"ConfigDefinition undeclared definition basic test\", \"[config]\")\n{\n  llarp::ConfigDefinition config{true};\n\n  bool invoked = false;\n\n  config.addUndeclaredHandler(\"foo\", [&](std::string_view section, std::string_view name, std::string_view value) {\n    CHECK(section == \"foo\");\n    CHECK(name == \"bar\");\n    CHECK(value == \"val\");\n\n    invoked = true;\n  });\n\n  REQUIRE_NOTHROW(config.addConfigValue(\"foo\", \"bar\", \"val\"));\n\n  CHECK(invoked);\n}\n\nTEST_CASE(\"ConfigDefinition undeclared add more than once test\", \"[config]\")\n{\n  llarp::ConfigDefinition config{true};\n\n  std::string calledBy = \"\";\n\n  config.addUndeclaredHandler(\"foo\", [&](std::string_view, std::string_view, std::string_view) {\n      calledBy = \"a\";\n  });\n  REQUIRE_THROWS_WITH(\n    config.addUndeclaredHandler(\"foo\", [&](std::string_view, std::string_view, std::string_view) {\n        calledBy = \"b\";\n    }),\n    \"section foo already has a handler\");\n\n  REQUIRE_NOTHROW(config.addConfigValue(\"foo\", \"bar\", \"val\"));\n\n  CHECK(calledBy == \"a\");\n}\n\nTEST_CASE(\"ConfigDefinition undeclared add/remove test\", \"[config]\")\n{\n  llarp::ConfigDefinition config{true};\n\n  std::string calledBy = \"\";\n\n  // add...\n  REQUIRE_NOTHROW(config.addUndeclaredHandler(\"foo\", [&](std::string_view, std::string_view, std::string_view) {\n    calledBy = \"a\";\n  }));\n\n  REQUIRE_NOTHROW(config.addConfigValue(\"foo\", \"bar\", \"val\"));\n\n  CHECK(calledBy == \"a\");\n\n  calledBy = \"\";\n\n  // ...then remove...\n  REQUIRE_NOTHROW(config.removeUndeclaredHandler(\"foo\"));\n\n  CHECK_THROWS_WITH(config.addConfigValue(\"foo\", \"bar\", \"val\"), \"unrecognized section [foo]\");\n\n  // ...then add again\n  REQUIRE_NOTHROW(config.addUndeclaredHandler(\"foo\", [&](std::string_view, std::string_view, std::string_view) {\n    calledBy = \"b\";\n  }));\n\n  REQUIRE_NOTHROW(config.addConfigValue(\"foo\", \"bar\", \"val\"));\n\n  CHECK(calledBy == \"b\");\n}\n\nTEST_CASE(\"ConfigDefinition undeclared handler exception propagation test\", \"[config]\")\n{\n  llarp::ConfigDefinition config{true};\n\n  config.addUndeclaredHandler(\"foo\", [](std::string_view, std::string_view, std::string_view) {\n      throw std::runtime_error(\"FAIL\");\n  });\n\n  REQUIRE_THROWS_WITH(config.addConfigValue(\"foo\", \"bar\", \"val\"), \"FAIL\");\n}\n\nTEST_CASE(\"ConfigDefinition undeclared handler wrong section\", \"[config]\")\n{\n  llarp::ConfigDefinition config{true};\n\n  config.addUndeclaredHandler(\"foo\", [](std::string_view, std::string_view, std::string_view) {\n      throw std::runtime_error(\"FAIL\");\n  });\n\n  REQUIRE_THROWS_WITH(config.addConfigValue(\"argle\", \"bar\", \"val\"), \"unrecognized section [argle]\");\n}\n\nTEST_CASE(\"ConfigDefinition undeclared handler duplicate names\", \"[config]\")\n{\n  llarp::ConfigDefinition config{true};\n\n  int count = 0;\n\n  config.addUndeclaredHandler(\"foo\", [&](std::string_view, std::string_view, std::string_view) {\n      count++;\n  });\n\n  REQUIRE_NOTHROW(config.addConfigValue(\"foo\", \"k\", \"v\"));\n  REQUIRE_NOTHROW(config.addConfigValue(\"foo\", \"k\", \"v\"));\n  REQUIRE_NOTHROW(config.addConfigValue(\"foo\", \"k\", \"v\"));\n\n  REQUIRE(count == 3);\n}\n\nTEST_CASE(\"ConfigDefinition AssignmentAcceptor\", \"[config]\")\n{\n  llarp::ConfigDefinition config{true};\n\n  int val = -1;\n  config.defineOption<int>(\"foo\", \"bar\", Default{2}, AssignmentAcceptor(val));\n\n  config.addConfigValue(\"foo\", \"bar\", \"3\");\n  CHECK_NOTHROW(config.acceptAllOptions());\n\n  REQUIRE(val == 3);\n}\n\nTEST_CASE(\"ConfigDefinition multiple values\", \"[config]\")\n{\n  llarp::ConfigDefinition config{true};\n\n  std::vector<int> values;\n  config.defineOption<int>(\"foo\", \"bar\", MultiValue, Default{2}, [&](int arg) {\n    values.push_back(arg);\n  });\n\n  config.addConfigValue(\"foo\", \"bar\", \"1\");\n  config.addConfigValue(\"foo\", \"bar\", \"2\");\n  config.addConfigValue(\"foo\", \"bar\", \"3\");\n  CHECK_NOTHROW(config.acceptAllOptions());\n\n  REQUIRE(values.size() == 3);\n  CHECK(values[0] == 1);\n  CHECK(values[1] == 2);\n  CHECK(values[2] == 3);\n}\n\nTEST_CASE(\"ConfigDefinition [bind]iface regression\", \"[config regression]\")\n{\n  llarp::ConfigDefinition config{true};\n\n  std::string val1;\n  std::string undeclaredName;\n  std::string undeclaredValue;\n\n  config.defineOption<std::string>(\n      \"bind\", \"*\", Default{\"1090\"}, [&](std::string arg) { val1 = arg; });\n\n  config.addUndeclaredHandler(\"bind\", [&](std::string_view, std::string_view name, std::string_view value) {\n    undeclaredName = std::string(name);\n    undeclaredValue = std::string(value);\n  });\n\n  config.addConfigValue(\"bind\", \"enp35s0\", \"1091\");\n  CHECK_NOTHROW(config.acceptAllOptions());\n\n  CHECK(val1 == \"1090\");\n  CHECK(undeclaredName == \"enp35s0\");\n  CHECK(undeclaredValue == \"1091\");\n}\n\nTEST_CASE(\"ConfigDefinition truthy/falsy bool values\", \"[config]\")\n{\n  // truthy values\n  for (auto val : {\"true\", \"on\", \"yes\", \"1\"})\n  {\n    llarp::OptionDefinition<bool> def(\"foo\", \"bar\", Default{false});\n\n    // defaults to false\n    auto maybe = def.getValue();\n    REQUIRE(maybe);\n    CHECK(*maybe == false);\n\n    // val should result in true\n    CHECK_NOTHROW(def.parseValue(val));\n    maybe = def.getValue();\n    REQUIRE(maybe);\n    CHECK(*maybe);\n  }\n\n  // falsy values\n  for (auto val : {\"false\", \"off\", \"no\", \"0\"})\n  {\n    llarp::OptionDefinition<bool> def(\"foo\", \"bar\", Default{true});\n\n    // defaults to true\n    auto maybe = def.getValue();\n    REQUIRE(maybe);\n    CHECK(maybe == true);\n\n    // val should result in false\n    CHECK_NOTHROW(def.parseValue(val));\n    maybe = def.getValue();\n    REQUIRE(maybe);\n    CHECK(maybe == false);\n  }\n\n  // illegal values\n  for (auto val : {\"\", \" \", \"TRUE\", \"argle\", \" false\", \"2\"})\n  {\n    llarp::OptionDefinition<bool> def(\"foo\", \"bar\", Default{true});\n    CHECK_THROWS(def.parseValue(val));\n  }\n}\n"
  },
  {
    "path": "test/config/test_llarp_config_ini.cpp",
    "content": "#include <llarp/config/ini.hpp>\n\n#include <catch2/catch.hpp>\n\nTEST_CASE(\"ConfigParser\", \"[config]\")\n{\n  llarp::ConfigParser parser;\n\n  SECTION(\"Parse empty\")\n  {\n    REQUIRE(parser.LoadFromStr(\"\"));\n  }\n\n  SECTION(\"Parse one section\")\n  {\n    llarp::ConfigParser::SectionValues_t sect;\n    // this is an anti pattern don't write this kind of code with configpaser\n    auto assertVisit = [&sect](const auto& section) -> bool {\n      sect = section;\n      return true;\n    };\n    REQUIRE(parser.LoadFromStr(\"[test]\\nkey=val   \\n\"));\n    REQUIRE(parser.VisitSection(\"test\", assertVisit));\n    auto itr = sect.find(\"notfound\");\n    REQUIRE(itr == sect.end());\n    itr = sect.find(\"key\");\n    REQUIRE(itr != sect.end());\n    REQUIRE(itr->second == \"val\");\n  }\n\n  SECTION(\"Parse section duplicate keys\")\n  {\n    REQUIRE(parser.LoadFromStr(\"[test]\\nkey1=val1\\nkey1=val2\"));\n    size_t num = 0;\n    auto visit = [&num](const auto& section) -> bool {\n      num = section.count(\"key1\");\n      return true;\n    };\n    REQUIRE(parser.VisitSection(\"test\", visit));\n    REQUIRE(num == size_t(2));\n  }\n\n  SECTION(\"No key\")\n  {\n    REQUIRE_THROWS(parser.LoadFromStr(\"[test]\\n=1090\\n\"));\n  }\n\n  SECTION(\"Parse invalid\")\n  {\n    REQUIRE_THROWS(\n        parser.LoadFromStr(\"srged5ghe5\\nf34wtge5\\nw34tgfs4ygsd5yg=4;\\n#\"\n                           \"g4syhgd5\\n\"));\n  }\n\n  parser.Clear();\n}\n"
  },
  {
    "path": "test/config/test_llarp_config_output.cpp",
    "content": "#include <llarp/config/definition.hpp>\n\n#include <catch2/catch.hpp>\n\nusing namespace llarp::config;\n\nTEST_CASE(\"ConfigDefinition simple generate test\", \"[config]\")\n{\n  llarp::ConfigDefinition config{true};\n\n  config.defineOption<int>(\"foo\", \"bar\", Required);\n  config.defineOption<int>(\"foo\", \"baz\", Default{2});\n  config.defineOption(\n      std::make_unique<llarp::OptionDefinition<std::string>>(\"foo\", \"quux\", Default{\"hello\"}));\n\n  config.defineOption<int>(\"argle\", \"bar\", RelayOnly, Required);\n  config.defineOption<int>(\"argle\", \"baz\", Default{4});\n  config.defineOption<std::string>(\"argle\", \"quux\", Default{\"the quick brown fox\"});\n\n  config.defineOption<int>(\"not\", \"for-routers\", ClientOnly, Default{1});\n\n  std::string output = config.generateINIConfig();\n\n  CHECK(output == R\"raw([foo]\n\n\n#bar=\n\n#baz=2\n\n#quux=hello\n\n\n[argle]\n\n\n#bar=\n\n#baz=4\n\n#quux=the quick brown fox\n)raw\");\n}\n\nTEST_CASE(\"ConfigDefinition useValue test\", \"[config]\")\n{\n  llarp::ConfigDefinition config{true};\n\n  config.defineOption<int>(\"foo\", \"bar\", Default{1});\n\n  constexpr auto expected = R\"raw([foo]\n\n\n#bar=1\n)raw\";\n\n  CHECK(config.generateINIConfig(false) == expected);\n  CHECK(config.generateINIConfig(true) == expected);\n\n  config.addConfigValue(\"foo\", \"bar\", \"2\");\n\n  constexpr auto expectedWhenValueProvided = R\"raw([foo]\n\n\nbar=2\n)raw\";\n\n  CHECK(config.generateINIConfig(false) == expected);\n  CHECK(config.generateINIConfig(true) == expectedWhenValueProvided);\n}\n\nTEST_CASE(\"ConfigDefinition section comments test\")\n{\n  llarp::ConfigDefinition config{true};\n\n  config.addSectionComments(\"foo\", {\"test comment\"});\n  config.addSectionComments(\"foo\", {\"test comment 2\"});\n  config.defineOption(std::make_unique<llarp::OptionDefinition<int>>(\"foo\", \"bar\", Default{1}));\n\n  std::string output = config.generateINIConfig();\n\n  CHECK(output == R\"raw([foo]\n# test comment\n# test comment 2\n\n\n#bar=1\n)raw\");\n}\n\nTEST_CASE(\"ConfigDefinition option comments test\")\n{\n  llarp::ConfigDefinition config{true};\n\n  config.addOptionComments(\"foo\", \"bar\", {\"test comment 1\"});\n  config.addOptionComments(\"foo\", \"bar\", {\"test comment 2\"});\n  config.defineOption<int>(\"foo\", \"bar\", Default{1});\n\n  config.defineOption<std::string>(\n      \"foo\",\n      \"far\",\n      Default{\"abc\"},\n      Comment{\n          \"Fill in the missing values:\",\n          \"___defg\",\n      });\n\n  config.defineOption<int>(\"client\", \"omg\", ClientOnly, Default{1}, Comment{\"hi\"});\n  config.defineOption<int>(\"relay\", \"ftw\", RelayOnly, Default{1}, Comment{\"bye\"});\n\n  // has comment, but is hidden: we show only the comment but not the value/default.\n  config.defineOption<int>(\"foo\", \"old-bar\", Hidden, Default{456});\n  config.addOptionComments(\"foo\", \"old-bar\", {\"old bar option\"});\n\n  // no comment, should be omitted.\n  config.defineOption<int>(\"foo\", \"older-bar\", Hidden);\n\n  std::string output = config.generateINIConfig();\n\n  CHECK(output == R\"raw([foo]\n\n\n# test comment 1\n# test comment 2\n#bar=1\n\n# Fill in the missing values:\n# ___defg\n#far=abc\n\n# old bar option\n\n\n[relay]\n\n\n# bye\n#ftw=1\n)raw\");\n}\n\nTEST_CASE(\"ConfigDefinition empty comments test\")\n{\n  llarp::ConfigDefinition config{true};\n\n  config.addSectionComments(\"foo\", {\"section comment\"});\n  config.addSectionComments(\"foo\", {\"\"});\n\n  config.addOptionComments(\"foo\", \"bar\", {\"option comment\"});\n  config.addOptionComments(\"foo\", \"bar\", {\"\"});\n  config.defineOption<int>(\"foo\", \"bar\", Default{1});\n\n  std::string output = config.generateINIConfig();\n\n  CHECK(output == R\"raw([foo]\n# section comment\n# \n\n\n# option comment\n# \n#bar=1\n)raw\");\n}\n\nTEST_CASE(\"ConfigDefinition multi option comments\")\n{\n  llarp::ConfigDefinition config{true};\n\n  config.addSectionComments(\"foo\", {\"foo section comment\"});\n\n  config.addOptionComments(\"foo\", \"bar\", {\"foo bar option comment\"});\n  config.defineOption<int>(\"foo\", \"bar\", Default{1});\n\n  config.addOptionComments(\"foo\", \"baz\", {\"foo baz option comment\"});\n  config.defineOption<int>(\"foo\", \"baz\", Default{1});\n\n  std::string output = config.generateINIConfig();\n\n  CHECK(output == R\"raw([foo]\n# foo section comment\n\n\n# foo bar option comment\n#bar=1\n\n# foo baz option comment\n#baz=1\n)raw\");\n}\n"
  },
  {
    "path": "test/config/test_llarp_config_values.cpp",
    "content": "#include <llarp/config/config.hpp>\n\n#include <catch2/catch.hpp>\n#include \"mocks/mock_context.hpp\"\n\nusing namespace std::literals;\n\nstruct UnitTestConfigGenParameters : public llarp::ConfigGenParameters\n{\n  const mocks::Network* const _plat;\n  UnitTestConfigGenParameters(const mocks::Network* plat)\n      : llarp::ConfigGenParameters{}, _plat{plat}\n  {}\n\n  const llarp::net::Platform*\n  Net_ptr() const override\n  {\n    return _plat;\n  }\n};\n\nstruct UnitTestConfig : public llarp::Config\n{\n  const mocks::Network* const _plat;\n\n  explicit UnitTestConfig(const mocks::Network* plat) : llarp::Config{std::nullopt}, _plat{plat}\n  {}\n\n  std::unique_ptr<llarp::ConfigGenParameters>\n  MakeGenParams() const override\n  {\n    return std::make_unique<UnitTestConfigGenParameters>(_plat);\n  }\n};\n\nstd::shared_ptr<UnitTestConfig>\nmake_config_for_test(const mocks::Network* env, std::string_view ini_str = \"\")\n{\n  auto conf = std::make_shared<UnitTestConfig>(env);\n  conf->LoadString(ini_str, true);\n  conf->lokid.whitelistRouters = false;\n  conf->bootstrap.seednode = true;\n  conf->bootstrap.files.clear();\n  return conf;\n}\n\nstd::shared_ptr<UnitTestConfig>\nmake_config(mocks::Network env, std::string_view ini_str = \"\")\n{\n  auto conf = std::make_shared<UnitTestConfig>(&env);\n  conf->LoadString(ini_str, true);\n  conf->lokid.whitelistRouters = false;\n  conf->bootstrap.seednode = true;\n  conf->bootstrap.files.clear();\n  return conf;\n}\n\nvoid\nrun_config_test(mocks::Network env, std::string_view ini_str)\n{\n  auto conf = make_config_for_test(&env, ini_str);\n  const auto opts = env.Opts();\n  auto context = std::make_shared<mocks::MockContext>(env);\n\n  context->Configure(conf);\n  context->Setup(opts);\n  int ib_links{};\n  int ob_links{};\n\n  context->router->linkManager().ForEachInboundLink([&ib_links](auto) { ib_links++; });\n  context->router->linkManager().ForEachOutboundLink([&ob_links](auto) { ob_links++; });\n  REQUIRE(ib_links == 1);\n  REQUIRE(ob_links == 1);\n  if (context->Run(opts))\n    throw std::runtime_error{\"non zero return\"};\n}\n\nconst std::string ini_minimal = \"[lokid]\\nrpc=ipc://dummy\\n\";\n\nTEST_CASE(\"service node bind section on valid network\", \"[config]\")\n{\n  std::unordered_multimap<std::string, llarp::IPRange> env{\n      {\"mock0\", llarp::IPRange::FromIPv4(1, 1, 1, 1, 32)},\n      {\"lo\", llarp::IPRange::FromIPv4(127, 0, 0, 1, 8)},\n  };\n\n  SECTION(\"mock network is sane\")\n  {\n    mocks::Network mock_net{env};\n    REQUIRE(mock_net.GetInterfaceAddr(\"mock0\"sv, AF_INET6) == std::nullopt);\n    auto maybe_addr = mock_net.GetInterfaceAddr(\"mock0\"sv, AF_INET);\n    REQUIRE(maybe_addr != std::nullopt);\n    REQUIRE(maybe_addr->hostString() == \"1.1.1.1\");\n    REQUIRE(not mock_net.IsBogon(*maybe_addr));\n  }\n\n  SECTION(\"minimal config\")\n  {\n    REQUIRE_NOTHROW(run_config_test(env, ini_minimal));\n  }\n\n  SECTION(\"explicit bind via ifname\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[bind]\nmock0=443\n)\";\n    run_config_test(env, ini_str);\n  }\n  SECTION(\"explicit bind via ip address\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[bind]\ninbound=1.1.1.1:443\n)\";\n    REQUIRE_NOTHROW(run_config_test(env, ini_str));\n  }\n  SECTION(\"explicit bind via ip address with old syntax\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[bind]\n1.1.1.1=443\n)\";\n\n    REQUIRE_NOTHROW(run_config_test(env, ini_str));\n  }\n  SECTION(\"ip spoof fails\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[router]\npublic-ip=8.8.8.8\npublic-port=443\n[bind]\ninbound=1.1.1.1:443\n)\";\n    REQUIRE_THROWS(run_config_test(env, ini_str));\n  }\n  SECTION(\"explicit bind via ifname but fails from non existing ifname\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[bind]\nligma0=443\n)\";\n    REQUIRE_THROWS(make_config(env, ini_str));\n  }\n\n  SECTION(\"explicit bind via ifname but fails from using loopback\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[bind]\nlo=443\n)\";\n    REQUIRE_THROWS(make_config(env, ini_str));\n  }\n\n  SECTION(\"explicit bind via explicit loopback\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[bind]\ninbound=127.0.0.1:443\n)\";\n    REQUIRE_THROWS(make_config(env, ini_str));\n  }\n  SECTION(\"public ip provided but no bind section\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[router]\npublic-ip=1.1.1.1\npublic-port=443\n)\";\n    REQUIRE_NOTHROW(run_config_test(env, ini_str));\n  }\n  SECTION(\"public ip provided with ip in bind section\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[router]\npublic-ip=1.1.1.1\npublic-port=443\n[bind]\n1.1.1.1=443\n)\";\n    REQUIRE_NOTHROW(run_config_test(env, ini_str));\n  }\n}\n\nTEST_CASE(\"service node bind section on nat network\", \"[config]\")\n{\n  std::unordered_multimap<std::string, llarp::IPRange> env{\n      {\"mock0\", llarp::IPRange::FromIPv4(10, 1, 1, 1, 32)},\n      {\"lo\", llarp::IPRange::FromIPv4(127, 0, 0, 1, 8)},\n  };\n  SECTION(\"no public ip set should fail\")\n  {\n    std::string_view ini_str = \"\";\n    REQUIRE_THROWS(run_config_test(env, ini_str));\n  }\n\n  SECTION(\"public ip provided via inbound directive\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[router]\npublic-ip=1.1.1.1\npublic-port=443\n\n[bind]\ninbound=10.1.1.1:443\n)\";\n    REQUIRE_NOTHROW(run_config_test(env, ini_str));\n  }\n\n  SECTION(\"public ip provided with bind via ifname\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[router]\npublic-ip=1.1.1.1\npublic-port=443\n\n[bind]\nmock0=443\n)\";\n    REQUIRE_NOTHROW(run_config_test(env, ini_str));\n  }\n\n  SECTION(\"public ip provided bind via wildcard ip\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[router]\npublic-ip=1.1.1.1\npublic-port=443\n\n[bind]\ninbound=0.0.0.0:443\n)\";\n    REQUIRE_THROWS(run_config_test(env, ini_str));\n  }\n}\n\nTEST_CASE(\"service node bind section with multiple public ip\", \"[config]\")\n{\n  std::unordered_multimap<std::string, llarp::IPRange> env{\n      {\"mock0\", llarp::IPRange::FromIPv4(1, 1, 1, 1, 32)},\n      {\"mock0\", llarp::IPRange::FromIPv4(2, 1, 1, 1, 32)},\n      {\"lo\", llarp::IPRange::FromIPv4(127, 0, 0, 1, 8)},\n  };\n  SECTION(\"with old style wildcard for inbound and no public ip, fails\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[bind]\n0.0.0.0=443\n)\";\n    REQUIRE_THROWS(run_config_test(env, ini_str));\n  }\n  SECTION(\"with old style wildcard for outbound\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[bind]\n*=1443\n)\";\n    REQUIRE_NOTHROW(run_config_test(env, ini_str));\n  }\n  SECTION(\"with wildcard via inbound directive no public ip given, fails\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[bind]\ninbound=0.0.0.0:443\n)\";\n\n    REQUIRE_THROWS(run_config_test(env, ini_str));\n  }\n  SECTION(\"with wildcard via inbound directive primary public ip given\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[router]\npublic-ip=1.1.1.1\npublic-port=443\n[bind]\ninbound=0.0.0.0:443\n)\";\n\n    REQUIRE_NOTHROW(run_config_test(env, ini_str));\n  }\n  SECTION(\"with wildcard via inbound directive secondary public ip given\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[router]\npublic-ip=2.1.1.1\npublic-port=443\n[bind]\ninbound=0.0.0.0:443\n)\";\n\n    REQUIRE_NOTHROW(run_config_test(env, ini_str));\n  }\n  SECTION(\"with bind via interface name\")\n  {\n    auto ini_str = ini_minimal + R\"(\n[bind]\nmock0=443\n)\";\n    REQUIRE_NOTHROW(run_config_test(env, ini_str));\n  }\n}\n"
  },
  {
    "path": "test/crypto/test_llarp_crypto.cpp",
    "content": "#include <llarp/crypto/crypto_libsodium.hpp>\n\n#include <iostream>\n\n#include <catch2/catch.hpp>\n\nusing namespace llarp;\n\nTEST_CASE(\"Identity key\")\n{\n  SecretKey secret;\n  crypto.identity_keygen(secret);\n\n  SECTION(\"Keygen\")\n  {\n    REQUIRE_FALSE(secret.IsZero());\n  }\n\n  SECTION(\"Sign-verify\")\n  {\n    AlignedBuffer<128> random;\n    random.Randomize();\n    Signature sig;\n    const PubKey pk = secret.toPublic();\n\n    const llarp_buffer_t buf(random.data(), random.size());\n    REQUIRE(crypto.sign(sig, secret, buf));\n    REQUIRE(crypto.verify(pk, buf, sig));\n    random.Randomize();\n    // mangle body\n    REQUIRE_FALSE(crypto.verify(pk, buf, sig));\n  }\n}\n\nTEST_CASE(\"PQ crypto\")\n{\n  PQKeyPair keys;\n  crypto.pqe_keygen(keys);\n  PQCipherBlock block;\n  SharedSecret shared, otherShared;\n  auto c = &crypto;\n\n  REQUIRE(keys.size() == PQ_KEYPAIRSIZE);\n  REQUIRE(c->pqe_encrypt(block, shared, PQPubKey(pq_keypair_to_public(keys))));\n  REQUIRE(c->pqe_decrypt(block, otherShared, pq_keypair_to_secret(keys)));\n  REQUIRE(otherShared == shared);\n}\n\n#ifdef HAVE_CRYPT\n\nTEST_CASE(\"passwd hash valid\")\n{\n  // poggers password hashes\n  std::set<std::string> valid_hashes;\n  // UNIX DES\n  valid_hashes.emplace(\"CVu85Ms694POo\");\n  // sha256 salted\n  valid_hashes.emplace(\n      \"$5$cIghotiBGjfPC7Fu$\"\n      \"TXXxPhpUcEiF9tMnjhEVJFi9AlNDSkNRQFTrXPQTKS9\");\n  // sha512 salted\n  valid_hashes.emplace(\n      \"$6$qB77ms3wCIo.xVKP$Hl0RLuDgWNmIW4s.\"\n      \"5KUbFmnauoTfrWSPJzDCD8ZTSSfwRbMgqgG6F9y3K.YEYVij8g/\"\n      \"Js0DRT2RhgXoX0sHGb.\");\n\n  for (const auto& hash : valid_hashes)\n  {\n    // make sure it is poggers ...\n    REQUIRE(crypto.check_passwd_hash(hash, \"poggers\"));\n    // ... and not inscrutible\n    REQUIRE(not crypto.check_passwd_hash(hash, \"inscrutible\"));\n  }\n}\n\nTEST_CASE(\"passwd hash malformed\")\n{\n  llarp::sodium::CryptoLibSodium crypto;\n\n  std::set<std::string> invalid_hashes = {\n      \"stevejobs\",\n      \"$JKEDbzgzym1N6\",  // crypt() for \"stevejobs\" with a $ at the begining\n      \"$0$zero$AAAAAAAAAAA\",\n      \"$$$AAAAAAAAAAAA\",\n      \"$LIGMA$BALLS$LMAOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO.\"};\n  for (const auto& hash : invalid_hashes)\n    REQUIRE(not crypto.check_passwd_hash(hash, \"stevejobs\"));\n}\n\n#endif\n"
  },
  {
    "path": "test/crypto/test_llarp_crypto_types.cpp",
    "content": "#include <llarp/crypto/types.hpp>\n\n#include <fstream>\n#include <string>\n\n#include \"test_util.hpp\"\n#include <catch2/catch.hpp>\n\nextern \"C\" {\n#include <unistd.h>\n}\n\n// This used to be implied via the headers above *shrug*\n#ifdef _WIN32\n#include <windows.h>\n#endif\n\nstruct ToStringData\n{\n  llarp::PubKey::Data input;\n  std::string output;\n};\n\nllarp::PubKey::Data empty = {};\nllarp::PubKey::Data full = {{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,\n                             0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,\n                             0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}};\n\n// clang-format off\nstd::vector<ToStringData> toStringData{\n    {empty, \"0000000000000000000000000000000000000000000000000000000000000000\"},\n    {full, \"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"},\n};\n// clang-format on\n\nTEST_CASE(\"PubKey-string conversion\")\n{\n  auto d = GENERATE(from_range(toStringData));\n\n  SECTION(\"To string\")\n  {\n    llarp::PubKey key(d.input);\n\n    REQUIRE(key.ToString() == d.output);\n  }\n\n  SECTION(\"From string\")\n  {\n    llarp::PubKey key;\n\n    REQUIRE(key.FromString(d.output));\n    REQUIRE(key == llarp::PubKey(d.input));\n  }\n}\n\n// Concerns\n// - file missing\n// - file empty\n// - file too small\n// - file too large\n// - raw buffer\n// - bencoded\n\nstruct TestCryptoTypesSecret\n{\n  std::string filename;\n  fs::path p;\n\n  TestCryptoTypesSecret() : filename(llarp::test::randFilename()), p(filename)\n  {}\n};\n\nTEST_CASE_METHOD(TestCryptoTypesSecret, \"secret_key_from_file_missing\")\n{\n  // Verify loading an empty file fails cleanly.\n  REQUIRE_FALSE(fs::exists(fs::status(p)));\n\n  llarp::SecretKey key;\n  REQUIRE_FALSE(key.LoadFromFile(filename.c_str()));\n\n  // Verify we didn't create a file\n  REQUIRE_FALSE(fs::exists(fs::status(p)));\n}\n\nTEST_CASE_METHOD(TestCryptoTypesSecret, \"secret_key_from_file_empty\")\n{\n  // Verify loading an empty file fails cleanly.\n  REQUIRE_FALSE(fs::exists(fs::status(p)));\n\n  // Create empty file\n  std::fstream f;\n  f.open(filename, std::ios::out | std::ios::binary);\n  f.close();\n\n  llarp::test::FileGuard guard(p);\n\n  llarp::SecretKey key;\n  REQUIRE_FALSE(key.LoadFromFile(filename.c_str()));\n\n  // Verify we didn't delete the file\n  REQUIRE(fs::exists(fs::status(fs::path(filename))));\n}\n\nTEST_CASE_METHOD(TestCryptoTypesSecret, \"secret_key_from_file_smaller\")\n{\n  // Verify loading a file which is too small fails cleanly.\n  REQUIRE_FALSE(fs::exists(fs::status(p)));\n\n  // Create empty file\n  std::fstream f;\n  f.open(filename, std::ios::out | std::ios::binary);\n  std::fill_n(std::ostream_iterator<byte_t>(f), llarp::SecretKey::SIZE / 2, 0xAA);\n  f.close();\n\n  llarp::test::FileGuard guard(p);\n\n  llarp::SecretKey key;\n  REQUIRE_FALSE(key.LoadFromFile(filename.c_str()));\n\n  // Verify we didn't delete the file\n  REQUIRE(fs::exists(fs::status(fs::path(filename))));\n}\n\nTEST_CASE_METHOD(TestCryptoTypesSecret, \"secret_key_from_file_smaller_bencode\")\n{\n  // Verify loading a file which is too small fails cleanly.\n  REQUIRE_FALSE(fs::exists(fs::status(p)));\n\n  // Create empty file\n  std::fstream f;\n  f.open(filename, std::ios::out | std::ios::binary);\n  f.write(\"32:\", 3);\n  std::fill_n(std::ostream_iterator<byte_t>(f), 32, 0xAA);\n  f.close();\n\n  llarp::test::FileGuard guard(p);\n\n  llarp::SecretKey key;\n  REQUIRE_FALSE(key.LoadFromFile(filename.c_str()));\n\n  // Verify we didn't delete the file\n  REQUIRE(fs::exists(fs::status(fs::path(filename))));\n}\n\nTEST_CASE_METHOD(TestCryptoTypesSecret, \"secret_key_from_file_smaller_corrupt_bencode\")\n{\n  // Verify loading a file which is too small + corrupt fails cleanly.\n  REQUIRE_FALSE(fs::exists(fs::status(p)));\n\n  // Create empty file\n  std::fstream f;\n  f.open(filename, std::ios::out | std::ios::binary);\n  f.write(\"256:\", 4);\n  std::fill_n(std::ostream_iterator<byte_t>(f), 32, 0xAA);\n  f.close();\n\n  llarp::test::FileGuard guard(p);\n\n  llarp::SecretKey key;\n  REQUIRE_FALSE(key.LoadFromFile(filename.c_str()));\n\n  // Verify we didn't delete the file\n  REQUIRE(fs::exists(fs::status(fs::path(filename))));\n}\n\nTEST_CASE_METHOD(TestCryptoTypesSecret, \"secret_key_from_file_larger\")\n{\n  // Verify loading a file which is too large fails cleanly.\n  REQUIRE_FALSE(fs::exists(fs::status(p)));\n\n  // Create empty file\n  std::fstream f;\n  f.open(filename, std::ios::out | std::ios::binary);\n  std::fill_n(std::ostream_iterator<byte_t>(f), llarp::SecretKey::SIZE * 2, 0xAA);\n  f.close();\n\n  llarp::test::FileGuard guard(p);\n\n  llarp::SecretKey key;\n  REQUIRE_FALSE(key.LoadFromFile(filename.c_str()));\n\n  // Verify we didn't delete the file\n  REQUIRE(fs::exists(fs::status(fs::path(filename))));\n}\n\nTEST_CASE_METHOD(TestCryptoTypesSecret, \"secret_key_from_file_larger_bencode\")\n{\n  // Verify loading a file which is too large fails cleanly.\n  REQUIRE_FALSE(fs::exists(fs::status(p)));\n\n  // Create empty file\n  std::fstream f;\n  f.open(filename, std::ios::out | std::ios::binary);\n  f.write(\"256:\", 4);\n  std::fill_n(std::ostream_iterator<byte_t>(f), 256, 0xAA);\n  f.close();\n\n  llarp::test::FileGuard guard(p);\n\n  llarp::SecretKey key;\n  REQUIRE_FALSE(key.LoadFromFile(filename.c_str()));\n\n  // Verify we didn't delete the file\n  REQUIRE(fs::exists(fs::status(fs::path(filename))));\n}\n\nTEST_CASE_METHOD(TestCryptoTypesSecret, \"secret_key_from_file_happy_raw\")\n{\n  // Verify loading a valid raw file succeeds.\n  REQUIRE_FALSE(fs::exists(fs::status(p)));\n\n  // Create empty file\n  std::fstream f;\n  f.open(filename, std::ios::out | std::ios::binary);\n  std::fill_n(std::ostream_iterator<byte_t>(f), llarp::SecretKey::SIZE, 0xAA);\n  f.close();\n\n  llarp::test::FileGuard guard(p);\n\n  llarp::SecretKey key;\n  REQUIRE(key.LoadFromFile(filename.c_str()));\n\n  // Verify we didn't delete the file\n  REQUIRE(fs::exists(fs::status(fs::path(filename))));\n}\n\nTEST_CASE_METHOD(TestCryptoTypesSecret, \"secret_key_from_file_happy_bencode\")\n{\n  // Verify loading a valid bencoded file succeeds.\n  REQUIRE_FALSE(fs::exists(fs::status(p)));\n\n  // Create empty file\n  std::fstream f;\n  f.open(filename, std::ios::out | std::ios::binary);\n  f.write(\"64:\", 4);\n  std::fill_n(std::ostream_iterator<byte_t>(f), llarp::SecretKey::SIZE, 0xAA);\n  f.close();\n\n  llarp::test::FileGuard guard(p);\n\n  llarp::SecretKey key;\n  REQUIRE(key.LoadFromFile(filename.c_str()));\n\n  // Verify we didn't delete the file\n  REQUIRE(fs::exists(fs::status(fs::path(filename))));\n}\n\n// Save to file\n\n// Concerns\n// - file not writeable\n// - happy path\n\n// Win32: check for root/admin/elevation privileges\n#ifdef _WIN32\nBOOL\nIsRunAsAdmin()\n{\n  BOOL fIsRunAsAdmin = FALSE;\n  DWORD dwError = ERROR_SUCCESS;\n  PSID pAdministratorsGroup = NULL;\n\n  // Allocate and initialize a SID of the administrators group.\n  SID_IDENTIFIER_AUTHORITY NtAuthority = SECURITY_NT_AUTHORITY;\n  if (!AllocateAndInitializeSid(\n          &NtAuthority,\n          2,\n          SECURITY_BUILTIN_DOMAIN_RID,\n          DOMAIN_ALIAS_RID_ADMINS,\n          0,\n          0,\n          0,\n          0,\n          0,\n          0,\n          &pAdministratorsGroup))\n  {\n    dwError = GetLastError();\n    goto Cleanup;\n  }\n\n  // Determine whether the SID of administrators group is enabled in\n  // the primary access token of the process.\n  if (!CheckTokenMembership(NULL, pAdministratorsGroup, &fIsRunAsAdmin))\n  {\n    dwError = GetLastError();\n    goto Cleanup;\n  }\n\nCleanup:\n  // Centralized cleanup for all allocated resources.\n  if (pAdministratorsGroup)\n  {\n    FreeSid(pAdministratorsGroup);\n    pAdministratorsGroup = NULL;\n  }\n\n  // Throw the error if something failed in the function.\n  if (ERROR_SUCCESS != dwError)\n  {\n    throw dwError;\n  }\n\n  return fIsRunAsAdmin;\n}\n#endif\n\nTEST_CASE_METHOD(TestCryptoTypesSecret, \"secret_key_to_missing_file\")\n{\n  // Verify writing to an unwritable file fails.\n  // Assume we're not running as root, so can't write to [C:]/\n  // if we are root just skip this test\n#ifndef _WIN32\n  if (getuid() == 0)\n    return;\n#else\n  if (IsRunAsAdmin())\n    return;\n#endif\n  filename = \"/\" + filename;\n  p = filename;\n  REQUIRE_FALSE(fs::exists(fs::status(p)));\n\n  llarp::test::FileGuard guard(p);\n\n  llarp::SecretKey key;\n  REQUIRE_FALSE(key.SaveToFile(filename.c_str()));\n\n  // Verify we didn't create the file\n  REQUIRE_FALSE(fs::exists(fs::status(fs::path(filename))));\n}\n\nTEST_CASE_METHOD(TestCryptoTypesSecret, \"secret_key_to_file\")\n{\n  REQUIRE_FALSE(fs::exists(fs::status(p)));\n\n  llarp::test::FileGuard guard(p);\n\n  llarp::SecretKey key;\n  key.Randomize();\n  REQUIRE(key.SaveToFile(filename.c_str()));\n\n  // Verify we created the file\n  REQUIRE(fs::exists(fs::status(fs::path(filename))));\n\n  llarp::SecretKey other;\n  other.LoadFromFile(filename.c_str());\n\n  REQUIRE(other == key);\n}\n"
  },
  {
    "path": "test/crypto/test_llarp_key_manager.cpp",
    "content": "#include \"test_util.hpp\"\n\n#include <llarp/config/key_manager.hpp>\n\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/crypto/crypto_libsodium.hpp>\n\n#include <functional>\n#include <random>\n\n#include <string>\n#include <catch2/catch.hpp>\n\nusing namespace ::llarp;\n\nstruct KeyManagerTest\n{\n  // paranoid file guards for anything KeyManager might touch\n  test::FileGuard m_rcFileGuard;\n  test::FileGuard m_encFileGuard;\n  test::FileGuard m_transportFileGuard;\n  test::FileGuard m_identFileGuard;\n\n  KeyManagerTest()\n      : m_rcFileGuard(our_rc_filename)\n      , m_encFileGuard(our_enc_key_filename)\n      , m_transportFileGuard(our_transport_key_filename)\n      , m_identFileGuard(our_identity_filename)\n  {}\n\n  /// generate a valid \"rc.signed\" file\n  bool\n  generateRcFile()\n  {\n    RelayContact rc;\n    return rc.Write(our_rc_filename);\n  }\n};\n\nTEST_CASE_METHOD(KeyManagerTest, \"Backup file by moving moves existing files\")\n{\n  fs::path p = test::randFilename();\n  REQUIRE_FALSE(fs::exists(p));\n\n  // touch file\n  std::fstream f;\n  f.open(p.string(), std::ios::out);\n  f.close();\n\n  KeyManager::backupFileByMoving(p.string());\n\n  REQUIRE_FALSE(fs::exists(p));\n\n  fs::path moved = p.string() + \".0.bak\";\n\n  REQUIRE(fs::exists(moved));\n\n  test::FileGuard guard(moved);\n};\n\nTEST_CASE_METHOD(KeyManagerTest, \"Backup file by moving doesnt touch non existent files\")\n{\n  fs::path p = test::randFilename();\n  REQUIRE_FALSE(fs::exists(p));\n\n  KeyManager::backupFileByMoving(p.string());\n\n  REQUIRE_FALSE(fs::exists(p));\n\n  fs::path moved = p.string() + \".0.bak\";\n\n  REQUIRE_FALSE(fs::exists(moved));\n}\n\nTEST_CASE_METHOD(KeyManagerTest, \"Backup file by moving fails if backup names are exausted\")\n{\n  fs::path base = test::randFilename();\n  REQUIRE_FALSE(fs::exists(base));\n\n  // touch file\n  {\n    std::fstream f;\n    f.open(base.string(), std::ios::out);\n    f.close();\n  }\n\n  test::FileGuard guard(base);\n\n  constexpr uint32_t numBackupNames = 9;\n  std::vector<test::FileGuard> guards;\n  guards.reserve(numBackupNames);\n\n  // generate backup files foo.0.bak through foo.9.bak\n  for (uint32_t i = 0; i < numBackupNames; ++i)\n  {\n    fs::path p = base.string() + \".\" + std::to_string(i) + \".bak\";\n\n    std::fstream f;\n    f.open(p.string(), std::ios::out);\n    f.close();\n\n    guards.emplace_back(p);\n\n    REQUIRE(fs::exists(p));\n  }\n\n  REQUIRE_FALSE(KeyManager::backupFileByMoving(base.string()));\n};\n\nTEST_CASE_METHOD(KeyManagerTest, \"Initialize makes keyfiles\")\n{\n  llarp::Config conf{fs::current_path()};\n  conf.Load();\n\n  KeyManager keyManager;\n  REQUIRE(keyManager.initialize(conf, true, true));\n\n  // KeyManager doesn't generate RC file, but should generate others\n  REQUIRE_FALSE(fs::exists(our_rc_filename));\n\n  REQUIRE(fs::exists(our_enc_key_filename));\n  REQUIRE(fs::exists(our_transport_key_filename));\n  REQUIRE(fs::exists(our_identity_filename));\n}\n\nTEST_CASE_METHOD(KeyManagerTest, \"Initialize respects gen flag\")\n{\n  llarp::Config conf{fs::current_path()};\n  conf.Load();\n\n  KeyManager keyManager;\n  REQUIRE_FALSE(keyManager.initialize(conf, false, true));\n\n  // KeyManager shouldn't have touched any files without (genIfAbsent == true)\n  REQUIRE_FALSE(fs::exists(our_rc_filename));\n  REQUIRE_FALSE(fs::exists(our_enc_key_filename));\n  REQUIRE_FALSE(fs::exists(our_transport_key_filename));\n  REQUIRE_FALSE(fs::exists(our_identity_filename));\n}\n\nTEST_CASE_METHOD(KeyManagerTest, \"Initialize detects bad rc file\")\n{\n  llarp::Config conf{fs::current_path()};\n  conf.Load();\n\n  conf.lokid.whitelistRouters = false;\n\n  std::fstream f;\n  f.open(our_rc_filename, std::ios::out);\n  f << \"bad_rc_file\";\n  f.close();\n\n  KeyManager keyManager;\n  REQUIRE(keyManager.initialize(conf, true, true));\n  REQUIRE(keyManager.needBackup());\n\n  REQUIRE(fs::exists(our_enc_key_filename));\n  REQUIRE(fs::exists(our_transport_key_filename));\n  REQUIRE(fs::exists(our_identity_filename));\n\n  // test that keys are sane\n  SecretKey key;\n\n  key.Zero();\n  REQUIRE(key.LoadFromFile(our_enc_key_filename));\n  REQUIRE_FALSE(key.IsZero());\n\n  key.Zero();\n  REQUIRE(key.LoadFromFile(our_transport_key_filename));\n  REQUIRE_FALSE(key.IsZero());\n\n  key.Zero();\n  REQUIRE(key.LoadFromFile(our_identity_filename));\n  REQUIRE_FALSE(key.IsZero());\n}\n"
  },
  {
    "path": "test/dns/test_llarp_dns_dns.cpp",
    "content": "#include <catch2/catch.hpp>\n#include <llarp/dns/dns.hpp>\n#include <llarp/dns/message.hpp>\n#include <llarp/dns/name.hpp>\n#include <llarp/dns/rr.hpp>\n#include <llarp/net/net.hpp>\n#include <llarp/net/ip.hpp>\n#include <llarp/util/buffer.hpp>\n\n#include <algorithm>\n\nconstexpr auto tld = \".loki\";\n\nTEST_CASE(\"Test Has TLD\", \"[dns]\")\n{\n  llarp::dns::Question question;\n  question.qname = \"a.loki.\";\n  CHECK(question.HasTLD(tld));\n  question.qname = \"a.loki..\";\n  CHECK(not question.HasTLD(tld));\n  question.qname = \"bepis.loki.\";\n  CHECK(question.HasTLD(tld));\n  question.qname = \"bepis.logi.\";\n  CHECK(not question.HasTLD(tld));\n  question.qname = \"a.net.\";\n  CHECK(not question.HasTLD(tld));\n  question.qname = \"a.boki.\";\n  CHECK(not question.HasTLD(tld));\n  question.qname = \"t.co.\";\n  CHECK(not question.HasTLD(tld));\n};\n\nTEST_CASE(\"Test Is Localhost.loki\", \"[dns]\")\n{\n  llarp::dns::Question question;\n\n  question.qname = \"localhost.loki.\";\n  CHECK(question.IsLocalhost());\n  question.qname = \"foo.localhost.loki.\";\n  CHECK(question.IsLocalhost());\n  question.qname = \"foo.bar.localhost.loki.\";\n  CHECK(question.IsLocalhost());\n\n  question.qname = \"something.loki.\";\n  CHECK(not question.IsLocalhost());\n  question.qname = \"localhost.something.loki.\";\n  CHECK(not question.IsLocalhost());\n  question.qname = \"notlocalhost.loki.\";\n  CHECK(not question.IsLocalhost());\n};\n\nTEST_CASE(\"Test Get Subdomains\" , \"[dns]\")\n{\n  llarp::dns::Question question;\n  std::string expected;\n\n  question.qname = \"localhost.loki.\";\n  expected = \"\";\n  CHECK(question.Subdomains() == expected);\n\n  question.qname = \"foo.localhost.loki.\";\n  expected = \"foo\";\n  CHECK(question.Subdomains() == expected);\n\n  question.qname = \"foo.bar.localhost.loki.\";\n  expected = \"foo.bar\";\n  CHECK(question.Subdomains() == expected);\n\n  // not legal, but test it anyway\n  question.qname = \".localhost.loki.\";\n  expected = \"\";\n  CHECK(question.Subdomains() == expected);\n\n  question.qname = \".loki.\";\n  expected = \"\";\n  CHECK(question.Subdomains() == expected);\n\n  question.qname = \"loki.\";\n  expected = \"\";\n  CHECK(question.Subdomains() == expected);\n\n  question.qname = \".\";\n  expected = \"\";\n  CHECK(question.Subdomains() == expected);\n\n  question.qname = \"\";\n  expected = \"\";\n  CHECK(question.Subdomains() == expected);\n};\n\nTEST_CASE(\"Test PTR records\", \"[dns]\")\n{\n  llarp::huint128_t expected =\n      llarp::net::ExpandV4(llarp::ipaddr_ipv4_bits(10, 10, 10, 1));\n  auto ip = llarp::dns::DecodePTR(\"1.10.10.10.in-addr.arpa.\");\n  CHECK(ip);\n  CHECK(*ip == expected);\n\n  expected.h.upper = 0x0123456789abcdefUL;\n  expected.h.lower = 0xeeee888812341234UL;\n  ip = llarp::dns::DecodePTR(\"4.3.2.1.4.3.2.1.8.8.8.8.e.e.e.e.f.e.d.c.b.a.9.8.7.6.5.4.3.2.1.0.ip6.arpa.\");\n  CHECK(ip);\n  CHECK(oxenc::to_hex(std::string_view{reinterpret_cast<char*>(&expected.h), 16}) ==\n          oxenc::to_hex(std::string_view{reinterpret_cast<char*>(&ip->h), 16}));\n  CHECK(*ip == expected);\n}\n\nTEST_CASE(\"Test Serialize Header\", \"[dns]\")\n{\n  std::array<byte_t, 1500> data{};\n  llarp_buffer_t buf(data);\n  llarp::dns::MessageHeader hdr, other;\n  hdr.id       = 0x1234;\n  hdr.fields   = (1 << 15);\n  hdr.qd_count = 1;\n  hdr.an_count = 1;\n  hdr.ns_count = 0;\n  hdr.ar_count = 0;\n  \n  CHECK(hdr.Encode(&buf));\n  CHECK((buf.cur - buf.base) == llarp::dns::MessageHeader::Size);\n\n  // rewind\n  buf.cur = buf.base;\n\n  CHECK(other.Decode(&buf));\n  CHECK(hdr == other);\n  CHECK(other.id == 0x1234);\n  CHECK(other.fields == (1 << 15));\n}\n\nTEST_CASE(\"Test Serialize Name\" , \"[dns]\")\n{\n  const std::string name     = \"whatever.tld\";\n  const std::string expected = \"whatever.tld.\";\n  std::array<byte_t, 1500> data{};\n  llarp_buffer_t buf(data);\n  \n  CHECK(llarp::dns::EncodeNameTo(&buf, name));\n\n  buf.cur = buf.base;\n  \n  CHECK(buf.base[0] == 8);\n  CHECK(buf.base[1] == 'w');\n  CHECK(buf.base[2] == 'h');\n  CHECK(buf.base[3] == 'a');\n  CHECK(buf.base[4] == 't');\n  CHECK(buf.base[5] == 'e');\n  CHECK(buf.base[6] == 'v');\n  CHECK(buf.base[7] == 'e');\n  CHECK(buf.base[8] == 'r');\n  CHECK(buf.base[9] == 3);\n  CHECK(buf.base[10] == 't');\n  CHECK(buf.base[11] == 'l');\n  CHECK(buf.base[12] == 'd');\n  CHECK(buf.base[13] == 0);\n  auto other = llarp::dns::DecodeName(&buf);\n  CHECK(other);\n  CHECK(expected == *other);\n}\n\nTEST_CASE(\"Test serialize question\", \"[dns]\")\n{\n  const std::string name          = \"whatever.tld\";\n  const std::string expected_name = name + \".\";\n  llarp::dns::Question q, other;\n\n  std::array<byte_t, 1500> data{};\n  llarp_buffer_t buf(data);\n  \n  q.qname  = name;\n  q.qclass = 1;\n  q.qtype  = 1;\n  CHECK(q.Encode(&buf));\n\n  buf.cur = buf.base;\n  \n  CHECK(other.Decode(&buf));\n  CHECK(other.qname == expected_name);\n  CHECK(q.qclass == other.qclass);\n  CHECK(q.qtype == other.qtype);\n}\n\nTEST_CASE(\"Test Encode/Decode RData\" , \"[dns]\")\n{\n  std::array<byte_t, 1500> data{};\n  llarp_buffer_t buf(data);\n\n  static constexpr size_t rdatasize = 32;\n  llarp::dns::RR_RData_t rdata(rdatasize);\n  std::fill(rdata.begin(), rdata.end(), 'a');\n  llarp::dns::RR_RData_t other_rdata;\n\n  CHECK(llarp::dns::EncodeRData(&buf, rdata));\n  CHECK(buf.cur - buf.base == rdatasize + sizeof(uint16_t));\n\n  buf.cur = buf.base;\n  \n  CHECK(llarp::dns::DecodeRData(&buf, other_rdata));\n  CHECK(rdata == other_rdata);\n}\n\nTEST_CASE(\"Test reserved names\", \"[dns]\")\n{\n    using namespace llarp::dns;\n    CHECK(NameIsReserved(\"loki.loki\"));\n    CHECK(NameIsReserved(\"loki.loki.\"));\n    CHECK(NameIsReserved(\"snode.loki\"));\n    CHECK(NameIsReserved(\"snode.loki.\"));\n    CHECK(NameIsReserved(\"foo.loki.loki\"));\n    CHECK(NameIsReserved(\"foo.loki.loki.\"));\n    CHECK(NameIsReserved(\"bar.snode.loki\"));\n    CHECK(NameIsReserved(\"bar.snode.loki.\"));\n    CHECK_FALSE(NameIsReserved(\"barsnode.loki.\"));\n    CHECK_FALSE(NameIsReserved(\"barsnode.loki\"));\n    CHECK_FALSE(NameIsReserved(\"alltheloki.loki\"));\n    CHECK_FALSE(NameIsReserved(\"alltheloki.loki.\"));\n}\n"
  },
  {
    "path": "test/hive/conftest.py",
    "content": "#!/usr/bin/env python3\nimport hive\nimport pytest\n\n@pytest.fixture(scope=\"session\")\ndef HiveTenRTenC():\n  router_hive = hive.RouterHive(n_relays=10, n_clients=10, netid=\"hive\")\n  router_hive.Start()\n\n  yield router_hive\n\n  router_hive.Stop()\n\n@pytest.fixture(scope=\"session\")\ndef HiveThirtyRTenC():\n  router_hive = hive.RouterHive(n_relays=30, n_clients=10, netid=\"hive\")\n  router_hive.Start()\n\n  yield router_hive\n\n  router_hive.Stop()\n\n@pytest.fixture()\ndef HiveArbitrary():\n  router_hive = None\n  def _make(n_relays=10, n_clients=10, netid=\"hive\"):\n    nonlocal router_hive\n    router_hive = hive.RouterHive(n_relays=30, n_clients=10, netid=\"hive\")\n    router_hive.Start()\n    return router_hive\n\n  yield _make\n  if router_hive:\n    router_hive.Stop()\n\n@pytest.fixture()\ndef HiveForPeerStats():\n  router_hive = None\n  def _make(n_relays, n_clients, netid):\n    nonlocal router_hive\n    router_hive = hive.RouterHive(n_relays, n_clients, netid)\n    router_hive.Start()\n    return router_hive\n\n  yield _make\n  if router_hive:\n    router_hive.Stop()\n"
  },
  {
    "path": "test/hive/hive.py",
    "content": "#!/usr/bin/env python3\nimport pyllarp\nfrom time import sleep\nfrom signal import signal, SIGINT\nfrom shutil import rmtree\nfrom os import makedirs\nfrom socket import AF_INET, htons, inet_aton\nfrom pprint import pprint\nimport sys\nfrom argparse import ArgumentParser as ap\nimport threading\nfrom collections import deque\nimport traceback\n\nclass RouterHive(object):\n\n  def __init__(self, n_relays=10, n_clients=10, netid=\"hive\", shutup=True):\n    self._log = pyllarp.LogContext()\n    self._log.shutup = shutup\n    try:\n\n      self.endpointName = \"pyllarp\"\n      self.tmpdir = \"/tmp/lokinet_hive\"\n      self.netid = netid\n\n      self.n_relays = n_relays\n      self.n_clients = n_clients\n\n      self.addrs = []\n      self.events = deque()\n\n      self.hive = None\n      self.RCs = []\n\n      pyllarp.EnableDebug()\n      if not self.RemoveTmpDir():\n        raise RuntimeError(\"Failed to initialize Router Hive\")\n\n    except Exception as error:\n      print(\"Exception in __init__: \", error);\n\n  def RemoveTmpDir(self):\n    if self.tmpdir.startswith(\"/tmp/\") and len(self.tmpdir) > 5:\n      print(\"calling rmdir -r %s\" % self.tmpdir)\n      rmtree(self.tmpdir, ignore_errors=True)\n      return True\n    else:\n      print(\"not removing dir %s because it doesn't start with /tmp/\" % self.tmpdir)\n\n    return False\n    \n  def AddRelay(self, index):\n    dirname = \"%s/relays/%d\" % (self.tmpdir, index)\n    makedirs(\"%s/nodedb\" % dirname, exist_ok=True)\n\n    config = pyllarp.Config(dirname)\n    config.Load(None, True)\n\n    port = index + 30000\n    tunname = \"lokihive%d\" % index\n\n    config.router.dataDir = dirname\n    config.router.netid = self.netid\n    config.router.nickname = \"Router%d\" % index\n    config.router.overrideAddress('127.0.0.1:{}'.format(port))\n    config.router.blockBogons = False\n\n    config.network.enableProfiling = False\n    config.network.endpointType = 'null'\n\n    config.links.addInboundLink(\"lo\", AF_INET, port)\n    config.links.setOutboundLink(\"lo\", AF_INET, 0)\n\n    # config.dns.options = {\"local-dns\": (\"127.3.2.1:%d\" % port)}\n    if index == 0:\n      config.bootstrap.seednode = True\n    else:\n      config.bootstrap.routers = [\"%s/relays/0/self.signed\" % self.tmpdir]\n\n    config.api.enableRPCServer = False\n\n    config.lokid.whitelistRouters = False\n    print(\"adding relay at index %d\" % index)\n    self.hive.AddRelay(config)\n\n  def AddClient(self, index):\n    dirname = \"%s/clients/%d\" % (self.tmpdir, index)\n    makedirs(\"%s/nodedb\" % dirname, exist_ok=True)\n\n    config = pyllarp.Config(dirname)\n    config.Load(None, False);\n\n    tunname = \"lokihive%d\" % index\n\n    config.paths.netmask = 0\n    config.router.dataDir = dirname\n    config.router.netid = self.netid\n    config.router.blockBogons = False\n\n    config.network.enableProfiling = False\n    config.network.endpointType = 'null'\n\n    config.links.setOutboundLink(\"lo\", AF_INET, 0);\n\n    # config.dns.options = {\"local-dns\": (\"127.3.2.1:%d\" % port)}\n\n    config.bootstrap.routers = [\"%s/relays/0/self.signed\" % self.tmpdir]\n\n    config.api.enableRPCServer = False\n\n    config.lokid.whitelistRouters = False\n\n    self.hive.AddClient(config)\n\n  def InitFirstRC(self):\n    print(\"Starting first router to init its RC for bootstrap\")\n    self.hive = pyllarp.RouterHive()\n    self.AddRelay(0)\n    self.hive.StartRelays()\n    print(\"sleeping 2 sec to give plenty of time to save bootstrap rc\")\n    sleep(2)\n\n    self.hive.StopAll()\n\n  def Start(self):\n\n    self.InitFirstRC()\n\n    print(\"Resetting hive.  Creating %d relays and %d clients\" % (self.n_relays, self.n_clients))\n\n    self.hive = pyllarp.RouterHive()\n\n    for i in range(0, self.n_relays):\n      self.AddRelay(i)\n\n    for i in range(0, self.n_clients):\n      self.AddClient(i)\n\n    print(\"Starting relays\")\n    self.hive.StartRelays()\n\n    print(\"Sleeping 2 seconds before starting clients\")\n    sleep(2)\n\n    self.RCs = self.hive.GetRelayRCs()\n\n    self.hive.StartClients()\n\n  def Stop(self):\n    self.hive.StopAll()\n\n  def CollectNextEvent(self):\n    self.events.append(self.hive.GetNextEvent())\n\n  def CollectAllEvents(self):\n    self.events.extend(self.hive.GetAllEvents())\n\n  def PopEvent(self):\n    self.CollectAllEvents()\n    if len(self.events):\n      return self.events.popleft()\n    return None\n\n  def DistanceSortedRCs(self, dht_key):\n    rcs = []\n    distances = []\n    for rc in self.RCs:\n      distances.append(rc.AsDHTKey ^ dht_key)\n      rcs.append(rc)\n\n    distances, rcs = (list(t) for t in zip(*sorted(zip(distances, rcs))))\n    return rcs\n\n\ndef main(n_relays=10, n_clients=10, print_each_event=True, verbose=False):\n\n  running = True\n\n  def handle_sigint(sig, frame):\n    nonlocal running\n    running = False\n    print(\"SIGINT received, attempting to stop all routers\")\n\n  signal(SIGINT, handle_sigint)\n\n  try:\n    hive = RouterHive(n_relays, n_clients, shutup=not verbose)\n    hive.Start()\n\n  except Exception as err:\n    print(err)\n    return 1\n\n  first_dht_pub = False\n  dht_pub_sorted = None\n  dht_pub_location = None\n  total_events = 0\n  event_counts = dict()\n  while running:\n    hive.CollectAllEvents()\n    print(\"Hive collected {} events this second.\".format(len(hive.events)))\n    for event in hive.events:\n      event_name = event.__class__.__name__\n      if event:\n        if print_each_event:\n          print(\"Event: %s -- Triggered: %s\" % (event_name, event.triggered))\n          print(event)\n          hops = getattr(event, \"hops\", None)\n          if hops:\n            for hop in hops:\n              print(hop)\n\n        total_events = total_events + 1\n        if event_name in event_counts:\n          event_counts[event_name] = event_counts[event_name] + 1\n        else:\n          event_counts[event_name] = 1\n\n        if total_events % 10 == 0:\n          pprint(event_counts)\n\n        if event_name == \"DhtPubIntroReceivedEvent\":\n          if not first_dht_pub:\n            dht_pub_sorted = hive.DistanceSortedRCs(event.location)\n            dht_pub_location = event.location\n            print(\"location: {} landed at: {}, sorted distance list:\".format(dht_pub_location.ShortString(), event.routerID.ShortString()))\n            print([x.routerID.ShortString() for x in dht_pub_sorted[:4]])\n            first_dht_pub = True\n          else:\n            if event.location == dht_pub_location:\n              print(\"location: {}, landed at: {}\".format(dht_pub_location.ShortString(), event.routerID.ShortString()))\n\n    # won't have printed event count above in this case.\n    if len(hive.events) == 0:\n      pprint(event_counts)\n\n    hive.events = []\n    for _ in range(100):\n      sleep(1.0 / 100)\n\n  print('stopping')\n  hive.Stop()\n  print('stopped')\n  del hive\n\nif __name__ == '__main__':\n  parser = ap()\n  print_events = False\n  relay_count = 10\n  client_count = 10\n  parser.add_argument('--print-events', dest=\"print_events\", action='store_true')\n  parser.add_argument('--relay-count', dest=\"relay_count\", type=int, default=10)\n  parser.add_argument('--client-count', dest=\"client_count\", type=int, default=10)\n  parser.add_argument('--verbose', action='store_true', dest='verbose')\n  args = parser.parse_args()\n  main(n_relays=args.relay_count, n_clients=args.client_count, print_each_event = args.print_events, verbose=args.verbose)\n"
  },
  {
    "path": "test/hive/test_path_builds.py",
    "content": "from time import time\n\ndef test_path_builds(HiveArbitrary):\n  h = HiveArbitrary(n_relays=30, n_clients=10)\n\n  start_time = time()\n  cur_time = start_time\n  test_duration = 5 #seconds\n\n  log_attempts = True\n\n  paths = []\n\n  while cur_time < start_time + test_duration:\n\n    h.CollectAllEvents()\n\n    for event in h.events:\n      event_name = event.__class__.__name__\n\n      if log_attempts and event_name == \"PathAttemptEvent\":\n        path = dict()\n        path[\"hops\"] = event.hops\n        path[\"received\"] = [False] * len(event.hops)\n        path[\"prev\"] = [None] * len(event.hops)\n        for i in range(1, len(event.hops)):\n          path[\"prev\"][i] = event.hops[i-1].rc.routerID\n        path[\"prev\"][0] = event.routerID\n        path[\"rxid\"] = event.hops[0].rxid\n        path[\"status\"] = None\n        paths.append(path)\n\n      elif event_name == \"PathRequestReceivedEvent\":\n        for path in paths:\n          for i in range(len(path[\"hops\"])):\n            assert type(path[\"hops\"][i].upstreamRouter) == type(event.nextHop)\n            assert type(path[\"prev\"][i]) == type(event.prevHop)\n            assert type(path[\"hops\"][i].txid) == type(event.txid)\n            assert type(path[\"hops\"][i].rxid) == type(event.rxid)\n            if (path[\"hops\"][i].upstreamRouter == event.nextHop and\n                path[\"prev\"][i] == event.prevHop and\n                path[\"hops\"][i].txid == event.txid and\n                path[\"hops\"][i].rxid == event.rxid):\n              path[\"received\"][i] = True\n\n      elif event_name == \"PathStatusReceivedEvent\":\n        for path in paths:\n          if event.rxid == path[\"rxid\"]:\n            path[\"status\"] = event\n\n    h.events = []\n    cur_time = time()\n\n    # only collect path attempts for 3 seconds\n    if cur_time > start_time + 3:\n      log_attempts = False\n\n  assert len(paths) > 0\n\n  fail_status_count = 0\n  missing_status_count = 0\n  missing_rcv_count = 0\n  expected_count = 0\n\n  for path in paths:\n    if path[\"status\"]:\n      if not path[\"status\"].Successful:\n        print(path[\"status\"])\n        fail_status_count = fail_status_count + 1\n    else:\n      missing_status_count = missing_status_count + 1\n\n    for rcv in path[\"received\"]:\n      expected_count = expected_count + 1\n      if not rcv:\n        missing_rcv_count = missing_rcv_count + 1\n\n\n  print(\"Path count: {}, Expected rcv: {}, missing rcv: {}, fail_status_count: {}, missing_status_count: {}\".format(len(paths), expected_count, missing_rcv_count, fail_status_count, missing_status_count))\n\n  assert fail_status_count == 0\n  assert missing_rcv_count == 0\n  assert missing_status_count == 0\n\n"
  },
  {
    "path": "test/hive/test_peer_stats.py",
    "content": "import pyllarp\nfrom time import time\n\ndef test_peer_stats(HiveForPeerStats):\n  return\n  numRelays = 12\n\n  hive = HiveForPeerStats(n_relays=numRelays, n_clients=0, netid=\"hive\")\n  someRouterId = None\n\n  def collectStatsForAWhile(duration):\n    print(\"collecting router hive stats for {} seconds...\", duration)\n\n    start_time = time()\n    cur_time = start_time\n\n    # we track the number of attempted sessions and inbound/outbound established sessions\n    numInbound = 0\n    numOutbound = 0\n    numAttempts = 0\n\n    nonlocal someRouterId\n\n    while cur_time < start_time + duration:\n      hive.CollectAllEvents()\n\n      for event in hive.events:\n        event_name = event.__class__.__name__\n\n        if event_name == \"LinkSessionEstablishedEvent\":\n          if event.inbound:\n            numInbound += 1\n          else:\n            numOutbound += 1\n\n        if event_name == \"ConnectionAttemptEvent\":\n          numAttempts += 1\n\n          # we pick an arbitrary router out of our routers\n          if someRouterId is None:\n            someRouterId = event.remoteId;\n\n      hive.events = []\n      cur_time = time()\n\n    # these should be strictly equal, although there is variation because of\n    # the time we sample\n    print(\"test duration exceeded\")\n    print(\"in: {} out: {} attempts: {}\", numInbound, numOutbound, numAttempts);\n    totalReceived = tally_rc_received_for_peer(hive.hive, someRouterId)\n\n    # every router should have received this relay's RC exactly once\n    print(\"total times RC received: {} numRelays: {}\", totalReceived, numRelays)\n\n    return {\n      \"numInbound\": numInbound,\n      \"numOutbound\": numOutbound,\n      \"numAttempts\": numAttempts,\n      \"totalTargetRCsReceived\": totalReceived,\n    };\n\n  results1 = collectStatsForAWhile(30);\n  assert(results1[\"totalTargetRCsReceived\"] == numRelays)\n\n  # stop our router from gossiping\n  router = hive.hive.GetRelay(someRouterId, True)\n  router.disableGossiping();\n\n  ignore = collectStatsForAWhile(30);\n\n  # ensure that no one hears a fresh RC from this router again\n  print(\"Starting second (longer) stats collection...\")\n  results2 = collectStatsForAWhile(3600);\n  assert(results2[\"totalTargetRCsReceived\"] == numRelays) # should not have increased\n\ndef tally_rc_received_for_peer(hive, routerId):\n\n  numFound = 0\n\n  def visit(context):\n    nonlocal numFound\n\n    peerDb = context.getRouterAsHiveRouter().peerDb()\n    stats = peerDb.getCurrentPeerStats(routerId);\n\n    assert(stats.routerId == routerId)\n\n    numFound += stats.numDistinctRCsReceived\n\n  hive.ForEachRelay(visit)\n\n  return numFound;\n\n\nif __name__ == \"__main__\":\n  main()\n"
  },
  {
    "path": "test/mocks/mock_context.hpp",
    "content": "#pragma once\n#include <llarp.hpp>\n\n#include \"mock_network.hpp\"\n#include \"mock_router.hpp\"\n#include \"mock_vpn.hpp\"\n\nnamespace mocks\n{\n  class MockContext : public llarp::Context\n  {\n    const Network& _net;\n\n   public:\n    MockContext(const Network& net) : llarp::Context{}, _net{net}\n    {\n      loop = std::shared_ptr<llarp::EventLoop>{const_cast<Network*>(&_net), [](Network*) {}};\n    }\n\n    std::shared_ptr<llarp::Router>\n    makeRouter(const std::shared_ptr<llarp::EventLoop>&) override\n    {\n      return std::static_pointer_cast<llarp::Router>(\n          std::make_shared<MockRouter>(_net, makeVPNPlatform()));\n    }\n\n    std::shared_ptr<llarp::vpn::Platform>\n    makeVPNPlatform() override\n    {\n      return std::static_pointer_cast<llarp::vpn::Platform>(std::make_shared<MockVPN>(_net));\n    }\n\n    std::shared_ptr<llarp::NodeDB>\n    makeNodeDB() override\n    {\n      return std::make_shared<llarp::NodeDB>();\n    }\n  };\n\n}  // namespace mocks\n"
  },
  {
    "path": "test/mocks/mock_network.hpp",
    "content": "#pragma once\n\n#include <unordered_map>\n#include <llarp/net/net.hpp>\n#include <llarp/ev/libuv.hpp>\n#include <oxenc/variant.h>\n\nnamespace mocks\n{\n  class Network;\n\n  class MockUDPHandle : public llarp::UDPHandle\n  {\n    Network* const _net;\n    std::optional<llarp::SockAddr> _addr;\n\n   public:\n      MockUDPHandle(Network* net, llarp::UDPHandle::ReceiveFunc recv)\n        : llarp::UDPHandle{recv}, _net{net}\n    {}\n\n    std::optional<llarp::SockAddr>\n    LocalAddr() const override\n    {\n      return _addr;\n    }\n\n    bool\n    listen(const llarp::SockAddr& addr) override;\n\n    bool\n    send(const llarp::SockAddr&, const llarp_buffer_t&) override\n    {\n      return true;\n    };\n\n    void\n    close() override{};\n  };\n\n  class Network : public llarp::net::Platform, public llarp::uv::Loop\n  {\n    std::unordered_multimap<std::string, llarp::IPRange> _network_interfaces;\n    bool _snode;\n\n    const Platform* const m_Default{Platform::Default_ptr()};\n\n   public:\n    Network(\n        std::unordered_multimap<std::string, llarp::IPRange> network_interfaces, bool snode = true)\n        : llarp::net::Platform{}\n        , llarp::uv::Loop{1024}\n        , _network_interfaces{std::move(network_interfaces)}\n        , _snode{snode}\n    {}\n\n      const llarp::net::Platform*\n    Net_ptr() const override\n    {\n      return this;\n    }\n\n    void\n    run() override\n    {\n      m_EventLoopThreadID = std::this_thread::get_id();\n      m_Impl->run<uvw::Loop::Mode::ONCE>();\n      m_Impl->close();\n      // reset the event loop for reuse\n      m_Impl = uvw::Loop::create();\n    };\n\n    llarp::RuntimeOptions\n    Opts() const\n    {\n      return llarp::RuntimeOptions{false, false, _snode};\n    }\n\n    std::shared_ptr<llarp::UDPHandle>\n    make_udp(UDPReceiveFunc recv) override\n    {\n      return std::make_shared<MockUDPHandle>(this, recv);\n    }\n\n    std::optional<std::string>\n    GetBestNetIF(int af) const override\n    {\n      for (const auto& [k, range] : _network_interfaces)\n        if (range.Family() == af and not IsBogonRange(range))\n          return k;\n      return std::nullopt;\n    }\n\n    std::optional<std::string>\n    FindFreeTun() const override\n    {\n      return \"mocktun0\";\n    }\n\n    std::optional<llarp::SockAddr>\n    GetInterfaceAddr(std::string_view ifname, int af) const override\n    {\n      for (const auto& [name, range] : _network_interfaces)\n        if (range.Family() == af and name == ifname)\n          return llarp::SockAddr{range.addr};\n      return std::nullopt;\n    }\n\n    bool\n    HasInterfaceAddress(llarp::net::ipaddr_t ip) const override\n    {\n      for (const auto& item : _network_interfaces)\n        if (var::visit([range = item.second](auto&& ip) { return range.Contains(ToHost(ip)); }, ip))\n          return true;\n      // check for wildcard\n      return IsWildcardAddress(ip);\n    }\n\n    std::optional<llarp::SockAddr>\n    AllInterfaces(llarp::SockAddr fallback) const override\n    {\n      return m_Default->AllInterfaces(fallback);\n    }\n\n    std::optional<int>\n    GetInterfaceIndex(llarp::net::ipaddr_t ip) const override\n    {\n      return m_Default->GetInterfaceIndex(ip);\n    }\n\n    std::optional<llarp::IPRange>\n    FindFreeRange() const override\n    {\n      auto ownsRange = [this](const auto& range) {\n        for (const auto& [name, ownRange] : _network_interfaces)\n        {\n          if (ownRange * range)\n            return true;\n        }\n        return false;\n      };\n      using namespace llarp;\n      // generate possible ranges to in order of attempts\n      std::list<IPRange> possibleRanges;\n      for (byte_t oct = 16; oct < 32; ++oct)\n      {\n        possibleRanges.emplace_back(IPRange::FromIPv4(172, oct, 0, 1, 16));\n      }\n      for (byte_t oct = 0; oct < 255; ++oct)\n      {\n        possibleRanges.emplace_back(IPRange::FromIPv4(10, oct, 0, 1, 16));\n      }\n      for (byte_t oct = 0; oct < 255; ++oct)\n      {\n        possibleRanges.emplace_back(IPRange::FromIPv4(192, 168, oct, 1, 24));\n      }\n      // for each possible range pick the first one we don't own\n      for (const auto& range : possibleRanges)\n      {\n        if (not ownsRange(range))\n          return range;\n      }\n      return std::nullopt;\n    }\n\n    std::string\n    LoopbackInterfaceName() const override\n    {\n      for (const auto& [name, range] : _network_interfaces)\n        if (IsLoopbackAddress(ToNet(range.addr)))\n          return name;\n      throw std::runtime_error{\"no loopback interface?\"};\n    }\n\n      std::vector<llarp::net::InterfaceInfo>\n      AllNetworkInterfaces() const override\n      {\n          std::map<std::string, llarp::net::InterfaceInfo> _addrs;\n          for(const auto & [ifname, range] : _network_interfaces)\n          {\n             auto & ent = _addrs[ifname];\n             ent.name = ifname;\n             ent.addrs.emplace_back(range);\n          }\n          std::vector<llarp::net::InterfaceInfo> infos;\n          for(const auto & [name, info] : _addrs)\n          {\n              infos.emplace_back(info);\n              infos.back().index = infos.size();\n          }\n          return infos;\n      }\n  };\n\n  bool\n  MockUDPHandle::listen(const llarp::SockAddr& addr)\n  {\n    if (not _net->HasInterfaceAddress(addr.getIP()))\n      return false;\n    _addr = addr;\n    return true;\n  }\n\n}  // namespace mocks\n"
  },
  {
    "path": "test/mocks/mock_router.hpp",
    "content": "#pragma once\n#include <llarp/router/router.hpp>\n\n#include \"mock_network.hpp\"\n\nnamespace mocks\n{\n  class MockRouter : public llarp::Router\n  {\n    const Network& _net;\n\n   public:\n    explicit MockRouter(const Network& net, std::shared_ptr<llarp::vpn::Platform> vpnPlatform)\n        : llarp::\n            Router{std::shared_ptr<llarp::EventLoop>{const_cast<Network*>(&net), [](Network*) {}}, vpnPlatform}\n        , _net{net}\n    {}\n\n    const llarp::net::Platform&\n    Net() const override\n    {\n      return _net;\n    };\n  };\n}  // namespace mocks\n"
  },
  {
    "path": "test/mocks/mock_vpn.hpp",
    "content": "#pragma once\n#include <llarp/vpn/platform.hpp>\n#include \"mock_network.hpp\"\n\nnamespace mocks\n{\n  class MockInterface : public llarp::vpn::NetworkInterface\n  {\n    int _pipes[2];\n\n   public:\n      MockInterface(llarp::vpn::InterfaceInfo info) : llarp::vpn::NetworkInterface{std::move(info)}\n    {\n      if (pipe(_pipes))\n        throw std::runtime_error{strerror(errno)};\n    }\n\n    virtual ~MockInterface()\n    {\n      close(_pipes[1]);\n    }\n\n    int\n    PollFD() const override\n    {\n      return _pipes[0];\n    };\n\n    llarp::net::IPPacket\n    ReadNextPacket() override\n    {\n      return llarp::net::IPPacket{};\n    };\n\n    bool WritePacket(llarp::net::IPPacket) override\n    {\n      return true;\n    }\n  };\n\n  class MockVPN : public llarp::vpn::Platform, public llarp::vpn::IRouteManager\n  {\n    const Network& _net;\n\n   public:\n    MockVPN(const Network& net) : llarp::vpn::Platform{}, llarp::vpn::IRouteManager{}, _net{net}\n    {}\n\n    virtual std::shared_ptr<llarp::vpn::NetworkInterface>\n    obtain_interface(llarp::vpn::InterfaceInfo info, llarp::Router*) override\n    {\n      return std::make_shared<MockInterface>(info);\n    };\n\n    const llarp::net::Platform*\n    Net_ptr() const override\n    {\n      return &_net;\n    };\n\n    void AddRoute(llarp::net::ipaddr_t, llarp::net::ipaddr_t) override{};\n\n    void DelRoute(llarp::net::ipaddr_t, llarp::net::ipaddr_t) override{};\n\n    void AddDefaultRouteViaInterface(llarp::vpn::NetworkInterface&) override{};\n\n    void DelDefaultRouteViaInterface(llarp::vpn::NetworkInterface&) override{};\n\n    void\n    AddRouteViaInterface(llarp::vpn::NetworkInterface&, llarp::IPRange) override{};\n\n    void\n    DelRouteViaInterface(llarp::vpn::NetworkInterface&, llarp::IPRange) override{};\n\n    std::vector<llarp::net::ipaddr_t> GetGatewaysNotOnInterface(llarp::vpn::NetworkInterface&) override\n    {\n      return std::vector<llarp::net::ipaddr_t>{};\n    };\n\n    /// get owned ip route manager for managing routing table\n    virtual llarp::vpn::IRouteManager&\n    RouteManager() override\n    {\n      return *this;\n    };\n  };\n}  // namespace mocks\n"
  },
  {
    "path": "test/net/test_ip_address.cpp",
    "content": "#include <llarp/net/ip_address.hpp>\n\n#include <catch2/catch.hpp>\n\nTEST_CASE(\"IpAddress empty constructor\", \"[IpAdress]\")\n{\n  llarp::IpAddress address;\n  CHECK(address.isEmpty() == true);\n}\n"
  },
  {
    "path": "test/net/test_llarp_net.cpp",
    "content": "#include <llarp/net/net_int.hpp>\n#include <llarp/net/ip.hpp>\n#include <llarp/net/ip_range.hpp>\n#include <llarp/net/net.hpp>\n#include <oxenc/hex.h>\n\n#include <catch2/catch.hpp>\n\nnamespace\n{\n    template<typename T>\n    bool IsBogon(T ip)\n    {\n       return llarp::net::Platform::Default_ptr()->IsBogon(ip);\n    }\n}\n\n\nTEST_CASE(\"In6Addr\")\n{\n  llarp::huint128_t ip;\n\n  SECTION(\"From string\")\n  {\n    REQUIRE(ip.FromString(\"fc00::1\"));\n  }\n\n  SECTION(\"From string fail\")\n  {\n    REQUIRE_FALSE(ip.FromString(\"10.1.1.1\"));\n  }\n}\n\nTEST_CASE(\"In6AddrToHUIntLoopback\")\n{\n  llarp::huint128_t loopback = {0};\n  REQUIRE(loopback.FromString(\"::1\"));\n  in6_addr addr = IN6ADDR_LOOPBACK_INIT;\n  auto huint = llarp::net::In6ToHUInt(addr);\n  REQUIRE(huint == loopback);\n}\n\nTEST_CASE(\"In6AddrToHUInt\")\n{\n  llarp::huint128_t huint_parsed = {0};\n  REQUIRE(huint_parsed.FromString(\"fd00::1\"));\n  in6_addr addr = {{{0xfd, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01}}};\n  auto huint = llarp::net::In6ToHUInt(addr);\n  REQUIRE(huint == huint_parsed);\n  huint_parsed.h++;\n  REQUIRE(huint != huint_parsed);\n}\n\nTEST_CASE(\"Range\")\n{\n  SECTION(\"Contains 8\")\n  {\n    REQUIRE(\n        llarp::IPRange::FromIPv4(10, 0, 0, 1, 8).Contains(llarp::ipaddr_ipv4_bits(10, 40, 11, 6)));\n  }\n\n  SECTION(\"Contains 24\")\n  {\n    REQUIRE(llarp::IPRange::FromIPv4(10, 200, 0, 1, 24)\n                .Contains(llarp::ipaddr_ipv4_bits(10, 200, 0, 253)));\n  }\n\n  SECTION(\"Contains fail\")\n  {\n    REQUIRE(!llarp::IPRange::FromIPv4(192, 168, 0, 1, 24)\n                 .Contains(llarp::ipaddr_ipv4_bits(10, 200, 0, 253)));\n  }\n  SECTION(\"Intersecting networks\")\n  {\n      const auto range_16 = llarp::IPRange::FromIPv4(10,9,0,1, 16);\n      const auto range_24a = llarp::IPRange::FromIPv4(10,9,0,1, 24);\n      const auto range_24b = llarp::IPRange::FromIPv4(10,9,1,1, 24);\n      const auto range_unrelated = llarp::IPRange::FromIPv4(1,9,1,1, 8);\n      REQUIRE(range_16 * range_24a);\n      REQUIRE(range_16 * range_24b);\n      REQUIRE(not(range_24a * range_24b));\n      REQUIRE(not(range_16 * range_unrelated));\n  }\n}\n\nTEST_CASE(\"IPv4 netmask\")\n{\n  REQUIRE(llarp::netmask_ipv4_bits(8) == llarp::huint32_t{0xFF000000});\n  REQUIRE(llarp::netmask_ipv4_bits(24) == llarp::huint32_t{0xFFFFFF00});\n}\n\nTEST_CASE(\"Bogon\")\n{\n  SECTION(\"Bogon_10_8\")\n  {\n    REQUIRE(IsBogon(llarp::ipaddr_ipv4_bits(10, 40, 11, 6)));\n  }\n\n  SECTION(\"Bogon_192_168_16\")\n  {\n    REQUIRE(IsBogon(llarp::ipaddr_ipv4_bits(192, 168, 1, 111)));\n  }\n\n  SECTION(\"Bogon_127_8\")\n  {\n    REQUIRE(IsBogon(llarp::ipaddr_ipv4_bits(127, 0, 0, 1)));\n  }\n\n  SECTION(\"Bogon_0_8\")\n  {\n    REQUIRE(IsBogon(llarp::ipaddr_ipv4_bits(0, 0, 0, 0)));\n  }\n\n  SECTION(\"Non-bogon\")\n  {\n    REQUIRE_FALSE(IsBogon(llarp::ipaddr_ipv4_bits(1, 1, 1, 1)));\n    REQUIRE_FALSE(IsBogon(llarp::ipaddr_ipv4_bits(8, 8, 6, 6)));\n    REQUIRE_FALSE(IsBogon(llarp::ipaddr_ipv4_bits(141, 55, 12, 99)));\n    REQUIRE_FALSE(IsBogon(llarp::ipaddr_ipv4_bits(79, 12, 3, 4)));\n  }\n}\n\nTEST_CASE(\"uint128_t\")\n{\n    SECTION(\"layout\")\n    {\n        llarp::uint128_t i{0x0011223f44556677ULL, 0x8899aabbc3ddeeffULL};\n        REQUIRE(oxenc::to_hex(std::string_view{reinterpret_cast<const char*>(&i), sizeof(i)}) ==\n#ifdef __BIG_ENDIAN__\n                \"0011223f445566778899aabbc3ddeeff\"\n#else\n                \"ffeeddc3bbaa9988776655443f221100\"\n#endif\n               );\n    }\n    SECTION(\"ntoh\")\n    {\n        llarp::uint128_t i{0x0011223f44556677ULL, 0x8899aabbc3ddeeffULL};\n        auto be = ntoh128(i);\n        REQUIRE(be == llarp::uint128_t{0xffeeddc3bbaa9988ULL, 0x776655443f221100ULL});\n    }\n    SECTION(\"hton\")\n    {\n        llarp::uint128_t i{0x0011223f44556677ULL, 0x8899aabbc3ddeeffULL};\n        auto be = ntoh128(i);\n        REQUIRE(be == llarp::uint128_t{0xffeeddc3bbaa9988ULL, 0x776655443f221100ULL});\n    }\n}\n"
  },
  {
    "path": "test/net/test_sock_addr.cpp",
    "content": "#include <llarp/util/mem.hpp>\n#include <llarp/net/sock_addr.hpp>\n#include <llarp/net/net_if.hpp>\n#include <llarp/util/logging.hpp>\n\n#include <catch2/catch.hpp>\n\nTEST_CASE(\"SockAddr from IPv4\", \"[SockAddr]\")\n{\n  llarp::SockAddr addr(1, 2, 3, 4);\n  CHECK(addr.ToString() == \"1.2.3.4:0\");\n}\n\nTEST_CASE(\"SockAddr test port\", \"[SockAddr]\")\n{\n  llarp::SockAddr addr;\n  addr.setPort(42);\n  CHECK(addr.getPort() == 42);\n}\n\nTEST_CASE(\"SockAddr fromString\", \"[SockAddr]\")\n{\n  llarp::SockAddr addr;\n  CHECK_NOTHROW(addr.fromString(\"1.2.3.4\"));\n  CHECK(addr.ToString() == \"1.2.3.4:0\");\n\n  CHECK(llarp::SockAddr(\"1.3.5.7\").ToString() == \"1.3.5.7:0\");\n\n  CHECK(llarp::SockAddr(\"0.0.0.0\").ToString() == \"0.0.0.0:0\");\n  CHECK(llarp::SockAddr(\"0.0.0.0:0\").ToString() == \"0.0.0.0:0\");\n  CHECK(llarp::SockAddr(\"255.255.255.255\").ToString() == \"255.255.255.255:0\");\n  CHECK(llarp::SockAddr(\"255.255.255.255:255\").ToString() == \"255.255.255.255:255\");\n  CHECK(llarp::SockAddr(\"255.255.255.255:65535\").ToString() == \"255.255.255.255:65535\");\n  CHECK(llarp::SockAddr(\"5.6.7.8\", llarp::huint16_t{5678}).ToString() == \"5.6.7.8:5678\");\n\n  CHECK_THROWS_WITH(llarp::SockAddr(\"abcd\"), \"abcd is not a valid IPv4 address\");\n\n  CHECK_THROWS_WITH(llarp::SockAddr(\"0.0.0.0:foo\"), \"foo is not a valid port\");\n\n  CHECK_THROWS_WITH(llarp::SockAddr(\"256.257.258.259\"), \"256.257.258.259 contains invalid numeric value\");\n\n  CHECK_THROWS_WITH(llarp::SockAddr(\"-1.-2.-3.-4\"), \"-1.-2.-3.-4 contains invalid numeric value\");\n\n  CHECK_THROWS_WITH(llarp::SockAddr(\"1.2.3\"), \"1.2.3 is not a valid IPv4 address\");\n\n  CHECK_THROWS_WITH(llarp::SockAddr(\"1.2.3.\"), \"1.2.3. contains invalid numeric value\");\n\n  CHECK_THROWS_WITH(llarp::SockAddr(\".1.2.3\"), \".1.2.3 contains invalid numeric value\");\n\n  CHECK_THROWS_WITH(llarp::SockAddr(\"1.2.3.4.5\"), \"1.2.3.4.5 is not a valid IPv4 address\");\n\n  CHECK_THROWS_WITH(llarp::SockAddr(\"1.2.3. \"), \"1.2.3.  contains invalid numeric value\");\n\n  CHECK_THROWS_WITH(llarp::SockAddr(\"1a.2b.3c.4z\"), \"1a.2b.3c.4z contains invalid numeric value\");\n\n  // TODO: there's no reason this couldn't be supported\n  CHECK_THROWS_WITH(\n      llarp::SockAddr(\"0xFF.0xFF.0xFF.0xFF\"), \"0xFF.0xFF.0xFF.0xFF contains invalid numeric value\");\n\n  // This *is* supported now; it gives you an empty address (same as default constructed).\n  //CHECK_THROWS_WITH(llarp::SockAddr(\"\"), \"cannot construct IPv4 from empty string\");\n\n  CHECK_THROWS_WITH(llarp::SockAddr(\" \"), \"  is not a valid IPv4 address\");\n\n  CHECK_THROWS_WITH(llarp::SockAddr(\"1.2.3.4:65536\"), \"65536 is not a valid port\");\n\n  CHECK_THROWS_WITH(llarp::SockAddr(\"1.2.3.4:1a\"), \"1a is not a valid port\");\n\n  CHECK_THROWS_WITH(llarp::SockAddr(\"5.6.7.8:1234\", llarp::huint16_t{5678}), \"invalid ip address (port not allowed here): 5.6.7.8:1234\");\n}\n\nTEST_CASE(\"SockAddr from sockaddr_in\", \"[SockAddr]\")\n{\n  sockaddr_in sin4;\n  llarp::Zero(&sin4, sizeof(sockaddr_in));\n  sin4.sin_family = AF_INET;\n  sin4.sin_addr.s_addr = inet_addr(\"127.0.0.1\");\n  sin4.sin_port = htons(1234);\n\n  llarp::SockAddr addr(sin4);\n\n  CHECK(addr.ToString() == \"127.0.0.1:1234\");\n}\n\nTEST_CASE(\"SockAddr from sockaddr_in6\", \"[SockAddr]\")\n{\n  sockaddr_in6 sin6;\n  llarp::Zero(&sin6, sizeof(sockaddr_in6));\n  sin6.sin6_family = AF_INET6;\n  inet_pton(AF_INET6, \"::ffff:127.0.0.1\", &sin6.sin6_addr);\n\n  sin6.sin6_port = htons(53);\n\n  llarp::SockAddr addr(sin6);\n\n  CHECK(addr.ToString() == \"127.0.0.1:53\");\n}\n"
  },
  {
    "path": "test/nodedb/test_nodedb.cpp",
    "content": "#include <catch2/catch.hpp>\n\n#include <llarp/config/config.hpp>\n#include <llarp/relay_contact.hpp>\n#include <llarp/nodedb.hpp>\n\nusing llarp_nodedb = llarp::NodeDB;\n\nTEST_CASE(\"FindClosestTo returns correct number of elements\", \"[nodedb][dht]\")\n{\n  llarp_nodedb nodeDB{fs::current_path(), nullptr};\n\n  constexpr uint64_t numRCs = 3;\n  for (uint64_t i = 0; i < numRCs; ++i)\n  {\n    llarp::RelayContact rc;\n    rc.pubkey[0] = i;\n    nodeDB.Put(rc);\n  }\n\n  REQUIRE(numRCs == nodeDB.NumLoaded());\n\n  llarp::dht::Key_t key;\n\n  std::vector<llarp::RelayContact> results = nodeDB.FindManyClosestTo(key, 4);\n\n  // we asked for more entries than nodedb had\n  REQUIRE(numRCs == results.size());\n}\n\nTEST_CASE(\"FindClosestTo returns properly ordered set\", \"[nodedb][dht]\")\n{\n  llarp_nodedb nodeDB{fs::current_path(), nullptr};\n\n  // insert some RCs: a < b < c\n  llarp::RelayContact a;\n  a.pubkey[0] = 1;\n  nodeDB.Put(a);\n\n  llarp::RelayContact b;\n  b.pubkey[0] = 2;\n  nodeDB.Put(b);\n\n  llarp::RelayContact c;\n  c.pubkey[0] = 3;\n  nodeDB.Put(c);\n\n  REQUIRE(3 == nodeDB.NumLoaded());\n\n  llarp::dht::Key_t key;\n\n  std::vector<llarp::RelayContact> results = nodeDB.FindManyClosestTo(key, 2);\n  REQUIRE(2 == results.size());\n\n  // we xor'ed with 0x0, so order should be a,b,c\n  REQUIRE(a.pubkey == results[0].pubkey);\n  REQUIRE(b.pubkey == results[1].pubkey);\n\n  llarp::dht::Key_t compKey;\n  compKey.Fill(0xFF);\n\n  results = nodeDB.FindManyClosestTo(compKey, 2);\n\n  // we xor'ed with 0xF...F, so order should be inverted (c,b,a)\n  REQUIRE(c.pubkey == results[0].pubkey);\n  REQUIRE(b.pubkey == results[1].pubkey);\n}\n"
  },
  {
    "path": "test/path/test_path.cpp",
    "content": "#include <llarp/path/path.hpp>\n#include <catch2/catch.hpp>\n\nusing Path_t   = llarp::path::Path;\nusing Path_ptr = llarp::path::Path_ptr;\nusing Set_t    = llarp::path::Path::UniqueEndpointSet_t;\nusing RC_t     = llarp::RelayContact;\n\nstatic RC_t\nMakeHop(const char name)\n{\n  RC_t rc;\n  rc.pubkey.Fill(name);\n  return rc;\n}\n\nstatic Path_ptr\nMakePath(std::vector< char > hops)\n{\n  std::vector< RC_t > pathHops;\n  for(const auto& hop : hops)\n    pathHops.push_back(MakeHop(hop));\n  return std::make_shared< Path_t >(pathHops, std::weak_ptr<llarp::path::PathSet>{}, 0, \"test\");\n}\n\nTEST_CASE(\"UniqueEndpointSet_t has unique endpoints\", \"[path]\")\n{\n  Set_t set;\n  REQUIRE(set.empty());\n  const auto inserted_first =\n      set.emplace(MakePath({'a', 'b', 'c', 'd'})).second;\n  REQUIRE(inserted_first);\n  const auto inserted_again =\n      set.emplace(MakePath({'a', 'b', 'c', 'd'})).second;\n  REQUIRE(not inserted_again);\n  const auto inserted_second =\n      set.emplace(MakePath({'d', 'c', 'b', 'a'})).second;\n  REQUIRE(inserted_second);\n}\n"
  },
  {
    "path": "test/peerstats/test_peer_db.cpp",
    "content": "#include <llarp/peerstats/peer_db.hpp>\n#include <test_util.hpp>\n\n#include <numeric>\n#include <catch2/catch.hpp>\n#include <llarp/peerstats/types.hpp>\n#include <llarp/relay_contact.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/util/time.hpp>\n\nTEST_CASE(\"Test PeerDb PeerStats memory storage\", \"[PeerDb]\")\n{\n  const llarp::RouterID id = llarp::test::makeBuf<llarp::RouterID>(0x01);\n  const llarp::PeerStats empty(id);\n\n  llarp::PeerDb db;\n  CHECK(db.getCurrentPeerStats(id).has_value() == false);\n\n  llarp::PeerStats delta(id);\n  delta.numConnectionAttempts = 4;\n  delta.peakBandwidthBytesPerSec = 5;\n  db.accumulatePeerStats(id, delta);\n  CHECK(* db.getCurrentPeerStats(id) == delta);\n\n  delta = llarp::PeerStats(id);\n  delta.numConnectionAttempts = 5;\n  delta.peakBandwidthBytesPerSec = 6;\n  db.accumulatePeerStats(id, delta);\n\n  llarp::PeerStats expected(id);\n  expected.numConnectionAttempts = 9;\n  expected.peakBandwidthBytesPerSec = 6;\n  CHECK(* db.getCurrentPeerStats(id) == expected);\n}\n\nTEST_CASE(\"Test PeerDb flush before load\", \"[PeerDb]\")\n{\n  llarp::PeerDb db;\n  CHECK_THROWS_WITH(db.flushDatabase(), \"Cannot flush database before it has been loaded\");\n}\n\nTEST_CASE(\"Test PeerDb load twice\", \"[PeerDb]\")\n{\n  llarp::PeerDb db;\n  CHECK_NOTHROW(db.loadDatabase(std::nullopt));\n  CHECK_THROWS_WITH(db.loadDatabase(std::nullopt), \"Reloading database not supported\");\n}\n\nTEST_CASE(\"Test PeerDb nukes stats on load\", \"[PeerDb]\")\n{\n  const llarp::RouterID id = llarp::test::makeBuf<llarp::RouterID>(0x01);\n\n  llarp::PeerDb db;\n\n  llarp::PeerStats stats(id);\n  stats.numConnectionAttempts = 1;\n\n  db.accumulatePeerStats(id, stats);\n  CHECK(* db.getCurrentPeerStats(id) == stats);\n\n  db.loadDatabase(std::nullopt);\n\n  CHECK(db.getCurrentPeerStats(id).has_value() == false);\n}\n\nTEST_CASE(\"Test PeerDb file-backed database reloads properly\", \"[PeerDb]\")\n{\n  const std::string filename = \"/tmp/peerdb_test_tmp2.db.sqlite\";\n  const llarp::RouterID id = llarp::test::makeBuf<llarp::RouterID>(0x02);\n\n  {\n    llarp::PeerDb db;\n    db.loadDatabase(filename);\n\n    llarp::PeerStats stats(id);\n    stats.numConnectionAttempts = 43;\n\n    db.accumulatePeerStats(id, stats);\n\n    db.flushDatabase();\n  }\n\n  {\n    llarp::PeerDb db;\n    db.loadDatabase(filename);\n\n    auto stats = db.getCurrentPeerStats(id);\n    CHECK(stats.has_value() == true);\n    CHECK(stats->numConnectionAttempts == 43);\n  }\n\n  fs::remove(filename);\n}\n\nTEST_CASE(\"Test PeerDb modifyPeerStats\", \"[PeerDb]\")\n{\n  const llarp::RouterID id = llarp::test::makeBuf<llarp::RouterID>(0xF2);\n\n  int numTimesCalled = 0;\n\n  llarp::PeerDb db;\n  db.loadDatabase(std::nullopt);\n\n  db.modifyPeerStats(id, [&](llarp::PeerStats& stats) {\n    numTimesCalled++;\n\n    stats.numPathBuilds += 42;\n  });\n\n  db.flushDatabase();\n\n  CHECK(numTimesCalled == 1);\n\n  auto stats = db.getCurrentPeerStats(id);\n  CHECK(stats.has_value());\n  CHECK(stats->numPathBuilds == 42);\n}\n\nTEST_CASE(\"Test PeerDb handleGossipedRC\", \"[PeerDb]\")\n{\n  const llarp::RouterID id = llarp::test::makeBuf<llarp::RouterID>(0xCA);\n\n  auto rcLifetime = llarp::RelayContact::Lifetime;\n  llarp_time_t now = 0s;\n\n  llarp::RelayContact rc;\n  rc.pubkey = llarp::PubKey(id);\n  rc.last_updated = 10s;\n\n  llarp::PeerDb db;\n  db.handleGossipedRC(rc, now);\n\n  auto stats = db.getCurrentPeerStats(id);\n  CHECK(stats.has_value());\n  CHECK(stats->leastRCRemainingLifetime == 0ms);  // not calculated on first received RC\n  CHECK(stats->numDistinctRCsReceived == 1);\n  CHECK(stats->lastRCUpdated == 10000ms);\n\n  now = 9s;\n  db.handleGossipedRC(rc, now);\n  stats = db.getCurrentPeerStats(id);\n  CHECK(stats.has_value());\n  // these values should remain unchanged, this is not a new RC\n  CHECK(stats->leastRCRemainingLifetime == 0ms);\n  CHECK(stats->numDistinctRCsReceived == 1);\n  CHECK(stats->lastRCUpdated == 10000ms);\n\n  rc.last_updated = 11s;\n\n  db.handleGossipedRC(rc, now);\n  stats = db.getCurrentPeerStats(id);\n  // should be (previous expiration time - new received time)\n  CHECK(stats->leastRCRemainingLifetime == ((10s + rcLifetime) - now));\n  CHECK(stats->numDistinctRCsReceived == 2);\n  CHECK(stats->lastRCUpdated == 11000ms);\n}\n\nTEST_CASE(\"Test PeerDb handleGossipedRC expiry calcs\", \"[PeerDb]\")\n{\n  const llarp::RouterID id = llarp::test::makeBuf<llarp::RouterID>(0xF9);\n\n  // see comments in peer_db.cpp above PeerDb::handleGossipedRC() for some context around these\n  // tests and esp. these numbers\n  const llarp_time_t ref = 48h;\n  const llarp_time_t rcLifetime = llarp::RelayContact::Lifetime;\n\n  // rc1, first rc received\n  const llarp_time_t s1 = ref;\n  const llarp_time_t r1 = s1 + 30s;\n  const llarp_time_t e1 = s1 + rcLifetime;\n  llarp::RelayContact rc1;\n  rc1.pubkey = llarp::PubKey(id);\n  rc1.last_updated = s1;\n\n  // rc2, second rc received\n  // received \"healthily\", with lots of room to spare before rc1 expires\n  const llarp_time_t s2 = s1 + 8h;\n  const llarp_time_t r2 = s2 + 30s;  // healthy recv time\n  const llarp_time_t e2 = s2 + rcLifetime;\n  llarp::RelayContact rc2;\n  rc2.pubkey = llarp::PubKey(id);\n  rc2.last_updated = s2;\n\n  // rc3, third rc received\n  // received \"unhealthily\" (after rc2 expires)\n  const llarp_time_t s3 = s2 + 8h;\n  const llarp_time_t r3 = e2 + 1h;  // received after e2\n  llarp::RelayContact rc3;\n  rc3.pubkey = llarp::PubKey(id);\n  rc3.last_updated = s3;\n\n  llarp::PeerDb db;\n\n  db.handleGossipedRC(rc1, r1);\n  auto stats1 = db.getCurrentPeerStats(id);\n  CHECK(stats1.has_value());\n  CHECK(stats1->leastRCRemainingLifetime == 0ms);\n  CHECK(stats1->numDistinctRCsReceived == 1);\n  CHECK(stats1->lastRCUpdated == s1);\n\n  db.handleGossipedRC(rc2, r2);\n  auto stats2 = db.getCurrentPeerStats(id);\n  CHECK(stats2.has_value());\n  CHECK(stats2->leastRCRemainingLifetime == (e1 - r2));\n  CHECK(stats2->leastRCRemainingLifetime > 0ms);  // ensure positive indicates healthy\n  CHECK(stats2->numDistinctRCsReceived == 2);\n  CHECK(stats2->lastRCUpdated == s2);\n\n  db.handleGossipedRC(rc3, r3);\n  auto stats3 = db.getCurrentPeerStats(id);\n  CHECK(stats3.has_value());\n  CHECK(stats3->leastRCRemainingLifetime == (e2 - r3));\n  CHECK(\n      stats3->leastRCRemainingLifetime\n      < 0ms);  // ensure negative indicates unhealthy and we use min()\n  CHECK(stats3->numDistinctRCsReceived == 3);\n  CHECK(stats3->lastRCUpdated == s3);\n}\n"
  },
  {
    "path": "test/peerstats/test_peer_types.cpp",
    "content": "#include <numeric>\n#include <llarp/peerstats/types.hpp>\n#include <test_util.hpp>\n\n#include <catch2/catch.hpp>\n\nTEST_CASE(\"Test PeerStats operator+=\", \"[PeerStats]\")\n{\n  llarp::RouterID id = {};\n\n  // TODO: test all members\n  llarp::PeerStats stats(id);\n  stats.numConnectionAttempts = 1;\n  stats.peakBandwidthBytesPerSec = 12;\n\n  llarp::PeerStats delta(id);\n  delta.numConnectionAttempts = 2;\n  delta.peakBandwidthBytesPerSec = 4;\n\n  stats += delta;\n\n  CHECK(stats.numConnectionAttempts == 3);\n  CHECK(stats.peakBandwidthBytesPerSec == 12);  // should take max(), not add\n}\n\nTEST_CASE(\"Test PeerStats BEncode\", \"[PeerStats]\")\n{\n  llarp::RouterID id = llarp::test::makeBuf<llarp::RouterID>(0x01);\n\n  llarp::PeerStats stats(id);\n\n  stats.numConnectionAttempts = 1;\n  stats.numConnectionSuccesses = 2;\n  stats.numConnectionRejections = 3;\n  stats.numConnectionTimeouts = 4;\n  stats.numPathBuilds = 5;\n  stats.numPacketsAttempted = 6;\n  stats.numPacketsSent = 7;\n  stats.numPacketsDropped = 8;\n  stats.numPacketsResent = 9;\n  stats.numDistinctRCsReceived = 10;\n  stats.numLateRCs = 11;\n  stats.peakBandwidthBytesPerSec = 12.1;  // should truncate to 12\n  stats.longestRCReceiveInterval = 13ms;\n  stats.leastRCRemainingLifetime = 14ms;\n  stats.lastRCUpdated = 15ms;\n\n  constexpr size_t bufSize = 4096;\n  std::array<byte_t, bufSize> raw{};\n  llarp_buffer_t buf(raw);\n\n  CHECK_NOTHROW(stats.BEncode(&buf));\n\n  std::string asString = (const char*)raw.data();\n  constexpr std::string_view expected =\n      \"d\"\n      \"13:lastRCUpdated\"\n      \"i15e\"\n      \"24:leastRCRemainingLifetime\"\n      \"i14e\"\n      \"24:longestRCReceiveInterval\"\n      \"i13e\"\n      \"21:numConnectionAttempts\"\n      \"i1e\"\n      \"23:numConnectionRejections\"\n      \"i3e\"\n      \"22:numConnectionSuccesses\"\n      \"i2e\"\n      \"21:numConnectionTimeouts\"\n      \"i4e\"\n      \"22:numDistinctRCsReceived\"\n      \"i10e\"\n      \"10:numLateRCs\"\n      \"i11e\"\n      \"19:numPacketsAttempted\"\n      \"i6e\"\n      \"17:numPacketsDropped\"\n      \"i8e\"\n      \"16:numPacketsResent\"\n      \"i9e\"\n      \"14:numPacketsSent\"\n      \"i7e\"\n      \"13:numPathBuilds\"\n      \"i5e\"\n      \"24:peakBandwidthBytesPerSec\"\n      \"i12e\"\n      \"e\";\n\n  CHECK(asString == expected);\n}\n"
  },
  {
    "path": "test/readme.md",
    "content": "unit tests and such\n\nto enable unit tests, add cmake flag `-DLOKINET_TESTS=ON`\n\nunit tests can be built and run with the `check` target.\n"
  },
  {
    "path": "test/router/test_llarp_router_version.cpp",
    "content": "#include <llarp/router_version.hpp>\n#include <llarp/router/router.hpp>\n\n#include <catch2/catch.hpp>\n\nusing Catch::Matchers::Equals;\n\nTEST_CASE(\"Compatibility when protocol equal\", \"[RouterVersion]\")\n{\n  llarp::RouterVersion v1({0, 1, 2}, 1);\n  llarp::RouterVersion v2({0, 1, 2}, 1);\n\n  CHECK(v1.IsCompatableWith(v2));\n}\n\nTEST_CASE(\"Compatibility when protocol unequal\", \"[RouterVersion]\")\n{\n  llarp::RouterVersion older({0, 1, 2}, 1);\n  llarp::RouterVersion newer({0, 1, 2}, 2);\n\n  CHECK_FALSE(older.IsCompatableWith(newer));\n  CHECK_FALSE(newer.IsCompatableWith(older));\n}\n\nTEST_CASE(\"Empty compatibility\", \"[RouterVersion]\")\n{\n  llarp::RouterVersion v1({0, 0, 1}, llarp::constants::proto_version);\n\n  CHECK_FALSE(v1.IsCompatableWith(llarp::emptyRouterVersion));\n}\n\nTEST_CASE(\"IsEmpty\", \"[RouterVersion]\")\n{\n  llarp::RouterVersion notEmpty({0, 0, 1}, llarp::constants::proto_version);\n  CHECK_FALSE(notEmpty.IsEmpty());\n\n  CHECK(llarp::emptyRouterVersion.IsEmpty());\n}\n\nTEST_CASE(\"Clear\", \"[RouterVersion]\")\n{\n  llarp::RouterVersion version({0, 0, 1}, llarp::constants::proto_version);\n  CHECK_FALSE(version.IsEmpty());\n\n  version.Clear();\n\n  CHECK(version.IsEmpty());\n}\n\nTEST_CASE(\"BEncode\", \"[RouterVersion]\")\n{\n  llarp::RouterVersion v1235({1, 2, 3}, 5);\n\n  std::array<byte_t, 128> tmp{};\n  llarp_buffer_t buf(tmp);\n\n  CHECK(v1235.BEncode(&buf));\n\n  CHECK_THAT((const char*)buf.begin(), Equals(\"li5ei1ei2ei3ee\"));\n}\n\nTEST_CASE(\"BDecode\", \"[RouterVersion]\")\n{\n  llarp::RouterVersion version;\n  version.Clear();\n\n  const std::string bString(\"li9ei3ei2ei1ee\");\n  llarp_buffer_t buf(bString.data(), bString.size());\n  CHECK(version.BDecode(&buf));\n\n  llarp::RouterVersion expected({3, 2, 1}, 9);\n\n  CHECK(expected == version);\n}\n\nTEST_CASE(\"Decode long version array\", \"[RouterVersion]\")\n{\n  llarp::RouterVersion version;\n  version.Clear();\n\n  const std::string bString(\"li9ei3ei2ei1ei2ei3ei4ei5ei6ei7ei8ei9ee\");\n  llarp_buffer_t buf(bString.data(), bString.size());\n  CHECK_FALSE(version.BDecode(&buf));\n}\n"
  },
  {
    "path": "test/routing/test_llarp_routing_obtainexitmessage.cpp",
    "content": "#include <llarp/exit/exit_messages.hpp>\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/crypto/crypto_libsodium.hpp>\n\n#include <catch2/catch.hpp>\n\nusing namespace ::llarp;\nusing namespace ::llarp::test;\n\nusing ObtainExitMessage = routing::ObtainExitMessage;\n\nvoid\nfill(Signature& s)\n{\n  s.Fill(0xFF);\n}\n\nTEST_CASE(\"Sign-verify\")\n{\n  SecretKey alice{};\n  crypto::identity_keygen(alice);\n  REQUIRE(not alice.IsZero());\n  ObtainExitMessage msg{};\n  msg.S = randint();\n  msg.T = randint();\n  CHECK(msg.Sign(alice));\n  CHECK(msg.Verify());\n  CHECK(msg.I == PubKey{seckey_topublic(alice)});\n  CHECK(msg.version == llarp::constants::proto_version);\n  CHECK_FALSE(msg.Z.IsZero());\n}\n"
  },
  {
    "path": "test/routing/test_llarp_routing_transfer_traffic.cpp",
    "content": "#include <llarp/routing/transfer_traffic_message.hpp>\n\n#include <catch2/catch.hpp>\n\nusing TransferTrafficMessage = llarp::routing::TransferTrafficMessage;\n\nTEST_CASE(\"TransferTrafficMessage\", \"[TransferTrafficMessage]\")\n{\n  TransferTrafficMessage msg;\n\n  SECTION(\"Put buffer overflow\")\n  {\n    std::array<byte_t, llarp::routing::MaxExitMTU* 2> tmp = {{0}};\n    llarp_buffer_t buf(tmp);\n    REQUIRE_FALSE(msg.PutBuffer(buf, 1));\n  }\n\n  SECTION(\"Put buffer\")\n  {\n    std::array<byte_t, llarp::routing::MaxExitMTU> tmp = {{0}};\n    llarp_buffer_t buf(tmp);\n    REQUIRE(msg.PutBuffer(buf, 1));\n  }\n}\n"
  },
  {
    "path": "test/service/test_llarp_service_address.cpp",
    "content": "#include <llarp/service/address.hpp>\n\n#include <catch2/catch.hpp>\n\nTEST_CASE(\"Address\", \"[Address]\")\n{\n  const std::string snode = \"8zfiwpgonsu5zpddpxwdurxyb19x6r96xy4qbikff99jwsziws9y.snode\";\n  const std::string loki = \"7okic5x5do3uh3usttnqz9ek3uuoemdrwzto1hciwim9f947or6y.loki\";\n  const std::string sub = \"lokinet.test\";\n  const std::string invalid = \"7okic5x5do3uh3usttnqz9ek3uuoemdrwzto1hciwim9f947or6y.net\";\n  llarp::service::Address addr;\n\n  SECTION(\"Parse bad TLD\")\n  {\n    REQUIRE_FALSE(addr.FromString(snode, \".net\"));\n    REQUIRE_FALSE(addr.FromString(invalid, \".net\"));\n  }\n\n  SECTION(\"Parse bad TLD appened on end\")\n  {\n    const std::string bad = loki + \".net\";\n    REQUIRE_FALSE(addr.FromString(bad, \".net\"));\n  }\n\n  SECTION(\"Parse bad TLD appened on end with subdomain\")\n  {\n    const std::string bad = sub + \".\" + loki + \".net\";\n    REQUIRE_FALSE(addr.FromString(bad, \".net\"));\n  }\n\n  SECTION(\"Parse SNode not Loki\")\n  {\n    REQUIRE(addr.FromString(snode, \".snode\"));\n    REQUIRE_FALSE(addr.FromString(snode, \".loki\"));\n  }\n\n  SECTION(\"Parse Loki not SNode\")\n  {\n    REQUIRE_FALSE(addr.FromString(loki, \".snode\"));\n    REQUIRE(addr.FromString(loki, \".loki\"));\n  }\n\n  SECTION(\"Parse Loki with subdomain\")\n  {\n    const std::string addr_str = sub + \".\" + loki;\n    REQUIRE(addr.FromString(addr_str, \".loki\"));\n    REQUIRE(addr.subdomain == sub);\n    REQUIRE(addr.ToString() == addr_str);\n  };\n\n  SECTION(\"Parse SNode with subdomain\")\n  {\n    const std::string addr_str = sub + \".\" + snode;\n    REQUIRE(addr.FromString(addr_str, \".snode\"));\n    REQUIRE(addr.subdomain == sub);\n    REQUIRE(addr.ToString(\".snode\") == addr_str);\n  }\n}\n"
  },
  {
    "path": "test/service/test_llarp_service_identity.cpp",
    "content": "#include <llarp/crypto/crypto.hpp>\n#include <sodium/crypto_scalarmult_ed25519.h>\n#include <llarp/path/path.hpp>\n#include <llarp/service/intro_set.hpp>\n#include <llarp/util/time.hpp>\n\n#include <catch2/catch.hpp>\n\nusing namespace llarp;\n\nTEST_CASE(\"test service address from string\")\n{\n  \n  service::Identity ident{};\n  \n  auto str = ident.pub.Addr().ToString();\n  service::Address addr;\n  CHECK(addr.FromString(str));\n  CHECK(addr == ident.pub.Addr());\n}\n\nTEST_CASE(\"test service::Identity throws on error\")\n{\n  fs::path p = test::randFilename();\n  CHECK(not fs::exists(fs::status(p)));\n\n  test::FileGuard guard(p);\n  std::error_code code;\n\n  std::fstream file;\n  file.open(p.string(), std::ios::out);\n  CHECK(file.is_open());\n  file << p;\n  file.close();\n\n  service::Identity identity;\n  REQUIRE_THROWS(identity.EnsureKeys(p, false));\n}\n\n\nTEST_CASE(\"test subkey derivation\", \"[crypto]\")\n{\n  // These values came out of a run of Tor's test code, so that we can confirm we are doing the same\n  // blinding subkey crypto math as Tor.  Our hash value is generated differently so we use the hash\n  // from a Tor random test suite run.\n\n  AlignedBuffer<32> seed{{\n    0xd0, 0x98, 0x9d, 0x83, 0x0e, 0x03, 0xe1, 0x4e, 0xf6, 0xaf, 0x71, 0xa0, 0xa1, 0xfc, 0x88, 0x38, 0xac, 0xfc, 0xd8, 0x95, 0x06, 0x54, 0x9f, 0x3e, 0xdb, 0xb0, 0xf5, 0x3a, 0xc9, 0x0e, 0x47, 0x90,\n  }};\n  AlignedBuffer<64> root_key_data{{\n    0xc0, 0xe6, 0x58, 0xd6, 0x01, 0xc1, 0xb4, 0xc2, 0x94, 0xb8, 0xf7, 0xa3, 0xec, 0x3e, 0x81, 0xd6, 0x82, 0xb4, 0x89, 0x5c, 0x6d, 0xbf, 0x5c, 0x6e, 0x20, 0xad, 0x39, 0x8f, 0xf4, 0x8f, 0x43, 0x4f,\n    0x56, 0x4f, 0xdc, 0x22, 0x33, 0x19, 0xb9, 0xbb, 0x4e, 0xc0, 0xba, 0x84, 0x2d, 0xe3, 0xde, 0xf2, 0x26, 0xe8, 0xf7, 0xa8, 0x8f, 0x82, 0x41, 0xe3, 0x1f, 0x5d, 0xe5, 0x56, 0x3a, 0xf4, 0x5e, 0x3c,\n  }};\n\n  AlignedBuffer<32> root_pub_data{{\n    0x4a, 0x34, 0x3f, 0x9e, 0xf3, 0xda, 0x3d, 0x80, 0x07, 0xc7, 0x09, 0xf9, 0x2f, 0x72, 0xd3, 0x76, 0x56, 0x5a, 0x4c, 0x13, 0xdf, 0xb8, 0xce, 0xc8, 0x53, 0x77, 0x0a, 0x99, 0xbc, 0x06, 0xa7, 0xc0,\n  }};\n  AlignedBuffer<32> hash{{\n    0x64, 0xad, 0xde, 0x17, 0x69, 0x33, 0x92, 0x25, 0x9c, 0xa3, 0xd7, 0x85, 0xa5, 0x2d, 0x3a, 0xa5, 0xa3, 0x9c, 0xdb, 0x99, 0x57, 0xac, 0x54, 0x14, 0x4f, 0x11, 0xa9, 0x90, 0xa0, 0xca, 0xcb, 0xfe,\n  }};\n  AlignedBuffer<64> derived_key_data{{\n    0x96, 0x02, 0xba, 0x16, 0x87, 0x40, 0xb7, 0xb6, 0xc9, 0x0f, 0x85, 0x7b, 0xdc, 0xa9, 0x13, 0x9d, 0x1b, 0xf5, 0x01, 0x54, 0xd1, 0xd1, 0x8f, 0x75, 0x06, 0x4d, 0x4c, 0xea, 0x33, 0xc4, 0xc6, 0x00,\n    0xb0, 0xef, 0x29, 0x37, 0x7c, 0xe9, 0x84, 0x43, 0x5a, 0x79, 0xa2, 0x3b, 0xef, 0xcd, 0x1c, 0x43, 0xf1, 0x88, 0xff, 0x50, 0xaf, 0x9c, 0x07, 0x6a, 0xc6, 0x19, 0xfb, 0xcc, 0x5d, 0x48, 0x75, 0x92,\n  }};\n  AlignedBuffer<32> derived_pub_data{{\n    0x13, 0xa6, 0x61, 0x5b, 0x78, 0x64, 0x03, 0xd4, 0x8a, 0x88, 0xaa, 0x0d, 0x89, 0xdf, 0x08, 0x46, 0xb3, 0x2f, 0xa9, 0xbb, 0xa8, 0xcc, 0xe1, 0xac, 0x4c, 0xae, 0xc9, 0xd2, 0xf1, 0x35, 0xd1, 0x33,\n  }};\n\n  SecretKey root{seed};\n  CHECK(root.toPublic().as_array() == root_pub_data.as_array());\n\n  PrivateKey root_key;\n  CHECK(root.toPrivate(root_key));\n  CHECK(root_key.as_array() == root_key_data.as_array());\n\n  PrivateKey aprime; // a'\n  CHECK(crypto::derive_subkey_private(aprime, root, 0, &hash));\n\n  // We use a different signing hash than Tor\n  // only the private key value (the first 32 bytes) will match:\n  CHECK(std::memcmp(aprime.data(), derived_key_data.data(), 32) == 0);\n\n  PubKey Aprime; // A'\n  CHECK(crypto::derive_subkey(Aprime, root.toPublic(), 0, &hash));\n  CHECK(Aprime.as_array() == derived_pub_data.as_array());\n}\n\nTEST_CASE(\"test root key signing\" , \"[crypto]\")\n{\n  SecretKey root_key;\n  crypto::identity_keygen(root_key);\n\n  // We have our own reimplementation of sodium's signing function which can work with derived\n  // private keys (unlike sodium's built-in which requires starting from a seed).  This tests that\n  // signing using either path produces an identical signature.\n\n  const std::string nibbs = \"Nibbler\";\n  llarp_buffer_t nibbs_buf{nibbs.data(), nibbs.size()};\n\n  Signature sig_sodium;\n  CHECK(crypto::sign(sig_sodium, root_key, nibbs_buf));\n\n  PrivateKey root_privkey;\n  CHECK(root_key.toPrivate(root_privkey));\n  Signature sig_ours;\n  CHECK(crypto::sign(sig_ours, root_privkey, nibbs_buf));\n\n  CHECK(sig_sodium == sig_ours);\n}\n\nTEST_CASE(\"Test generate derived key\", \"[crypto]\")\n{\n  SecretKey root_key;\n  crypto::identity_keygen(root_key);\n\n  PrivateKey root_privkey;\n  CHECK(root_key.toPrivate(root_privkey));\n\n  PrivateKey a;\n  PubKey A;\n  CHECK(root_key.toPrivate(a));\n  CHECK(a.toPublic(A));\n  CHECK(A == root_key.toPublic());\n\n  {\n    // paranoid check to ensure this works as expected\n    PubKey aB;\n    crypto_scalarmult_ed25519_base(aB.data(), a.data());\n    CHECK(A == aB);\n  }\n\n  PrivateKey aprime; // a'\n  CHECK(crypto::derive_subkey_private(aprime, root_key, 1));\n\n  PubKey Aprime; // A'\n  CHECK(crypto::derive_subkey(Aprime, A, 1));\n\n  // We should also be able to derive A' via a':\n  PubKey Aprime_alt;\n  CHECK(aprime.toPublic(Aprime_alt));\n\n  CHECK(Aprime == Aprime_alt);\n\n  // Generate using the same constant and make sure we get an identical privkey (including the\n  // signing hash value)\n  PrivateKey aprime_repeat;\n  CHECK(crypto::derive_subkey_private(aprime_repeat, root_key, 1));\n  CHECK(aprime_repeat == aprime);\n\n  // Generate another using a different constant and make sure we get something different\n  PrivateKey a2;\n  PubKey A2;\n  CHECK(crypto::derive_subkey_private(a2, root_key, 2));\n  CHECK(crypto::derive_subkey(A2, A, 2));\n  CHECK(A2 != Aprime);\n  CHECK(a2.ToHex().substr(0, 64) != aprime.ToHex().substr(0, 64));\n  CHECK(a2.ToHex().substr(64) != aprime.ToHex().substr(64)); // The hash should be different too\n}\n\nTEST_CASE(\"Test signing with derived key\", \"[crypto]\")\n{\n  SecretKey root_key;\n  crypto::identity_keygen(root_key);\n\n  PrivateKey root_privkey;\n  root_key.toPrivate(root_privkey);\n\n  PrivateKey a;\n  PubKey A;\n  root_key.toPrivate(a);\n  a.toPublic(A);\n\n  PrivateKey aprime; // a'\n  crypto::derive_subkey_private(aprime, root_key, 1);\n\n  PubKey Aprime; // A'\n  crypto::derive_subkey(Aprime, A, 1);\n\n  const std::string s = \"Jeff loves one-letter variable names.\";\n  llarp_buffer_t buf(s.data(), s.size());\n\n  Signature sig;\n  \n  CHECK(crypto::sign(sig, aprime, buf));\n  CHECK(crypto::verify(Aprime, buf, sig));\n}\n\nTEST_CASE(\"Test sign and encrypt introset\", \"[crypto]\")\n{\n  service::Identity ident;\n  ident.RegenerateKeys();\n  service::Address addr;\n  CHECK(ident.pub.CalculateAddress(addr.as_array()));\n  service::IntroSetOld introset;\n  auto now = time_now_ms();\n  introset.timestampSignedAt = now;\n  while(introset.intros.size() < 10)\n  {\n    service::Introduction intro;\n    intro.expiresAt = now + (path::default_lifetime / 2);\n    intro.router.Randomize();\n    intro.pathID.Randomize();\n    introset.intros.emplace_back(std::move(intro));\n  }\n\n  const auto maybe = ident.EncryptAndSignIntroSet(introset, now);\n  CHECK(maybe.has_value());\n  CHECK(maybe->Verify(now));\n  PubKey blind_key;\n  const PubKey root_key(addr.as_array());\n  CHECK(crypto::derive_subkey(blind_key, root_key, 1));\n  CHECK(blind_key == maybe->derivedSigningKey);\n}\n"
  },
  {
    "path": "test/service/test_llarp_service_name.cpp",
    "content": "#include \"catch2/catch.hpp\"\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/service/name.hpp>\n#include <oxenc/hex.h>\n\nusing namespace std::literals;\n\nTEST_CASE(\"Test LNS name decrypt\", \"[lns]\")\n{\n  constexpr auto recordhex = \"0ba76cbfdb6dc8f950da57ae781912f31c8ad0c55dbf86b88cb0391f563261a9656571a817be4092969f8a78ee0fcee260424acb4a1f4bbdd27348b71de006b6152dd04ed11bf3c4\"sv;\n  const auto recordbin = oxenc::from_hex(recordhex);\n  CHECK(not recordbin.empty());\n  llarp::SymmNonce n{};\n  std::vector<byte_t> ciphertext{};\n  const auto len = recordbin.size() - n.size();\n  std::copy_n(recordbin.cbegin() + len, n.size(), n.data());\n  std::copy_n(recordbin.cbegin(), len, std::back_inserter(ciphertext));\n  const auto maybe = llarp::crypto::maybe_decrypt_name(std::string_view{reinterpret_cast<const char *>(ciphertext.data()), ciphertext.size()}, n, \"jason.loki\");\n  CHECK(maybe.has_value());\n  const llarp::service::Address addr{*maybe};\n  CHECK(addr.ToString() == \"azfoj73snr9f3neh5c6sf7rtbaeabyxhr1m4un5aydsmsrxo964o.loki\");\n}\n\n\nTEST_CASE(\"Test LNS validity\", \"[lns]\")\n{\n  CHECK(not llarp::service::NameIsValid(\"loki.loki\"));\n  CHECK(not llarp::service::NameIsValid(\"snode.loki\"));\n  CHECK(not llarp::service::NameIsValid(\"localhost.loki\"));\n  CHECK(not llarp::service::NameIsValid(\"gayballs22.loki.loki\"));\n  CHECK(not llarp::service::NameIsValid(\"-loki.loki\"));\n  CHECK(not llarp::service::NameIsValid(\"super-mario-gayballs-.loki\"));\n  CHECK(not llarp::service::NameIsValid(\"bn--lolexdeeeeee.loki\"));\n  CHECK(not llarp::service::NameIsValid(\"2222222222a-.loki\"));\n  CHECK(not llarp::service::NameIsValid(\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.loki\"));\n  \n  CHECK(llarp::service::NameIsValid(\"xn--animewasindeedamistake.loki\"));\n  CHECK(llarp::service::NameIsValid(\"memerionos.loki\"));\n  CHECK(llarp::service::NameIsValid(\"whyis.xn--animehorrible.loki\"));\n  CHECK(llarp::service::NameIsValid(\"the.goog.loki\"));\n  CHECK(llarp::service::NameIsValid(\"420.loki\"));\n}\n"
  },
  {
    "path": "test/test_llarp_encrypted_frame.cpp",
    "content": "#include \"llarp_test.hpp\"\n#include \"test_util.hpp\"\n#include <llarp/crypto/encrypted_frame.hpp>\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/messages/relay_commit.hpp>\n#include <catch2/catch.hpp>\n\nusing namespace ::llarp;\n\nusing EncryptedFrame = EncryptedFrame;\nusing SecretKey = SecretKey;\nusing PubKey = PubKey;\nusing LRCR = LR_CommitRecord;\n\nclass FrameTest\n{\n public:\n  FrameTest() : test::LlarpTest<>{}\n  {\n    crypto::encryption_keygen(alice);\n    crypto::encryption_keygen(bob);\n  }\n\n  SecretKey alice, bob;\n};\n\nTEST_CASE_METHOD(FrameTest, \"Frame crypto\")\n{\n  EncryptedFrame f{256};\n  f.Fill(0);\n  LRCR record{};\n  record.nextHop.Fill(1);\n  record.tunnelNonce.Fill(2);\n  record.rxid.Fill(3);\n  record.txid.Fill(4);\n\n  auto buf = f.Buffer();\n  buf->cur = buf->base + EncryptedFrameOverheadSize;\n\n  REQUIRE(record.BEncode(buf));\n\n  // rewind buffer\n  buf->cur = buf->base + EncryptedFrameOverheadSize;\n  // encrypt to alice\n  REQUIRE(f.EncryptInPlace(alice, bob.toPublic()));\n\n  // decrypt from alice\n  REQUIRE(f.DecryptInPlace(bob));\n\n  LRCR otherRecord;\n  REQUIRE(otherRecord.BDecode(buf));\n  REQUIRE(otherRecord == record);\n}\n"
  },
  {
    "path": "test/test_llarp_router_contact.cpp",
    "content": "#include <catch2/catch.hpp>\n\n#include <llarp/crypto/crypto.hpp>\n#include <llarp/relay_contact.hpp>\n#include <llarp/net/net_int.hpp>\n#include <llarp/util/time.hpp>\n\nnamespace llarp\n{\n\nTEST_CASE(\"RelayContact Sign and Verify\", \"[RC][RelayContact][signature][sign][verify]\")\n{\n  RelayContact rc;\n\n  SecretKey sign;\n  crypto::identity_keygen(sign);\n\n  SecretKey encr;\n  crypto::encryption_keygen(encr);\n\n  rc.enckey = encr.toPublic();\n  rc.pubkey = sign.toPublic();\n\n  REQUIRE(rc.Sign(sign));\n  REQUIRE(rc.Verify(time_now_ms()));\n}\n\nTEST_CASE(\"RelayContact Decode Version 1\", \"[RC][RelayContact][V1]\")\n{\n  RelayContact rc;\n\n  SecretKey sign;\n  crypto::identity_keygen(sign);\n\n  SecretKey encr;\n  crypto::encryption_keygen(encr);\n\n  rc.version = 1;\n\n  rc.enckey = encr.toPublic();\n  rc.pubkey = sign.toPublic();\n\n  REQUIRE(rc.Sign(sign));\n\n  std::array<byte_t, 5000> encoded_buffer;\n  llarp_buffer_t encoded_llarp(encoded_buffer);\n\n  rc.BEncode(&encoded_llarp);\n\n  encoded_llarp.sz = encoded_llarp.cur - encoded_llarp.base;\n  encoded_llarp.cur = encoded_llarp.base;\n\n  RelayContact decoded_rc;\n\n  REQUIRE(decoded_rc.BDecode(&encoded_llarp));\n\n  REQUIRE(decoded_rc.Verify(time_now_ms()));\n\n  REQUIRE(decoded_rc == rc);\n}\n\nTEST_CASE(\"RelayContact Decode Mixed Versions\", \"[RC][RelayContact]\")\n{\n  RelayContact rc1, rc2, rc3, rc4;\n\n  rc1.version = 0;\n  rc2.version = 1;\n  rc3.version = 0;\n  rc4.version = 1;\n\n  SecretKey sign1, sign2, sign3, sign4;\n  crypto::identity_keygen(sign1);\n  crypto::identity_keygen(sign2);\n  crypto::identity_keygen(sign3);\n  crypto::identity_keygen(sign4);\n\n  SecretKey encr1, encr2, encr3, encr4;\n  crypto::encryption_keygen(encr1);\n  crypto::encryption_keygen(encr2);\n  crypto::encryption_keygen(encr3);\n  crypto::encryption_keygen(encr4);\n\n  rc1.enckey = encr1.toPublic();\n  rc2.enckey = encr2.toPublic();\n  rc3.enckey = encr3.toPublic();\n  rc4.enckey = encr4.toPublic();\n  rc1.pubkey = sign1.toPublic();\n  rc2.pubkey = sign2.toPublic();\n  rc3.pubkey = sign3.toPublic();\n  rc4.pubkey = sign4.toPublic();\n\n  REQUIRE(rc1.Sign(sign1));\n  REQUIRE(rc2.Sign(sign2));\n  REQUIRE(rc3.Sign(sign3));\n  REQUIRE(rc4.Sign(sign4));\n\n  std::vector<RelayContact> rc_vec;\n  rc_vec.push_back(rc1);\n  rc_vec.push_back(rc2);\n  rc_vec.push_back(rc3);\n  rc_vec.push_back(rc4);\n\n  std::array<byte_t, 20000> encoded_buffer;\n  llarp_buffer_t encoded_llarp(encoded_buffer);\n\n  BEncodeWriteList(rc_vec.begin(), rc_vec.end(), &encoded_llarp);\n  encoded_llarp.sz = encoded_llarp.cur - encoded_llarp.base;\n  encoded_llarp.cur = encoded_llarp.base;\n\n  std::vector<RelayContact> rc_vec_out;\n\n  BEncodeReadList(rc_vec_out, &encoded_llarp);\n\n  REQUIRE(rc_vec.size() == rc_vec_out.size());\n  for (size_t i=0; i<4; i++)\n    REQUIRE(rc_vec[i] == rc_vec_out[i]);\n}\n\n} // namespace llarp\n"
  },
  {
    "path": "test/test_util.cpp",
    "content": "#include \"test_util.hpp\"\n\n#include <random>\n\nnamespace llarp\n{\n  namespace test\n  {\n    std::string\n    randFilename()\n    {\n      static const char alphabet[] = \"abcdefghijklmnopqrstuvwxyz\";\n\n      std::random_device rd;\n      std::uniform_int_distribution< size_t > dist{0, sizeof(alphabet) - 2};\n\n      std::string filename;\n      for(size_t i = 0; i < 5; ++i)\n      {\n        filename.push_back(alphabet[dist(rd)]);\n      }\n\n      filename.push_back('.');\n\n      for(size_t i = 0; i < 5; ++i)\n      {\n        filename.push_back(alphabet[dist(rd)]);\n      }\n\n      return filename;\n    }\n  }  // namespace test\n}  // namespace llarp\n"
  },
  {
    "path": "test/test_util.hpp",
    "content": "#ifndef TEST_UTIL_HPP\n#define TEST_UTIL_HPP\n\n#include <llarp/util/fs.hpp>\n#include <llarp/util/types.hpp>\n#include <bitset>\n#include <vector>\n\nnamespace llarp\n{\n  namespace test\n  {\n    std::string\n    randFilename();\n\n    template < typename Buf >\n    Buf\n    makeBuf(byte_t val)\n    {\n      Buf b;\n      b.Fill(val);\n      return b;\n    }\n\n    struct FileGuard\n    {\n      const fs::path p;\n\n      FileGuard(const fs::path &_p) : p(_p)\n      {\n      }\n\n      ~FileGuard()\n      {\n        if(fs::exists(fs::status(p)))\n        {\n          fs::remove_all(p);\n        }\n      }\n    };\n\n    inline void\n    randbytes_impl(byte_t *ptr, size_t sz)\n    {\n      std::fill_n(ptr, sz, 0xAA);\n    }\n\n    template < typename T >\n    inline void\n    keygen_val(T &val, byte_t x)\n    {\n      val.Fill(x);\n    }\n\n    template < typename T >\n    inline void\n    keygen(T &val)\n    {\n      keygen_val(val, 0xAA);\n    }\n\n    template < typename T >\n    struct CombinationIterator\n    {\n      std::vector< T > toCombine;\n      std::vector< T > currentCombo;\n\n      int bits;\n      int maxBits;\n\n      void\n      createCombo()\n      {\n        currentCombo.clear();\n        for(size_t i = 0; i < toCombine.size(); ++i)\n        {\n          if(bits & (1 << i))\n          {\n            currentCombo.push_back(toCombine[i]);\n          }\n        }\n      }\n\n      CombinationIterator(const std::vector< T > &values)\n          : toCombine(values), bits(0), maxBits((1 << values.size()) - 1)\n      {\n        currentCombo.reserve(values.size());\n        createCombo();\n      }\n\n      bool\n      next()\n      {\n        if(bits >= maxBits)\n        {\n          return false;\n        }\n\n        ++bits;\n        createCombo();\n        return true;\n      }\n\n      bool\n      includesElement(size_t index)\n      {\n        return bits & (1 << index);\n      }\n    };\n\n  }  // namespace test\n}  // namespace llarp\n\n#endif\n"
  },
  {
    "path": "test/util/meta/test_llarp_util_memfn.cpp",
    "content": "#include <llarp/util/meta/memfn.hpp>\n\n#include <catch2/catch.hpp>\n\nusing namespace llarp;\n\nstruct Foo\n{\n  bool\n  empty()\n  {\n    return false;\n  }\n\n  bool\n  constEmpty() const\n  {\n    return true;\n  }\n\n  int\n  arg(int v)\n  {\n    return v + 1;\n  }\n\n  int\n  constArg(int v) const\n  {\n    return v - 1;\n  }\n};\n\nTEST_CASE(\"memFn call\")\n{\n  Foo foo;\n  REQUIRE_FALSE(util::memFn(&Foo::empty, &foo)());\n  REQUIRE(util::memFn(&Foo::constEmpty, &foo)());\n  REQUIRE(11 == util::memFn(&Foo::arg, &foo)(10));\n  REQUIRE(9 == util::memFn(&Foo::constArg, &foo)(10));\n\n  REQUIRE(util::memFn(&Foo::constEmpty, &foo)());\n  REQUIRE(9 == util::memFn(&Foo::constArg, &foo)(10));\n}\n\n// clang-format off\nusing MemFnTypes = std::tuple<\n  Foo, const Foo>;\n// clang-format on\n\nTEMPLATE_LIST_TEST_CASE(\"memFn type smoke test\", \"\", MemFnTypes)\n{\n  TestType foo{};\n  REQUIRE(util::memFn(&Foo::constEmpty, &foo)());\n}\n"
  },
  {
    "path": "test/util/test_llarp_util_aligned.cpp",
    "content": "#include <catch2/catch.hpp>\n\n#include <llarp/util/aligned.hpp>\n\n#include <iostream>\n#include <sstream>\n#include <type_traits>\n#include <unordered_map>\n\nusing TestSizes = std::tuple<\n    std::integral_constant<std::size_t, 8>,\n    std::integral_constant<std::size_t, 12>,\n    std::integral_constant<std::size_t, 16>,\n    std::integral_constant<std::size_t, 32>,\n    std::integral_constant<std::size_t, 64>,\n    std::integral_constant<std::size_t, 77>,\n    std::integral_constant<std::size_t, 1024>,\n    std::integral_constant<std::size_t, 3333>>;\n\nTEMPLATE_LIST_TEST_CASE(\"AlignedBuffer\", \"[AlignedBuffer]\", TestSizes)\n{\n  using Buffer = llarp::AlignedBuffer<TestType::value>;\n\n  Buffer b;\n  CHECK(b.IsZero());\n\n  SECTION(\"Constructor\")\n  {\n    CHECK(b.size() == TestType::value);\n  }\n\n  SECTION(\"CopyConstructor\")\n  {\n    Buffer c = b;\n    CHECK(c.IsZero());\n\n    c.Fill(1);\n    CHECK_FALSE(c.IsZero());\n\n    Buffer d = c;\n    CHECK_FALSE(d.IsZero());\n  }\n\n  SECTION(\"AltConstructors\")\n  {\n    b.Fill(2);\n\n    Buffer c(b.as_array());\n    CHECK_FALSE(c.IsZero());\n\n    Buffer d(c.data());\n    CHECK_FALSE(d.IsZero());\n  }\n\n  SECTION(\"Assignment\")\n  {\n    Buffer c;\n    c = b;\n    CHECK(c.IsZero());\n\n    c.Fill(1);\n    CHECK_FALSE(c.IsZero());\n\n    Buffer d;\n    d = c;\n    CHECK_FALSE(d.IsZero());\n  }\n\n  SECTION(\"FmtOut\")\n  {\n    std::string out;\n    out = fmt::format(\"{}\", b);\n\n    CHECK(out == std::string(TestType::value * 2, '0'));\n\n    b.Fill(255);\n    out = fmt::format(\"{}\", b);\n\n    CHECK(out == std::string(TestType::value * 2, 'f'));\n  }\n\n  SECTION(\"BitwiseNot\")\n  {\n    Buffer c = ~b;\n    CHECK_FALSE(c.IsZero());\n\n    for (auto val : c.as_array())\n    {\n      CHECK(255 == val);\n    }\n\n    Buffer d = ~c;\n    CHECK(d.IsZero());\n  }\n\n  SECTION(\"Operators\")\n  {\n    Buffer c = b;\n    CHECK(b == c);\n    CHECK(b >= c);\n    CHECK(b <= c);\n    CHECK(c >= b);\n    CHECK(c <= b);\n\n    c.Fill(1);\n    CHECK(b != c);\n    CHECK(b < c);\n    CHECK(c > b);\n  }\n\n  SECTION(\"Xor\")\n  {\n    Buffer c;\n    b.Fill(255);\n    c.Fill(255);\n    CHECK_FALSE(b.IsZero());\n    CHECK_FALSE(c.IsZero());\n\n    Buffer d = b ^ c;\n    // 1 ^ 1 = 0\n    CHECK(d.IsZero());\n    // Verify unchanged\n    CHECK_FALSE(b.IsZero());\n    CHECK_FALSE(c.IsZero());\n\n    Buffer e, f;\n    e.Fill(255);\n    Buffer g = e ^ f;\n    // 1 ^ 0 = 1\n    CHECK_FALSE(g.IsZero());\n\n    Buffer h, i;\n    i.Fill(255);\n    Buffer j = h ^ i;\n    // 0 ^ 1 = 1\n    CHECK_FALSE(j.IsZero());\n  }\n\n  SECTION(\"XorAssign\")\n  {\n    Buffer c;\n    b.Fill(255);\n    c.Fill(255);\n    CHECK_FALSE(b.IsZero());\n    CHECK_FALSE(c.IsZero());\n\n    b ^= c;\n    CHECK(b.IsZero());\n  }\n\n  SECTION(\"Zero\")\n  {\n    b.Fill(127);\n    CHECK_FALSE(b.IsZero());\n\n    b.Zero();\n    CHECK(b.IsZero());\n  }\n\n  SECTION(\"TestHash\")\n  {\n    using Map_t = std::unordered_map<Buffer, int>;\n\n    Buffer k, other_k;\n    k.Randomize();\n    other_k.Randomize();\n    Map_t m;\n    CHECK(m.empty());\n    CHECK(m.emplace(k, 1).second);\n    CHECK(m.find(k) != m.end());\n    CHECK(m[k] == 1);\n    CHECK_FALSE(m.find(other_k) != m.end());\n    CHECK(m.size() == 1);\n    Buffer k_copy = k;\n    CHECK_FALSE(m.emplace(k_copy, 2).second);\n    CHECK_FALSE(m[k_copy] == 2);\n    CHECK(m[k_copy] == 1);\n  }\n}\n"
  },
  {
    "path": "test/util/test_llarp_util_bencode.cpp",
    "content": "#include <llarp/util/bencode.h>\n#include <llarp/util/bencode.hpp>\n\n#include <iostream>\n#include <string>\n#include <utility>\n#include <vector>\n\n#include <catch2/catch.hpp>\n\nusing TestBuffer = std::vector<byte_t>;\n\ntemplate <typename Result>\nstruct TestReadData\n{\n  TestBuffer buffer;\n  bool rc;\n  Result result;\n};\n\nusing TestReadInt = TestReadData<uint64_t>;\nusing TestReadString = TestReadData<std::string>;\n\ntemplate <typename Result>\nstd::ostream&\noperator<<(std::ostream& os, const TestReadData<Result>& d)\n{\n  os << \"buf = [ \";\n  for (auto x : d.buffer)\n  {\n    os << x << \" \";\n  }\n\n  os << \"] rc = \";\n\n  os << std::boolalpha << d.rc << \" result = \" << d.result;\n  return os;\n}\n\nstatic constexpr byte_t i = 'i';\nstatic constexpr byte_t e = 'e';\nstatic constexpr byte_t zero = '0';\nstatic constexpr byte_t one = '1';\nstatic constexpr byte_t two = '2';\nstatic constexpr byte_t f = 'f';\nstatic constexpr byte_t z = 'z';\nstatic constexpr byte_t colon = ':';\n\nstd::vector<TestReadInt> testReadInt{\n    // good cases\n    {{i, 0, e}, true, 0},\n    {{i, zero, e}, true, 0},\n    {{i, one, e}, true, 1},\n    {{i, two, e}, true, 2},\n    {{i, two, e, e, e}, true, 2},\n    {{i, one, one, one, two, e}, true, 1112},\n    {{i, f, e}, true, 0},\n    {{i, z, e}, true, 0},\n    {{i, one, two, e, one, one}, true, 12},\n    // failure cases\n    {{i, e}, false, 0},\n    {{e}, false, 0},\n    {{z}, false, 0},\n};\n\nTEST_CASE(\"Read int\", \"[bencode]\")\n{\n  auto d = GENERATE(from_range(testReadInt));\n\n  llarp_buffer_t buffer(d.buffer);\n\n  uint64_t result = 0;\n  bool rc = bencode_read_integer(&buffer, &result);\n\n  CHECK(rc == d.rc);\n  CHECK(result == d.result);\n}\n\nstd::vector<TestReadString> testReadStr{\n    // good cases\n    {{one, colon, 'a'}, true, \"a\"},\n    {{one, colon, 'b'}, true, \"b\"},\n    {{two, colon, f, z}, true, \"fz\"},\n    {{two, colon, f, z, f, f}, true, \"fz\"},\n    {{zero, colon}, true, \"\"},\n    // failure cases\n    {{two, colon, f}, false, \"\"},\n    {{two, f}, false, \"\"},\n    {{'-', one, colon, f}, false, \"\"},\n    {{f}, false, \"\"},\n    {{one, f, colon}, false, \"\"},\n    {{colon}, false, \"\"},\n    {{colon, colon}, false, \"\"},\n};\n\nTEST_CASE(\"Read str\", \"[bencode]\")\n{\n  auto d = GENERATE(from_range(testReadStr));\n\n  llarp_buffer_t buffer(d.buffer);\n\n  llarp_buffer_t result;\n  bool rc = bencode_read_string(&buffer, &result);\n\n  CHECK(rc == d.rc);\n  CHECK(result.sz == d.result.size());\n  CHECK(std::string(result.base, result.base + result.sz) == d.result);\n}\n\ntemplate <typename Input>\nstruct TestWriteData\n{\n  Input input;\n  size_t bufferSize;\n  bool rc;\n  std::string output;\n};\n\nusing TestWriteByteString = TestWriteData<std::string>;\nusing TestWriteInt = TestWriteData<uint64_t>;\n\nstatic constexpr size_t MAX_1 = static_cast<size_t>(std::numeric_limits<int16_t>::max()) + 1;\n\nstd::vector<TestWriteByteString> testWriteByteString{\n    // good cases\n    {\"abacus\", 100, true, \"6:abacus\"},\n    {\"  abacus\", 100, true, \"8:  abacus\"},\n    {\"\", 100, true, \"0:\"},\n    {std::string(\"\\0\\0\\0\", 3), 100, true, std::string(\"3:\\0\\0\\0\", 5)},\n    {std::string(MAX_1, 'a'),\n     MAX_1 + 100,\n     true,\n     std::to_string(MAX_1) + std::string(\":\") + std::string(MAX_1, 'a')},\n    // bad cases\n    {\"a\", 1, false, \"\"},\n};\n\nTEST_CASE(\"Write byte str\", \"[bencode]\")\n{\n  auto d = GENERATE(from_range(testWriteByteString));\n\n  std::vector<byte_t> backingBuffer(d.bufferSize, 0);\n  llarp_buffer_t buffer(backingBuffer);\n\n  bool rc = bencode_write_bytestring(&buffer, d.input.data(), d.input.size());\n\n  REQUIRE(rc == d.rc);\n  REQUIRE(std::string(buffer.base, buffer.cur) == d.output);\n}\n\nstd::vector<TestWriteInt> testWriteInt{\n    // Good cases\n    {0, 100, true, \"i0e\"},\n    {1234, 100, true, \"i1234e\"},\n    {uint64_t(-1), 100, true, \"i18446744073709551615e\"},\n    // Bad cases\n    {1234567, 3, false, \"\"},\n};\n\nTEST_CASE(\"Write int\", \"[bencode]\")\n{\n  auto d = GENERATE(from_range(testWriteInt));\n\n  std::vector<byte_t> backingBuffer(d.bufferSize, 0);\n  llarp_buffer_t buffer(backingBuffer);\n\n  bool rc = bencode_write_uint64(&buffer, d.input);\n\n  REQUIRE(rc == d.rc);\n  REQUIRE(std::string(buffer.base, buffer.cur) == d.output);\n}\n\nTEST_CASE(\"Write int values\", \"[bencode]\")\n{\n  // test we can encode any uint64_t into a buffer.\n  uint64_t val = GENERATE(\n      std::numeric_limits<uint64_t>::min(),\n      std::numeric_limits<uint64_t>::max(),\n      std::numeric_limits<uint64_t>::max() / 2,\n      std::numeric_limits<uint64_t>::max() / 3);\n\n  std::vector<byte_t> backingBuffer(100, 0);\n\n  {\n    llarp_buffer_t buffer(backingBuffer);\n\n    bool rc = bencode_write_uint64(&buffer, val);\n    REQUIRE(rc);\n  }\n\n  {\n    uint64_t result = 0;\n    llarp_buffer_t buffer(backingBuffer);\n    bool rc = bencode_read_integer(&buffer, &result);\n    REQUIRE(rc);\n    REQUIRE(result == val);\n  }\n}\n\nTEST_CASE(\"Bencode: good uint64 entry\", \"[bencode]\")\n{\n  std::vector<byte_t> backingBuffer(100, 0);\n  llarp_buffer_t buffer(backingBuffer);\n\n  REQUIRE(bencode_write_uint64_entry(&buffer, \"v\", 1, 0));\n\n  REQUIRE(std::string(buffer.base, buffer.cur) == \"1:vi0e\");\n}\n\nTEST_CASE(\"Bencode: bad uint64 entry\", \"[bencode]\")\n{\n  std::vector<byte_t> otherBuffer(1, 0);\n  llarp_buffer_t buffer(otherBuffer);\n\n  REQUIRE_FALSE(bencode_write_uint64_entry(&buffer, \"v\", 1, 0));\n}\n\nstruct ValueData\n{\n  // Variant-ish\n  std::string theString;\n  uint64_t theInt;\n  bool isString;\n};\n\nstruct ListTestData\n{\n  std::vector<ValueData> list;\n  size_t bufferSize;\n  std::string result;\n};\n\nstd::vector<ListTestData> listTestData{\n    {{}, 100, \"le\"},\n    {{{\"\", 0, true}}, 100, \"l0:e\"},\n    {{{\"\", 0, false}}, 100, \"li0ee\"},\n    {{{\"\", 0, false}, {\"\", 0, true}}, 100, \"li0e0:e\"},\n    {{{\"\", 123, false}, {\"abc\", 0, true}}, 100, \"li123e3:abce\"},\n    {{{\"\", 123, false}, {\"abc\", 0, true}, {\"abc\", 0, true}}, 100, \"li123e3:abc3:abce\"},\n};\n\nTEST_CASE(\"List test\", \"[bencode]\")\n{\n  auto d = GENERATE(from_range(listTestData));\n\n  std::vector<byte_t> backingBuffer(d.bufferSize, 0);\n  llarp_buffer_t buffer(backingBuffer);\n\n  REQUIRE(bencode_start_list(&buffer));\n\n  for (const auto& x : d.list)\n  {\n    if (x.isString)\n    {\n      REQUIRE(bencode_write_bytestring(&buffer, x.theString.data(), x.theString.size()));\n    }\n    else\n    {\n      REQUIRE(bencode_write_uint64(&buffer, x.theInt));\n    }\n  }\n\n  REQUIRE(bencode_end(&buffer));\n\n  REQUIRE(std::string(buffer.base, buffer.cur) == d.result);\n}\n\nstruct DictTestData\n{\n  std::vector<std::pair<char, ValueData>> list;\n  size_t bufferSize;\n  std::string result;\n};\n\nstd::vector<DictTestData> dictTestData{\n    {{}, 100, \"de\"},\n    {{{'a', {\"\", 0, true}}}, 100, \"d1:a0:e\"},\n    {{{'b', {\"\", 0, false}}}, 100, \"d1:bi0ee\"},\n    {{{'c', {\"\", 0, false}}, {'d', {\"\", 0, true}}}, 100, \"d1:ci0e1:d0:e\"},\n    {{{'e', {\"\", 123, false}}, {'f', {\"abc\", 0, true}}}, 100, \"d1:ei123e1:f3:abce\"},\n    {{{'a', {\"\", 123, false}}, {'b', {\"abc\", 0, true}}, {'c', {\"abc\", 0, true}}},\n     100,\n     \"d1:ai123e1:b3:abc1:c3:abce\"},\n};\n\nTEST_CASE(\"Dict test\", \"[bencode]\")\n{\n  auto d = GENERATE(from_range(dictTestData));\n\n  std::vector<byte_t> backingBuffer(d.bufferSize, 0);\n  llarp_buffer_t buffer(backingBuffer);\n\n  REQUIRE(bencode_start_dict(&buffer));\n\n  for (const auto& x : d.list)\n  {\n    REQUIRE(bencode_write_bytestring(&buffer, &x.first, 1));\n    if (x.second.isString)\n    {\n      REQUIRE(\n          bencode_write_bytestring(&buffer, x.second.theString.data(), x.second.theString.size()));\n    }\n    else\n    {\n      REQUIRE(bencode_write_uint64(&buffer, x.second.theInt));\n    }\n  }\n\n  REQUIRE(bencode_end(&buffer));\n\n  REQUIRE(std::string(buffer.base, buffer.cur) == d.result);\n}\n\nstruct ReadData\n{\n  std::string input;\n  std::vector<std::string> output;\n};\n\nstd::vector<ReadData> dictReadData{\n    {\"de\", {}}, {\"d1:a0:e\", {\"a\", \"\"}}, {\"d1:be\", {\"b\"}}, {\"d1:b2:23e\", {\"b\", \"23\"}}};\n\nTEST_CASE(\"Read dict\", \"[bencode]\")\n{\n  auto d = GENERATE(from_range(dictReadData));\n\n  byte_t* input = const_cast<byte_t*>(reinterpret_cast<const byte_t*>(d.input.data()));\n\n  llarp_buffer_t buffer(input, input, d.input.size());\n\n  std::vector<std::string> result;\n\n  REQUIRE(llarp::bencode_read_dict(\n      [&](llarp_buffer_t*, llarp_buffer_t* key) {\n        if (key)\n        {\n          result.emplace_back(key->base, key->base + key->sz);\n        }\n        return true;\n      },\n      &buffer));\n  REQUIRE(result == d.output);\n}\n\nstd::vector<ReadData> listReadData{\n    {\"le\", {}}, {\"l1:ae\", {\"a\"}}, {\"l1:be\", {\"b\"}}, {\"l1:b2:23e\", {\"b\", \"23\"}}};\n\nTEST_CASE(\"Read list\", \"[bencode]\")\n{\n  auto d = GENERATE(from_range(listReadData));\n\n  byte_t* input = const_cast<byte_t*>(reinterpret_cast<const byte_t*>(d.input.data()));\n\n  llarp_buffer_t buffer(input, input, d.input.size());\n\n  std::vector<std::string> result;\n\n  REQUIRE(llarp::bencode_read_list(\n      [&](llarp_buffer_t* b, bool cont) {\n        if (cont)\n        {\n          llarp_buffer_t tmp;\n          bencode_read_string(b, &tmp);\n          result.emplace_back(tmp.base, tmp.base + tmp.sz);\n        }\n        return true;\n      },\n      &buffer));\n  REQUIRE(result == d.output);\n}\n\nTEST_CASE(\"Read dict to empty buffer\", \"[bencode]\")\n{\n  llarp_buffer_t buf((byte_t*)nullptr, 0);\n  REQUIRE_FALSE(\n      llarp::bencode_read_dict([](llarp_buffer_t*, llarp_buffer_t*) { return true; }, &buf));\n}\n"
  },
  {
    "path": "test/util/test_llarp_util_bits.cpp",
    "content": "#include <catch2/catch.hpp>\n#include <llarp/util/bits.hpp>\n\nusing namespace llarp::bits;\n\nTEST_CASE(\"test bit counting, 8-bit\", \"[bits]\") {\n  // Workaround for gcc 5's stdlib; we can drop this crap (and drop all the `T`'s below) once we\n  // stop supporting it.\n  using T =    std::tuple<unsigned char, size_t>;\n  auto x = GENERATE(table<unsigned char, size_t>({\n    T{0b00000000, 0},\n    T{0b00000001, 1},\n    T{0b00000010, 1},\n    T{0b00000100, 1},\n    T{0b00001000, 1},\n    T{0b00010000, 1},\n    T{0b00100000, 1},\n    T{0b01000000, 1},\n    T{0b10000000, 1},\n    T{0b11111111, 8},\n  }));\n  std::array<unsigned char, 1> arr{{std::get<0>(x)}};\n  auto expected = std::get<1>(x);\n  REQUIRE( count_array_bits(arr) == expected );\n}\n\nTEST_CASE(\"test bit counting, 20 x 8-bit\", \"[bits]\") {\n  std::array<unsigned char, 20> x{{\n      0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,\n      0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,\n      0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000,\n      0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000}};\n  REQUIRE( count_array_bits(x) == 0 );\n\n  x = {{\n      0b11111111, 0b00000100, 0b00000100, 0b00000100, 0b00000100,\n      0b11111111, 0b00000100, 0b00000100, 0b00000100, 0b00000100,\n      0b11111111, 0b00000100, 0b00000100, 0b00000100, 0b00000100,\n      0b11111111, 0b00000100, 0b00000100, 0b00000100, 0b00000100}};\n  REQUIRE( count_array_bits(x) == 48 );\n}\n\nTEST_CASE(\"test bit counting, unsigned int\", \"[bits]\") {\n  using T =    std::tuple<unsigned int, size_t>; // gcc 5 workaround\n  auto x = GENERATE(table<unsigned int, size_t>({\n    T{0b00000000000000000000000000000000, 0},\n    T{0b00101010101010101010101010101010, 15},\n    T{0b10101010101010101010101010101010, 16},\n    T{0b01010101010101010101010101010101, 16},\n    T{0b11111111111111111111111111111111, 32},\n  }));\n\n  std::array<unsigned int, 1> arr{{std::get<0>(x)}};\n  auto expected = std::get<1>(x);\n  REQUIRE( llarp::bits::count_array_bits(arr) == expected );\n}\n\nTEST_CASE(\"test bit counting, unsigned long long\", \"[bits]\") {\n  using T =    std::tuple<unsigned long long, size_t>; // gcc 5 workaround\n  auto x = GENERATE(table<unsigned long long, size_t>({\n    T{0b0000000000000000000000000000000000000000000000000000000000000000, 0},\n    T{0b0010101010101010101010101010101000101010101010101010101010101010, 30},\n    T{0b1010101010101010101010101010101010101010101010101010101010101010, 32},\n    T{0b0101010101010101010101010101010101010101010101010101010101010101, 32},\n    T{0b1111111111111111111111111111111111111111111111111111111111111111, 64},\n  }));\n\n  std::array<unsigned long long, 1> arr{{std::get<0>(x)}};\n  auto expected = std::get<1>(x);\n  REQUIRE( llarp::bits::count_array_bits(arr) == expected );\n}\n"
  },
  {
    "path": "test/util/test_llarp_util_decaying_hashset.cpp",
    "content": "#include <llarp/util/decaying_hashset.hpp>\n#include <llarp/router_id.hpp>\n#include <catch2/catch.hpp>\n\nTEST_CASE(\"DecayingHashSet test decay static time\", \"[decaying-hashset]\")\n{\n  static constexpr auto timeout = 5s;\n  static constexpr auto now = 1s;\n  llarp::util::DecayingHashSet<llarp::RouterID> hashset{timeout};\n  const llarp::RouterID zero{};\n  REQUIRE(zero.IsZero());\n  REQUIRE(not hashset.Contains(zero));\n  REQUIRE(hashset.Insert(zero, now));\n  REQUIRE(hashset.Contains(zero));\n  hashset.Decay(now + 1s);\n  REQUIRE(hashset.Contains(zero));\n  hashset.Decay(now + timeout);\n  REQUIRE(not hashset.Contains(zero));\n  hashset.Decay(now + timeout + 1s);\n  REQUIRE(not hashset.Contains(zero));\n}\n\nTEST_CASE(\"DecayingHashSet test decay dynamic time\", \"[decaying-hashset]\")\n{\n  static constexpr llarp_time_t timeout = 5s;\n  const auto now = llarp::time_now_ms();\n  llarp::util::DecayingHashSet<llarp::RouterID> hashset{timeout};\n  const llarp::RouterID zero{};\n  REQUIRE(zero.IsZero());\n  REQUIRE(not hashset.Contains(zero));\n  REQUIRE(hashset.Insert(zero, now));\n  REQUIRE(hashset.Contains(zero));\n  hashset.Decay(now + 1s);\n  REQUIRE(hashset.Contains(zero));\n  hashset.Decay(now + timeout);\n  REQUIRE(not hashset.Contains(zero));\n  hashset.Decay(now + timeout + 1s);\n  REQUIRE(not hashset.Contains(zero));\n}\n"
  },
  {
    "path": "test/util/test_llarp_util_log_level.cpp",
    "content": "#include <catch2/catch.hpp>\n#include <llarp/util/logging.hpp>\n#include <llarp/config/config.hpp>\n#include <oxen/log/level.hpp>\n\nusing TestString = std::string;\n\nstruct TestParseLog\n{\n  TestString input;\n  std::optional<llarp::log::Level> level;\n};\n\nstd::vector<TestParseLog> testParseLog{// bad cases\n                                       {\"bogus\", {}},\n                                       {\"BOGUS\", {}},\n                                       {\"\", {}},\n                                       {\" \", {}},\n                                       {\"infogarbage\", {}},\n                                       {\"notcritical\", {}},\n                                       // good cases\n                                       {\"info\", llarp::log::Level::info},\n                                       {\"infO\", llarp::log::Level::info},\n                                       {\"iNfO\", llarp::log::Level::info},\n                                       {\"InfO\", llarp::log::Level::info},\n                                       {\"INFO\", llarp::log::Level::info},\n                                       {\"trace\", llarp::log::Level::trace},\n                                       {\"debug\", llarp::log::Level::debug},\n                                       {\"warn\", llarp::log::Level::warn},\n                                       {\"warning\", llarp::log::Level::warn},\n                                       {\"error\", llarp::log::Level::err},\n                                       {\"err\", llarp::log::Level::err},\n                                       {\"Critical\", llarp::log::Level::critical},\n                                       {\"off\", llarp::log::Level::off},\n                                       {\"none\", llarp::log::Level::off}};\n\nTEST_CASE(\"parseLevel\")\n{\n  const auto& [input, expected] = GENERATE(from_range(testParseLog));\n\n  if (not expected)\n    REQUIRE_THROWS_AS(llarp::log::level_from_string(input), std::invalid_argument);\n  else\n  {\n    llarp::log::Level level;\n    REQUIRE_NOTHROW(level = llarp::log::level_from_string(input));\n    CHECK(level == *expected);\n  }\n}\n\nTEST_CASE(\"TestLogLevelToString\")\n{\n  CHECK(\"trace\" == llarp::log::to_string(llarp::log::Level::trace));\n  CHECK(\"debug\" == llarp::log::to_string(llarp::log::Level::debug));\n  CHECK(\"info\" == llarp::log::to_string(llarp::log::Level::info));\n  CHECK(\"warning\" == llarp::log::to_string(llarp::log::Level::warn));\n  CHECK(\"error\" == llarp::log::to_string(llarp::log::Level::err));\n  CHECK(\"critical\" == llarp::log::to_string(llarp::log::Level::critical));\n  CHECK(\"off\" == llarp::log::to_string(llarp::log::Level::off));\n}\n"
  },
  {
    "path": "test/util/test_llarp_util_str.cpp",
    "content": "#include <llarp/util/str.hpp>\n#include <catch2/catch.hpp>\n\n#include <vector>\n\nusing namespace std::literals;\n\nTEST_CASE(\"TrimWhitespace -- positive tests\", \"[str][trim]\")\n{\n  // Test that things that should be trimmed actually get trimmed\n  auto fee = \"    J a c k\"s;\n  auto fi = \"\\ra\\nd\"s;\n  auto fo = \"\\fthe   \"s;\n  auto fum = \" \\t\\r\\n\\v\\f Beanstalk\\n\\n\\n\\t\\r\\f\\v   \\n\\n\\r\\f\\f\\f\\f\\v\"s;\n  for (auto* s: {&fee, &fi, &fo, &fum})\n    *s = llarp::TrimWhitespace(*s);\n\n  REQUIRE( fee == \"J a c k\" );\n  REQUIRE( fi == \"a\\nd\" );\n  REQUIRE( fo == \"the\" );\n  REQUIRE( fum == \"Beanstalk\" );\n}\n\nTEST_CASE(\"TrimWhitespace -- negative tests\", \"[str][trim]\")\n{\n  // Test that things that shouldn't be trimmed don't get trimmed\n  auto c = GENERATE(range(std::numeric_limits<char>::min(), std::numeric_limits<char>::max()));\n  std::string plant = c + \"bean\"s + c;\n  plant = llarp::TrimWhitespace(plant);\n  if (c == ' ' || c == '\\t' || c == '\\r' || c == '\\n' || c == '\\f' || c == '\\v')\n    REQUIRE( plant == \"bean\" );\n  else\n  {\n    REQUIRE( plant.size() == 6 );\n    REQUIRE( plant.substr(1, 4) == \"bean\" );\n  }\n}\n\nTEST_CASE(\"caseless comparison tests - less than\", \"[str][lt]\") {\n  using namespace llarp;\n  CaselessLessThan lt;\n  auto expect_less_than = GENERATE(table<const char*, const char*>({\n        {\"\", \"1\"},\n        {\"1\", \"11\"},\n        {\"abc\", \"abcd\"},\n        {\"ABC\", \"abcd\"},\n        {\"abc\", \"ABCD\"},\n        {\"abc\", \"Abcd\"},\n        {\"abc\", \"abcD\"},\n        {\"abc\", \"abCd\"},\n        {\"abc\", \"zz\"},\n        {\"abc\", \"zzzz\"},\n        {\"abc\", \"abd\"},\n        {\"abc\", \"aBd\"},\n        {\"abc\", \"abD\"},\n        {\"ABC\", \"abd\"},\n        {\"abC\", \"abd\"},\n  }));\n  REQUIRE(  lt(std::get<0>(expect_less_than), std::get<1>(expect_less_than)) );\n  REQUIRE( !lt(std::get<1>(expect_less_than), std::get<0>(expect_less_than)) );\n}\n\nTEST_CASE(\"caseless comparison tests - equality\", \"[str][eq]\") {\n  using namespace llarp;\n  CaselessLessThan lt;\n  auto expect_equal = GENERATE(table<const char*, const char*>({\n        {\"1\", \"1\"},\n        {\"a\", \"A\"},\n        {\"abc\", \"ABC\"},\n        {\"abc\", \"aBc\"},\n        {\"ABC\", \"abc\"},\n  }));\n  REQUIRE( !lt(std::get<0>(expect_equal), std::get<1>(expect_equal)) );\n  REQUIRE( !lt(std::get<1>(expect_equal), std::get<0>(expect_equal)) );\n}\n\nTEST_CASE(\"truthy string values\", \"[str][truthy]\") {\n  auto val = GENERATE(\"true\", \"TruE\", \"yes\", \"yeS\", \"yES\", \"yes\", \"YES\", \"1\", \"on\", \"oN\", \"ON\");\n  REQUIRE( llarp::IsTrueValue(val) );\n}\n\nTEST_CASE(\"falsey string values\", \"[str][falsey]\") {\n  auto val = GENERATE(\"false\", \"FalSe\", \"no\", \"NO\", \"No\", \"nO\", \"0\", \"off\", \"OFF\");\n  REQUIRE( llarp::IsFalseValue(val) );\n}\n\nTEST_CASE(\"neither true nor false string values\", \"[str][nottruefalse]\") {\n  auto val = GENERATE(\"false y\", \"maybe\", \"not on\", \"2\", \"yesno\", \"YESNO\", \"-1\", \"default\", \"OMG\");\n  REQUIRE( !llarp::IsTrueValue(val) );\n  REQUIRE( !llarp::IsFalseValue(val) );\n}\n\nTEST_CASE(\"split strings with multiple matches\", \"[str]\") {\n  auto splits = llarp::split(\"this is a test\", \" \");\n  REQUIRE(splits.size() == 4);\n  REQUIRE(splits[0] == \"this\");\n  REQUIRE(splits[1] == \"is\");\n  REQUIRE(splits[2] == \"a\");\n  REQUIRE(splits[3] == \"test\");\n}\n\nTEST_CASE(\"split strings with single match\", \"[str]\") {\n  auto splits = llarp::split(\"uno\", \";\");\n  REQUIRE(splits.size() == 1);\n  REQUIRE(splits[0] == \"uno\");\n}\n\nTEST_CASE(\"split_any strings with consecutive delimiters\", \"[str]\") {\n  auto splits = llarp::split_any(\"a  o   e    u\", \" \");\n  REQUIRE(splits.size() == 4);\n  REQUIRE(splits[0] == \"a\");\n  REQUIRE(splits[1] == \"o\");\n  REQUIRE(splits[2] == \"e\");\n  REQUIRE(splits[3] == \"u\");\n}\n\nTEST_CASE(\"split delimiter-only string\", \"[str]\") {\n  {\n    auto splits = llarp::split(\"    \", \" \");\n    REQUIRE(splits.size() == 5);\n  }\n\n  {\n    auto splits = llarp::split_any(\"    \", \" \");\n    REQUIRE(splits.size() == 2);\n  }\n\n  {\n    auto splits = llarp::split(\"    \", \" \", true);\n    REQUIRE(splits.size() == 0);\n  }\n\n  {\n    auto splits = llarp::split_any(\"    \", \" \", true);\n    REQUIRE(splits.size() == 0);\n  }\n}\n\nTEST_CASE(\"split empty string\", \"[str]\") {\n  {\n    auto splits = llarp::split(\"\", \" \");\n    REQUIRE(splits.size() == 1);\n  }\n\n  {\n    auto splits = llarp::split(\"\", \" \", true);\n    REQUIRE(splits.size() == 0);\n  }\n}\n"
  },
  {
    "path": "test/util/thread/test_llarp_util_queue.cpp",
    "content": "#include <llarp/util/thread/queue.hpp>\n#include <llarp/util/thread/threading.hpp>\n#include <llarp/util/thread/barrier.hpp>\n\n#include <array>\n#include <condition_variable>\n#include <functional>\n#include <thread>\n\n#include <catch2/catch.hpp>\n\nusing namespace llarp;\nusing namespace llarp::thread;\n\nusing namespace std::literals;\n\nusing LockGuard = std::unique_lock<std::mutex>;\n\nclass Element\n{\n private:\n  double data;\n  bool shouldStop;\n\n public:\n  Element(double d, bool _stop = false) : data(d), shouldStop(_stop)\n  {}\n\n  double\n  val() const\n  {\n    return data;\n  }\n\n  bool\n  stop() const\n  {\n    return shouldStop;\n  }\n};\n\nbool\noperator==(const Element& lhs, const Element& rhs)\n{\n  return lhs.val() == rhs.val();\n}\n\nusing ObjQueue = Queue<Element>;\n\nclass Args\n{\n public:\n  std::condition_variable startCond;\n  std::condition_variable runCond;\n  std::mutex mutex;\n  std::condition_variable cv;\n\n  ObjQueue queue;\n\n  // Use volatile over atomic int in order to verify the thread safety.\n  // If we used atomics here, we would introduce new potential synchronisation\n  // points.\n  volatile size_t iterations;\n  volatile size_t count;\n  volatile size_t startSignal;\n  volatile size_t runSignal;\n  volatile size_t endSignal;\n\n  Args(size_t _iterations, size_t size = 20 * 1000)\n      : queue(size), iterations(_iterations), count(0), startSignal(0), runSignal(0), endSignal(0)\n  {}\n\n  bool\n  signal() const\n  {\n    return !!runSignal;\n  }\n};\n\nvoid\npopFrontTester(Args& args)\n{\n  {\n    LockGuard lock(args.mutex);\n    args.count++;\n    args.cv.wait(lock, [&] { return args.signal(); });\n  }\n\n  for (;;)\n  {\n    Element e = args.queue.popFront();\n    if (e.stop())\n    {\n      break;\n    }\n  }\n}\n\nvoid\npushBackTester(Args& args)\n{\n  {\n    LockGuard lock(args.mutex);\n    args.count++;\n    args.cv.wait(lock, [&] { return args.signal(); });\n  }\n\n  for (size_t i = 0; i < args.iterations; ++i)\n  {\n    Element e{static_cast<double>(i)};\n    args.queue.pushBack(e);\n  }\n\n  args.queue.pushBack(Element{0, true});\n}\n\nvoid\nabaThread(char* firstValue, char* lastValue, Queue<char*>& queue, util::Barrier& barrier)\n{\n  barrier.Block();\n\n  for (char* val = firstValue; val <= lastValue; ++val)\n  {\n    queue.pushBack(val);\n  }\n}\n\nstruct Exception : public std::exception\n{};\n\nstruct ExceptionTester\n{\n  static std::atomic<std::thread::id> throwFrom;\n\n  void\n  test()\n  {\n    if (throwFrom != std::thread::id() && std::this_thread::get_id() == throwFrom)\n    {\n      throw Exception();\n    }\n  }\n\n  ExceptionTester()\n  {}\n\n  ExceptionTester(const ExceptionTester&)\n  {\n    test();\n  }\n\n  ExceptionTester&\n  operator=(const ExceptionTester&)\n  {\n    test();\n    return *this;\n  }\n};\n\nstd::atomic<std::thread::id> ExceptionTester::throwFrom = {std::thread::id()};\n\nvoid\nsleepNWait(std::chrono::microseconds microseconds, util::Barrier& barrier)\n{\n  std::this_thread::sleep_for(microseconds);\n  barrier.Block();\n}\n\nvoid\nexceptionProducer(\n    Queue<ExceptionTester>& queue, util::Semaphore& semaphore, std::atomic_size_t& caught)\n{\n  static constexpr size_t iterations = 3;\n\n  for (size_t i = 0; i < iterations; ++i)\n  {\n    try\n    {\n      queue.pushBack(ExceptionTester());\n    }\n    catch (const Exception&)\n    {\n      ++caught;\n    }\n\n    semaphore.notify();\n  }\n}\n\nstruct MoveTester\n{\n  bool moved;\n  size_t& moveCounter;\n  size_t value;\n\n  explicit MoveTester(size_t& counter, size_t val) : moved(false), moveCounter(counter), value(val)\n  {}\n\n  explicit MoveTester(const MoveTester& rhs) = delete;\n\n  MoveTester&\n  operator=(const MoveTester& rhs) = delete;\n\n  MoveTester(MoveTester&& rhs) : moved(false), moveCounter(rhs.moveCounter), value(rhs.value)\n  {\n    rhs.moved = true;\n    moveCounter++;\n  }\n\n  MoveTester&\n  operator=(MoveTester&& rhs)\n  {\n    value = rhs.value;\n    rhs.moved = true;\n    moveCounter = rhs.moveCounter;\n\n    moveCounter++;\n\n    return *this;\n  }\n};\n\nTEST_CASE(\"single\")\n{\n  ObjQueue queue(1u);\n\n  REQUIRE(0u == queue.size());\n  REQUIRE(1u == queue.capacity());\n}\n\nTEST_CASE(\"breathing\")\n{\n  static constexpr size_t DEFAULT_CAP = 10 * 1000;\n\n  ObjQueue queue(DEFAULT_CAP);\n\n  REQUIRE(0u == queue.size());\n  REQUIRE(DEFAULT_CAP == queue.capacity());\n\n  Element e1(1.0);\n  Element e2(2.0);\n  Element e3(3.0);\n\n  queue.pushBack(e1);\n  queue.pushBack(e2);\n  queue.pushBack(e3);\n\n  Element p1 = queue.popFront();\n  Element p2 = queue.popFront();\n  Element p3 = queue.popFront();\n\n  REQUIRE(e1 == p1);\n  REQUIRE(e2 == p2);\n  REQUIRE(e3 == p3);\n}\n\nTEST_CASE(\"Single producer many consumer\")\n{\n  static constexpr size_t iterations = 100 * 1000;\n  static constexpr size_t numThreads = 5;\n\n  std::array<std::thread, numThreads> threads;\n\n  Args args{iterations};\n\n  {\n    LockGuard lock(args.mutex);\n\n    for (size_t i = 0; i < threads.size(); ++i)\n    {\n      threads[i] = std::thread(std::bind(&popFrontTester, std::ref(args)));\n      args.cv.wait(lock, [&] { return args.count != i + 1; });\n    }\n\n    args.runSignal++;\n  }\n\n  for (size_t i = 0; i < iterations; ++i)\n  {\n    Element e{static_cast<double>(i)};\n    args.queue.pushBack(e);\n  }\n\n  for (size_t i = 0; i < numThreads; ++i)\n  {\n    Element e{0.0, true};\n    args.queue.pushBack(e);\n  }\n\n  for (size_t i = 0; i < numThreads; ++i)\n  {\n    threads[i].join();\n  }\n\n  REQUIRE(0u == args.queue.size());\n}\n\nTEST_CASE(\"Many producer many consumer\")\n{\n  static constexpr size_t iterations = 100 * 1000;\n  static constexpr size_t numThreads = 5;\n\n  std::array<std::thread, numThreads * 2> threads;\n\n  Args args{iterations};\n\n  {\n    LockGuard lock(args.mutex);\n\n    for (size_t i = 0; i < numThreads; ++i)\n    {\n      threads[i] = std::thread(std::bind(&popFrontTester, std::ref(args)));\n      args.cv.wait(lock, [&] { return args.count != i + 1; });\n    }\n\n    for (size_t i = 0; i < numThreads; ++i)\n    {\n      threads[i + numThreads] = std::thread(std::bind(&pushBackTester, std::ref(args)));\n      args.cv.wait(lock, [&] { return args.count != numThreads + i + 1; });\n    }\n\n    args.runSignal++;\n  }\n\n  for (auto& thread : threads)\n  {\n    thread.join();\n  }\n\n  REQUIRE(0u == args.queue.size());\n}\n\nTEST_CASE(\"ABA empty\")\n{\n  // Verify we avoid the ABA problem, where multiple threads try to push an\n  // object to the same \"empty\" position in the queue.\n\n  static constexpr size_t numThreads = 50;\n  static constexpr size_t numValues = 6;\n  static constexpr size_t numIterations = 1000;\n  static constexpr size_t numEntries = numThreads * numValues;\n\n  char block[numEntries];\n\n  for (size_t i = 0; i < numIterations; ++i)\n  {\n    util::Barrier barrier{numThreads + 1};\n\n    Queue<char*> queue{numEntries + 1};\n\n    std::array<std::thread, numThreads + 1> threads;\n\n    char* nextValue[numThreads];\n    char* lastValue[numThreads];\n\n    for (size_t j = 0; j < numThreads; ++j)\n    {\n      nextValue[j] = block + (numValues * j);\n      lastValue[j] = block + (numValues * (j + 1)) - 1;\n\n      threads[j] =\n          std::thread([&, n = nextValue[j], l = lastValue[j]] { abaThread(n, l, queue, barrier); });\n    }\n\n    threads[numThreads] = std::thread([&] {\n      std::this_thread::sleep_for(100us);\n      barrier.Block();\n    });\n\n    for (size_t j = 0; j < numEntries; ++j)\n    {\n      char* val = queue.popFront();\n\n      size_t k = 0;\n\n      for (k = 0; k < numThreads; ++k)\n      {\n        if (val == nextValue[k])\n        {\n          nextValue[k] += (val == lastValue[k] ? 0 : 1);\n          REQUIRE(nextValue[k] <= lastValue[k]);\n          break;\n        }\n      }\n\n      REQUIRE(k < numThreads);\n    }\n\n    for (auto& thread : threads)\n    {\n      thread.join();\n    }\n\n    REQUIRE(0u == queue.size());\n  }\n}\n\nTEST_CASE(\"Generation count\")\n{\n  // Verify functionality after running through a full cycle (and invoking the\n  // generation rollover logic).\n  // For a queue of size 3, this is currently 508 cycles, implying we need to go\n  // through at least 3048 objects (3 * 508 * 2) to trigger this logic twice.\n  static constexpr size_t numThreads = 6;\n  static constexpr size_t queueSize = 3;\n  static constexpr size_t numEntries = 3060;\n  static constexpr size_t numValues = numEntries / numThreads;\n\n  char block[numEntries];\n\n  util::Barrier barrier{numThreads + 1};\n\n  Queue<char*> queue{queueSize};\n\n  std::array<std::thread, numThreads + 1> threads;\n\n  char* nextValue[numThreads];\n  char* lastValue[numThreads];\n\n  for (size_t j = 0; j < numThreads; ++j)\n  {\n    nextValue[j] = block + (numValues * j);\n    lastValue[j] = block + (numValues * (j + 1)) - 1;\n\n    threads[j] =\n        std::thread([&, n = nextValue[j], l = lastValue[j]] { abaThread(n, l, queue, barrier); });\n  }\n\n  threads[numThreads] = std::thread([&] {\n    std::this_thread::sleep_for(100ms);\n    barrier.Block();\n  });\n\n  for (size_t j = 0; j < numEntries; ++j)\n  {\n    char* val = queue.popFront();\n\n    size_t k = 0;\n\n    for (k = 0; k < numThreads; ++k)\n    {\n      if (val == nextValue[k])\n      {\n        nextValue[k] += (val == lastValue[k] ? 0 : 1);\n        REQUIRE(nextValue[k] <= lastValue[k]);\n        break;\n      }\n    }\n\n    REQUIRE(k < numThreads);\n  }\n\n  for (auto& thread : threads)\n  {\n    thread.join();\n  }\n\n  REQUIRE(0u == queue.size());\n}\n\nTEST_CASE(\"Basic exception safety\")\n{\n  ExceptionTester::throwFrom = std::this_thread::get_id();\n\n  Queue<ExceptionTester> queue{1};\n\n  REQUIRE_THROWS_AS(queue.pushBack(ExceptionTester()), Exception);\n\n  ExceptionTester::throwFrom = std::thread::id();\n}\n\nTEST_CASE(\"Exception safety\")\n{\n  ExceptionTester::throwFrom = std::thread::id();\n  static constexpr size_t queueSize = 3;\n\n  Queue<ExceptionTester> queue{queueSize};\n\n  REQUIRE(QueueReturn::Success == queue.pushBack(ExceptionTester()));\n  REQUIRE(QueueReturn::Success == queue.pushBack(ExceptionTester()));\n  REQUIRE(QueueReturn::Success == queue.pushBack(ExceptionTester()));\n  REQUIRE(QueueReturn::Success != queue.tryPushBack(ExceptionTester()));\n\n  util::Semaphore semaphore{0};\n\n  std::atomic_size_t caught = {0};\n\n  std::thread producer(\n      std::bind(&exceptionProducer, std::ref(queue), std::ref(semaphore), std::ref(caught)));\n\n  ExceptionTester::throwFrom = std::this_thread::get_id();\n\n  REQUIRE_THROWS_AS(queue.popFront(), Exception);\n\n  using namespace std::literals;\n  // Now the queue is not full, and the producer thread can start adding items.\n  REQUIRE(semaphore.waitFor(1s));\n\n  REQUIRE(queueSize == queue.size());\n\n  REQUIRE_THROWS_AS(queue.popFront(), Exception);\n\n  // Now the queue is not full, and the producer thread can start adding items.\n  REQUIRE(semaphore.waitFor(1s));\n\n  REQUIRE(queueSize == queue.size());\n\n  // Pushing into the queue with exception empties the queue.\n  ExceptionTester::throwFrom = producer.get_id();\n\n  // pop an item to unblock the pusher\n  (void)queue.popFront();\n\n  REQUIRE(semaphore.waitFor(1s));\n\n  REQUIRE(1u == caught);\n\n  REQUIRE(0u == queue.size());\n  REQUIRE(queue.empty());\n\n  // after throwing, the queue works fine.\n\n  REQUIRE(QueueReturn::Success == queue.pushBack(ExceptionTester()));\n  REQUIRE(QueueReturn::Success == queue.pushBack(ExceptionTester()));\n  REQUIRE(QueueReturn::Success == queue.pushBack(ExceptionTester()));\n  REQUIRE(QueueReturn::Success != queue.tryPushBack(ExceptionTester()));\n\n  ExceptionTester::throwFrom = std::thread::id();\n\n  producer.join();\n}\n\nTEST_CASE(\"Move it\")\n{\n  static constexpr size_t queueSize = 40;\n\n  Queue<MoveTester> queue{queueSize};\n\n  size_t counter = 0;\n\n  queue.pushBack(MoveTester{counter, 0});\n\n  REQUIRE(1u == counter);\n\n  MoveTester tester2(counter, 2);\n  queue.pushBack(std::move(tester2));\n\n  REQUIRE(tester2.moved);\n  REQUIRE(2u == counter);\n\n  REQUIRE(QueueReturn::Success == queue.tryPushBack(MoveTester{counter, 3}));\n  REQUIRE(3u == counter);\n\n  MoveTester tester4(counter, 4);\n  REQUIRE(QueueReturn::Success == queue.tryPushBack(std::move(tester4)));\n\n  REQUIRE(tester4.moved);\n  REQUIRE(4u == counter);\n\n  MoveTester popped = queue.popFront();\n  (void)popped;\n\n  REQUIRE(5u == counter);\n\n  std::optional<MoveTester> optPopped = queue.tryPopFront();\n\n  REQUIRE(optPopped);\n\n  // Moved twice here to construct the optional.\n  REQUIRE(6u == counter);\n}\n"
  },
  {
    "path": "test/util/thread/test_llarp_util_queue_manager.cpp",
    "content": "#include <llarp/util/thread/queue_manager.hpp>\n\n#include <optional>\n#include <vector>\n#include <catch2/catch.hpp>\n\nusing namespace llarp::thread;\n\nvoid\ngeneration(QueueManager& manager, uint32_t pushIndex, uint32_t popIndex)\n{\n  REQUIRE(pushIndex >= popIndex);\n  REQUIRE(pushIndex - popIndex <= manager.capacity());\n\n  for (uint32_t i = 0; i < popIndex; ++i)\n  {\n    uint32_t gen = 0;\n    uint32_t index = 0;\n\n    (void)manager.reservePushIndex(gen, index);\n    manager.commitPushIndex(gen, index);\n\n    auto result = manager.reservePopIndex(gen, index);\n\n    REQUIRE(result == QueueReturn::Success);\n    REQUIRE(index == i % manager.capacity());\n\n    manager.commitPopIndex(gen, index);\n  }\n\n  for (uint32_t i = popIndex; i < pushIndex; ++i)\n  {\n    uint32_t gen = 0;\n    uint32_t index = 0;\n\n    auto result = manager.reservePushIndex(gen, index);\n    REQUIRE(result == QueueReturn::Success);\n    REQUIRE(index == i % manager.capacity());\n\n    manager.commitPushIndex(gen, index);\n  }\n}\n\nclass IntQueue\n{\n private:\n  QueueManager manager;\n\n  std::vector<int> data;\n\n public:\n  IntQueue(const IntQueue&) = delete;\n\n  explicit IntQueue(size_t capacity) : manager(capacity), data(capacity, 0)\n  {}\n\n  bool\n  tryPushBack(int value)\n  {\n    uint32_t gen = 0;\n    uint32_t index = 0;\n\n    if (manager.reservePushIndex(gen, index) == QueueReturn::Success)\n    {\n      data[index] = value;\n      manager.commitPushIndex(gen, index);\n      return true;\n    }\n    else\n    {\n      return false;\n    }\n  }\n\n  std::optional<int>\n  tryPopFront()\n  {\n    uint32_t gen = 0;\n    uint32_t index = 0;\n\n    if (manager.reservePopIndex(gen, index) == QueueReturn::Success)\n    {\n      int result = data[index];\n      manager.commitPopIndex(gen, index);\n      return result;\n    }\n    else\n    {\n      return std::nullopt;\n    }\n  }\n\n  size_t\n  size() const\n  {\n    return manager.size();\n  }\n\n  size_t\n  capacity() const\n  {\n    return manager.capacity();\n  }\n};\n\n// This class exactly mirrors the data of the QueueManager, and is used for\n// both debugging and whitebox testing.\nstruct QueueData\n{\n public:\n  QueueManager::AtomicIndex m_pushIndex;  // Index in the buffer that the next\n                                          // element will be added to.\n\n  char m_pushPadding[QueueManager::Alignment - sizeof(QueueManager::AtomicIndex)];\n\n  QueueManager::AtomicIndex m_popIndex;  // Index in the buffer that the next\n                                         // element will be removed from.\n\n  char m_popPadding[QueueManager::Alignment - sizeof(QueueManager::AtomicIndex)];\n\n  const size_t m_capacity;  // max size of the manager.\n\n  const uint32_t m_maxGeneration;  // Maximum generation for this object.\n\n  const uint32_t m_maxCombinedIndex;  // Maximum combined value of index and\n                                      // generation for this object.\n\n  std::uint32_t* m_states;  // Array of index states.\n};\n\nstatic_assert(sizeof(QueueData) == sizeof(QueueManager), \"QueueData not updated\");\n\nstatic constexpr uint32_t GENERATION_COUNT_SHIFT = 0x2;\nstatic constexpr uint32_t ELEMENT_STATE_MASK = 0x3;\n\nstruct QueueIntrospection\n{\n private:\n  const QueueData* data;\n\n public:\n  QueueIntrospection(const QueueManager& manager)\n      : data(reinterpret_cast<const QueueData*>(&manager))\n  {}\n\n  uint32_t\n  pushIndex() const\n  {\n    return data->m_pushIndex % capacity();\n  }\n\n  uint32_t\n  pushGeneration() const\n  {\n    return data->m_pushIndex / capacity();\n  }\n\n  uint32_t\n  popIndex() const\n  {\n    return data->m_popIndex % capacity();\n  }\n\n  uint32_t\n  popGeneration() const\n  {\n    return data->m_popIndex / capacity();\n  }\n\n  uint32_t\n  elementGen(uint32_t index) const\n  {\n    return data->m_states[index] >> GENERATION_COUNT_SHIFT;\n  }\n\n  ElementState\n  elementState(uint32_t index) const\n  {\n    return static_cast<ElementState>(data->m_states[index] & ELEMENT_STATE_MASK);\n  }\n\n  uint32_t\n  maxGen() const\n  {\n    return data->m_maxGeneration;\n  }\n\n  uint32_t\n  maxCombinedIndex() const\n  {\n    return data->m_maxCombinedIndex;\n  }\n\n  uint32_t\n  capacity() const\n  {\n    return data->m_capacity;\n  }\n};\n\nvoid\nadjustGeneration(QueueManager& manager, uint32_t gen)\n{\n  QueueData* data = reinterpret_cast<QueueData*>(&manager);\n\n  auto capacity = manager.capacity();\n\n  for (size_t i = 0; i < capacity; ++i)\n  {\n    data->m_states[i] = gen << GENERATION_COUNT_SHIFT;\n  }\n\n  *reinterpret_cast<QueueManager::AtomicIndex*>(&data->m_pushIndex) = (gen * capacity);\n  *reinterpret_cast<QueueManager::AtomicIndex*>(&data->m_popIndex) = (gen * capacity);\n}\n\nvoid\ndirtyGenerate(QueueManager& manager, uint32_t pushCombinedIndex, uint32_t popCombinedIndex)\n{\n  REQUIRE(pushCombinedIndex >= popCombinedIndex);\n  REQUIRE(pushCombinedIndex - popCombinedIndex <= manager.capacity());\n\n  uint32_t capacity = manager.capacity();\n\n  uint32_t start = static_cast<uint32_t>(popCombinedIndex / manager.capacity());\n\n  adjustGeneration(manager, start);\n  generation(\n      manager, pushCombinedIndex - (start * capacity), popCombinedIndex - (start * capacity));\n}\n\nTEST_CASE(\"Simple usage\")\n{\n  IntQueue queue(2);\n\n  bool rc = queue.tryPushBack(1);\n  REQUIRE(rc);\n\n  rc = queue.tryPushBack(2);\n  REQUIRE(rc);\n\n  rc = queue.tryPushBack(3);\n  REQUIRE_FALSE(rc);\n\n  REQUIRE(2u == queue.size());\n\n  auto result = queue.tryPopFront();\n\n  REQUIRE(result);\n  REQUIRE(1 == *result);\n}\n\nTEST_CASE(\"Push\")\n{\n  uint32_t val = GENERATE(range(1u, 100u));\n\n  QueueManager manager(val);\n\n  REQUIRE(0u == manager.size());\n\n  uint32_t gen = 0;\n  uint32_t index = 0;\n\n  for (uint32_t i = 0; i < val; ++i)\n  {\n    REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n    REQUIRE(i == index);\n    REQUIRE(0u == gen);\n    REQUIRE(i == manager.size() - 1);\n    manager.commitPushIndex(gen, index);\n  }\n\n  REQUIRE(QueueReturn::QueueFull == manager.reservePushIndex(gen, index));\n  REQUIRE(val == manager.size());\n}\n\nTEST_CASE(\"Basic functionality, acquiringPopIndex\")\n{\n  uint32_t capacity = GENERATE(range(1u, 100u));\n\n  QueueManager manager(capacity);\n\n  REQUIRE(0u == manager.size());\n\n  uint32_t gen = 0;\n  uint32_t index = 0;\n\n  for (uint32_t g = 0; g < 3; ++g)\n  {\n    for (uint32_t idx = 0; idx < capacity; ++idx)\n    {\n      REQUIRE(QueueReturn::QueueEmpty == manager.reservePopIndex(gen, index));\n\n      REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n      REQUIRE(g == gen);\n      REQUIRE(index == idx);\n      REQUIRE(1u == manager.size());\n\n      manager.commitPushIndex(gen, index);\n      REQUIRE(1u == manager.size());\n\n      REQUIRE(QueueReturn::Success == manager.reservePopIndex(gen, index));\n      REQUIRE(g == gen);\n      REQUIRE(index == idx);\n      REQUIRE(0u == manager.size());\n\n      manager.commitPopIndex(gen, index);\n      REQUIRE(0u == manager.size());\n    }\n  }\n}\n\nTEST_CASE(\"Basic functionality, pushIndex\")\n{\n  uint32_t capacity = GENERATE(range(1u, 100u));\n\n  QueueManager manager(capacity);\n\n  REQUIRE(0u == manager.size());\n\n  uint32_t generation = 0;\n  uint32_t index = 0;\n\n  // Fill the queue\n  for (uint32_t idx = 0; idx < capacity; ++idx)\n  {\n    manager.reservePushIndex(generation, index);\n    manager.commitPushIndex(generation, index);\n  }\n\n  REQUIRE(capacity == manager.size());\n\n  for (uint32_t gen = 0; gen < 3; ++gen)\n  {\n    for (uint32_t idx = 0; idx < capacity; ++idx)\n    {\n      REQUIRE(QueueReturn::QueueFull == manager.reservePushIndex(generation, index));\n\n      REQUIRE(QueueReturn::Success == manager.reservePopIndex(generation, index));\n\n      REQUIRE(generation == gen);\n      REQUIRE(index == idx);\n      REQUIRE(capacity - 1 == manager.size());\n\n      manager.commitPopIndex(generation, index);\n      REQUIRE(capacity - 1 == manager.size());\n\n      REQUIRE(QueueReturn::Success == manager.reservePushIndex(generation, index));\n\n      REQUIRE(generation == gen + 1);\n      REQUIRE(index == idx);\n      REQUIRE(manager.size() == capacity);\n\n      manager.commitPushIndex(generation, index);\n      REQUIRE(manager.size() == capacity);\n    }\n  }\n}\n\n// Potential issues:\n// - That pushing an element at the max combined index will push the next\n// element at index 0\n// - That popping an element at the max combined index will pop the next\n// element at index 0\n// - That size returns the correct size when the push index has gone past the\n// max combined index\n// - That reservePopIndexForClear and abortPushIndexReservation clear the\n// correct element and increment push/pop\n\nTEST_CASE(\"Push at max\")\n{\n  QueueManager manager(1);\n\n  QueueIntrospection state{manager};\n\n  const uint32_t MAX_COMBINED_INDEX = std::numeric_limits<uint32_t>::max() >> 2;\n  const uint32_t MAX_GENERATION = std::numeric_limits<uint32_t>::max() >> 2;\n\n  const uint32_t maxGeneration = QueueIntrospection(manager).maxGen();\n  const uint32_t maxCombinedIndex = QueueIntrospection(manager).maxCombinedIndex();\n\n  REQUIRE(maxGeneration == MAX_GENERATION);\n  REQUIRE(maxCombinedIndex == MAX_COMBINED_INDEX);\n\n  dirtyGenerate(manager, MAX_COMBINED_INDEX, MAX_COMBINED_INDEX);\n\n  uint32_t gen = 0;\n  uint32_t index = 0;\n\n  REQUIRE(0u == manager.size());\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n  REQUIRE(MAX_GENERATION == gen);\n  REQUIRE(0u == index);\n  manager.commitPushIndex(gen, index);\n\n  REQUIRE(QueueReturn::Success == manager.reservePopIndex(gen, index));\n  REQUIRE(MAX_GENERATION == gen);\n  REQUIRE(0u == index);\n  manager.commitPopIndex(gen, index);\n  REQUIRE(0u == manager.size());\n\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n  REQUIRE(0u == gen);\n  REQUIRE(0u == index);\n  manager.commitPushIndex(gen, index);\n  REQUIRE(1u == manager.size());\n\n  REQUIRE(QueueReturn::Success == manager.reservePopIndex(gen, index));\n  REQUIRE(0u == gen);\n  REQUIRE(0u == index);\n  manager.commitPopIndex(gen, index);\n  REQUIRE(0u == manager.size());\n}\n\nstruct CombinedIndexData\n{\n  uint32_t capacity;\n  uint32_t pushIndex;\n  uint32_t popIndex;\n};\n\nstd::ostream&\noperator<<(std::ostream& os, CombinedIndexData d)\n{\n  os << \"[ capacity = \" << d.capacity << \" pushIndex = \" << d.pushIndex\n     << \" popIndex = \" << d.popIndex << \" ]\";\n  return os;\n}\n\nstd::vector<CombinedIndexData> PopAtMaxData{// Capacity 2 queues for a couple generations\n                                            {2, 1, 0},\n                                            {2, 2, 0},\n                                            {2, 2, 1},\n                                            {2, 2, 2},\n                                            {2, 3, 1},\n                                            {2, 3, 2},\n                                            {2, 3, 3},\n                                            {2, 4, 2},\n                                            {2, 4, 3},\n                                            {2, 4, 4},\n\n                                            // Capacity 3 queues for a couple generations\n                                            {3, 2, 0},\n                                            {3, 3, 0},\n                                            {3, 3, 1},\n                                            {3, 3, 2},\n                                            {3, 3, 3},\n                                            {3, 4, 1},\n                                            {3, 4, 2},\n                                            {3, 4, 3},\n                                            {3, 4, 4},\n                                            {3, 5, 2},\n                                            {3, 5, 3},\n                                            {3, 5, 4},\n                                            {3, 5, 5},\n\n                                            // Capacity 7 queue\n                                            {7, 6, 0},\n                                            {7, 7, 0},\n                                            {7, 7, 6},\n                                            {7, 13, 7},\n                                            {7, 14, 7}};\n\nTEST_CASE(\"Pop at max\")\n{\n  const auto& d = GENERATE(from_range(PopAtMaxData));\n\n  QueueManager manager(d.capacity);\n\n  const uint32_t NUM_GEN = QueueManager::numGenerations(d.capacity);\n  const uint32_t MAX_GEN = NUM_GEN - 1;\n\n  adjustGeneration(manager, MAX_GEN);\n\n  uint32_t gen = 0;\n  uint32_t index = 0;\n\n  // Push and pop elements up until the pop-index.\n\n  for (size_t j = 0; j < d.popIndex; ++j)\n  {\n    uint32_t INDEX = j % d.capacity;\n    uint32_t GEN = (MAX_GEN + j / d.capacity) % NUM_GEN;\n\n    REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n    REQUIRE(INDEX == index);\n    REQUIRE(GEN == gen);\n    manager.commitPushIndex(gen, index);\n    REQUIRE(1u == manager.size());\n\n    REQUIRE(QueueReturn::Success == manager.reservePopIndex(gen, index));\n\n    REQUIRE(INDEX == index);\n    REQUIRE(GEN == gen);\n    manager.commitPopIndex(gen, index);\n    REQUIRE(0u == manager.size());\n  }\n\n  // Push elements up to the push index\n\n  for (size_t j = d.popIndex; j < d.pushIndex; ++j)\n  {\n    uint32_t INDEX = j % d.capacity;\n    uint32_t GEN = (MAX_GEN + j / d.capacity) % NUM_GEN;\n\n    REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n\n    REQUIRE(INDEX == index);\n    REQUIRE(GEN == gen);\n    manager.commitPushIndex(gen, index);\n    REQUIRE(j - d.popIndex + 1 == manager.size());\n  }\n\n  // Pop elements until the queue is empty.\n\n  for (size_t j = d.popIndex; j < d.pushIndex; ++j)\n  {\n    uint32_t INDEX = j % d.capacity;\n    uint32_t GEN = (MAX_GEN + j / d.capacity) % NUM_GEN;\n\n    REQUIRE(QueueReturn::Success == manager.reservePopIndex(gen, index));\n\n    REQUIRE(INDEX == index);\n    REQUIRE(GEN == gen);\n    manager.commitPopIndex(gen, index);\n    REQUIRE(d.pushIndex - j - 1 == manager.size());\n  }\n}\n\nstd::vector<CombinedIndexData> ReservePopIndexForClearData{\n    // Capacity 2 queues for a couple generations\n    {2, 1, 0},\n    {2, 2, 1},\n    {2, 2, 2},\n    {2, 3, 2},\n    {2, 3, 3},\n    {2, 4, 3},\n    {2, 4, 4},\n\n    // Capacity 3 queues for a couple generations\n    {3, 2, 0},\n    {3, 3, 1},\n    {3, 3, 2},\n    {3, 3, 3},\n    {3, 4, 2},\n    {3, 4, 3},\n    {3, 4, 4},\n    {3, 5, 3},\n    {3, 5, 4},\n    {3, 5, 5},\n\n    // Capacity 7 queue\n    {7, 6, 0},\n    {7, 7, 6},\n    {7, 13, 7},\n};\n\nTEST_CASE(\"Reserve pop index for clear\")\n{\n  const auto& d = GENERATE(from_range(ReservePopIndexForClearData));\n\n  QueueManager manager(d.capacity);\n  const uint32_t NUM_GEN = QueueManager::numGenerations(d.capacity);\n  const uint32_t MAX_GEN = NUM_GEN - 1;\n\n  adjustGeneration(manager, MAX_GEN);\n\n  generation(manager, d.pushIndex, d.popIndex);\n\n  // Pop elements until the queue is empty\n\n  uint32_t endGeneration = 0;\n  uint32_t endIndex = 0;\n  uint32_t gen = 0;\n  uint32_t index = 0;\n\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(endGeneration, endIndex));\n\n  for (uint32_t j = d.popIndex; j < d.pushIndex; ++j)\n  {\n    uint32_t INDEX = j % d.capacity;\n    uint32_t GEN = (MAX_GEN + j / d.capacity) % NUM_GEN;\n\n    REQUIRE(manager.reservePopForClear(gen, index, endGeneration, endIndex));\n\n    REQUIRE(INDEX == index);\n    REQUIRE(GEN == gen);\n    manager.commitPopIndex(gen, index);\n  }\n\n  REQUIRE_FALSE(manager.reservePopForClear(gen, index, endGeneration, endIndex));\n  manager.abortPushIndexReservation(endGeneration, endIndex);\n  REQUIRE(0u == manager.size());\n}\n\nstruct CircularDifferenceData\n{\n  uint32_t minuend;\n  uint32_t subtrahend;\n  uint32_t maxSize;\n  int32_t expectedValue;\n};\n\nstd::ostream&\noperator<<(std::ostream& os, CircularDifferenceData d)\n{\n  os << \"[ minuend = \" << d.minuend << \" subtrahend = \" << d.subtrahend\n     << \" maxSize = \" << d.maxSize << \" expectedValue = \" << d.expectedValue << \" ]\";\n  return os;\n}\n\nconstexpr uint32_t OUR_INT32_MAX = std::numeric_limits<int32_t>::max();\nconstexpr uint32_t OUR_INT32_MAX_1 = OUR_INT32_MAX + 1;\nconstexpr int32_t OUR_INT32_MAX_DIV = OUR_INT32_MAX_1 / 2;\n\nstd::vector<CircularDifferenceData> circularDifferenceData{\n    // capacity 1\n    {0, 0, 1, 0},\n\n    // capacity 2\n    {1, 1, 2, 0},\n    {1, 0, 2, 1},\n    {0, 1, 2, -1},\n\n    // capacity 3\n    {2, 0, 3, -1},\n    {2, 1, 3, 1},\n    {2, 2, 3, 0},\n    {1, 0, 3, 1},\n    {1, 1, 3, 0},\n    {1, 2, 3, -1},\n    {0, 0, 3, 0},\n    {0, 1, 3, -1},\n    {0, 2, 3, 1},\n\n    // capacity 4\n    {3, 0, 4, -1},\n    {3, 1, 4, 2},\n    {3, 2, 4, 1},\n    {3, 3, 4, 0},\n    {0, 3, 4, 1},\n    {1, 3, 4, -2},\n    {2, 3, 4, -1},\n    {3, 3, 4, 0},\n\n    // capacity INT_MAX\n    {OUR_INT32_MAX, 0, OUR_INT32_MAX_1, -1},\n    {0, OUR_INT32_MAX, OUR_INT32_MAX_1, 1},\n    {OUR_INT32_MAX_DIV, 0, OUR_INT32_MAX_1, OUR_INT32_MAX_DIV},\n    {0, OUR_INT32_MAX_DIV, OUR_INT32_MAX_1, -OUR_INT32_MAX_DIV},\n\n    // Examples circularDifference( 0, 359, 360) == 1\n    // circularDifference( 359, 0, 360) == -1 circularDifference(\n    // 180, 0, 360) == 180 circularDifference( 0, 180, 360) == -180\n\n    {0, 359, 360, 1},\n    {359, 0, 360, -1},\n    {180, 0, 360, 180},\n    {0, 180, 360, -180},\n};\n\nTEST_CASE(\"Circular difference\")\n{\n  const auto& data = GENERATE(from_range(circularDifferenceData));\n\n  REQUIRE(\n      data.expectedValue\n      == QueueManager::circularDifference(data.minuend, data.subtrahend, data.maxSize));\n}\n\nstd::vector<uint32_t> GenerationData{\n    1, 2, 3, 4, 15, 16, 17, QueueManager::MAX_CAPACITY - 1, QueueManager::MAX_CAPACITY};\n\nTEST_CASE(\"Num generations\")\n{\n  uint32_t capacity = GENERATE(from_range(GenerationData));\n  uint32_t numGen = QueueManager::numGenerations(capacity);\n\n  static const uint32_t MAX_ELEMENT_STATE_GEN = std::numeric_limits<uint32_t>::max() >> 2;\n\n  static const uint32_t MAX_COMBINED_INDEX = std::numeric_limits<uint32_t>::max() >> 1;\n\n  REQUIRE(numGen >= 2u);\n  REQUIRE(\n      ((MAX_ELEMENT_STATE_GEN == numGen - 1)\n       || ((numGen * capacity - 1 <= MAX_COMBINED_INDEX)\n           && ((numGen + 1) * capacity - 1 > MAX_COMBINED_INDEX))));\n}\n\nTEST_CASE(\"Abort push index reservation\")\n{\n  uint32_t genA = 0;\n  uint32_t genB = 0;\n  uint32_t indexA = 0;\n  uint32_t indexB = 0;\n\n  QueueManager manager(1);\n\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(genA, indexA));\n  REQUIRE(QueueReturn::Success != manager.reservePushIndex(genA, indexA));\n\n  manager.abortPushIndexReservation(genA, indexA);\n\n  REQUIRE(0u == manager.size());\n\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(genB, indexB));\n  REQUIRE(genA + 1 == genB);\n  REQUIRE(indexA == indexB);\n}\n\nstruct AbortData\n{\n  uint32_t capacity;\n  uint32_t pushIndex;\n  uint32_t popIndex;\n  uint32_t expectedClears;\n};\n\nstd::ostream&\noperator<<(std::ostream& os, AbortData d)\n{\n  os << \"[ capacity = \" << d.capacity << \" pushIndex = \" << d.pushIndex\n     << \" popIndex = \" << d.popIndex << \" expectedClears = \" << d.expectedClears << \" ]\";\n  return os;\n}\n\nstd::vector<AbortData> abortData{\n    {1, 0, 0, 0},\n\n    // Capacity 2 queues for a couple generations\n    {2, 0, 0, 0},\n    {2, 1, 0, 1},\n    {2, 1, 1, 0},\n    {2, 2, 1, 1},\n    {2, 2, 2, 0},\n    {2, 3, 2, 1},\n    {2, 3, 3, 0},\n\n    // Capacity 3 queues for a couple generations\n    {3, 0, 0, 0},\n    {3, 1, 0, 1},\n    {3, 1, 1, 0},\n    {3, 2, 0, 2},\n    {3, 2, 1, 1},\n    {3, 2, 2, 0},\n    {3, 3, 1, 2},\n    {3, 3, 2, 1},\n    {3, 3, 3, 0},\n    {3, 4, 2, 2},\n    {3, 4, 3, 1},\n    {3, 4, 4, 0},\n\n    // Capacity 7 queue\n    {7, 14, 14, 0},\n    {7, 15, 14, 1},\n    {7, 20, 14, 6},\n    {7, 18, 18, 0},\n    {7, 19, 18, 1},\n    {7, 24, 18, 6},\n};\n\nTEST_CASE(\"Abort push\")\n{\n  const auto& data = GENERATE(from_range(abortData));\n\n  QueueManager manager(data.capacity);\n\n  generation(manager, data.pushIndex, data.popIndex);\n\n  const uint32_t END_GENERATION = data.pushIndex / data.capacity;\n  const uint32_t END_INDEX = data.pushIndex % data.capacity;\n\n  uint32_t gen = 0;\n  uint32_t index = 0;\n\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n  REQUIRE(END_GENERATION == gen);\n  REQUIRE(END_INDEX == index);\n\n  for (uint32_t i = 0; i < data.expectedClears; ++i)\n  {\n    REQUIRE(manager.reservePopForClear(gen, index, END_GENERATION, END_INDEX));\n\n    REQUIRE((data.popIndex + i) / data.capacity == gen);\n    REQUIRE((data.popIndex + i) % data.capacity == index);\n\n    manager.commitPopIndex(gen, index);\n  }\n\n  REQUIRE_FALSE(manager.reservePopForClear(gen, index, END_GENERATION, END_INDEX));\n\n  manager.abortPushIndexReservation(END_GENERATION, END_INDEX);\n\n  // Verify the queue is now empty, and the current push index has changed\n\n  REQUIRE(0u == manager.size());\n  for (uint32_t i = 0; i < data.capacity; ++i)\n  {\n    REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n    REQUIRE(i + 1 == manager.size());\n\n    REQUIRE(END_GENERATION * data.capacity + END_INDEX + i + 1 == gen * data.capacity + index);\n  }\n\n  REQUIRE(QueueReturn::Success != manager.reservePushIndex(gen, index));\n  REQUIRE(data.capacity == manager.size());\n}\n\n// Testing reservePopForClear\n// - Failure is returned when the head of the queue is the same as the given end\n// generation and index\n// - Success is returned and clears the queue head when the current pop index is\n// not the given end generation and index\n// - We do not clear an index reserved for popping\n\nTEST_CASE(\"Capacity 1\")\n{\n  // It is not possible to clear a pop index when the capacity is 1.\n\n  uint32_t gen = 0;\n  uint32_t index = 0;\n\n  // Random values to verify we didn't change them.\n  uint32_t resultGen = 1024;\n  uint32_t resultIndex = 1023;\n\n  QueueManager manager(1);\n\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n\n  REQUIRE_FALSE(manager.reservePopForClear(resultGen, resultIndex, gen, index));\n\n  REQUIRE(1024u == resultGen);\n  REQUIRE(1023u == resultIndex);\n\n  REQUIRE(1u == manager.size());\n}\n\nTEST_CASE(\"Capacity 2\")\n{\n  uint32_t gen = 0;\n  uint32_t index = 0;\n\n  // Random values to verify we didn't change them.\n  uint32_t resultGen = 1024;\n  uint32_t resultIndex = 1023;\n\n  QueueManager manager(2);\n\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n\n  REQUIRE_FALSE(manager.reservePopForClear(resultGen, resultIndex, gen, index));\n\n  REQUIRE(1024u == resultGen);\n  REQUIRE(1023u == resultIndex);\n  manager.commitPushIndex(gen, index);\n\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n\n  REQUIRE(manager.reservePopForClear(resultGen, resultIndex, gen, index));\n  REQUIRE(0u == resultGen);\n  REQUIRE(0u == resultIndex);\n  manager.commitPopIndex(resultGen, resultIndex);\n\n  manager.commitPushIndex(gen, index);\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n\n  REQUIRE(manager.reservePopForClear(resultGen, resultIndex, gen, index));\n  REQUIRE(0u == resultGen);\n  REQUIRE(1u == resultIndex);\n  manager.commitPopIndex(resultGen, resultIndex);\n  manager.commitPushIndex(gen, index);\n\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n\n  REQUIRE(manager.reservePopForClear(resultGen, resultIndex, gen, index));\n  REQUIRE(1u == resultGen);\n  REQUIRE(0u == resultIndex);\n  manager.commitPopIndex(resultGen, resultIndex);\n  manager.commitPushIndex(gen, index);\n\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n\n  REQUIRE(manager.reservePopForClear(resultGen, resultIndex, gen, index));\n  REQUIRE(1u == resultGen);\n  REQUIRE(1u == resultIndex);\n  manager.commitPopIndex(resultGen, resultIndex);\n  manager.commitPushIndex(gen, index);\n\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n\n  REQUIRE(manager.reservePopForClear(resultGen, resultIndex, gen, index));\n  REQUIRE(2u == resultGen);\n  REQUIRE(0u == resultIndex);\n  manager.commitPopIndex(resultGen, resultIndex);\n  manager.commitPushIndex(gen, index);\n}\n\nstruct ReserveData\n{\n  uint32_t capacity;\n  uint32_t pushIndex;\n  uint32_t popIndex;\n  uint32_t expectedClears;\n};\n\nstd::ostream&\noperator<<(std::ostream& os, ReserveData d)\n{\n  os << \"[ capacity = \" << d.capacity << \" pushIndex = \" << d.pushIndex\n     << \" popIndex = \" << d.popIndex << \" expectedClears = \" << d.expectedClears << \" ]\";\n  return os;\n}\n\nstd::vector<ReserveData> reserveData{\n    {1, 0, 0, 0},\n\n    // Capacity 2 queues for a couple generations\n    {2, 0, 0, 0},\n    {2, 1, 0, 1},\n    {2, 1, 1, 0},\n    {2, 2, 1, 1},\n    {2, 2, 2, 0},\n    {2, 3, 2, 1},\n    {2, 3, 3, 0},\n\n    // Capacity 3 queues for a couple generations\n    {3, 0, 0, 0},\n    {3, 1, 0, 1},\n    {3, 1, 1, 0},\n    {3, 2, 0, 2},\n    {3, 2, 1, 1},\n    {3, 2, 2, 0},\n    {3, 3, 1, 2},\n    {3, 3, 2, 1},\n    {3, 3, 3, 0},\n    {3, 4, 2, 2},\n    {3, 4, 3, 1},\n    {3, 4, 4, 0},\n\n    // Capacity 7 queue\n    {7, 14, 14, 0},\n    {7, 15, 14, 1},\n    {7, 20, 14, 6},\n    {7, 18, 18, 0},\n    {7, 19, 18, 1},\n    {7, 24, 18, 6},\n};\n\nTEST_CASE(\"Reserve, clear\")\n{\n  const auto& data = GENERATE(from_range(reserveData));\n  QueueManager manager(data.capacity);\n\n  generation(manager, data.pushIndex, data.popIndex);\n\n  const uint32_t endGen = data.pushIndex / data.capacity;\n  const uint32_t endIdx = data.pushIndex % data.capacity;\n\n  uint32_t gen = 0;\n  uint32_t index = 0;\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n  REQUIRE(endGen == gen);\n  REQUIRE(endIdx == index);\n\n  for (unsigned int j = 0; j < data.expectedClears; ++j)\n  {\n    REQUIRE(manager.reservePopForClear(gen, index, endGen, endIdx));\n    REQUIRE((data.popIndex + j) / data.capacity == gen);\n    REQUIRE((data.popIndex + j) % data.capacity == index);\n    manager.commitPopIndex(gen, index);\n  }\n  REQUIRE_FALSE(manager.reservePopForClear(gen, index, endGen, endIdx));\n  manager.commitPushIndex(endGen, endIdx);\n  REQUIRE(1u == manager.size());\n}\n\nTEST_CASE(\"Enabled\")\n{\n  QueueManager manager(3);\n\n  REQUIRE(manager.enabled());\n\n  uint32_t gen = 0;\n  uint32_t index = 0;\n\n  // Insert 2 elements.\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n  manager.commitPushIndex(gen, index);\n  REQUIRE(1u == manager.size());\n\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n  manager.commitPushIndex(gen, index);\n  REQUIRE(2u == manager.size());\n\n  // Disable the queue.\n  manager.disable();\n  REQUIRE_FALSE(manager.enabled());\n\n  // Test that attempting to push fails.\n  REQUIRE(QueueReturn::QueueDisabled == manager.reservePushIndex(gen, index));\n  REQUIRE(2u == manager.size());\n\n  // Test that attempting to pop succeeds.\n  REQUIRE(QueueReturn::Success == manager.reservePopIndex(gen, index));\n  manager.commitPopIndex(gen, index);\n  REQUIRE(1u == manager.size());\n\n  // Test that attempting to push still fails.\n  REQUIRE(QueueReturn::QueueDisabled == manager.reservePushIndex(gen, index));\n  REQUIRE(1u == manager.size());\n\n  // Disable the queue a second time, and verify that has no effect.\n  manager.disable();\n  REQUIRE_FALSE(manager.enabled());\n\n  // Test that attempting to push still fails.\n  REQUIRE(QueueReturn::QueueDisabled == manager.reservePushIndex(gen, index));\n  REQUIRE(1u == manager.size());\n\n  // Enable the queue.\n  manager.enable();\n  REQUIRE(manager.enabled());\n\n  // Test that attempting to push succeeds.\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n  manager.commitPushIndex(gen, index);\n  REQUIRE(2u == manager.size());\n\n  // Test that attempting to pop succeeds.\n  REQUIRE(QueueReturn::Success == manager.reservePopIndex(gen, index));\n  manager.commitPopIndex(gen, index);\n  REQUIRE(1u == manager.size());\n\n  // Enable the queue a second time, and verify that has no effect.\n  manager.enable();\n  REQUIRE(manager.enabled());\n\n  // Test that attempting to push succeeds.\n  REQUIRE(QueueReturn::Success == manager.reservePushIndex(gen, index));\n  manager.commitPushIndex(gen, index);\n  REQUIRE(2u == manager.size());\n}\n"
  },
  {
    "path": "test/win32/test.rc",
    "content": "// WARNING: for the love of all that is good and holy\n// please DO NOT convert this file to UTF-8, much less\n// UTF-16 - the UNIX cross-rc does not understand UTF-16,\n// and UTF-8 chews up the copyright symbols.\n// -rick\n//\n// Microsoft Visual C++ generated resource script.\n//\n#include <win32/resource.h>\n#include <constants/version.h>\n#ifdef __GNUC__\n#include <winresrc.h>\n#endif\n/////////////////////////////////////////////////////////////////////////////\n// English (United States) resources\n\n#define STRINGIZER(version) #version\n\n#ifdef LLARP_RELEASE_MOTTO\n#define VERSION_STRING(version, codename, revision) \\\n  STRINGIZER(version) \"-release [\" STRINGIZER(codename) \"] (rev-\" STRINGIZER(revision) \")\"\n#else\n#define VERSION_STRING(version, revision) \\\n  STRINGIZER(version) STRINGIZER(revision)\n#endif\n\n#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\n#ifdef _WIN32\nLANGUAGE 1033,1\n#pragma code_page(1252)\n#endif //_WIN32\n\n#ifdef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// TEXTINCLUDE\n//\n\n1 TEXTINCLUDE\nBEGIN\n    \"resource.h\\0\"\nEND\n\n2 TEXTINCLUDE\nBEGIN\n    \"\\0\"\nEND\n\n3 TEXTINCLUDE\nBEGIN\n    \"\\r\\n\"\n    \"\\0\"\nEND\n\n#endif    // APSTUDIO_INVOKED\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// Version\n//\n\nVS_VERSION_INFO VERSIONINFO\n FILEVERSION LLARP_VERSION\n PRODUCTVERSION LLARP_VERSION\n FILEFLAGSMASK 0x17L\n#ifdef _DEBUG\n FILEFLAGS 0x3L\n#else\n FILEFLAGS 0x2L\n#endif\n FILEOS 0x40004L\n FILETYPE 0x1L\n FILESUBTYPE 0x0L\nBEGIN\n    BLOCK \"StringFileInfo\"\n    BEGIN\n        BLOCK \"040904b0\"\n        BEGIN\n            VALUE \"Comments\", \"LokiNET test suite\"\n            VALUE \"CompanyName\", \"Loki Foundation\"\n            VALUE \"FileDescription\", \"LokiNET for Microsoft Windows NT\"\n            VALUE \"FileVersion\", VERSION_STRING(LLARP_VERSION_TRIPLET, LLARP_RELEASE_MOTTO, VERSIONTAG)\n            VALUE \"InternalName\", \"llarpd\"\n            VALUE \"LegalCopyright\", \"Copyright 2018-2020 Jeff Becker, Rick V for the Loki Foundation. All rights reserved. This software is provided under the terms of the zlib-libpng licence; see the file LICENSE for details.\"\n            VALUE \"OriginalFilename\", \"llarpd.exe\"\n            VALUE \"ProductName\", \"LokiNET for Windows\"\n            VALUE \"ProductVersion\", VERSION_STRING(LLARP_VERSION_TRIPLET, LLARP_RELEASE_MOTTO, VERSIONTAG)\n        END\n    END\n    BLOCK \"VarFileInfo\"\n    BEGIN\n        VALUE \"Translation\", 0x409, 1200\n    END\nEND\n\n\n/////////////////////////////////////////////////////////////////////////////\n//\n// RT_MANIFEST\n//\n\n// Uncomment if your toolchain does not provide a default-manifest.o\n//4                       RT_MANIFEST             \"app.xml\"\n\n#endif    // English (United States) resources\n/////////////////////////////////////////////////////////////////////////////\n\n\n\n#ifndef APSTUDIO_INVOKED\n/////////////////////////////////////////////////////////////////////////////\n//\n// Generated from the TEXTINCLUDE 3 resource.\n//\n\n\n/////////////////////////////////////////////////////////////////////////////\n#endif    // not APSTUDIO_INVOKED\n"
  },
  {
    "path": "win32-setup/.gitignore",
    "content": "mbedtls*.tgz*\r\nmbedtls-*/\r\ncurl*.tar.xz*\r\ncurl-*/\r\ninclude/\r\nlib/\r\n*.pem\r\nLICENSE\r\n7z.exe\r\ndbghelp*\r\n*.7z"
  },
  {
    "path": "win32-setup/Makefile",
    "content": "# Makefile for windows install pkg and helper library\n# debug/internal builds need not support xp\n# release builds *should*, but this does NOT remain a hard\n# requirement for cutting releases, it can be pulled at any\n# time really\n\nCC=i686-w64-mingw32-gcc\nCXX=i686-w64-mingw32-g++\nCC64=x86_64-w64-mingw32-gcc\nCXX64=x86_64-w64-mingw32-g++\nCFLAGS=-Ofast -march=nocona -mfpmath=sse\nLIBS=-lws2_32\nLDFLAGS=-static\n\nall: regdbhelper.dll lokinet-bootstrap.exe 7z.exe dbghelp tcpv6 tap-win32 qt5-ui\ndefault: all\n\nifndef RELEASE\nregdbhelper.dll:\n\t$(CC) regdb_helper.c -o $@ -shared -Os -s\n\nmbedtls:\n\twget https://tls.mbed.org/download/mbedtls-2.16.3-apache.tgz\n\ttar xvf mbedtls-2.16.3-apache.tgz\n\tpatch -p0 -d mbedtls-2.16.3 < mbedtls-win32.patch\n\t$(MAKE) -j4 -C mbedtls-2.16.3/library CC=$(CC) CXX=$(CXX) CFLAGS=\"$(CFLAGS)\" LDFLAGS=$(LIBS)\n\tmkdir -p lib; mkdir -p include\n\tcp mbedtls-2.16.3/library/*.a lib\n\tcp -r mbedtls-2.16.3/include/mbedtls include\n\ncurl:\n\twget https://curl.haxx.se/download/curl-7.66.0.tar.xz\n\ttar xvf curl-7.66.0.tar.xz\n\tpatch -p1 < curl-win32.patch\n\tcd curl-7.66.0; ./configure --host=i686-w64-mingw32 --target=i686-w64-mingw32 CC=$(CC) CXX=$(CXX) CFLAGS=\"$(CFLAGS)\" LIBS=$(LIBS) --disable-shared --without-zlib --without-ssl --with-mbedtls=$(PWD) --enable-optimize --enable-http --disable-ftp --prefix=$(PWD) --disable-file --disable-ldap  --disable-ldaps  --disable-rtsp --enable-proxy --disable-dict --disable-telnet --disable-tftp --disable-pop3 --disable-imap --disable-smb --disable-smtp --disable-gopher --enable-manual\n\t$(MAKE) -j4 -C curl-7.66.0 install\n\nlokinet-bootstrap.exe: mbedtls curl dbghelp\n\tcp bin/curl.exe $@\n\twget -O rootcerts.pem https://curl.haxx.se/ca/cacert.pem\n\tcp ../LICENSE .;unix2dos LICENSE LICENSE\n\ndbghelp:\n\twget http://installer.lokinet.org/win32/dbghelp32.dll\n\twget http://installer.lokinet.org/win32/dbghelp64.dll\n\nelse\nregdbhelper.dll:\n\twget http://installer.lokinet.org/win32/regdbhelper.dll\n\nlokinet-bootstrap.exe:\n\twget http://installer.lokinet.org/win32/lokinet-bootstrap.exe\n\twget -O rootcerts.pem https://curl.haxx.se/ca/cacert.pem\n\tcp ../LICENSE .;unix2dos LICENSE LICENSE\n\ndbghelp:\n\twget http://installer.lokinet.org/win32/dbghelp32.dll\n\twget http://installer.lokinet.org/win32/dbghelp64.dll\nendif\n\n# Common rules\n7z.exe:\n\twget http://installer.lokinet.org/win32/7z.exe\n\ntcpv6:\n\twget http://installer.lokinet.org/win32/inet6.7z\n\ntap-win32:\n\twget http://installer.lokinet.org/win32/tap-windows-9.21.2.7z\n\tmv tap-windows-9.21.2.7z tuntapv9_n6.7z\n\twget http://installer.lokinet.org/win32/tap-windows-9.9.2_3.7z\n\tmv tap-windows-9.9.2_3.7z tuntapv9.7z\n\nlibuv:\n\tgit clone https://github.com/despair86/libuv.git\n\tcd libuv; ./autogen.sh; ./configure --host=i686-w64-mingw32 --target=i686-w64-mingw32 CC=$(CC) CXX=$(CXX) CFLAGS=\"$(CFLAGS)\" --disable-shared --prefix=$(PWD)\n\tmake -C libuv -j4 install\n\tmake -C libuv -j4 distclean\n\tcd libuv; ./configure --host=x86_64-w64-mingw32 --target=x86_64-w64-mingw32 CC=$(CC64) CXX=$(CXX64) CFLAGS=\"$(CFLAGS)\" --disable-shared\n\tmake -C libuv -j4\n\tcp libuv/.libs/libuv.a $(PWD)/lib64\n\nqt5-ui:\n\twget http://installer.lokinet.org/win32/lokinet-qt5-ui.7z\n\nclean:\n\t-rm -rf curl-7* include lib mbedtls-2* *.exe *.dll *.pem *.7z\n"
  },
  {
    "path": "win32-setup/config_migration.bat",
    "content": "@echo off\r\ncopy lokinet.ini lokinet.old.ini\r\ndel lokinet.ini\r\n%PROGRAMFILES%\\Loki Project\\Lokinet\\lokinet -g"
  },
  {
    "path": "win32-setup/curl-win32.patch",
    "content": "diff --git a/curl-7.66.0/include/curl/curl.h b/curl-patched/include/curl/curl.h\nindex ff0c7749..4d3fdbb5 100644\n--- a/curl-7.66.0/include/curl/curl.h\n+++ b/curl-patched/include/curl/curl.h\n@@ -65,6 +65,7 @@\n    included, since they can't co-exist without problems */\n #include <winsock2.h>\n #include <ws2tcpip.h>\n+#include <wspiapi.h>\n #endif\n #endif\n \ndiff --git a/curl-7.66.0/include/curl/system.h b/curl-patched/include/curl/system.h\nindex cd37c2bf..b96cfd8c 100644\n--- a/curl-7.66.0/include/curl/system.h\n+++ b/curl-patched/include/curl/system.h\n@@ -411,6 +411,7 @@\n #  include <winsock2.h>\n #  include <windows.h>\n #  include <ws2tcpip.h>\n+#  include <wspiapi.h>\n #endif\n \n /* CURL_PULL_SYS_TYPES_H is defined above when inclusion of header file  */\ndiff --git a/curl-7.66.0/lib/curl_setup.h b/curl-patched/lib/curl_setup.h\nindex 13af8cde..a0408d5c 100644\n--- a/curl-7.66.0/lib/curl_setup.h\n+++ b/curl-patched/lib/curl_setup.h\n@@ -255,6 +255,7 @@\n #    include <winsock2.h>\n #    ifdef HAVE_WS2TCPIP_H\n #      include <ws2tcpip.h>\n+#      include <wspiapi.h>\n #    endif\n #  else\n #    ifdef HAVE_WINSOCK_H\ndiff --git a/curl-7.66.0/lib/inet_pton.h b/curl-patched/lib/inet_pton.h\nindex 0209b9b7..67774fb9 100644\n--- a/curl-7.66.0/lib/inet_pton.h\n+++ b/curl-patched/lib/inet_pton.h\n@@ -32,6 +32,7 @@ int Curl_inet_pton(int, const char *, void *);\n #elif defined(HAVE_WS2TCPIP_H)\n /* inet_pton() exists in Vista or later */\n #include <ws2tcpip.h>\n+#include <wspiapi.h>\n #endif\n #define Curl_inet_pton(x,y,z) inet_pton(x,y,z)\n #endif\ndiff --git a/curl-7.66.0/src/tool_util.c b/curl-patched/src/tool_util.c\nindex 9990a463..8ea37f37 100644\n--- a/curl-7.66.0/src/tool_util.c\n+++ b/curl-patched/src/tool_util.c\n@@ -40,12 +40,7 @@ struct timeval tvnow(void)\n   ** is typically in the range of 10 milliseconds to 16 milliseconds.\n   */\n   struct timeval now;\n-#if defined(_WIN32_WINNT) && (_WIN32_WINNT >= 0x0600) && \\\n-    (!defined(__MINGW32__) || defined(__MINGW64_VERSION_MAJOR))\n-  ULONGLONG milliseconds = GetTickCount64();\n-#else\n   DWORD milliseconds = GetTickCount();\n-#endif\n   now.tv_sec = (long)(milliseconds / 1000);\n   now.tv_usec = (long)((milliseconds % 1000) * 1000);\n   return now;\ndiff --git a/curl-7.66.0/tests/server/util.c b/curl-patched/tests/server/util.c\nindex b0613380..00d0b0c3 100644\n--- a/curl-7.66.0/tests/server/util.c\n+++ b/curl-patched/tests/server/util.c\n@@ -415,12 +415,7 @@ static struct timeval tvnow(void)\n   ** is typically in the range of 10 milliseconds to 16 milliseconds.\n   */\n   struct timeval now;\n-#if defined(_WIN32_WINNT) && (_WIN32_WINNT >= 0x0600) && \\\n-    (!defined(__MINGW32__) || defined(__MINGW64_VERSION_MAJOR))\n-  ULONGLONG milliseconds = GetTickCount64();\n-#else\n   DWORD milliseconds = GetTickCount();\n-#endif\n   now.tv_sec = (long)(milliseconds / 1000);\n   now.tv_usec = (long)((milliseconds % 1000) * 1000);\n   return now;\n"
  },
  {
    "path": "win32-setup/extra_create_icons.nsis",
    "content": "CreateShortCut '$SMPROGRAMS\\$STARTMENU_FOLDER\\Lokinet.lnk' '$INSTDIR\\share\\gui\\lokinet-gui.exe'\n"
  },
  {
    "path": "win32-setup/extra_delete_icons.nsis",
    "content": "CreateShortCut '$SMPROGRAMS\\$STARTMENU_FOLDER\\Lokinet.lnk' '$INSTDIR\\share\\gui\\lokinet-gui.exe'\n"
  },
  {
    "path": "win32-setup/extra_install.nsis",
    "content": "ExecWait '$INSTDIR\\bin\\lokinet.exe --install'\nExecWait '$INSTDIR\\bin\\lokinet.exe -g C:\\ProgramData\\lokinet\\lokinet.ini'\nCopyFiles '$INSTDIR\\share\\bootstrap.signed' C:\\ProgramData\\lokinet\\bootstrap.signed\n\nifFileExists $INSTDIR\\share\\conf.d 0 +3\nCreateDirectory C:\\ProgramData\\lokinet\\conf.d\nCopyFiles '$INSTDIR\\share\\conf.d\\*.ini' C:\\ProgramData\\lokinet\\conf.d\n\nIfFileExists $INSTDIR\\bin\\WinDivert64.sys +2 0\nCopyFiles '$INSTDIR\\lib\\WinDivert64.sys' '$INSTDIR\\bin\\WinDivert64.sys'\n\nIfFileExists $INSTDIR\\bin\\WinDivert.sys +2 0\nCopyFiles '$INSTDIR\\lib\\WinDivert.sys' '$INSTDIR\\bin\\WinDivert.sys'"
  },
  {
    "path": "win32-setup/extra_preinstall.nsis",
    "content": "ExecWait 'taskkill /f /t /im lokinet-gui.exe'\n\nIfFileExists $INSTDIR\\bin\\lokinet.exe 0 +3\nExecWait 'net stop lokinet'\nExecWait '$INSTDIR\\bin\\lokinet.exe --remove'\n\n"
  },
  {
    "path": "win32-setup/extra_uninstall.nsis",
    "content": "ExecWait 'taskkill /f /t /im lokinet-gui.exe'\nExecWait 'net stop lokinet'\nExecWait 'sc stop windivert'\nExecWait '$INSTDIR\\bin\\lokinet.exe --remove'\n\nIfFileExists '$INSTDIR\\bin\\WinDivert64.sys' 0 +2\nDelete /REBOOTOK '$INSTDIR\\bin\\WinDivert64.sys'\n\nIfFileExists '$INSTDIR\\bin\\WinDivert.sys' 0 +2\nDelete /REBOOTOK '$INSTDIR\\bin\\WinDivert.sys'\n\nRMDir /r /REBOOTOK C:\\ProgramData\\lokinet\nRMDir /r /REBOOTOK '$INSTDIR\\share\\conf.d'\n"
  },
  {
    "path": "win32-setup/libsodium-1.0.17-win32.patch",
    "content": "diff --git a/src/libsodium/randombytes/salsa20/randombytes_salsa20_random.c b/src/libsodium/randombytes/salsa20/randombytes_salsa20_random.c\nindex 64c4cec5..42d02235 100644\n--- a/src/libsodium/randombytes/salsa20/randombytes_salsa20_random.c\n+++ b/src/libsodium/randombytes/salsa20/randombytes_salsa20_random.c\n@@ -58,15 +58,13 @@\n #ifdef _WIN32\n # include <windows.h>\n # include <sys/timeb.h>\n-# define RtlGenRandom SystemFunction036\n-# if defined(__cplusplus)\n-extern \"C\"\n-# endif\n-BOOLEAN NTAPI RtlGenRandom(PVOID RandomBuffer, ULONG RandomBufferLength);\n-# pragma comment(lib, \"advapi32.lib\")\n+# include <wincrypt.h>\n+# include <bcrypt.h>\n+ typedef NTSTATUS(FAR PASCAL *CNGAPI_DRBG)(BCRYPT_ALG_HANDLE, UCHAR *, ULONG,\n+                                          ULONG);\n # ifdef __BORLANDC__\n-#  define _ftime ftime\n-#  define _timeb timeb\n+# define _ftime ftime\n+# define _timeb timeb\n # endif\n #endif\n \n@@ -373,11 +371,44 @@ randombytes_salsa20_random_stir(void)\n # endif\n \n #else /* _WIN32 */\n-    if (! RtlGenRandom((PVOID) stream.key, (ULONG) sizeof stream.key)) {\n-        sodium_misuse(); /* LCOV_EXCL_LINE */\n+  HANDLE hCAPINg;\n+  BOOL rtld;\n+  CNGAPI_DRBG getrandom;\n+  HCRYPTPROV hProv;\n+  /* load bcrypt dynamically, see if we're already loaded */\n+  rtld    = FALSE;\n+  hCAPINg = GetModuleHandle(\"bcrypt.dll\");\n+  /* otherwise, load CNG manually */\n+  if(!hCAPINg)\n+  {\n+    hCAPINg = LoadLibraryEx(\"bcrypt.dll\", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);\n+    rtld    = TRUE;\n+  }\n+  if(hCAPINg)\n+  {\n+    /* call BCryptGenRandom(2) */\n+    getrandom = (CNGAPI_DRBG)GetProcAddress(hCAPINg, \"BCryptGenRandom\");\n+   if(!BCRYPT_SUCCESS(\n+           getrandom(NULL, stream.key, sizeof stream.key, BCRYPT_USE_SYSTEM_PREFERRED_RNG)))\n+    {\n+      sodium_misuse();\n     }\n+   /* don't leak lib refs */\n+    if(rtld)\n+      FreeLibrary(hCAPINg);\n+  }\n+  /* if that fails use the regular ARC4-SHA1 RNG (!!!) *cringes* */\n+  else\n+  {\n+    CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL,\n+                        CRYPT_VERIFYCONTEXT | CRYPT_SILENT);\n+    if(!CryptGenRandom(hProv, sizeof stream.key, stream.key))\n+    {\n+      sodium_misuse(); /* LCOV_EXCL_LINE */\n+    }\n+    CryptReleaseContext(hProv, 0);\n+  }\n #endif\n-\n     stream.initialized = 1;\n }\n \ndiff --git a/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c b/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c\nindex 99018f35..e2cacfb9 100644\n--- a/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c\n+++ b/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c\n@@ -48,28 +48,12 @@\n #include \"utils.h\"\n \n #ifdef _WIN32\n-/* `RtlGenRandom` is used over `CryptGenRandom` on Microsoft Windows based systems:\n- *  - `CryptGenRandom` requires pulling in `CryptoAPI` which causes unnecessary\n- *     memory overhead if this API is not being used for other purposes\n- *  - `RtlGenRandom` is thus called directly instead. A detailed explanation\n- *     can be found here: https://blogs.msdn.microsoft.com/michael_howard/2005/01/14/cryptographically-secure-random-number-on-windows-without-using-cryptoapi/\n- *\n- * In spite of the disclaimer on the `RtlGenRandom` documentation page that was\n- * written back in the Windows XP days, this function is here to stay. The CRT\n- * function `rand_s()` directly depends on it, so touching it would break many\n- * applications released since Windows XP.\n- *\n- * Also note that Rust, Firefox and BoringSSL (thus, Google Chrome and everything\n- * based on Chromium) also depend on it, and that libsodium allows the RNG to be\n- * replaced without patching nor recompiling the library.\n- */\n # include <windows.h>\n-# define RtlGenRandom SystemFunction036\n-# if defined(__cplusplus)\n-extern \"C\"\n-# endif\n-BOOLEAN NTAPI RtlGenRandom(PVOID RandomBuffer, ULONG RandomBufferLength);\n-# pragma comment(lib, \"advapi32.lib\")\n+# include <sys/timeb.h>\n+# include <wincrypt.h>\n+# include <bcrypt.h>\n+ typedef NTSTATUS(FAR PASCAL *CNGAPI_DRBG)(BCRYPT_ALG_HANDLE, UCHAR *, ULONG,\n+                                           ULONG);\n #endif\n \n #if defined(__OpenBSD__) || defined(__CloudABI__)\n@@ -359,9 +343,43 @@ randombytes_sysrandom_buf(void * const buf, const size_t size)\n     if (size > (size_t) 0xffffffffUL) {\n         sodium_misuse(); /* LCOV_EXCL_LINE */\n     }\n-    if (! RtlGenRandom((PVOID) buf, (ULONG) size)) {\n-        sodium_misuse(); /* LCOV_EXCL_LINE */\n+  HANDLE hCAPINg;\n+  BOOL rtld;\n+  CNGAPI_DRBG getrandom;\n+  HCRYPTPROV hProv;\n+  /* load bcrypt dynamically, see if we're already loaded */\n+  rtld    = FALSE;\n+  hCAPINg = GetModuleHandle(\"bcrypt.dll\");\n+  /* otherwise, load CNG manually */\n+  if(!hCAPINg)\n+  {\n+    hCAPINg = LoadLibraryEx(\"bcrypt.dll\", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);\n+    rtld    = TRUE;\n+  }\n+  if(hCAPINg)\n+  {\n+    /* call BCryptGenRandom(2) */\n+    getrandom = (CNGAPI_DRBG)GetProcAddress(hCAPINg, \"BCryptGenRandom\");\n+   if(!BCRYPT_SUCCESS(\n+           getrandom(NULL, buf, size, BCRYPT_USE_SYSTEM_PREFERRED_RNG)))\n+    {\n+      sodium_misuse();\n+    }\n+   /* don't leak lib refs */\n+    if(rtld)\n+      FreeLibrary(hCAPINg);\n+  }\n+  /* if that fails use the regular ARC4-SHA1 RNG (!!!) *cringes* */\n+  else\n+  {\n+    CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL,\n+                        CRYPT_VERIFYCONTEXT | CRYPT_SILENT);\n+    if(!CryptGenRandom(hProv, size, buf))\n+    {\n+      sodium_misuse(); /* LCOV_EXCL_LINE */\n     }\n+    CryptReleaseContext(hProv, 0);\n+  }\n # endif /* _WIN32 */\n }\n \n"
  },
  {
    "path": "win32-setup/libsodium-1.0.18-win32.patch",
    "content": "diff --git a/src/libsodium/randombytes/internal/randombytes_internal_random.c b/src/libsodium/randombytes/internal/randombytes_internal_random.c\nindex f0794f80..4e949ecc 100644\n--- a/src/libsodium/randombytes/internal/randombytes_internal_random.c\n+++ b/src/libsodium/randombytes/internal/randombytes_internal_random.c\n@@ -59,18 +59,16 @@\n #include \"utils.h\"\n \n #ifdef _WIN32\n-# include <windows.h>\n-# include <sys/timeb.h>\n-# define RtlGenRandom SystemFunction036\n-# if defined(__cplusplus)\n-extern \"C\"\n-# endif\n-BOOLEAN NTAPI RtlGenRandom(PVOID RandomBuffer, ULONG RandomBufferLength);\n-# pragma comment(lib, \"advapi32.lib\")\n-# ifdef __BORLANDC__\n-#  define _ftime ftime\n-#  define _timeb timeb\n-# endif\n+#include <windows.h>\n+#include <sys/timeb.h>\n+#include <wincrypt.h>\n+#include <bcrypt.h>\n+typedef NTSTATUS(FAR PASCAL *CNGAPI_DRBG)(BCRYPT_ALG_HANDLE, UCHAR *, ULONG,\n+                                          ULONG);\n+#ifdef __BORLANDC__\n+#define _ftime ftime\n+#define _timeb timeb\n+#endif\n #endif\n \n #define INTERNAL_RANDOM_BLOCK_SIZE crypto_core_hchacha20_OUTPUTBYTES\n@@ -431,11 +429,44 @@ randombytes_internal_random_stir(void)\n # else\n     sodium_misuse();\n # endif\n-\n #else /* _WIN32 */\n-    if (! RtlGenRandom((PVOID) stream.key, (ULONG) sizeof stream.key)) {\n-        sodium_misuse(); /* LCOV_EXCL_LINE */\n+  HANDLE hCAPINg;\n+  BOOL rtld;\n+  CNGAPI_DRBG getrandom;\n+  HCRYPTPROV hProv;\n+  /* load bcrypt dynamically, see if we're already loaded */\n+  rtld    = FALSE;\n+  hCAPINg = GetModuleHandle(\"bcrypt.dll\");\n+  /* otherwise, load CNG manually */\n+  if(!hCAPINg)\n+  {\n+    hCAPINg = LoadLibraryEx(\"bcrypt.dll\", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);\n+    rtld    = TRUE;\n+  }\n+  if(hCAPINg)\n+  {\n+    /* call BCryptGenRandom(2) */\n+    getrandom = (CNGAPI_DRBG)GetProcAddress(hCAPINg, \"BCryptGenRandom\");\n+    if(!BCRYPT_SUCCESS(\n+           getrandom(NULL, stream.key, sizeof stream.key, BCRYPT_USE_SYSTEM_PREFERRED_RNG)))\n+    {\n+      sodium_misuse();\n+    }\n+    /* don't leak lib refs */\n+    if(rtld)\n+      FreeLibrary(hCAPINg);\n+  }\n+  /* if that fails use the regular ARC4-SHA1 RNG (!!!) *cringes* */\n+  else\n+  {\n+    CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL,\n+                        CRYPT_VERIFYCONTEXT | CRYPT_SILENT);\n+    if(!CryptGenRandom(hProv, sizeof stream.key, stream.key))\n+    {\n+      sodium_misuse(); /* LCOV_EXCL_LINE */\n     }\n+    CryptReleaseContext(hProv, 0);\n+  }\n #endif\n \n     stream.initialized = 1;\ndiff --git a/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c b/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c\nindex 6657e8e6..37db3f97 100644\n--- a/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c\n+++ b/src/libsodium/randombytes/sysrandom/randombytes_sysrandom.c\n@@ -67,12 +67,11 @@\n  * replaced without patching nor recompiling the library.\n  */\n # include <windows.h>\n-# define RtlGenRandom SystemFunction036\n-# if defined(__cplusplus)\n-extern \"C\"\n-# endif\n-BOOLEAN NTAPI RtlGenRandom(PVOID RandomBuffer, ULONG RandomBufferLength);\n-# pragma comment(lib, \"advapi32.lib\")\n+#include <sys/timeb.h>\n+#include <wincrypt.h>\n+#include <bcrypt.h>\n+typedef NTSTATUS(FAR PASCAL *CNGAPI_DRBG)(BCRYPT_ALG_HANDLE, UCHAR *, ULONG,\n+                                          ULONG);\n #endif\n \n #if defined(__OpenBSD__) || defined(__CloudABI__) || defined(__wasi__)\n@@ -357,15 +356,45 @@ randombytes_sysrandom_buf(void * const buf, const size_t size)\n         safe_read(stream.random_data_source_fd, buf, size) != (ssize_t) size) {\n         sodium_misuse(); /* LCOV_EXCL_LINE */\n     }\n-# else /* _WIN32 */\n-    COMPILER_ASSERT(randombytes_BYTES_MAX <= 0xffffffffUL);\n-    if (size > (size_t) 0xffffffffUL) {\n-        sodium_misuse(); /* LCOV_EXCL_LINE */\n+#else /* _WIN32 */\n+  HANDLE hCAPINg;\n+  BOOL rtld;\n+  CNGAPI_DRBG getrandom;\n+  HCRYPTPROV hProv;\n+  /* load bcrypt dynamically, see if we're already loaded */\n+  rtld    = FALSE;\n+  hCAPINg = GetModuleHandle(\"bcrypt.dll\");\n+  /* otherwise, load CNG manually */\n+  if(!hCAPINg)\n+  {\n+    hCAPINg = LoadLibraryEx(\"bcrypt.dll\", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);\n+    rtld    = TRUE;\n+  }\n+  if(hCAPINg)\n+  {\n+    /* call BCryptGenRandom(2) */\n+    getrandom = (CNGAPI_DRBG)GetProcAddress(hCAPINg, \"BCryptGenRandom\");\n+    if(!BCRYPT_SUCCESS(\n+           getrandom(NULL, buf, size, BCRYPT_USE_SYSTEM_PREFERRED_RNG)))\n+    {\n+      sodium_misuse();\n     }\n-    if (! RtlGenRandom((PVOID) buf, (ULONG) size)) {\n-        sodium_misuse(); /* LCOV_EXCL_LINE */\n+    /* don't leak lib refs */\n+    if(rtld)\n+      FreeLibrary(hCAPINg);\n+  }\n+  /* if that fails use the regular ARC4-SHA1 RNG (!!!) *cringes* */\n+  else\n+  {\n+    CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL,\n+                        CRYPT_VERIFYCONTEXT | CRYPT_SILENT);\n+    if(!CryptGenRandom(hProv, size, buf))\n+    {\n+      sodium_misuse(); /* LCOV_EXCL_LINE */\n     }\n-# endif /* _WIN32 */\n+    CryptReleaseContext(hProv, 0);\n+  }\n+#endif\n }\n \n static uint32_t\n"
  },
  {
    "path": "win32-setup/lokinet-win32.iss",
    "content": "; Script generated by the Inno Script Studio Wizard.\r\n; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!\r\n\r\n#define MyAppName \"Lokinet\"\r\n#define MyAppVersion \"0.7.0\"\r\n#define MyAppPublisher \"Loki Project\"\r\n#define MyAppURL \"https://lokinet.org\"\r\n#define MyAppExeName \"lokinetui.exe\"\r\n; change this to avoid compiler errors  -despair\r\n#ifndef DevPath\r\n#define DevPath \"D:\\dev\\external\\llarp\\\"\r\n#endif\r\n#include \"version.txt\"\r\n\r\n; see ../LICENSE\r\n\r\n[Setup]\r\n; NOTE: The value of AppId uniquely identifies this application.\r\n; Do not use the same AppId value in installers for other applications.\r\n; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)\r\nAppId={{11335EAC-0385-4C78-A3AA-67731326B653}}\r\nAppName={#MyAppName}\r\nAppVersion={#MyAppVersion}\r\n#ifndef RELEASE\r\nAppVerName={#MyAppName} {#MyAppVersion}-dev\r\n#else\r\nAppVerName={#MyAppName} {#MyAppVersion}\r\n#endif\r\nAppPublisher={#MyAppPublisher}\r\nAppPublisherURL={#MyAppURL}\r\nAppSupportURL={#MyAppURL}\r\nAppUpdatesURL={#MyAppURL}\r\nDefaultDirName={pf}\\{#MyAppPublisher}\\{#MyAppName}\r\nDefaultGroupName={#MyAppName}\r\nAllowNoIcons=yes\r\nLicenseFile={#DevPath}LICENSE\r\nOutputDir={#DevPath}win32-setup\r\nOutputBaseFilename=lokinet-win32\r\nCompression=lzma2/ultra64\r\nSolidCompression=yes\r\nVersionInfoVersion=0.7.0\r\nVersionInfoCompany=Loki Project\r\nVersionInfoDescription=Lokinet for Microsoft Windows NT\r\n#ifndef RELEASE\r\nVersionInfoTextVersion=0.7.0-dev-{#VCSRev}\r\nVersionInfoProductTextVersion=0.7.0-dev-{#VCSRev}\r\n#else\r\nVersionInfoTextVersion=0.7.0\r\nVersionInfoProductTextVersion=0.7.0 ({#Codename})\r\n#endif\r\nVersionInfoProductName=Lokinet\r\nVersionInfoProductVersion=0.7.0\r\nInternalCompressLevel=ultra64\r\nMinVersion=0,5.0\r\nArchitecturesInstallIn64BitMode=x64\r\nVersionInfoCopyright=Copyright 2018-2020 Loki Project\r\nAppMutex=lokinet_win32_daemon,lokinet_qt5_ui,lokinet_dotnet_ui\r\n\r\n[Languages]\r\nName: \"english\"; MessagesFile: \"compiler:Default.isl\"\r\n\r\n[Tasks]\r\nName: \"desktopicon\"; Description: \"{cm:CreateDesktopIcon}\"; GroupDescription: \"{cm:AdditionalIcons}\"; Flags: unchecked\r\nName: \"quicklaunchicon\"; Description: \"{cm:CreateQuickLaunchIcon}\"; GroupDescription: \"{cm:AdditionalIcons}\"; Flags: unchecked;\r\nName: \"migrateconfigs\"; Description: \"Migrate existing configuration to enable modern UI\"; MinVersion: 6.0\r\n\r\n[Files]\r\n; only one of these is installed\r\n#ifdef SINGLE_ARCH\r\nSource: \"{#DevPath}build\\lokinet.exe\"; DestDir: \"{app}\"; Flags: ignoreversion\r\n; don't ship it, we don't have a public api yet!\r\n;Source: \"{#DevPath}build\\liblokinet-shared.dll\"; DestDir: \"{app}\"; Flags: ignoreversion\r\nSource: \"dbghelp64.dll\"; DestName: \"dbghelp.dll\"; DestDir: \"{app}\"; Flags: ignoreversion\r\n#else\r\nSource: \"{#DevPath}build\\lokinet.exe\"; DestDir: \"{app}\"; Flags: ignoreversion 32bit; Check: not IsWin64\r\n;Source: \"{#DevPath}build\\liblokinet-shared.dll\"; DestDir: \"{app}\"; Flags: ignoreversion 32bit; Check: not IsWin64\r\nSource: \"dbghelp32.dll\"; DestName: \"dbghelp.dll\"; DestDir: \"{app}\"; Flags: ignoreversion; Check: not IsWin64\r\nSource: \"{#DevPath}build64\\lokinet.exe\"; DestDir: \"{app}\"; Flags: ignoreversion 64bit; Check: IsWin64\r\n;Source: \"{#DevPath}build64\\liblokinet-shared.dll\"; DestDir: \"{app}\"; Flags: ignoreversion 64bit; Check: IsWin64\r\nSource: \"dbghelp64.dll\"; DestDir: \"{app}\"; DestName: \"dbghelp.dll\"; Flags: ignoreversion; Check: IsWin64\r\n#endif\r\n; UI has landed!\r\n#ifndef RELEASE\r\nSource: \"{#DevPath}ui-win32\\bin\\debug\\lokinetui.exe\"; DestDir: \"{app}\"; Flags: ignoreversion\r\nSource: \"{#DevPath}ui-win32\\bin\\debug\\lokinetui.exe.config\"; DestDir: \"{app}\"; Flags: ignoreversion\r\nSource: \"{#DevPath}ui-win32\\bin\\debug\\lokinetui.pdb\"; DestDir: \"{app}\"; Flags: ignoreversion\r\n#else\r\nSource: \"{#DevPath}ui-win32\\bin\\release\\lokinetui.exe\"; DestDir: \"{app}\"; Flags: ignoreversion\r\nSource: \"{#DevPath}ui-win32\\bin\\release\\lokinetui.exe.config\"; DestDir: \"{app}\"; Flags: ignoreversion\r\nSource: \"{#DevPath}ui-win32\\bin\\release\\lokinetui.pdb\"; DestDir: \"{app}\"; Flags: ignoreversion\r\n#endif\r\n; eh, might as well ship the 32-bit port of everything else\r\n;Source: \"{#DevPath}build\\catchAll.exe\"; DestDir: \"{app}\"; Flags: ignoreversion\r\nSource: \"{#DevPath}build\\lokinetctl.exe\"; DestDir: \"{app}\"; Flags: ignoreversion\r\nSource: \"LICENSE\"; DestDir: \"{app}\"; Flags: ignoreversion\r\nSource: \"lokinet-bootstrap.exe\"; DestDir: \"{app}\"; Flags: ignoreversion\r\nSource: \"rootcerts.pem\"; DestDir: \"{app}\"; Flags: ignoreversion\r\nSource: \"7z.exe\"; DestDir: \"{tmp}\"; Flags: deleteafterinstall\r\nSource: \"inet6.7z\"; DestDir: \"{app}\"; Flags: ignoreversion deleteafterinstall skipifsourcedoesntexist; MinVersion: 0,5.0; OnlyBelowVersion: 0,5.1; Check: not IsTcp6Installed\r\nSource: \"lokinet-qt5-ui.7z\"; DestDir: \"{app}\"; Flags: ignoreversion deleteafterinstall; MinVersion: 0,6.0;\r\nSource: \"lokinet.ico\"; DestDir: \"{app}\"; Flags: ignoreversion;\r\n; Copy the correct tuntap driver for the selected platform\r\nSource: \"tuntapv9.7z\"; DestDir: \"{app}\"; Flags: ignoreversion deleteafterinstall; OnlyBelowVersion: 0, 6.0; Check: not IsTapInstalled\r\nSource: \"tuntapv9_n6.7z\"; DestDir: \"{app}\"; Flags: ignoreversion deleteafterinstall; MinVersion: 0,6.0; Check: not IsTapInstalled\r\n\r\n; NOTE: Don't use \"Flags: ignoreversion\" on any shared system files\r\nSource: \"regdbhelper.dll\"; Flags: dontcopy\r\nSource: \"config_migration.bat\"; DestDir: \"{userappdata}\\.lokinet\"; Flags: deleteafterinstall; MinVersion: 0,6.0\r\n\r\n; build only if we have the 32-bit bins as well\r\n; (i.e. *not* a Travis CI build, travis isn't expected to have these around)\r\n#ifndef SINGLE_ARCH\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-bold.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Bold\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-bolditalic.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Bold Italic\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-boldoblique.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Bold Oblique\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-extralight.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Extralight\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-extralightitalic.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Extralight Italic\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-extralightoblique.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Extralight Oblique\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-heavy.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Heavy\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-heavyitalic.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Heavy Italic\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-heavyoblique.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Heavy Oblique\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-italic.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Italic\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-light.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Light\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-lightitalic.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Light Italic\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-lightoblique.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Light Oblique\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-medium.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Medium\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-mediumitalic.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Medium Italic\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-mediumoblique.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Medium Oblique\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-oblique.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Oblique\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-regular.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-thin.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Thin\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-thinitalic.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Thin Italic\"; Flags: onlyifdoesntexist uninsneveruninstall\r\nSource: \"C:\\Windows\\Fonts\\iosevka-term-thinoblique.ttf\"; DestDir: \"{fonts}\"; FontInstall: \"Iosevka Term Thin Oblique\"; Flags: onlyifdoesntexist uninsneveruninstall\r\n#endif\r\n\r\n[UninstallDelete]\r\nType: filesandordirs; Name: \"{app}\\tap-windows*\"\r\nType: filesandordirs; Name: \"{app}\\inet6_driver\"; MinVersion: 0,5.0; OnlyBelowVersion: 0,5.1\r\nType: filesandordirs; Name: \"{app}\\lokinet-qt5-ui\"; MinVersion: 0,6.0\r\nType: filesandordirs; Name: \"{userappdata}\\.lokinet\"\r\n\r\n[UninstallRun]\r\nFilename: \"{app}\\tap-windows-9.21.2\\remove.bat\"; WorkingDir: \"{app}\\tap-windows-9.21.2\"; Flags: runascurrentuser waituntilterminated skipifdoesntexist; RunOnceId: \"RemoveTap\"; MinVersion: 0,6.0\r\nFilename: \"{app}\\tap-windows-9.9.2\\remove.bat\"; WorkingDir: \"{app}\\tap-windows-9.9.2\"; Flags: runascurrentuser waituntilterminated skipifdoesntexist; RunOnceId: \"RemoveTap\"; OnlyBelowVersion: 0,6.0\r\n\r\n[Dirs]\r\nName: \"{userappdata}\\.lokinet\"\r\n\r\n[Code]\r\nvar\r\nTapInstalled: Boolean;\r\nVersion: TWindowsVersion;\r\nfunction reg_query_helper(): Integer;\r\nexternal 'reg_query_helper@files:regdbhelper.dll cdecl setuponly';\r\n\r\nprocedure CurStepChanged(CurStep: TSetupStep);\r\nbegin\r\n  if CurStep = ssPostInstall then\r\n  begin\r\n  if Version.NTPlatform and (Version.Major = 5) and (Version.Minor = 0) and (FileExists(ExpandConstant('{tmp}\\inet6.7z')) = true) then\r\n     // I need a better message...\r\n    MsgBox('Restart your computer, then set up IPv6 in Network Connections. [Adapter] > Properties > Install... > Protocol > Microsoft IPv6 Driver...', mbInformation, MB_OK);\r\n  if IsTaskSelected('migrateconfigs') then\r\n    MsgBox('Lokinet JSON-RPC API endpoint enabled. Any custom configuration was retained in %APPDATA%\\.lokinet\\lokinet.old.ini.', mbInformation, MB_OK);\r\n  end;\r\nend;\r\n\r\nfunction IsTapInstalled(): Boolean;\r\nbegin\r\nResult := TapInstalled;\r\nend;\r\n\r\nfunction IsTcp6Installed(): Boolean;\r\nbegin\r\n  if (FileExists(ExpandConstant('{sys}\\drivers\\tcpip6.sys')) = false) then\r\n  begin\r\n    Result := true;\r\n  end\r\n  else\r\n  begin\r\n    Result := false;\r\n  end;\r\nend;\r\n\r\nprocedure InitializeWizard();\r\nbegin\r\nGetWindowsVersionEx(Version);\r\nif (reg_query_helper() = 0) then\r\n  begin\r\n    TapInstalled := false;\r\n  end\r\nelse\r\n  begin\r\n    TapInstalled := true;\r\n  end;\r\nend;\r\n\r\n[Icons]\r\nName: \"{group}\\{#MyAppName}\"; Filename: \"{app}\\{#MyAppExeName}\"; OnlyBelowVersion: 0, 6.0; IconFilename: \"{app}\\lokinet.ico\"\r\nName: \"{group}\\{#MyAppName}\"; Filename: \"{app}\\lokinet-qt5-ui\\lokinetui.exe\"; MinVersion: 0, 6.0; IconFilename: \"{app}\\lokinet.ico\"\r\nName: \"{group}\\{cm:ProgramOnTheWeb,{#MyAppName}}\"; Filename: \"{#MyAppURL}\"\r\nName: \"{group}\\{cm:UninstallProgram,{#MyAppName}}\"; Filename: \"{uninstallexe}\"\r\nName: \"{commondesktop}\\{#MyAppName}\"; Filename: \"{app}\\{#MyAppExeName}\"; Tasks: desktopicon; OnlyBelowVersion: 0,6.0; IconFilename: \"{app}\\lokinet.ico\"\r\nName: \"{commondesktop}\\{#MyAppName}\"; Filename: \"{app}\\lokinet-qt5-ui\\lokinetui.exe\"; Tasks: desktopicon; MinVersion: 0,6.0; IconFilename: \"{app}\\lokinet.ico\"\r\nName: \"{userappdata}\\Microsoft\\Internet Explorer\\Quick Launch\\{#MyAppName}\"; Filename: \"{app}\\{#MyAppExeName}\"; Tasks: quicklaunchicon; OnlyBelowVersion: 0, 6.1; IconFilename: \"{app}\\lokinet.ico\"\r\nName: \"{userappdata}\\Microsoft\\Internet Explorer\\Quick Launch\\User Pinned\\TaskBar\\{#MyAppName}\"; Filename: \"{app}\\lokinet-qt5-ui\\lokinetui.exe\"; Tasks: quicklaunchicon; MinVersion: 0, 6.1; IconFilename: \"{app}\\lokinet.ico\"\r\n\r\n[Run]\r\nFilename: \"{app}\\{#MyAppExeName}\"; Flags: nowait postinstall skipifsilent; Description: \"{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}\"; OnlyBelowVersion: 0, 6.0\r\nFilename: \"{app}\\lokinet-qt5-ui\\lokinetui.exe\"; Flags: nowait postinstall skipifsilent; Description: \"{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}\"; MinVersion: 0,6.0\r\n; wait until either one or two of these terminates\r\nFilename: \"{tmp}\\7z.exe\"; Parameters: \"x tuntapv9.7z\"; WorkingDir: \"{app}\"; Flags: runascurrentuser waituntilterminated skipifdoesntexist; Description: \"extract TUN/TAP-v9 driver\"; StatusMsg: \"Extracting driver...\"; OnlyBelowVersion: 0, 6.0\r\nFilename: \"{tmp}\\7z.exe\"; Parameters: \"x tuntapv9_n6.7z\"; WorkingDir: \"{app}\"; Flags: runascurrentuser waituntilterminated skipifdoesntexist; Description: \"extract TUN/TAP-v9 driver\"; StatusMsg: \"Extracting driver...\"; MinVersion: 0, 6.0\r\nFilename: \"{tmp}\\7z.exe\"; Parameters: \"x inet6.7z\"; WorkingDir: \"{app}\"; Flags: skipifdoesntexist runascurrentuser waituntilterminated skipifdoesntexist; Description: \"extract inet6 driver\"; StatusMsg: \"Extracting IPv6 driver...\"; MinVersion: 0, 5.0; OnlyBelowVersion: 0, 5.1\r\nFilename: \"{tmp}\\7z.exe\"; Parameters: \"x lokinet-qt5-ui.7z -aoa\"; WorkingDir: \"{app}\"; Flags: runascurrentuser waituntilterminated skipifdoesntexist; Description: \"installing modern Qt5 UI\"; StatusMsg: \"Installing modern Qt5 UI...\"; MinVersion: 0, 6.0;\r\nFilename: \"{app}\\lokinet-bootstrap.exe\"; Parameters:\"-L https://seed.lokinet.org/lokinet.signed --cacert rootcerts.pem -o \"\"{userappdata}\\.lokinet\\bootstrap.signed\"\"\"; WorkingDir: \"{app}\"; Flags: runascurrentuser waituntilterminated; Description: \"bootstrap dht\"; StatusMsg: \"Downloading initial RC...\"\r\n; then ask to install drivers\r\nFilename: \"{app}\\tap-windows-9.9.2\\install.bat\"; WorkingDir: \"{app}\\tap-windows-9.9.2\\\"; Flags: runascurrentuser waituntilterminated skipifdoesntexist; Description: \"Install TUN/TAP-v9 driver\"; StatusMsg: \"Installing driver...\"; OnlyBelowVersion: 0, 6.0; Check: not IsTapInstalled\r\nFilename: \"{app}\\tap-windows-9.21.2\\install.bat\"; WorkingDir: \"{app}\\tap-windows-9.21.2\\\"; Flags: runascurrentuser waituntilterminated skipifdoesntexist; Description: \"Install TUN/TAP-v9 driver\"; StatusMsg: \"Installing driver...\"; MinVersion: 0, 6.0; Check: not IsTapInstalled\r\n; install inet6 if not present. (I'd assume netsh displays something helpful if inet6 is already set up and configured.)\r\n; if it doesn't exist, then the inet6 driver appears to be installed\r\nFilename: \"{app}\\inet6_driver\\setup\\hotfix.exe\"; Parameters: \"/m /z\"; WorkingDir: \"{app}\\inet6_driver\\setup\\\"; Flags: runascurrentuser waituntilterminated skipifdoesntexist; Description: \"Install IPv6 driver\"; StatusMsg: \"Installing IPv6...\"; OnlyBelowVersion: 0, 5.1;  Check: not FileExists(ExpandConstant('{sys}\\drivers\\tcpip6.sys'))\r\nFilename: \"{sys}\\netsh.exe\"; Parameters: \"int ipv6 install\"; Flags: runascurrentuser waituntilterminated; Description: \"install ipv6 on whistler\"; StatusMsg: \"Installing IPv6...\"; MinVersion: 0,5.1; OnlyBelowVersion: 0,6.0\r\nFilename: \"{userappdata}\\.lokinet\\config_migration.bat\"; WorkingDir: \"{userappdata}\\.lokinet\"; Flags: runascurrentuser waituntilterminated; Description: \"migrate existing config\"; StatusMsg: \"Migrating old configuration...\"; MinVersion: 0,6.0; Tasks: migrateconfigs;"
  },
  {
    "path": "win32-setup/mbedtls-win32.patch",
    "content": "diff -ruN include/mbedtls/aesni.h include/mbedtls/aesni.h\n--- include/mbedtls/aesni.h\t2018-03-16 11:25:12.000000000 -0500\n+++ include/mbedtls/aesni.h\t2018-04-17 15:47:59.320514100 -0500\n@@ -26,17 +26,16 @@\n \n #include \"aes.h\"\n \n+/* \n+ * despair: This code appears to be 32-bit clean. Remove the CPP macros\n+ * that restrict usage to AMD64 and EM64T processors.\n+ * Obviously, you still need to have this insn set available in order to\n+ * use it in either of protected or long mode anyway.\n+ */\n+\n #define MBEDTLS_AESNI_AES      0x02000000u\n #define MBEDTLS_AESNI_CLMUL    0x00000002u\n \n-#if defined(MBEDTLS_HAVE_ASM) && defined(__GNUC__) &&  \\\n-    ( defined(__amd64__) || defined(__x86_64__) )   &&  \\\n-    ! defined(MBEDTLS_HAVE_X86_64)\n-#define MBEDTLS_HAVE_X86_64\n-#endif\n-\n-#if defined(MBEDTLS_HAVE_X86_64)\n-\n #ifdef __cplusplus\n extern \"C\" {\n #endif\n@@ -107,6 +106,4 @@\n }\n #endif\n \n-#endif /* MBEDTLS_HAVE_X86_64 */\n-\n #endif /* MBEDTLS_AESNI_H */\ndiff -ruN include/mbedtls/bn_mul.h include/mbedtls/bn_mul.h\n--- include/mbedtls/bn_mul.h\t2018-03-16 11:25:12.000000000 -0500\n+++ include/mbedtls/bn_mul.h\t2018-04-17 15:42:09.045117300 -0500\n@@ -754,7 +754,9 @@\n #if defined(MBEDTLS_HAVE_SSE2)\n \n #define EMIT __asm _emit\n-\n+/* Because the Visual C++ inline assembler STILL does\n+   not support MMX insns! reeeeee (old -GM flag no longer exists)\n+ */\n #define MULADDC_HUIT                            \\\n     EMIT 0x0F  EMIT 0x6E  EMIT 0xC9             \\\n     EMIT 0x0F  EMIT 0x6E  EMIT 0xC3             \\\ndiff -ruN include/mbedtls/config.h include/mbedtls/config.h\n--- include/mbedtls/config.h\t2018-03-16 11:25:12.000000000 -0500\n+++ include/mbedtls/config.h\t2018-04-17 17:27:18.350938700 -0500\n@@ -91,7 +91,7 @@\n  *\n  * Uncomment if the CPU supports SSE2 (IA-32 specific).\n  */\n-//#define MBEDTLS_HAVE_SSE2\n+#define MBEDTLS_HAVE_SSE2\n \n /**\n  * \\def MBEDTLS_HAVE_TIME\n@@ -1571,7 +1571,7 @@\n  * Module:  library/aesni.c\n  * Caller:  library/aes.c\n  *\n- * Requires: MBEDTLS_HAVE_ASM\n+ * Requires: None. Enable only for i386 or AMD64 targets only! -despair\n  *\n  * This modules adds support for the AES-NI instructions on x86-64\n  */\n@@ -1850,7 +1850,7 @@\n  * Requires: MBEDTLS_AES_C or MBEDTLS_DES_C\n  *\n  */\n-//#define MBEDTLS_CMAC_C\n+#define MBEDTLS_CMAC_C\n \n /**\n  * \\def MBEDTLS_CTR_DRBG_C\n@@ -2055,7 +2055,7 @@\n  *\n  * Uncomment to enable the HAVEGE random generator.\n  */\n-//#define MBEDTLS_HAVEGE_C\n+#define MBEDTLS_HAVEGE_C\n \n /**\n  * \\def MBEDTLS_HMAC_DRBG_C\ndiff -ruN library/aes.c library/aes.c\n--- library/aes.c\t2018-03-16 11:25:12.000000000 -0500\n+++ library/aes.c\t2018-04-17 16:51:37.098413400 -0500\n@@ -514,7 +514,7 @@\n #endif\n     ctx->rk = RK = ctx->buf;\n \n-#if defined(MBEDTLS_AESNI_C) && defined(MBEDTLS_HAVE_X86_64)\n+#if defined(MBEDTLS_AESNI_C)\n     if( mbedtls_aesni_has_support( MBEDTLS_AESNI_AES ) )\n         return( mbedtls_aesni_setkey_enc( (unsigned char *) ctx->rk, key, keybits ) );\n #endif\n@@ -621,7 +621,7 @@\n \n     ctx->nr = cty.nr;\n \n-#if defined(MBEDTLS_AESNI_C) && defined(MBEDTLS_HAVE_X86_64)\n+#if defined(MBEDTLS_AESNI_C)\n     if( mbedtls_aesni_has_support( MBEDTLS_AESNI_AES ) )\n     {\n         mbedtls_aesni_inverse_key( (unsigned char *) ctx->rk,\n@@ -1016,7 +1016,7 @@\n    AES_VALIDATE_RET( mode == MBEDTLS_AES_ENCRYPT ||\n                      mode == MBEDTLS_AES_DECRYPT );\n\n-#if defined(MBEDTLS_AESNI_C) && defined(MBEDTLS_HAVE_X86_64)\n+#if defined(MBEDTLS_AESNI_C)\n     if( mbedtls_aesni_has_support( MBEDTLS_AESNI_AES ) )\n         return( mbedtls_aesni_crypt_ecb( ctx, mode, input, output ) );\n #endif\ndiff -ruN library/aesni.c library/aesni.c\n--- library/aesni.c\t2018-03-16 11:25:12.000000000 -0500\n+++ library/aesni.c\t2018-04-17 16:09:26.050605000 -0500\n@@ -30,7 +30,16 @@\n #include MBEDTLS_CONFIG_FILE\n #endif\n \n-#if defined(MBEDTLS_AESNI_C)\n+\n+/* \n+ * despair: This code appears to be 32-bit clean. Remove the CPP macros\n+ * that restrict usage to AMD64 and EM64T processors.\n+ * Obviously, you still need to have this insn set available in order to\n+ * use it in either of protected or long mode anyway.\n+ * GCC or Clang only, no MSVC here, sorry. (Must pass -march=core2 or later\n+ * if your compiler's default is anything older or generic.)\n+ */\n+#if defined(MBEDTLS_AESNI_C) && !defined(_MSC_VER)\n \n #include \"mbedtls/aesni.h\"\n \n@@ -40,8 +49,6 @@\n #define asm __asm\n #endif\n \n-#if defined(MBEDTLS_HAVE_X86_64)\n-\n /*\n  * AES-NI support detection routine\n  */\n@@ -459,6 +466,4 @@\n     return( 0 );\n }\n \n-#endif /* MBEDTLS_HAVE_X86_64 */\n-\n #endif /* MBEDTLS_AESNI_C */\ndiff -ruN library/entropy_poll.c library/entropy_poll.c\n--- library/entropy_poll.c\t2018-03-16 11:25:12.000000000 -0500\n+++ library/entropy_poll.c\t2018-04-17 15:52:13.013004200 -0500\n@@ -56,6 +56,12 @@\n #include <windows.h>\n #include <wincrypt.h>\n \n+/* \n+ * WARNING(despair): The next release of PolarSSL will remove the existing codepaths\n+ * to enable Windows RT and UWP app support. This also breaks NT 5.x and early Longhorn.\n+ *\n+ * TODO(despair): create CPP macro to switch between old and new CAPI codepaths\n+ */\n int mbedtls_platform_entropy_poll( void *data, unsigned char *output, size_t len,\n                            size_t *olen )\n {\ndiff -ruN library/gcm.c library/gcm.c\n--- library/gcm.c\t2018-03-16 11:25:12.000000000 -0500\n+++ library/gcm.c\t2018-04-17 16:53:18.630262400 -0500\n@@ -126,7 +126,7 @@\n     ctx->HL[8] = vl;\n     ctx->HH[8] = vh;\n \n-#if defined(MBEDTLS_AESNI_C) && defined(MBEDTLS_HAVE_X86_64)\n+#if defined(MBEDTLS_AESNI_C)\n     /* With CLMUL support, we need only h, not the rest of the table */\n     if( mbedtls_aesni_has_support( MBEDTLS_AESNI_CLMUL ) )\n         return( 0 );\n@@ -217,7 +217,7 @@\n     unsigned char lo, hi, rem;\n     uint64_t zh, zl;\n \n-#if defined(MBEDTLS_AESNI_C) && defined(MBEDTLS_HAVE_X86_64)\n+#if defined(MBEDTLS_AESNI_C)\n     if( mbedtls_aesni_has_support( MBEDTLS_AESNI_CLMUL ) ) {\n         unsigned char h[16];\n \ndiff -ruN library/net_sockets.c library/net_sockets.c\n--- library/net_sockets.c\t2018-03-16 11:25:12.000000000 -0500\n+++ library/net_sockets.c\t2018-04-17 15:50:08.118440600 -0500\n@@ -51,7 +51,8 @@\n /* Enables getaddrinfo() & Co */\n #define _WIN32_WINNT 0x0501\n #include <ws2tcpip.h>\n-\n+/* despair: re-enable Windows 2000/XP */\n+#include <wspiapi.h>\n #include <winsock2.h>\n #include <windows.h>\n \n"
  },
  {
    "path": "win32-setup/notes.txt",
    "content": "[16:24:37] (Channel) despair86: http://files.jrsoftware.org/is/5/innosetup-5.6.1-unicode.exe\n[16:29:23] (Channel) despair86: wine [path to ISCC.exe] -DSINGLE_ARCH lokinet-win32.iss\n[16:29:28] (Channel) despair86: should work\n[16:30:11] (Channel) despair86: make -C win32-setup\n[16:30:33] (Channel) despair86: cmake . -DCMAKE_TOOLCHAIN_FILE [options] build\n[16:30:37] (Channel) despair86: make -C build\n[16:30:51] (Channel) despair86: then use wine to build package\n"
  },
  {
    "path": "win32-setup/regdb_helper.c",
    "content": "/*\n * Copyright (c)2018-2019 Rick V. All rights reserved.\n *\n * This software is provided 'as-is', without any express or implied\n * warranty. In no event will the authors be held liable for any damages\n * arising from the use of this software.\n *\n * Permission is granted to anyone to use this software for any purpose,\n * including commercial applications, and to alter it and redistribute it\n * freely, subject to the following restrictions:\n *\n * 1. The origin of this software must not be misrepresented; you must not\n * claim that you wrote the original software. If you use this software\n * in a product, an acknowledgment in the product documentation would be\n * appreciated but is not required.\n * 2. Altered source versions must be plainly marked as such, and must not be\n * misrepresented as being the original software.\n * 3. This notice may not be removed or altered from any source distribution.\n *------------------------------------------------------------------------------\n * Shared object loaded by lokinet installer to properly detect the presence\n * of the TAP v9 adapter\n * -rick\n */\n\n#include <sys/types.h>\n#include <windows.h>\n#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n\n/* Windows registry crap */\n#define MAX_KEY_LENGTH 255\n#define MAX_VALUE_NAME 16383\n#define ETHER_ADDR_LEN 6\n\n/* checks if TAP-Win32 v9 is already installed */\nBOOL\nreg_query_helper()\n{\n  HKEY adapters, adapter;\n  DWORD i, ret, len;\n  char *deviceid = NULL;\n  DWORD sub_keys = 0;\n  BOOL found     = FALSE;\n\n  ret = RegOpenKeyEx(HKEY_LOCAL_MACHINE,\n                     TEXT(\"SYSTEM\\\\CurrentControlSet\\\\Control\\\\Class\\\\{\"\n                          \"4D36E972-E325-11CE-BFC1-08002BE10318}\"),\n                     0, KEY_READ, &adapters);\n  if(ret != ERROR_SUCCESS)\n    return FALSE;\n\n  ret = RegQueryInfoKey(adapters, NULL, NULL, NULL, &sub_keys, NULL, NULL, NULL,\n                        NULL, NULL, NULL, NULL);\n  if(ret != ERROR_SUCCESS)\n    return FALSE;\n\n  if(sub_keys <= 0)\n    return FALSE;\n\n  /* Walk througt all adapters */\n  for(i = 0; i < sub_keys; i++)\n  {\n    char new_key[MAX_KEY_LENGTH];\n    char data[256];\n    TCHAR key[MAX_KEY_LENGTH];\n    DWORD keylen = MAX_KEY_LENGTH;\n\n    /* Get the adapter key name */\n    ret = RegEnumKeyEx(adapters, i, key, &keylen, NULL, NULL, NULL, NULL);\n    if(ret != ERROR_SUCCESS)\n      continue;\n\n    /* Append it to NETWORK_ADAPTERS and open it */\n    snprintf(new_key, sizeof new_key, \"%s\\\\%s\",\n             \"SYSTEM\\\\CurrentControlSet\\\\Control\\\\Class\\\\{4D36E972-E325-11CE-\"\n             \"BFC1-08002BE10318}\",\n             key);\n    ret =\n        RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT(new_key), 0, KEY_READ, &adapter);\n    if(ret != ERROR_SUCCESS)\n      continue;\n\n    /* Check its values */\n    len = sizeof data;\n    ret =\n        RegQueryValueEx(adapter, \"ComponentId\", NULL, NULL, (LPBYTE)data, &len);\n    if(ret != ERROR_SUCCESS)\n    {\n      /* This value doesn't exist in this adaptater tree */\n      goto clean;\n    }\n    /* If its a tap adapter, its all good */\n    /* We only support TAP 9.x, TAP 8.x users must upgrade. */\n    if(strncmp(data, \"tap0901\", 7) == 0)\n    {\n      DWORD type;\n\n      len = sizeof data;\n      ret = RegQueryValueEx(adapter, \"NetCfgInstanceId\", NULL, &type,\n                            (LPBYTE)data, &len);\n      if(ret != ERROR_SUCCESS)\n        goto clean;\n      found = TRUE;\n      break;\n    }\n  clean:\n    RegCloseKey(adapter);\n  }\n  RegCloseKey(adapters);\n  return found;\n}\n"
  }
]