From 8ba03de44ef7567046e58e0bf547650da4fb494b Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Tue, 16 Nov 2021 10:56:31 -0500 Subject: [PATCH 001/202] match timeouts in introset selection spread with logic for publishing --- llarp/service/endpoint.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/llarp/service/endpoint.cpp b/llarp/service/endpoint.cpp index aae60423d..f466c8192 100644 --- a/llarp/service/endpoint.cpp +++ b/llarp/service/endpoint.cpp @@ -104,8 +104,7 @@ namespace llarp std::set intros; if (const auto maybe = GetCurrentIntroductionsWithFilter([now](const service::Introduction& intro) -> bool { - return not intro.ExpiresSoon( - now, path::default_lifetime - path::min_intro_lifetime); + return not intro.ExpiresSoon(now, path::default_lifetime - path::intro_path_spread); })) { intros.insert(maybe->begin(), maybe->end()); From 189c4bfba488c927b754031dd6abace8a0a1e16a Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 16 Nov 2021 11:59:38 -0400 Subject: [PATCH 002/202] Also consider last publish; eliminate unwanted condition We don't really carry about when the last regen was attempted, but rather about when the last publish was attempted (or succeeded). --- llarp/service/endpoint.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/llarp/service/endpoint.cpp b/llarp/service/endpoint.cpp index f466c8192..b9367a0c7 100644 --- a/llarp/service/endpoint.cpp +++ b/llarp/service/endpoint.cpp @@ -733,13 +733,14 @@ namespace llarp if (not m_PublishIntroSet) return false; - auto next_pub = m_state->m_LastPublishAttempt + const auto lastEventAt = std::max(m_state->m_LastPublishAttempt, m_state->m_LastPublish); + const auto next_pub = lastEventAt + (m_state->m_IntroSet.HasStaleIntros( now, path::default_lifetime - path::intro_path_spread) ? IntrosetPublishRetryCooldown : IntrosetPublishInterval); - return now >= next_pub and m_LastIntrosetRegenAttempt + 1s <= now; + return now >= next_pub; } void From 172c2dec45b578001a66a57a64943c1a81b7bfcf Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Tue, 16 Nov 2021 12:21:11 -0500 Subject: [PATCH 003/202] create new constexpr for the staleness window for introsets and use it in publishing introsets and intro selection --- llarp/constants/path.hpp | 3 +++ llarp/service/endpoint.cpp | 5 ++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/llarp/constants/path.hpp b/llarp/constants/path.hpp index 5b8f15cfa..849fabf59 100644 --- a/llarp/constants/path.hpp +++ b/llarp/constants/path.hpp @@ -24,6 +24,9 @@ namespace llarp constexpr auto intro_spread_slices = 4; /// spacing frequency at which we try to build paths for introductions constexpr std::chrono::milliseconds intro_path_spread = default_lifetime / intro_spread_slices; + /// how long away from expiration in millseconds do we consider an intro to become stale + constexpr std::chrono::milliseconds intro_stale_threshold = + default_lifetime - intro_path_spread; /// Minimum paths to keep around for intros; mainly used at startup (the /// spread, above, should be able to maintain more than this number of paths /// normally once things are going). diff --git a/llarp/service/endpoint.cpp b/llarp/service/endpoint.cpp index b9367a0c7..b513903aa 100644 --- a/llarp/service/endpoint.cpp +++ b/llarp/service/endpoint.cpp @@ -104,7 +104,7 @@ namespace llarp std::set intros; if (const auto maybe = GetCurrentIntroductionsWithFilter([now](const service::Introduction& intro) -> bool { - return not intro.ExpiresSoon(now, path::default_lifetime - path::intro_path_spread); + return not intro.ExpiresSoon(now, path::intro_stale_threshold); })) { intros.insert(maybe->begin(), maybe->end()); @@ -735,8 +735,7 @@ namespace llarp const auto lastEventAt = std::max(m_state->m_LastPublishAttempt, m_state->m_LastPublish); const auto next_pub = lastEventAt - + (m_state->m_IntroSet.HasStaleIntros( - now, path::default_lifetime - path::intro_path_spread) + + (m_state->m_IntroSet.HasStaleIntros(now, path::intro_stale_threshold) ? IntrosetPublishRetryCooldown : IntrosetPublishInterval); From 5de5091e8d4e8dd5c22d52eaa9c5ac6702135d09 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 16 Nov 2021 20:24:38 -0400 Subject: [PATCH 004/202] docker CI: Fix error message --- contrib/ci/docker/rebuild-docker-images.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/ci/docker/rebuild-docker-images.py b/contrib/ci/docker/rebuild-docker-images.py index 916fa2ee2..11f40eb39 100755 --- a/contrib/ci/docker/rebuild-docker-images.py +++ b/contrib/ci/docker/rebuild-docker-images.py @@ -82,7 +82,7 @@ def run_or_report(*args, myline): log.write(e.output.encode()) global failure failure = True - print_line(myline, "\033[31;1mError! See {} for details", log.name) + print_line(myline, "\033[31;1mError! See {} for details".format(log.name)) raise e From 55fadab54be7de1080fecbb9dbaaeb202a0502c2 Mon Sep 17 00:00:00 2001 From: necro-nemesis Date: Sat, 20 Nov 2021 18:27:11 -0500 Subject: [PATCH 005/202] Update lokinet.spec --- SPECS/lokinet.spec | 1 + 1 file changed, 1 insertion(+) diff --git a/SPECS/lokinet.spec b/SPECS/lokinet.spec index 65cd62b15..ae195a60f 100644 --- a/SPECS/lokinet.spec +++ b/SPECS/lokinet.spec @@ -158,6 +158,7 @@ fi %changelog * Wed Nov 17 2021 Technical Tumbleweed - 0.9.8-1 - bump version +- bumped to build Sat Nov 20 2021 * Wed Oct 20 2021 Technical Tumbleweed - 0.9.7-1 - bump version From 8c6bf31c5292d42fafac65898e50f2c015b6ba72 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Tue, 23 Nov 2021 12:37:22 -0500 Subject: [PATCH 006/202] paralellize android build much better * split up configure step and build step so that build steps goes all at once * update ci to use new build structure for android --- .drone.jsonnet | 2 +- contrib/android-configure.sh | 53 ++++++++++++++++++++++++++++++++++++ contrib/android.sh | 47 ++------------------------------ 3 files changed, 57 insertions(+), 45 deletions(-) create mode 100755 contrib/android-configure.sh diff --git a/.drone.jsonnet b/.drone.jsonnet index 5758d9a57..fed2b0709 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -98,7 +98,7 @@ local apk_builder(name, image, extra_cmds=[], allow_fail=false, jobs=6) = { commands: [ 'VERBOSE=1 JOBS=' + jobs + ' NDK=/usr/lib/android-ndk ./contrib/android.sh', 'git clone https://github.com/oxen-io/lokinet-flutter-app lokinet-mobile', - 'cp -av lokinet-jni-*/* lokinet-mobile/lokinet_lib/android/src/main/jniLibs/', + 'cp -av build-android/out/* lokinet-mobile/lokinet_lib/android/src/main/jniLibs/', 'cd lokinet-mobile', 'flutter build apk --debug', 'cd ..', diff --git a/contrib/android-configure.sh b/contrib/android-configure.sh new file mode 100755 index 000000000..ba6a08a7f --- /dev/null +++ b/contrib/android-configure.sh @@ -0,0 +1,53 @@ +#!/bin/bash +set -e +set +x + +default_abis="armeabi-v7a arm64-v8a x86 x86_64" +build_abis=${ABIS:-$default_abis} + +test x$NDK = x && echo "NDK env var not set" +test x$NDK = x && exit 1 + +echo "building abis: $build_abis" + +root="$(readlink -f $(dirname $0)/../)" +build=$root/build-android +mkdir -p $build +cd $build + +for abi in $build_abis; do + mkdir -p build-$abi + cd build-$abi + cmake \ + -G 'Unix Makefiles' \ + -DANDROID=ON \ + -DANDROID_ABI=$abi \ + -DANDROID_ARM_MODE=arm \ + -DANDROID_PLATFORM=android-23 \ + -DANDROID_STL=c++_static \ + -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \ + -DBUILD_STATIC_DEPS=ON \ + -DBUILD_PACKAGE=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_TESTING=OFF \ + -DBUILD_LIBLOKINET=OFF \ + -DWITH_TESTS=OFF \ + -DNATIVE_BUILD=OFF \ + -DSTATIC_LINK=ON \ + -DWITH_SYSTEMD=OFF \ + -DFORCE_OXENMQ_SUBMODULE=ON \ + -DSUBMODULE_CHECK=OFF \ + -DWITH_LTO=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + $@ $root + cd - +done +rm -f $build/Makefile +echo "# generated makefile" >> $build/Makefile +echo "all: $build_abis" >> $build/Makefile +for abi in $build_abis; do + echo -ne "$abi:\n\t" >> $build/Makefile + echo -ne '$(MAKE) -C ' >> $build/Makefile + echo "build-$abi lokinet-android" >> $build/Makefile + echo -ne "\tmkdir -p out/$abi && cp build-$abi/jni/liblokinet-android.so out/$abi/liblokinet-android.so\n\n" >> $build/Makefile +done diff --git a/contrib/android.sh b/contrib/android.sh index 9dfd64df2..a5d720206 100755 --- a/contrib/android.sh +++ b/contrib/android.sh @@ -2,50 +2,9 @@ set -e set +x -default_abis="armeabi-v7a arm64-v8a x86 x86_64" -build_abis=${ABIS:-$default_abis} - test x$NDK = x && echo "NDK env var not set" test x$NDK = x && exit 1 - -echo "building abis: $build_abis" - root="$(readlink -f $(dirname $0)/../)" -out=$root/lokinet-jni-$(git describe || echo unknown) -mkdir -p $out -mkdir -p $root/build-android -cd $root/build-android - -for abi in $build_abis; do - mkdir -p build-$abi $out/$abi - cd build-$abi - cmake \ - -G 'Unix Makefiles' \ - -DANDROID=ON \ - -DANDROID_ABI=$abi \ - -DANDROID_ARM_MODE=arm \ - -DANDROID_PLATFORM=android-23 \ - -DANDROID_STL=c++_static \ - -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \ - -DBUILD_STATIC_DEPS=ON \ - -DBUILD_PACKAGE=ON \ - -DBUILD_SHARED_LIBS=OFF \ - -DBUILD_TESTING=OFF \ - -DBUILD_LIBLOKINET=OFF \ - -DWITH_TESTS=OFF \ - -DNATIVE_BUILD=OFF \ - -DSTATIC_LINK=ON \ - -DWITH_SYSTEMD=OFF \ - -DFORCE_OXENMQ_SUBMODULE=ON \ - -DSUBMODULE_CHECK=OFF \ - -DWITH_LTO=OFF \ - -DCMAKE_BUILD_TYPE=Release \ - $@ $root - make lokinet-android -j${JOBS:-$(nproc)} - cp jni/liblokinet-android.so $out/$abi/liblokinet-android.so - cd - -done - - -echo -echo "build artifacts outputted to $out" +cd "$root" +./contrib/android-configure.sh $@ +make -C build-android -j ${JOBS:-$(nproc)} From ec8d990163caf02b86266cccae0ab6461322b30f Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Sun, 28 Nov 2021 09:56:53 -0500 Subject: [PATCH 007/202] demote log statement levels --- llarp/iwp/linklayer.cpp | 2 +- llarp/iwp/session.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/llarp/iwp/linklayer.cpp b/llarp/iwp/linklayer.cpp index 7615a28d2..7be2419fe 100644 --- a/llarp/iwp/linklayer.cpp +++ b/llarp/iwp/linklayer.cpp @@ -76,7 +76,7 @@ namespace llarp::iwp bool success = session->Recv_LL(std::move(pkt)); if (not success and isNewSession) { - LogWarn("Brand new session failed; removing from pending sessions list"); + LogDebug("Brand new session failed; removing from pending sessions list"); m_Pending.erase(from); } WakeupPlaintext(); diff --git a/llarp/iwp/session.cpp b/llarp/iwp/session.cpp index a3e504222..6eefcfc71 100644 --- a/llarp/iwp/session.cpp +++ b/llarp/iwp/session.cpp @@ -590,7 +590,7 @@ namespace llarp const ShortHash expected{buf.base}; if (H != expected) { - LogError( + LogDebug( m_Parent->PrintableName(), " keyed hash mismatch ", H, @@ -954,7 +954,7 @@ namespace llarp } else { - LogWarn("bad intro from ", m_RemoteAddr); + LogDebug("bad intro from ", m_RemoteAddr); return false; } } From ce8b3c83a7b360393110766dac95193bb1c27628 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sun, 28 Nov 2021 14:01:12 -0500 Subject: [PATCH 008/202] Update high-level.txt add notice to file as very out of date. --- docs/high-level.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/high-level.txt b/docs/high-level.txt index a7ae94076..704a4cc5b 100644 --- a/docs/high-level.txt +++ b/docs/high-level.txt @@ -1,3 +1,6 @@ +THIS DOCUMENT IS 3 YEARS OUT OF DATE AND NEEDS TO BE REWRITTEN + + LLARP - Low Latency Anon Routing Protocol TL;DR edition: an onion router with a tun interface for transporting ip packets From 0e2b0edaf650560b83295297686d25154b6f084f Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Fri, 3 Dec 2021 16:12:10 -0500 Subject: [PATCH 009/202] when lokinet looses ip4 connectivity libunbound used to freak out and only use ip6 after such an event. as a result dns queries stop working because we blackhole ip6 routes if exit mode is on. this prevents this case from being hit. --- llarp/dns/unbound_resolver.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/llarp/dns/unbound_resolver.cpp b/llarp/dns/unbound_resolver.cpp index 50c56f5fc..82022cce0 100644 --- a/llarp/dns/unbound_resolver.cpp +++ b/llarp/dns/unbound_resolver.cpp @@ -106,6 +106,9 @@ namespace llarp::dns return false; } + // disable ip6 for upstream dns + ub_ctx_set_option(unboundContext, "prefer-ip6", "0"); + // enable async ub_ctx_async(unboundContext, 1); #ifdef _WIN32 runner = std::thread{[&]() { From e0df1875fb3ee0c0f2ead949b7b399add94670c1 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sun, 5 Dec 2021 14:17:58 -0500 Subject: [PATCH 010/202] drop x86 for android as flutter does not support it --- contrib/android-configure.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/android-configure.sh b/contrib/android-configure.sh index ba6a08a7f..c17929814 100755 --- a/contrib/android-configure.sh +++ b/contrib/android-configure.sh @@ -2,7 +2,7 @@ set -e set +x -default_abis="armeabi-v7a arm64-v8a x86 x86_64" +default_abis="armeabi-v7a arm64-v8a x86_64" build_abis=${ABIS:-$default_abis} test x$NDK = x && echo "NDK env var not set" From 29df7bec74c39af67cc19cb6189d67a6ca70aeb0 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Sun, 28 Nov 2021 14:06:35 -0500 Subject: [PATCH 011/202] remove old out of date documentation --- docs/api_v0.txt | 263 ---------- docs/code_structure.org | 117 ----- docs/crypto_v0.txt | 39 -- docs/dht_v0.txt | 153 ------ docs/dht_v1.txt | 94 ---- docs/high-level.txt | 147 ------ docs/linux-setcap-readme.txt | 16 - docs/llarp_structure.dot | 102 ---- docs/logo.svg | 66 --- docs/lokinet_admin_v0.txt | 70 --- docs/old-wire-protocol.txt | 90 ---- docs/proto_v0.txt | 903 ----------------------------------- docs/tooling.txt | 38 -- docs/wire-protocol.txt | 309 ------------ 14 files changed, 2407 deletions(-) delete mode 100644 docs/api_v0.txt delete mode 100644 docs/code_structure.org delete mode 100644 docs/crypto_v0.txt delete mode 100644 docs/dht_v0.txt delete mode 100644 docs/dht_v1.txt delete mode 100644 docs/high-level.txt delete mode 100644 docs/linux-setcap-readme.txt delete mode 100644 docs/llarp_structure.dot delete mode 100644 docs/logo.svg delete mode 100644 docs/lokinet_admin_v0.txt delete mode 100644 docs/old-wire-protocol.txt delete mode 100644 docs/proto_v0.txt delete mode 100644 docs/tooling.txt delete mode 100644 docs/wire-protocol.txt diff --git a/docs/api_v0.txt b/docs/api_v0.txt deleted file mode 100644 index 0aaa9fec5..000000000 --- a/docs/api_v0.txt +++ /dev/null @@ -1,263 +0,0 @@ - -LLARP Traffic Routing Protocol (LTRP) - -LRTP is a protocol that instructs how to route hidden service traffic on LLARP -based networks. - -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", -"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this -document are to be interpreted as described in RFC 2119 [RFC2119]. - -Overview: - -LRTP is a message oriented data delivery and receival protocol for hidden -service traffic. All structures are BitTorrent Encoded dictionaries sent -over TCP. - -all structures are bencoded when sent over the networks. -In this document they are provided in JSON for ease of display. - -message format: - -<2 bytes length (N)> - - - -Nouns (data structures): - -Path: information about a path that we have built - -{ - H: [router_id_32_bytes, router_id_32_bytes, router_id_32_bytes, router_id_32_bytes], - R: "<16 bytes local rxid>", - T: "<16 bytes local txid>" -} - -Introduction: a hidden service introduction - -{ - E: expiration_ms_since_epoch_uint64, - L: advertised_latency_ms_uint64, - P: "<16 bytes pathid>", - R: "<32 bytes RouterID>" -} - -ServiceInfo: public key info for hidden service address - -{ - A: "<32 bytes .loki address>", - E: "<32 bytes public encryption key>", - S: "<32 bytes public signing key>" -} - -IntroSet: information about an introduction set from the network - -{ - E: expires_at_timestamp_ms_since_epoch_uint64, - I: [Intro0, Intro1, ... IntroN], - S: ServiceInfo -} - -Converstation: information about a loki network converstation - -{ - L: "<32 bytes loki address provided if a loki address>", - S: "<32 bytes snode address provided if a snode address>", - T: "<16 bytes convo tag>" -} - -SessionInfo: information about our current session - -{ - I: [inbound,convos,here], - O: [outbound,covos,here], - P: [Path0, Path1, .... PathN], - S: Current IntroSet, -} - -Verbs (methods): - -session requset (C->S) - -the first message sent by the client - -{ - A: "session", - B: "<8 bytes random>", - T: milliseconds_since_epoch_client_now_uint64, - Y: 0, - Z: "<32 bytes keyed hash>" -} - -session accept (S->C) - -sent in reply to a session message to indicate session accept and give -a session cookie to the client. - -{ - A: "session-reply", - B: "<8 bytes random from session request>", - C: "<16 bytes session cookie>", - T: milliseconds_since_epoch_server_now_uint64, - Y: 0, - Z: "<32 bytes keyed hash>" -} - -session reject (S->C) - -sent in reply to a session message to indicate session rejection - -{ - A: "session-reject", - B: "<8 bytes random from session request>", - R: "", - T: milliseconds_since_epoch_server_now_uint64, - Y: 0, - Z: "<32 bytes keyed hash>" -} - -spawn a hidden service (C->S) - -only one hidden service can be made per session - -{ - A: "spawn", - C: "<16 bytes session cookie>", - O: config_options_dict, - Y: 1, - Z: "<32 bytes keyed hash>" -} - -inform that we have spawned a new hidden service endpoint (S->C) - -{ - A: "spawn-reply", - C: "<16 bytes session cookie>", - S: ServiceInfo, - Y: 1, - Z: "<32 bytes keyed hash>" -} - -inform that we have not spaned a new hidden service endpint (S->C) - -after sending this message the server closes the connection - -{ - A: "spawn-reject", - C: "<16 bytes session cookie>", - E: "", - Y: 1, - Z: "<32 bytes keyed hash>" -} - -create a new convseration on a loki/snode address (C->S) - -{ - A: "start-convo", - B: "<8 bytes random>", - C: "<16 bytes session cookie>", - R: "human readable remote address .snode/.loki", - Y: sequence_num_uint64, - Z: "<32 bytes keyed hash>" -} - -sent in reply to a make-convo message to indicate rejection (S->C) - -{ - A: "start-convo-reject", - B: "<8 bytes random from start-convo message>", - C: "<16 bytes session cookie>", - S: status_bitmask_uint, - Y: sequence_num_uint64, - Z: "<32 bytes keyed hash>" -} - -sent in reply to a make-convo message to indicate that we have accepted this -new conversation and gives the convo tag it uses. - -{ - A: "start-convo-accept", - B: "<8 bytes random from start-convo message>", - C: "<16 bytes session cookie>", - Y: sequence_num_uint64, - Z: "<32 bytes keyed hash> -} - -infrom the status of a converstation on a loki address (S->C) - -for an outbund conversation it is sent every time the status bitmask changes. -for inbound convos it is sent immediately when a new inbound conversation is made. - -S bit 0 (LSB): we found the introset/endpoint for (set by outbound) -S bit 1: we found the router to align on (set by outbound) -S bit 2: we have a path right now (set by outbound) -S bit 3: we have made the converstation (set by both) -S bit 4: we are an inbound converstation (set by inbound) - -{ - A: "convo-status", - C: "<16 bytes session cookie>", - R: "human readable address .snode/.loki", - S: bitmask_status_uint64, - T: "<16 bytes convotag>", - Y: sequence_num_uint64, - Z: "<32 bytes keyed hash>" -} - -send or recieve authenticated data to or from the network (bidi) - -protocol numbers are - -1 for ipv4 -2 for ipv6 - -{ - A: "data", - C: "<16 bytes session cookie>", - T: "<16 bytes convotag>", - W: protocol_number_uint, - X: "", - Y: sequence_num_uint64, - Z: "<32 bytes keyed hash>" -} - -get session information (C->S) - -{ - A: "info", - C: "<16 bytes session cookie>", - Y: sequence_num_uint64, - Z: "<32 bytes keyed hash>" -} - - -session information update (S->C) - -sent in reply to a get session information message - -{ - A: "info-reply", - C: "<16 bytes session cookie>", - I: hiddenserviceinfo, - Y: sequence_num_uint64, - Z: "<32 bytes keyed hash>" -} - -Protocol Flow: - -all messages have an A, C, Y and Z value - -A is the function name being called - -C is the session cookie indicating the current session - -Y is the 64 bit message sequence number as an integer - -Z is the keyed hash computed by MDS(BE(msg), K) where K is HS(api_password) -with the msg.Z being set to 32 bytes of \x00 - -both client and server MUST know a variable length string api_password used to -authenticate access to the api subsystem. - -the Y value is incremented by 1 for each direction every time the sender sends -a message in that direction. diff --git a/docs/code_structure.org b/docs/code_structure.org deleted file mode 100644 index 51952c731..000000000 --- a/docs/code_structure.org +++ /dev/null @@ -1,117 +0,0 @@ - -* lokinet components - -** basic data structures -*** AlignedBuffer -*** RouterContact -**** self signed router descriptor -*** Crypto key types - - -** network / threading / sync utilities -*** threadpool -*** logic (single thread threadpool) - -** configuration -*** ini parser -*** llarp::Configuration - -** cryptography -*** llarp::Crypto interface -**** libsodium / sntrup implementation -*** llarp::CryptoManager -**** crypto implementation signleton manager - -** nodedb -*** llarp_nodedb -**** holds many RouterContacts loaded from disk in memory -**** uses a filesystem skiplist for on disk storage -**** stores in a std::unordered_map addressable via public identity key - -** event loop -*** llarp_event_loop -**** udp socket read/write -**** network interface packet read/write -**** stream connection (outbound stream) -**** stream acceptor (inbound stream) - -** link layer message transport: -*** ILinkSession -**** the interface for an entity that is single session with relay -**** responsible for delivery recieval of link layer messages in full -*** ILinkLayer -**** bound to an address / interface -**** has a direction, inbound / outbound -**** distinctly identified by the union of interface and direction -**** Holds many ILinkSessions bound on the distinctly idenitfied direction/interface - - -** link layer messages -*** link layer message parser -**** parses buffers as bencoded dicts -*** link layer message handler -**** handles each type of link layer message - - -** IHopHandler -*** llarp::PathHopConfig -**** txid, rxid, shared secret at hop -*** llarp::path::Path -**** a built path or a path being built -**** owns a std::vector for each hop's info -*** TransitHop -**** a single hop on a built path -**** has txid, rxid, shared secret, hash of shared secret - - -** pathset -*** path::Builder -**** builds and maintains a set of paths for a common use - - -** routing layer message router -*** routing::IMessageHandler -**** interface for routing layer message processing -**** transit hops implement this if they are an endpoint -**** path::Path implement this always - - -** dht "layer" / rc gossiper -*** TODO rewrite/refactor - -** hidden service data structures -*** IntroSet -**** decrypted plaintext hidden service descriptor -*** EncryptedIntroSet -**** public encrpyted / signed version of IntroSet - - -** service endpoint / outbound context connectivitybackend -*** service::Endpoint -**** backend for sending/recieving packets over the hidden service protocol layer -**** kitchen sink -*** service::SendContext -**** interface type for sending to a resource on the network -*** service::OutboundContext -**** implements SendContext extends path::Builder and path::PathSet -**** for maintaining a pathset that aligns on an introset's intros -~ - -** snode / exit connectivity backend -*** exit::BaseSession -**** extends path::Builder -**** obtains an exit/snode session from the router they are aligning to -*** exit::Endpoint -**** snode/exit side of an exit::Session - -** snapp / exit / mobile / null frontend handlers -*** handlers::TunEndpoint -**** extends service::Endpoint -**** provides tun interface frontend for hidden service backend -*** handlers::ExitEndpoint -**** provides tun interface frontend for exit/snode backend - - -** outbound message dispatcher -*** TODO tom please document these - diff --git a/docs/crypto_v0.txt b/docs/crypto_v0.txt deleted file mode 100644 index 84f234d4d..000000000 --- a/docs/crypto_v0.txt +++ /dev/null @@ -1,39 +0,0 @@ - - -cryptography: - -H(x) is 512 bit blake2b digest of x -HS(x) is 256 bit blake2b digest of x -MD(x, k) is 512 bit blake2b hmac of x with secret value k -MDS(x, k) is 256 bit blake2b hmac of x with secret value k -SE(k, n, x) is chacha20 encrypt data x using symettric key k and nounce n -SD(k, n, x) is chacha20 dectypt data x using symettric key k and nounce n -S(k, x) is sign x with ed25519 using secret key k -EDKG() is generate ec keypair (p, s) public key p (32 bytes), secret key s (64 bytes) -V(k, x, sig) is verify x data using signature sig using public key k -EDDH(a, b) is curve25519 scalar multiplication of a and b -HKE(a, b, x) is hashed key exchange between a and b using a secret key x HS(a + b + EDDH(x, b)) -TKE(a, b, x, n) is a transport shared secret kdf using MDS(n, HKE(a, b, x)) - -when A is client and B is server where n is a 32 bytes shared random - -client computes TKE(A.pk, B.pk, A.sk, n) -server computes TKE(A.pk, B.pk, B.sk, n) - -PDH(a, b, x) is path shared secret generation HS(a + b + EDDH(x, b)) - -PKE(a, b, x, n) is a path shared secret kdf using MDS(n, PDH(a, b, x)) - -given A is the path creator and B is a hop in the path and n is 32 bytes shared random - -A computes PKE(A.pk, B.pk, A.sk, n) as S_a -B computes PKE(A.pk, B.pk, B.sk, n) as S_b - -S_a is equal to S_b - -RAND(n) is n random bytes - -PQKG() is generate a sntrup4591761 key pair (sk, pk) -PQKE_A(pk) is alice generating (x, k) where x is sntrup4591761 ciphertext block and k is the session key -PQKE_B(x, sk) is bob calculating k where x is sntrup4591761 ciphertext block, sk is bob's sntrup4591761 secretkey and k is the session key - diff --git a/docs/dht_v0.txt b/docs/dht_v0.txt deleted file mode 100644 index 868ea3d57..000000000 --- a/docs/dht_v0.txt +++ /dev/null @@ -1,153 +0,0 @@ -DHT messages - -DHT messages can be either wrapped in a LIDM message or sent anonymously over a path inside a DHT routing message - -The distance function is A xor B (traditional kademlia) - -The dht implements both iterative and recursive lookups. - -Recursive lookups forward the request to a node closer if not found. -Iterative lookups return the key and of the DHT node who is closer. - -In the case of iterative FRCM the RC of the closer router is provided. -In the case of iterative FIM the pubkey of a dht node who is closer is provided in the GIM. - -find introduction message (FIM) - -variant 1: find an IS by SA - -{ - A: "F", - R: 0 for iterative and 1 for recurisve, - S: "<32 bytes SA>", - T: transaction_id_uint64, - V: 0 -} - -variant 2: recursively find many IS in a tag - -{ - A: "F", - E: [list, of, excluded, SA], - R: 0 for iterative and 1 for recurisve, - N: "<16 bytes topic tag>", - T: transaction_id_uint64, - V: 0 -} - -exclude adding service addresses in E if present - - -got introduction message (GIM) - -sent in reply to FIM containing the result of the search -sent in reply to PIM to acknoledge the publishing of an IS - -{ - A: "G", - I: [IS, IS, IS, ...], - K: "<32 bytes public key of router who is closer, provided ONLY if FIM.R is 0>", - T: transaction_id_uint64, - V: 0, -} - -The I value MUST NOT contain more than 4 IS. -The I value MUST contain either 1 or 0 IS for PIM and FIM variant 1. - - -publish introduction message (PIM) - -publish one IS to the DHT. - -version 0 uses the SA of the IS as the keyspace location. - -in the future the location will be determined by the dht kdf -which uses a shared random source to obfuscate keyspace location. - - -R is currently set to 0 by the sender. - -{ - A: "I", - I: IS, - R: random_walk_counter, - S: optional member 0 for immediate store otherwise non zero, - T: transaction_id_uint64, - V: 0 -} - -if R is greater than 0, decrement R and forward the request to a random DHT -node without storing the IS. -As of protocol version 0, R is always 0. - -If S is provided store the IS for later lookup unconditionally, then -decrement S by 1 and forward to dht peer who is next closest to -the SA of the IS. If S is greater than 3, don't store the IS and -discard this message. - -acknoledge introduction message (AIM) - -acknoledge the publishing of an introduction - -{ - A: "A", - P: published_to_counter, - T: transaction_id_uint64, - V: 0 -} - -increment P by 1 and forward to requester - - -find router contact message (FRCM) - -find a router by long term RC.k public key - -{ - A: "R", - E: 0 or 1 if exploritory lookup, - I: 0 or 1 if iterative lookup, - K: "<32 byte public key of router>", - T: transaction_id_uint64, - V: 0 -} - -if E is provided and non zero then return E dht nodes that are closest to K -if I is provided and non zero then this request is considered an iterative lookup -during an iterative lookup the response's GRCM.K is set to the pubkey of the router closer in key space. -during a recursive lookup the request is forwarded to a router who is closer in -keyspace to K. -If we have no peers that are closer to K and we don't have the RC known locally -we reply with a GRCM whose R value is emtpy. -In any case if we have a locally known RC with pubkey equal to K reply with -a GRCM with 1 RC in the R value corrisponding to that locally known RC. - -got router contact message (GRCM) - -R is a list containing a single RC if found or is an empty list if not found -sent in reply to FRCM only - -{ - A: "S", - K: "<32 bytes public identity key of router closer, provided ONLY if FRCM.I is 1>", - R: [RC], - T: transaction_id_uint64, - V: 0 -} - -in response to an exploritory router lookup, where FRCM.E is provided and non zero. - -{ - A: "S", - N: [list, of, router, publickeys, near, K], - T: transaction_id_uint64, - V: 0 -} - -sent in reply to a dht request to indicate transaction timeout - -{ - A: "T", - T: transaction_id_uint64, - V: 0 -} \ No newline at end of file diff --git a/docs/dht_v1.txt b/docs/dht_v1.txt deleted file mode 100644 index 5832caa7d..000000000 --- a/docs/dht_v1.txt +++ /dev/null @@ -1,94 +0,0 @@ -llarp's dht is a recusrive kademlia dht with optional request proxying via paths for requester anonymization. - -dht is separated into 2 different networks, one for router contacts, one for introsets. - - - - -format for consesus propagation messages: - -keys: A, H, K, N, O, T, U, V - -concensus request messages - -requester requests current table hash, H,N,O is set to zeros if not known - -C -> S - -{ - A: "C", - H: "<32 byte last hash of consensus table>", - N: uint64_number_of_entries_to_request, - O: uint64_offset_in_table, - T: uint64_txid, - V: [] -} - - -when H or N is set to zero from the requester, they are requesting the current consensus table's hash - -consensus response message is as follows for a zero H or N value - -S -> C - -{ - A: "C", - H: "<32 byte hash of current consensus table>", - N: uint64_number_of_entries_in_table, - T: uint64_txid, - U: uint64_ms_next_update_required, - V: [proto, major, minor, patch] -} - -requester requests a part of the current table for hash H - -N must be less than or equal to 512 - -C -> S - -{ - A: "C", - H: "<32 bytes current consensus table hash>", - N: 256, - O: 512, - T: uint64_txid, - V: [] -} - -consensus response message for routers 512 to 512 + 256 - -S -> C - -{ - A: "C", - H: "<32 bytes current concensus table hash>", - K: [list, of, N, pubkeys, from, request, starting, at, position, O], - T: uint64_txid, - V: [proto, major, minor, patch] -} - -consensus table is a concatination of all public keys in lexigraphical order. - -the hash function in use is 256 bit blake2 - - - -gossip RC message - -broadcast style RC publish message. sent to all peers infrequently. - -it is really an unwarrented GRCM, propagate to all peers. - -{ - A: "S", - R: [RC], - T: 0, - V: proto -} - -replays are dropped using a decaying hashset or decaying bloom filter. - - - -the introset dht has 3 message: GetIntroSet Message (GIM), PutIntroSet Message (PIM), FoundIntroSet Message (FIM) - diff --git a/docs/high-level.txt b/docs/high-level.txt deleted file mode 100644 index 704a4cc5b..000000000 --- a/docs/high-level.txt +++ /dev/null @@ -1,147 +0,0 @@ -THIS DOCUMENT IS 3 YEARS OUT OF DATE AND NEEDS TO BE REWRITTEN - - -LLARP - Low Latency Anon Routing Protocol - - TL;DR edition: an onion router with a tun interface for transporting ip packets - anonymously between you and the internet and internally inside itself to other users. - - - This document describes the high level outline of LLARP, specific all the - project's goals, non-goals and network architecture from a bird's eye view. - - - Preface: - - - Working on I2P has been a really big learning experience for everyone involved. - After much deliberation I have decided to start making a "next generation" onion - routing protocol. Specifically LLARP is (currently) a research project - to explore the question: - - - "What if I2P was made in the current year (2018)? What would be different?" - - - Project Non Goals: - - - This project does not attempt to solve traffic shape correlation or active nation - state sponsored network attacks. The former is an inherit property of low latency - computer networks that I personally do not think is possible to properly fully - "solve". The latter is a threat that lies outside the scope of what the current - toolset that is available to me at the moment provides. - - - This project does not attempt to be a magical application level cure-all for - application or end user security. At the end of the day that is a problem that - exists between chair and keyboard. - - - The Single Project Goal: - - - LLARP is a protocol suite meant to anonymize IP by providing an anonymous - network level (IPv4/IPv6) tunnel broker for both "hidden services" and - communication back to "the clearnet" (the normal internet). Both hidden service - and clearnet communication MUST permit both outbound and inbound traffic on the - network level without any NAT (except for IPv4 in which NAT is permitted due to - lack of address availability). - - - In short We want to permit both anonymous exit and entry network level traffic - between LLARP enabled networks and the internet. - - - Rationale for starting over: - - - Despite Tor Project's best efforts to popularize Tor use, Tor2Web seems to be - widely popular for people who do not wish to opt into the ecosystem. My proposed - solution would be to permit inbound traffic from "exit nodes" in addition to - allowing outbound exit traffic. I have no ideas on how this could be done with - the existing protocols in Tor or if it is possible or advisable to attempt such - as I am not familiar with their ecosystem. - - - I2P could have been used as a medium for encrypted anonymous IP transit but the - current network has issues with latency and throughput. Rebasing I2P atop more - modern cryptography has been going on internally inside I2P for at least 5 years - with less progress than desired. Like some before me, I have concluded that it - would be faster to redo the whole stack "the right way" than to wait for I2P to - finish rebasing. That being said, nothing is preventing I2P from be used for - encrypted anonymous IP transit traffic in a future where I2P finishes their - protocol migrations, I just don't want to wait. - - - In short, I want to take the "best parts" from Tor and I2P and make a new - protocol suite. - - - For both Tor and I2P I have 2 categories for the attributes they have. - - - the good - the bad and the ugly - - - The good (I2P): - - - I2P aims to provide an anonymous unspoofable load balanced network layer. - - - I want this feature. - - - I2P is trust agile, it does not have any hard coded trusts in its network - architecture. Even network boostrap can be done from a single router if the user - desires to (albeit this is currently ill advised). - - - I want this feature. - - - The good (Tor): - - - Tor embraces the reality of the current internet infrastructure by having a - client/server architecture. This allows very low barriers of entry in using the - Tor network and a higher barrier of entry for contributing routing - infrastructure. This promotes a healthy network shape of high capacity servers - serving low capacity clients that "dangle off of the side" of the network. - - - I want this feature. - - - The bad and the ugly (I2P): - - - Bad: I2P uses old cryptography, specially 2048 bit ElGamal using non standard primes. - The use of ElGamal is so pervasive throughout the I2P protocol stack that it - exists at every level of it. Removing it is a massive task that is taking a long - LONG time. - - - I don't want this feature. - - - Ugly: I2P cannot currently mitigate most sybil attacks with their current network - architecture. Recently I2P has added some blocklist solutions signed by release - signers but this probably won't scale in the event of a "big" attack. In - addition I2P isn't staffed for such attacks either. - - - This is a hard problem to solve that the Loki network may be able to help - with by creating a financial barrier to running multiple a relays. - - - - The bad and the ugly (Tor): - - - Bad: Tor is strictly TCP oriented. - I don't want this feature. - - diff --git a/docs/linux-setcap-readme.txt b/docs/linux-setcap-readme.txt deleted file mode 100644 index 01b94b3c8..000000000 --- a/docs/linux-setcap-readme.txt +++ /dev/null @@ -1,16 +0,0 @@ -Lokinet needs certain capabilities to run to set up a virtual network interface and provide a DNS server. The preferred approach to using this is through the linux capabilities mechanism, which allows assigning limited capabilities without needing to run the entire process as root. - -There are two main ways to do this: - -1. If you are running lokinet via an init system such as systemd, you can specify the capabilities in the service file by adding: - -CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE -AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE - - into the [Service] section of the systemd service file. This will assign the necessary permissions when running the process and allow lokinet to work while running as a non-root user. - -2. You can set the capabilities on the binary by using the setcap program (if not available you may need to install libcap2-bin on Debian/Ubuntu-based systems) and running: - -setcap cap_net_admin,cap_net_bind_service=+eip lokinet - - This grants the permissions whenever the lokinet binary is executed. diff --git a/docs/llarp_structure.dot b/docs/llarp_structure.dot deleted file mode 100644 index 1052f0998..000000000 --- a/docs/llarp_structure.dot +++ /dev/null @@ -1,102 +0,0 @@ -digraph { - constants -> util; - - crypto -> constants; - crypto -> llarp; - crypto -> util; - - dht -> crypto; - dht -> messages; - dht -> llarp; - dht -> path; - dht -> routing; - dht -> service; - dht -> util; - - dns -> crypto; - dns -> ev; - dns -> handlers; - dns -> llarp; - dns -> net; - dns -> service; - dns -> util; - - ev -> net; - ev -> util; - - exit -> crypto; - exit -> handlers; - exit -> messages; - exit -> net; - exit -> path; - exit -> routing; - exit -> util; - - handlers -> dns; - handlers -> ev; - handlers -> exit; - handlers -> net; - handlers -> service; - handlers -> util; - - link -> constants; - link -> crypto; - link -> ev; - link -> messages; - link -> net; - link -> util; - - messages -> crypto; - messages -> dht; - messages -> exit; - messages -> link; - messages -> llarp; - messages -> path; - messages -> routing; - messages -> service; - messages -> util; - - net -> crypto; - net -> util; - - path -> crypto; - path -> dht; - path -> llarp; - path -> messages; - path -> routing; - path -> service; - path -> util; - - routing -> llarp; - routing -> messages; - routing -> path; - routing -> util; - - service -> crypto; - service -> dht; - service -> ev; - service -> exit; - service -> handlers; - service -> messages; - service -> net; - service -> path; - service -> routing; - service -> util; - - util -> constants; - - llarp -> constants; - llarp -> crypto; - llarp -> dht; - llarp -> dns; - llarp -> ev; - llarp -> exit; - llarp -> handlers; - llarp -> link; - llarp -> messages; - llarp -> net; - llarp -> path; - llarp -> routing; - llarp -> service; - llarp -> util; -} diff --git a/docs/logo.svg b/docs/logo.svg deleted file mode 100644 index 423f32691..000000000 --- a/docs/logo.svg +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/lokinet_admin_v0.txt b/docs/lokinet_admin_v0.txt deleted file mode 100644 index 6ec5f2a5d..000000000 --- a/docs/lokinet_admin_v0.txt +++ /dev/null @@ -1,70 +0,0 @@ -LokiNET admin api - -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", -"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this -document are to be interpreted as described in RFC 2119 [RFC2119]. - ------- - -the admin api currently uses jsonrpc 2.0 over http - -the methods currently provided are: - -llarp.nodedb.rc.getbykey - - get rc by public identity key - - required parameters: - - key: 32 bytes public identity key - - returns: - - a list of RCs (see protocol v0 spec) that have this public identity key - usually 0 or 1 RCs - - -llarp.nodedb.rc.getbycidr - - get a list of RCs in an address range - - required parameters: - - cidr: ipv6 network cidr string, i.e. "::ffff.21.0.0.0/8" or "fc00::/7" - limit: integer max number of items to fetch, zero or positive integer, - if zero no limit. - - returns: - - a list of 0 to limit RCs that advertise themselves as being reachble via an - address in the given CIDR. - - -llarp.admin.sys.uptime (authentication required) - - required paramters: - - (none) - - returns: - - an integer milliseconds since unix epoch we've been online - -llarp.admin.link.neighboors - - get a list of connected service nodes on all links - - required parameters: - - (none) - - returns: - - list of 0 to N dicts in the following format: - - { - "connected" : uint64_milliseconds_timestamp_connected_at - "ident" : "<64 hex encoded public identity key>", - "laddr" : "local address", - "raddr" : "remote address" - } diff --git a/docs/old-wire-protocol.txt b/docs/old-wire-protocol.txt deleted file mode 100644 index 30f07659c..000000000 --- a/docs/old-wire-protocol.txt +++ /dev/null @@ -1,90 +0,0 @@ -Wire Protocol (version ½) - - -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", -"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this -document are to be interpreted as described in RFC 2119 [RFC2119]. - -LLARP supports by default an authenticated and framed transport over UTP [1] - -Handshake: - -Alice establishes a UTP "connection" with Bob. - -Alice sends a LIM a_L encrpyted with the initial b_K key - -if Bob accepts Alice's router, Bob replies with a LIM b_L encrpyted with the -b_K key. - -next the session keys are generated via: - -a_h = HS(a_K + a_L.n) -b_h = HS(b_K + b_L.n) -a_K = TKE(A.p, B_a.e, sk, a_h) -b_K = TKE(A.p, B_a.e, sk, b_h) - -A.tx_K = b_K -A.rx_K = a_K -B.tx_K = a_K -B.rx_K = B_K - -the initial value of a_K is HS(A.k) and b_K is HS(B.k) - -608 byte fragments are sent over UTP in an ordered fashion. - -The each fragment F has the following structure: - -[ 32 bytes blake2 keyed hash of the following 576 bytes (h)] -[ 32 bytes random nonce (n)] -[ 544 bytes encrypted payload (p)] - -the recipiant verifies F.h == MDS(F.n + F.p, rx_K) and the UTP session -is reset if verification fails. - -the decrypted payload P has the following structure: - -[ 24 bytes random (A) ] -[ big endian unsigned 32 bit message id (I) ] -[ big endian unsigned 16 bit fragment length (N) ] -[ big endian unsigned 16 bit fragment remaining bytes (R) ] -[ N bytes of plaintext payload (X) ] -[ trailing bytes discarded ] - -link layer messages fragmented and delievered in any order the sender chooses. - -recipaint ensures a buffer for message number P.I exists, allocating one if it -does not exist. - -recipiant appends P.X to the end of the buffer for message P.I - -if P.R is zero then message number P.I is completed and processed as a link -layer messages. otherwise the recipiant expects P.R additional bytes. -P.R's value MUST decrease by P.N in the next fragment sent. - -message size MUST NOT exceed 8192 bytes. - -if a message is not received in 2 seconds it is discarded and any further -fragments for the message are also discarded. - -P.I MUST have the initial value 0 -P.I MUST be incremeneted by 1 for each new messsage transmitted -P.I MAY wrap around back to 0 - - -after every fragment F the session key K is mutated via: - -K = HS(K + P.A) - -Periodically the connection initiator MUST renegotiate the session key by -sending a LIM after L.p milliseconds have elapsed. - -If the local RC changes while a connection is established they MUST -renegotioate the session keys by sending a LIM to ensure the new RC is sent. - - -references: - -[1] http://www.bittorrent.org/beps/bep_0029.html - - - diff --git a/docs/proto_v0.txt b/docs/proto_v0.txt deleted file mode 100644 index 6a3bb0ecf..000000000 --- a/docs/proto_v0.txt +++ /dev/null @@ -1,903 +0,0 @@ -LLARP v0 - -LLARP (Low Latency Anon Routing Protocol) is a protocol for anonymizing senders and -recipiants of encrypted messages sent over the internet without a centralised -trusted party. - -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", -"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this -document are to be interpreted as described in RFC 2119 [RFC2119]. - -basic structures: - -all structures are key, value dictionaries encoded with bittorrent encoding -notation: - -a + b is a concatanated with b - -a ^ b is a bitwise XOR b - -x[a:b] is a memory slice of x from index a to b - -BE(x) is bittorrent encode x - -BD(x) is bittorrent decode x - -{ a: b, y: z } is a dictionary with two keys a and y - whose values are b and z respectively - -[ a, b, c ... ] is a list containing a b c and more items in that order - -"" is a bytestring whose contents and length is described by the - quoted value - -"" * N is a bytestring containing the concatenated N times. - -cryptography: - -see crypto_v0.txt - ---- - -wire protocol - -see wire-protocol.txt - ---- - -datastructures: - -all datastructures are assumed version 0 if they lack a v value -otherwise version is provided by the v value - -all ip addresses can be ipv4 via hybrid dual stack ipv4 mapped ipv6 addresses, -i.e ::ffff.8.8.8.8. The underlying implementation MAY implement ipv4 as native -ipv4 instead of using a hybrid dual stack. - -net address: - -net addresses are a variable length byte string, if between 7 and 15 bytes it's -treated as a dot notation ipv4 address (xxx.xxx.xxx.xxx) -if it's exactly 16 bytes it's treated as a big endian encoding ipv6 address. - -address info (AI) - -An address info (AI) defines a publically reachable endpoint - -{ - c: transport_rank_uint16, - d: "", - e: "<32 bytes public encryption key>", - i: "", - p: port_uint16, - v: 0 -} - -example wank address info: - -{ - c: 1, - d: "wank", - e: "<32 bytes of 0x61>", - i: "123.123.123.123", - p: 1234, - v: 0 -} - -bencoded form: - -d1:ci1e1:d4:wank1:e32:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1:d3:iwp1:i15:123.123.123.1231:pi1234e1:vi0ee - -Traffic Policy (TP) - -Traffic policy (TP) defines port, protocol and QoS/drop policy. - -{ - a: protocol_integer, - b: port_integeger, - d: drop_optional_integer, - v: 0 -} - -drop d is set to 1 to indicate that packets of protocol a with source port b will be dropped. -if d is 0 or not provided this traffic policy does nothing. - -Exit Info (XI) - -An exit info (XI) defines a exit address that can relay exit traffic to the -internet. - -{ - a: "", - b: "", - k: "<32 bytes public encryption/signing key>", - p: [ list, of, traffic, policies], - v: 0 -} - - -Exit Route (XR) - -An exit route (XR) define an allocated exit address and any additional -information required to access the internet via that exit address. - -{ - a: "<16 bytes big endian ipv6 gateway address>", - b: "<16 bytes big endian ipv6 netmask>", - c: "<16 bytes big endian ipv6 source address>", - l: lifetime_in_milliseconds_uint64, - v: 0 -} - -router contact (RC) - -router's full identity - -{ - a: [ one, or, many, AI, here ... ], - e: extensions_supported, - i: "", - k: "<32 bytes public long term identity signing key>", - n: "", - p: "<32 bytes public path encryption key>", - s: [services, supported], - u: time_signed_at_milliseconds_since_epoch_uint64, - v: 0, - x: [ Exit, Infos ], - z: "<64 bytes signature using identity key>" -} - -e is a dict containing key/value of supported protocol extensions on this service node, keys are strings, values MUST be 0 or 1. - -s is a list of services on this service node: - -each service is a 6 item long, list of the following: - -["_service", "_proto", ttl_uint, priority_uint, weight_uint, port_uint] - -with the corrisponding SRV record: - -<_service>.<_proto>.router_pubkey_goes_here.snode. IN SRV router_pubkey_goes_here.snode - - -RC.t is the timestamp of when this RC was signed. -RC is valid for a maximum of 1 hour after which it MUST be resigned with the new -timestamp. - -RC.i is set to the network identifier. - -only routers with the same network identifier may connect to each other. - -"testnet" for testnet. -"lokinet" for the "official" lokinet mainline network. - -other values of RC.i indicate the router belongs to a network fork. - - -service info (SI) - -public information blob for a hidden service - -e is the long term public encryption key -s is the long term public signing key -v is the protocol version -x is a nounce value for generating vanity addresses that can be omitted - -if x is included it MUST be equal to 16 bytes - -{ - e: "<32 bytes public encryption key>", - s: "<32 bytes public signing key>", - v: 0, - x: "" -} - -service address (SA) - -the "network address" of a hidden service, which is computed as the blake2b -256 bit hash of the public infomration blob. - -HS(BE(SI)) - -when in string form it's encoded with z-base32 and uses the .loki tld - -introduction (I) - -a descriptor annoucing a path to a hidden service - -k is the rc.k value of the router to contact -p is the path id on the router that is owned by the service -v is the protocol version -x is the timestamp milliseconds since epoch that this introduction expires at - -{ - k: "<32 bytes public identity key of router>", - l: advertised_path_latency_ms_uint64, (optional) - p: "<16 bytes path id>", - v: 0, - x: time_expires_milliseconds_since_epoch_uint64 -} - -introduction set (IS) - -and introset is a signed set of introductions for a hidden service -a is the service info of the publisher -h is a list of srv records in same format as in RCs -i is the list of introductions that this service is advertising with -k is the public key to use when doing encryption to this hidden service -n is a 16 byte null padded utf-8 encoded string tagging the hidden service in - a topic searchable via a lookup (optional) -v is the protocol version -w is an optinal proof of work for DoS protection (slot for future) -z is the signature of the entire IS where z is set to zero signed by the hidden -service's signing key. - -{ - a: SI, - h: [list, of, advertised, services], - i: [ I, I, I, ... ], - k: "<1218 bytes sntrup4591761 public key block>", - n: "<16 bytes service topic (optional)>", - t: timestamp_uint64_milliseconds_since_epoch_published_at, - v: 0, - w: optional proof of work, - z: "<64 bytes signature using service info signing key>" -} - -h is a list of services on this endpoint - -each service is a 7 item long, list of the following: - -["_service", "_proto", ttl_uint, priority_uint, weight_uint, port_uint, "<32 bytes SA of the service>"] - -with the corrisponding SRV record: - -<_service>.<_proto>..loki. IN SRV .loki. - -recursion on SRV records is NOT permitted. - ---- - -Encrypted frames: - - -Encrypted frames are encrypted containers for link message records like LRCR. - -32 bytes hmac, h -32 bytes nounce, n -32 bytes ephmeral sender's public encryption key, k -remaining bytes ciphertext, x - -decryption: - -0) verify hmac - -S = PKE(n, k, our_RC.K) -verify h == MDS(n + k + x, S) - -If the hmac verification fails the entire parent message is discarded - -1) decrypt and decode - -new_x = SD(S, n[0:24], x) -msg = BD(new_x) - -If the decoding fails the entire parent message is discarded - -encryption: - -to encrypt a frame to a router with public key B.k - -0) prepare nounce n, ephemeral keypair (A.k, s) and derive shared secret S - -A.k, s = ECKG() -n = RAND(32) -S = PKE(p, A.k, B.k, n) - -1) encode and encrypt - -x = BE(msg) -new_x = SE(S, n[0:24], x) - -2) generate message authentication - -h = MDS(n + A.k + new_x, S) - -resulting frame is h + n + A.k + new_x - - ---- - -link layer messages: - -the link layer is responsible for anonymising the source and destination of -routing layer messages. - -any link layer message without a key v is assumed to be version 0 otherwise -indicates the protocol version in use. - - - -link introduce message (LIM) - -This message MUST be the first link message sent before any others. This message -identifies the sender as having the RC contained in r. The recipiant MUST -validate the RC's signature and ensure that the public key in use is listed in -the RC.a matching the ipv6 address it originated from. - -{ - a: "i", - e: "<32 bytes ephemeral public encryption key>", - n: "<32 bytes nonce for key exhcange>", - p: uint64_milliseconds_session_period, - r: RC, - v: 0, - z: "<64 bytes signature of entire message by r.k>" -} - -the link session will be kept open for p milliseconds after which -the session MUST be renegotiated. - -link relay commit message (LRCM) - -request a commit to relay traffic to another node. - -{ - a: "c", - c: [ list, of, encrypted, LRCR frames ], - v: 0 -} - -c MUST contain dummy records if the hop length is less than the maximum -hop length. - -link relay commit record (LRCR) - -record requesting relaying messages for 600 seconds to router -on network whose i is equal to RC.k and decrypt data any messages using -PKE(n, rc.p, c) as symmetric key for encryption and decryption. - -if l is provided and is less than 600 and greater than 10 then that lifespan -is used (in seconds) instead of 600 seconds. - -if w is provided and fits the required proof of work then the lifetime of -the path is extended by w.y seconds - -{ - c: "<32 byte public encryption key used for upstream>", - d: uint_optional_ms_delay, // TODO - i: "<32 byte RC.k of next hop>", - l: uint_optional_lifespan, - n: "<32 bytes nounce for key exchange>", - r: "<16 bytes rx path id>", - t: "<16 bytes tx path id>", - u: "", - v: 0, - w: proof of work -} - -w if provided is a dict with the following struct - -{ - t: time_created_seconds_since_epoch, - v: 0, - y: uint32_seconds_extended_lifetime, - z: "<32 bytes nonce>" -} - -the validity of the proof of work is that given - -h = HS(BE(w)) - -h has log_e(y) prefixed bytes being 0x00 - -this proof of work requirement is subject to change - -if i is equal to RC.k then any LRDM.x values are decrypted and interpreted as -routing layer messages. This indicates that we are the farthest hop in the path. - -link relay status message (LRSM) - -response to path creator about build status - -{ - a: "s", - c: [ list, of, encrypted, LRSR frames], - p: "<16 bytes rx path id>", - s: uint_status_flags, // for now, only set (or don't) success bit - v: 0 -} - -the creator of the LRSM MUST include dummy LRSR records -to pad out to the maximum path length - -link relay status record (LRSR) - -record indicating status of path build - -{ - s: uint_status_flags, - v: 0 -} - -uint_status_flags (bitwise booleans): - [0] = success - [1] = fail, hop timeout - [2] = fail, congestion - [3] = fail, refusal, next hop is not known to be a snode - [4] = fail, used by hop creator when decrypting frames if decryption fails - [5] = fail, used by hop creator when record decode fails - [4-63] reserved - -link relay upstream message (LRUM) - -sent to relay data via upstream direction of a previously created path. - -{ - a: "u", - p: "<16 bytes path id>", - v: 0, - x: "", - y: "<32 bytes nonce>" -} - -x1 = SD(k, y, x) - -if we are the farthest hop, process x1 as a routing message -otherwise transmit a LRUM to the next hop - -{ - a: "u", - p: p, - v: 0, - x: x1, - y: y ^ HS(k) -} - -link relay downstream message (LRDM) - -sent to relay data via downstream direction of a previously created path. - -{ - a: "d", - p: "<16 bytes path id>", - v: 0, - x: "", - y: "<32 bytes nonce>" -} - -if we are the creator of the path decrypt x for each hop key k - -x = SD(k, y, x) - -otherwise transmit LRDM to next hop - -x1 = SE(k, y, x) - -{ - a: "d", - p: p, - v: 0, - x: x1, - y: y ^ HS(k) -} - -link immediate dht message (LIDM): - -transfer one or more dht messages directly without a previously made path. - -{ - a: "m", - m: [many, dht, messages], - v: 0 -} - - -link immediate state message (LISM) - -transfer a state message between nodes - -{ - a: "s", - s: state_message, - v: 0 -} - ---- - -state message: - -NOTE: this message type is currently a documentation stub and remains unimplemented. - -state messages propagate changes to the service nodes concensous to thin clients. - -service node joined network - -{ - A: "J", - R: "<32 bytes public key>", - V: 0 -} - -service node parted network - -{ - A: "P", - R: "<32 bytes public key>", - V: 0 -} - -service node list request - -request the service node list starting at index I containing R entries - -{ - A: "R", - I: starting_offset_int, - R: number_of_entires_to_request_int, - V: 0 -} - -service node list response - -response to service node list request - -{ - A: "L", - S: { - "<32 bytes pubkey>" : 1, - "<32 bytes pubkey>" : 1, - .... - }, - V: 0 -} - ---- - -routing layer: - -the routing layer provides inter network communication between the LLARP link -layer and ip (internet protocol) for exit traffic or ap (anonymous protocol) for -hidden services. replies to messages are sent back via the path they -originated from inside a LRDM. all routing messages have an S value which -provides the sequence number of the message so the messages can be ordered. - -ipv4 addresses are allowed via ipv4 mapped ipv6 addresses, i.e. ::ffff.10.0.0.1 - -path confirm message (PCM) - -sent as the first message down a path after it's built to confirm it was built - -always the first message sent - -{ - A: "P", - L: uint64_milliseconds_path_lifetime, - S: 0, - T: uint64_milliseconds_sent_timestamp, - V: 0 -} - -path latency message (PLM) - -a latency measurement message, reply with a PLM response if we are the far end -of a path. - -variant 1, request, generated by the path creator: - -{ - A: "L", - S: uint64_sequence_number, - V: 0 -} - -variant 2, response, generated by the endpoint that recieved the request. - -{ - A: "L", - S: uint64_sequence_number, - T: uint64_timestamp_recieved, - V: 0 -} - -obtain exit message (OXM) - -sent to an exit router to obtain ip exit traffic context. -replies are sent down the path that messages originate from. - -{ - A: "X", - B: [list, of, permitted, blacklisted, traffic, policies], - E: 0 for snode communication or 1 for internet access, - I: "<32 bytes signing public key for future communication>", - S: uint64_sequence_number, - T: uint64_transaction_id, - V: 0, - W: [list, of, required, whitelisted, traffic, policies], - X: lifetime_of_address_mapping_in_seconds_uint64, - Z: "<64 bytes signature using I>" -} - -grant exit messsage (GXM) - -sent in response to an OXM to grant an ip for exit traffic from an external -ip address used for exit traffic. - -{ - A: "G", - S: uint64_sequence_number, - T: transaction_id_uint64, - Y: "<16 byte nonce>", - V: 0, - Z: "<64 bytes signature>" -} - -any TITM recieved on the same path will be forwarded out to the internet if -OXAM.E is not 0, otherwise it is interpreted as service node traffic. - - -reject exit message (RXM) - -sent in response to an OXAM to indicate that exit traffic is not allowed or -was denied. - -{ - A: "J", - B: backoff_milliseconds_uint64, - R: [list, of, rejected, traffic, policies], - S: uint64_sequence_number, - T: transaction_id_uint64, - V: 0, - Y: "<16 byte nonce>", - Z: "<64 bytes signature>" -} - - -discarded data fragment message (DDFM) - -sent in reply to TDFM when we don't have a path locally or are doing network -congestion control from a TITM. - -{ - A: "D", - P: "<16 bytes path id>", - S: uint64_sequence_number_of_fragment_dropped, - V: 0 -} - -transfer data fragment message (TDFM) - -transfer data between paths. - -{ - A: "T", - P: "<16 bytes path id>", - S: uint64_sequence_number, - T: hidden_service_frame, - V: 0 -} - -transfer data to another path with id P on the local router place a random 32 -byte and T values into y and z values into a LRDM message (respectively) and -send it in the downstream direction. if this path does not exist on the router -it is replied to with a DDFM. - - - -hidden service data (HSD) - -data sent anonymously over the network to a recipiant from a sender. -sent inside a HSFM encrypted with a shared secret. - -{ - a: protocol_number_uint, - d: "", - i: Introduction for reply, - n: uint_message_sequence_number, - o: N seconds until this converstation plans terminate, - s: SI of sender, - t: "<16 bytes converstation_tag>, - v: 0 -} - - -hidden service frame (HSF) - -TODO: document this better - -establish converstation tag message (variant 1) - -generate a new convotag that is contained inside an encrypted HSD - -{ - A: "H", - C: "<1048 bytes ciphertext block>", - D: "", - F: "<16 bytes source path_id>", - N: "<32 bytes nonce for key exchange>", - V: 0, - Z: "<64 bytes signature of entire message using sender's signing key>" -} - -alice (A) wants to talk to bob (B) over the network, both have hidden services -set up and are online on the network. - -A and B are both referring to alice and bob's SI respectively. -A_sk is alice's private signing key. - -for alice (A) to send the string "beep" to bob (B), alice picks an introduction -to use on one of her paths (I_A) such that I_A is aligning with one of bobs's -paths (I_B) - -alice generates: - -T = RAND(16) - -m = { - a: 0, - d: "beep", - i: I_A, - n: 0, - s: A, - t: T, - v: 0 -} - -X = BE(m) - -C, K = PQKE_A(I_B.k) -N = RAND(32) -D = SE(X, K, N) - -path = PickSendPath() - -M = { - A: "T", - P: I_B.P, - S: uint64_sequence_number, - T: { - A: "H", - C: C, - D: D, - F: path.lastHop.txID, - N: N, - V: 0, - Z: "\x00" * 64 - }, - V: 0 -} - -Z = S(A_sk, BE(M)) - -alice transmits a TDFM to router with public key I_B.K via her path that ends -with router with public key I_B.k - -path = PickSendPath() - -{ - A: "T", - P: I_B.P, - S: uint64_sequence_number, - T: { - A: "H", - C: C, - D: D, - F: path.lastHop.txID, - N: N, - V: 0, - Z: Z - }, - V: 0 -} - -the shared secret (S) for further message encryption is: - -S = HS(K + PKE(A, B, sk, N)) - -given sk is the local secret encryption key used by the current hidden service - -please note: -signature verification of the outer message can only be done after decryption -because the signing keys are inside the encrypted HSD. - -data from a previously made session (variant 2) - -transfer data on a converstation previously made - -{ - A: "H", - D: "", - F: "<16 bytes path id of soruce>", - N: "<32 bytes nonce for symettric cipher>", - T: "<16 bytes converstation tag>", - V: 0, - Z: "<64 bytes signature using sender's signing key>" -} - -reject a message sent on a convo tag, when a remote endpoint -sends this message a new converstation SHOULD be established. - -{ - A: "H", - F: "<16 bytes path id of soruce>", - R: 1, - T: "<16 bytes converstation tag>", - V: 0, - Z: "<64 bytes signature using sender's signing key>" -} - - -transfer ip traffic message (TITM) - -transfer ip traffic - -{ - A: "I", - S: uint64_sequence_number, - V: 0, - X: [list, of, ip, packet, buffers], -} - -an ip packet buffer is prefixed with a 64 bit big endian unsigned integer -denoting the sequence number for re-ordering followed by the ip packet itself. - -X is parsed as a list of IP packet buffers. -for each ip packet the source addresss is extracted and sent on the -appropriate network interface. - -When we receive an ip packet from the internet to an exit address, we put it -into a TITM, and send it downstream the corresponding path in an LRDM. - -update exit path message (UXPM) - -sent from a new path by client to indicate that a previously established exit -should use the new path that this message came from. - -{ - A: "U", - P: "<16 bytes previous tx path id>", - S: uint64_sequence_number, - T: uint64_txid, - V: 0, - Y: "<16 bytes nonce>", - Z: "<64 bytes signature using previously provided signing key>" -} - -close exit path message (CXPM) - -client sends a CXPM when the exit is no longer needed or by the exit if the -exit wants to close prematurely. -also sent by exit in reply to a CXPM to confirm close. - -{ - A: "C", - S: uint64_sequence_number, - V: 0, - Y: "<16 bytes nonce>", - Z: "<64 bytes signature>" -} - -update exit verify message (EXVM) - -sent in reply to a UXPM to verify that the path handover was accepted - -{ - A: "V", - S: uint64_sequence_number, - T: uint64_txid, - V: 0, - Y: "<16 bytes nonce>", - Z: "<64 bytes signature>" -} - - -DHT message holder message: - -wrapper message for sending many dht messages down a path. - -{ - A: "M", - M: [many, dht, messages, here], - S: uint64_sequence_number, - V: 0 -} diff --git a/docs/tooling.txt b/docs/tooling.txt deleted file mode 100644 index e41325330..000000000 --- a/docs/tooling.txt +++ /dev/null @@ -1,38 +0,0 @@ -What is a RouterEvent? - - A RouterEvent is a way of representing a conceptual event that took place in a "router" (relay or client). - - RouterEvents are used in order to collect information about a network all in one place and preserve causality. - -How do I make a new RouterEvent? - - Add your event following the structure in llarp/tooling/router_event.{hpp,cpp} - - Add your event to pybind in pybind/llarp/tooling/router_event.cpp - -What if a class my event uses is missing members in pybind? - - Find the relevant file pybind/whatever/class.cpp and remedy that! - -What if I need to refer to classes which aren't available already in pybind? - - Add pybind/namespace/namespace/etc/class.cpp and pybind it! - - You will need to edit the following files accordingly: - pybind/common.hpp - pybind/module.cpp - pybind/CMakeLists.txt - -How do I use a RouterEvent? - - From the cpp side, find the place in the code where it makes the most logical sense - that the conceptual event has taken place (and you have the relevant information to - create the "event" instance) and create it there as follows: - - #include - - where the event takes place, do the following: - auto event = std::make_unique(constructor_args...); - somehow_get_a_router->NotifyRouterEvent(std::move(event)); - - From the Python side...it's a python object! diff --git a/docs/wire-protocol.txt b/docs/wire-protocol.txt deleted file mode 100644 index 0cb51cc0c..000000000 --- a/docs/wire-protocol.txt +++ /dev/null @@ -1,309 +0,0 @@ -Wire Protocol (version 1) - - -The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", -"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this -document are to be interpreted as described in RFC 2119 [RFC2119]. - -LLARP's wire protocol is Internet Wire Protocol (IWP) - -The main goal of iwp is to provide an authenticated encrypted -reliable semi-ordered durable datagram transfer protocol supporting -datagrams of larger size than link mtu. - -in iwp there is an initiator who initiates a session to a recipiant. - -iwp has 3 phases. the first phase is the proof of flow phase. -the second is a session handshake phase, the third is data transmission. - -proof of flow: - -the purpose of the proof of flow phase is to verify the existence -of the initiator's endpoint. - -At any time before the data transfer phase a reject message -is sent the session is reset. - -Alice (A) is the sender and Bob (B) is the recipiant. - -A asks for a flow id from B. - -B MAY send a flow id to A or MAY reject the message from A. - -session handshake: - -an encrypted session is established using establish wire session messages -using a newly created flow id. - -message format: - -there are 2 layers in this protocol, outer messages and inner messages. - -outer messages are sent in plaintext and / or obfsucated with symettric -encryption using a preshared key. - -inner messages are inside an encrypted and authenticated envelope -wrapped by an outer messages, which is always a data tranmssion message. - -outer message format: - -every outer message MAY be obfsucated via symettric encryption for dpi -resistance reasons, this is not authenticated encryption. - -the message is first assumed to be sent in clear first. -if parsing of clear variant fails then the recipiant MUST fall back to assuming -the protocol is in obfuscated mode. - - -<16 bytes nounce, n> - - -obfuscated via: - -K = HS(B_k) -N = HS(n + K) -X = SD(K, m, N[0:24]) - -where -B_k is the long term identity public key of the recipient. -HS is blake2 256 bit non keyed hash -SD is xchacha20 symettric stream cipher (decryption) - -outer-header: - -<1 byte command> -<1 byte reserved set to 0x3d> - -command 'O' - obtain flow id - -obtain a flow id - - -<6 magic bytes "netid?"> -<8 bytes netid, I> -<8 bytes timestamp milliseconds since epoch, T> -<32 bytes public identity key of sender, A_k> -<0-N bytes discarded> - - -the if the network id differs from the current network's id a reject message -MUST be sent - -MUST be replied to with a message rejected or a give handshake cookie - -command 'G' - give flow id - - -<6 magic bytes "netid!"> -<16 bytes new flow id> -<32 bytes public identiy key of sender, A_k> -<0-N bytes ignored but included in signature> - - -after recieving a give flow id message a session negotiation can happen with that flow id. - -command 'R' - flow rejected - -reject new flow - - -<14 ascii bytes reason for rejection null padded> -<8 bytes timestamp> -<32 bytes public identity key of sender, A_k> -<0-N bytes ignored but included in signature> - - -command 'E' - establish wire session - -establish an encrypted session using a flow id - - -<2 bytes 0x0a 0x0d> -<4 bytes flags, F> -<16 bytes flow id, B> -<32 bytes ephemeral public encryption key, E> -<8 bytes packet counter starting at 0> - - - - -F is currently set to all zeros - -every time we try establishing a wire session we increment the counter -by 1 for the next message we send. - -when we get an establish wire session message -we reply with an establish wire session message with counter being counter + 1 - -if A is provided that is interpreted as being generated via: - -h0 = HS('') -h1 = EDDH(us, them) -A = HS(B + h0 + h1) - -each side establishes their own rx key using this message. -when each side has both established thier rx key data can be transmitted. - -command 'D' - encrypted data transmission - -transmit encrypted data on a wire session - - -<16 bytes flow-id, F> -<24 bytes nonce, N> - - - - -B is the flow id from the recipiant (from outer header) -N is a random nounce -X is encrypted data -Z is keyed hash of entire message - -Z is generated via: - -msg.Z = MDS(outer-header + F + N + X, tx_K) - -data tranmission: - -inner message format of X (after decryption): - -inner header: - -<1 byte protocol version> -<1 byte command> - - -command: 'k' (keep alive) - -tell other side to acknoledge they are alive - - -<2 bytes resevered, set to 0> -<2 bytes attempt counter, set to 0 and incremented every retransmit, reset when we get a keepalive ack> -<2 bytes milliseconds ping timeout> -<8 bytes current session TX limit in bytes per second> -<8 bytes current session RX use in bytes per second> -<8 bytes milliseconds since epoch our current time> - - -command: 'l' (keep alive ack) - -acknolege keep alive message - - -<6 bytes reserved, set to 0> -<8 bytes current session RX limit in bytes per second> -<8 bytes current session TX use in bytes per second> -<8 bytes milliseconds since epoch our current time> - - - -command: 'n' (advertise neighboors) - -tell peer about neighboors, only sent by non service nodes to other non service -nodes. - - - -<0 or more intermediate routes> - - -route: - -<1 byte route version (currently 0)> -<1 byte flags, lsb set indicates src is a service node> -<2 bytes latency in ms> -<2 bytes backpressure> -<2 bytes number of connected peers> -<8 bytes publish timestamp ms since epoch> -<32 bytes pubkey neighboor> -<32 bytes pubkey src> -<64 bytes signature of entire route signed by src> - -command: 'c' (congestion) - -tell other side to slow down - - -<2 bytes reduce TX rate by this many 1024 bytes per second> -<4 bytes milliseconds slowdown lifetime> - - -command: 'd' (anti-congestion) - -tell other side to speed up - - -<2 bytes increase TX rate by this many 1024 bytes per second> -<4 bytes milliseconds speedup lifetime> - - - -command: 's' (start transmission) - -initate the transmission of a message to the remote peer - - -<1 byte flags F> -<1 byte reserved R set to zero> -<2 bytes total size of full message> -<4 bytes sequence number S> -<32 bytes blake2 hash of full message> - - -if F lsb is set then there is no further fragments - -command: 't' (continued transmission) - -continue transmission of a bigger message - - -<1 byte flags F> -<1 bytes reserved R set to zero> -<2 bytes 16 byte block offset in message> -<4 bytes sequence number S> - - - -command: 'q' (acknoledge transmission) - -acknoledges a transmitted message - - - -command: 'r' (rotate keys) - -inform remote that their RX key should be rotated - -given alice(A) sends this message to bob(B) the new keys are computed as such: - -n_K = TKE(K, B_e, K_seed, N) - -A.tx_K = n_K -B.rx_K = n_K - - -<2 bytes milliseconds lifetime of old keys, retain them for this long and then discard> -<4 bytes reserved, set to 0> -<32 bytes key exchange nounce, N> -<32 bytes next public encryption key, K> - - -command: 'u' (upgrade) - -request protocol upgrade - - -<1 byte protocol min version to upgrade to> -<1 byte protocol max version to upgrade to> - - -command: 'v' (version upgrade) - -sent in response to upgrade message - - -<1 byte protocol version selected> -<1 byte protocol version highest we support> - From 2772a32907e9c52689f2d32b9be813f285685bce Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 10 Dec 2021 10:38:10 -0500 Subject: [PATCH 012/202] * fix up lokinet cli help opts * document doxygen --- daemon/lokinet.cpp | 25 +++++--------------- docs/README | 5 ---- docs/readme.md | 34 +++++++++++++++++++++++++++ docs/snap-config-options.md | 47 ------------------------------------- 4 files changed, 40 insertions(+), 71 deletions(-) delete mode 100644 docs/README create mode 100644 docs/readme.md delete mode 100644 docs/snap-config-options.md diff --git a/daemon/lokinet.cpp b/daemon/lokinet.cpp index 2c1a70794..ebddf84e1 100644 --- a/daemon/lokinet.cpp +++ b/daemon/lokinet.cpp @@ -401,19 +401,17 @@ lokinet_main(int argc, char* argv[]) "and IP based onion routing network"); // clang-format off options.add_options() - ("v,verbose", "Verbose", cxxopts::value()) #ifdef _WIN32 ("install", "install win32 daemon to SCM", cxxopts::value()) ("remove", "remove win32 daemon from SCM", cxxopts::value()) #endif - ("h,help", "help", cxxopts::value())("version", "version", cxxopts::value()) - ("g,generate", "generate client config", cxxopts::value()) - ("r,router", "run as router instead of client", cxxopts::value()) - ("f,force", "overwrite", cxxopts::value()) + ("h,help", "print this help message", cxxopts::value()) + ("version", "print version string", cxxopts::value()) + ("g,generate", "generate default configuration and exit", cxxopts::value()) + ("r,router", "run in routing mode instead of client only mode", cxxopts::value()) + ("f,force", "force writing config even if it already exists", cxxopts::value()) ("c,colour", "colour output", cxxopts::value()->default_value("true")) - ("b,background", "background mode (start, but do not connect to the network)", - cxxopts::value()) - ("config", "path to configuration file", cxxopts::value()) + ("config", "path to lokinet.ini configuration file", cxxopts::value()) ; // clang-format on @@ -426,12 +424,6 @@ lokinet_main(int argc, char* argv[]) { auto result = options.parse(argc, argv); - if (result.count("verbose") > 0) - { - SetLogLevel(llarp::eLogDebug); - llarp::LogDebug("debug logging activated"); - } - if (!result["colour"].as()) { llarp::LogContext::Instance().logStream = @@ -467,11 +459,6 @@ lokinet_main(int argc, char* argv[]) genconfigOnly = true; } - if (result.count("background") > 0) - { - opts.background = true; - } - if (result.count("router") > 0) { opts.isSNode = true; diff --git a/docs/README b/docs/README deleted file mode 100644 index f01a01d3b..000000000 --- a/docs/README +++ /dev/null @@ -1,5 +0,0 @@ -Protocol Specifications Directory - -All documents in this directory are licened CC0 and placed into the public domain. - -Please note that the reference implementation LokiNET is licensed under ZLIB license diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 000000000..91899d771 --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,34 @@ +# Lokinet Internals docs + +this is the area of the repo for documentation of lokinet internals + + + + + +## Doxygen + +building doxygen docs requires the following: + +* cmake +* doxygen +* sphinx-build +* sphinx readthedocs theme +* breathe +* exhale + +install packages: + + $ sudo apt install doxygen python3-sphinx python3-sphinx-rtd-theme python3-breathe python3-pip + $ pip3 install --user exhale + +build docs: + + $ mkdir -p build-docs + $ cd build-docs + $ cmake .. && make doc + +serve built docs via http, will be served at http://127.0.0.1:8000/ + + $ python3 -m http.server -d docs/html + diff --git a/docs/snap-config-options.md b/docs/snap-config-options.md deleted file mode 100644 index 0979fe22a..000000000 --- a/docs/snap-config-options.md +++ /dev/null @@ -1,47 +0,0 @@ - -# snapp config options - -## required - -### ifname -network interface name -### ifaddr -ip range of network interface - -## optional - -### keyfile -the private key to persist address with. -if not specified the address will be ephemeral. -### reachable -bool value that sets if we publish our snapp to the dht -`true`: we are reachable via dht -`false`: we are not reachable via dht -### hops -number of hops in a path, min is `1`, max is `8`, defaults to `4` -### paths -number of paths to maintain at any given time, defaults to `6`. -### blacklist-snode -adds a `.snode` to path build blacklist -### exit-node -specify a `.snode` or `.loki` address to use as an exit broker -### local-dns -address to bind local dns resoler to, defaults to `127.3.2.1:53` -if port is omitted it uses port `53` -### upstream-dns -address to forward non lokinet related queries to. if not set lokinet dns will reply with srvfail. -### mapaddr -perma map `.loki` address to an ip owned by the snapp -to map `whatever.loki` to `10.0.10.10` it can be specified via: -``` -mapaddr=whatever.loki:10.0.10.10 -``` - -## compile time optional features - -### on-up -path to shell script to call when our interface is up -### on-down -path to shell script to call when our interface is down -### on-ready -path to shell script to call when snapp is first ready From 479fba6bd002798e408e183e8685ea3b526c9761 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 10 Dec 2021 11:14:55 -0500 Subject: [PATCH 013/202] add big chunks of docs --- docs/doxygen.md | 27 +++++++++++++++++++++++++++ docs/net-comparisons.md | 35 +++++++++++++++++++++++++++++++++++ docs/readme.md | 33 ++++++++++----------------------- 3 files changed, 72 insertions(+), 23 deletions(-) create mode 100644 docs/doxygen.md create mode 100644 docs/net-comparisons.md diff --git a/docs/doxygen.md b/docs/doxygen.md new file mode 100644 index 000000000..3b2fcdffe --- /dev/null +++ b/docs/doxygen.md @@ -0,0 +1,27 @@ + +# Doxygen + +building doxygen docs requires the following: + +* cmake +* doxygen +* sphinx-build +* sphinx readthedocs theme +* breathe +* exhale + +install packages: + + $ sudo apt install doxygen python3-sphinx python3-sphinx-rtd-theme python3-breathe python3-pip + $ pip3 install --user exhale + +build docs: + + $ mkdir -p build-docs + $ cd build-docs + $ cmake .. && make doc + +serve built docs via http, will be served at http://127.0.0.1:8000/ + + $ python3 -m http.server -d docs/html + diff --git a/docs/net-comparisons.md b/docs/net-comparisons.md new file mode 100644 index 000000000..15ce8cf2d --- /dev/null +++ b/docs/net-comparisons.md @@ -0,0 +1,35 @@ +# How is lokinet different than ... + + +## Tor Browser + +Tor 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. +Lokinet 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. + +## Tor/Onion Services + +While Tor Browser is the main user facing product made by Tor Project the main powerhouse is Tor itself which provides a way to anonymize tcp connections made by an inititor and optionally additionally the recipiant in the case of 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). + +Lokinet differs greatly from the UX side vs Tor as by default we do not provide exit connectivity by default. This is primarily because we cannot do this by default as in practice each user's threat model greatly varies in scope and breadth thus there exists no one size fits all way to do exits that works for everyone. 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 un implemented. We think that by being hands of on exit node requirements a far more diverse set of exit nodes can exist. In addition to having totally open unrestrcited exits, permitting "specialized" exit providers that are allowed to do excessive filtering or geo blocking for security theatre checkbox compliance is something Tor does not seem to be able to provide at this time. + +Lokinet additionally encourges the manual selection and pinning of edge connections to fit each user's threat model. + +## I2P + +Integrating applications to opt into i2p's network layer is painful and greatly stunts mainstream adoption. +Lokinet takes the inverse approach of i2p: make app integration in lokinet should require zero custom shims or modifications to code to make it work. + +## DVPNs / Commercial VPN Proxies + +One 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. + +Lokinet can see only 1 of these 2 things, but NEVER both: + +* Encrypted data coming from your real IP going to the first hop Lokinet Router forwarded to another Lokinet Router. +* A lokinet exit sees traffic coming from a `.loki` address but has no idea what the real IP is for it. + +One Hop Commericial VPN Tunnels are no log by **policy**. You just have to trust that they are telling the truth. + +Lokinet is no log by **design** it doesn't have a choice in this matter because the technology greatly hampers efforts to do so. + +Any 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. diff --git a/docs/readme.md b/docs/readme.md index 91899d771..8be1dbd7c 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -1,34 +1,21 @@ -# Lokinet Internals docs +# Lokinet Docs -this is the area of the repo for documentation of lokinet internals +this is the area of the repo for documentation of lokinet +## High level +[How is Lokinet different to \[insert network technology name here\] ?](net-comparisons.md) +[How Do I use Lokinet?](lokinet-ideal-ux.md) +## Lokinet (SN)Application Developer Portal -## Doxygen -building doxygen docs requires the following: +[What are "SNApps" and how to develop them.](snapps-dev-guide.md) +[How do I embed lokinet into my application?](liblokinet-dev-guide.md) -* cmake -* doxygen -* sphinx-build -* sphinx readthedocs theme -* breathe -* exhale -install packages: +## Lokinet Internals - $ sudo apt install doxygen python3-sphinx python3-sphinx-rtd-theme python3-breathe python3-pip - $ pip3 install --user exhale - -build docs: +[Build Doxygen Docs for internals](doxygen.md) - $ mkdir -p build-docs - $ cd build-docs - $ cmake .. && make doc - -serve built docs via http, will be served at http://127.0.0.1:8000/ - - $ python3 -m http.server -d docs/html - From 4bbf5522b407e6bbcfe02e6bf61aa6e1f46e0b9c Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 10 Dec 2021 11:15:44 -0500 Subject: [PATCH 014/202] spaces --- docs/readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/readme.md b/docs/readme.md index 8be1dbd7c..24434f9bc 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -5,6 +5,7 @@ this is the area of the repo for documentation of lokinet ## High level [How is Lokinet different to \[insert network technology name here\] ?](net-comparisons.md) + [How Do I use Lokinet?](lokinet-ideal-ux.md) @@ -12,6 +13,7 @@ this is the area of the repo for documentation of lokinet [What are "SNApps" and how to develop them.](snapps-dev-guide.md) + [How do I embed lokinet into my application?](liblokinet-dev-guide.md) From be47299b5d3da864c5ffedf4557b2d16488ddd50 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 10 Dec 2021 11:45:51 -0500 Subject: [PATCH 015/202] more docs --- docs/ideal-ux.md | 3 +++ docs/liblokinet-dev-guide.md | 11 +++++++++++ docs/lokinet-ideal-ux.md | 3 +++ docs/net-comparisons.md | 8 ++++---- docs/readme.md | 6 ++++-- docs/snapps-dev-guide.md | 30 ++++++++++++++++++++++++++++++ docs/we-cannot-make-sandwiches.md | 19 +++++++++++++++++++ 7 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 docs/ideal-ux.md create mode 100644 docs/liblokinet-dev-guide.md create mode 100644 docs/lokinet-ideal-ux.md create mode 100644 docs/snapps-dev-guide.md create mode 100644 docs/we-cannot-make-sandwiches.md diff --git a/docs/ideal-ux.md b/docs/ideal-ux.md new file mode 100644 index 000000000..2050892d5 --- /dev/null +++ b/docs/ideal-ux.md @@ -0,0 +1,3 @@ +# How Do I use lokinet? + +`// TODO: this` diff --git a/docs/liblokinet-dev-guide.md b/docs/liblokinet-dev-guide.md new file mode 100644 index 000000000..f89f8e9f2 --- /dev/null +++ b/docs/liblokinet-dev-guide.md @@ -0,0 +1,11 @@ +# Embedding Lokinet into an existing application + +When 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. + +## Why you should avoid this route + +`// TODO: this` + +## When you cannot avoid this route, how do i use it? + +`// TODO: this` diff --git a/docs/lokinet-ideal-ux.md b/docs/lokinet-ideal-ux.md new file mode 100644 index 000000000..2050892d5 --- /dev/null +++ b/docs/lokinet-ideal-ux.md @@ -0,0 +1,3 @@ +# How Do I use lokinet? + +`// TODO: this` diff --git a/docs/net-comparisons.md b/docs/net-comparisons.md index 15ce8cf2d..65264cd37 100644 --- a/docs/net-comparisons.md +++ b/docs/net-comparisons.md @@ -8,15 +8,15 @@ Lokinet does not provide a web browser at this time because that is not a small ## Tor/Onion Services -While Tor Browser is the main user facing product made by Tor Project the main powerhouse is Tor itself which provides a way to anonymize tcp connections made by an inititor and optionally additionally the recipiant in the case of 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). +While 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). -Lokinet differs greatly from the UX side vs Tor as by default we do not provide exit connectivity by default. This is primarily because we cannot do this by default as in practice each user's threat model greatly varies in scope and breadth thus there exists no one size fits all way to do exits that works for everyone. 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 un implemented. We think that by being hands of on exit node requirements a far more diverse set of exit nodes can exist. In addition to having totally open unrestrcited exits, permitting "specialized" exit providers that are allowed to do excessive filtering or geo blocking for security theatre checkbox compliance is something Tor does not seem to be able to provide at this time. +The 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. -Lokinet additionally encourges the manual selection and pinning of edge connections to fit each user's threat model. +Lokinet additionally encourages the manual selection and pinning of edge connections to fit each user's threat model. ## I2P -Integrating applications to opt into i2p's network layer is painful and greatly stunts mainstream adoption. +Integrating applications to utilize i2p's network layer is painful and greatly stunts mainstream adoption. Lokinet takes the inverse approach of i2p: make app integration in lokinet should require zero custom shims or modifications to code to make it work. ## DVPNs / Commercial VPN Proxies diff --git a/docs/readme.md b/docs/readme.md index 24434f9bc..1b132b26f 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -1,12 +1,14 @@ # Lokinet Docs -this is the area of the repo for documentation of lokinet +This is where Lokinet documentation lives. ## High level [How is Lokinet different to \[insert network technology name here\] ?](net-comparisons.md) -[How Do I use Lokinet?](lokinet-ideal-ux.md) +[How Do I use Lokinet?](ideal-ux.md) + +[What Lokinet can't do](we-cannot-make-sandwiches.md) ## Lokinet (SN)Application Developer Portal diff --git a/docs/snapps-dev-guide.md b/docs/snapps-dev-guide.md new file mode 100644 index 000000000..24ebb8ff3 --- /dev/null +++ b/docs/snapps-dev-guide.md @@ -0,0 +1,30 @@ +# (SN)Apps Development Guide + + +## Our approach + +`// TODO: this` + +## Differences From Other approaches + +`// TODO: this` + +### SOCKS Proxy/HTTP Tunneling + +`// TODO: this` + +### Embedding a network stack + +`// TODO: this` + +## High Level Code Practices + +`// TODO: this` + +### Goodisms + +`// TODO: this` + +### Badisms + +`// TODO: this` diff --git a/docs/we-cannot-make-sandwiches.md b/docs/we-cannot-make-sandwiches.md new file mode 100644 index 000000000..3798f28b6 --- /dev/null +++ b/docs/we-cannot-make-sandwiches.md @@ -0,0 +1,19 @@ +# What Lokinet can't do + +Lokinet does a few things very well, but obviously can't do everything. + +## Anonymize OS/Application Fingerprints + +Mitigating 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. + +## Malware + +Lokinet cannot prevent running of malicious programs. Computer security unfortunately cannot be solved unilaterally by networking software without simply dropping all incoming and outgoing traffic. + +## Phoning Home + +Lokinet 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. + +## Make Sandwiches + +At 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. From 9c68f64929abcc2b1f92694e141cc0da0d3da750 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 10 Dec 2021 11:59:58 -0500 Subject: [PATCH 016/202] add deps to doxygen --- docs/doxygen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/doxygen.md b/docs/doxygen.md index 3b2fcdffe..f520f02b8 100644 --- a/docs/doxygen.md +++ b/docs/doxygen.md @@ -12,7 +12,7 @@ building doxygen docs requires the following: install packages: - $ sudo apt install doxygen python3-sphinx python3-sphinx-rtd-theme python3-breathe python3-pip + $ sudo apt install make cmake doxygen python3-sphinx python3-sphinx-rtd-theme python3-breathe python3-pip $ pip3 install --user exhale build docs: From fe07665ac5c897eddc200c0ec390bef329e2bb20 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 10 Dec 2021 15:26:14 -0500 Subject: [PATCH 017/202] remove duplicate file --- docs/lokinet-ideal-ux.md | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 docs/lokinet-ideal-ux.md diff --git a/docs/lokinet-ideal-ux.md b/docs/lokinet-ideal-ux.md deleted file mode 100644 index 2050892d5..000000000 --- a/docs/lokinet-ideal-ux.md +++ /dev/null @@ -1,3 +0,0 @@ -# How Do I use lokinet? - -`// TODO: this` From b7bbb7f40dd9708d70a67c3d95c4325ee8831938 Mon Sep 17 00:00:00 2001 From: majestrate Date: Mon, 13 Dec 2021 22:53:53 -0500 Subject: [PATCH 018/202] Update readme.md fix readme, do not use static deps. --- readme.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/readme.md b/readme.md index bd4e7a722..f9e5847dc 100644 --- a/readme.md +++ b/readme.md @@ -23,6 +23,7 @@ Build requirements: * C++ 17 capable C++ compiler * libuv >= 1.27.0 * libsodium >= 1.0.18 +* libssl (for lokinet-bootstrap) * libcurl (for lokinet-bootstrap) * libunbound * libzmq @@ -48,11 +49,6 @@ If you are not on a platform supported by the debian packages or if you want to $ cd lokinet $ mkdir build $ cd build - $ cmake .. -DBUILD_STATIC_DEPS=ON -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON -DCMAKE_BUILD_TYPE=Release - $ make -j$(nproc) - -If you dont want to do a static build install the dependancies and run: - $ cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF $ make -j$(nproc) From d186a887d3457b075c2ab7c502566ebd7dfc7c36 Mon Sep 17 00:00:00 2001 From: majestrate Date: Mon, 13 Dec 2021 22:55:51 -0500 Subject: [PATCH 019/202] Update readme.md remove note in readme --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index f9e5847dc..d547b2d72 100644 --- a/readme.md +++ b/readme.md @@ -42,7 +42,7 @@ You can install these using: $ sudo apt install lokinet -If you are not on a platform supported by the debian packages or if you want to build a dev build, this is the most "portable" way to do it: +If you want to build from source: $ 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 $ git clone --recursive https://github.com/oxen-io/lokinet From 6af589d845577eba38cfa61ad0de943d3e0f7a60 Mon Sep 17 00:00:00 2001 From: majestrate Date: Mon, 13 Dec 2021 22:57:11 -0500 Subject: [PATCH 020/202] Update readme.md correct cmake flag --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index d547b2d72..f2a9b5b82 100644 --- a/readme.md +++ b/readme.md @@ -92,7 +92,7 @@ build: $ cd lokinet $ mkdir build $ cd build - $ cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON -DBUILD_SHARED_DEPS=ON .. + $ cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON -DBUILD_STATIC_DEPS=ON .. $ make install (root): From 019a9f1611b80cdc76aaecccdf9153286fa054a9 Mon Sep 17 00:00:00 2001 From: majestrate Date: Mon, 13 Dec 2021 22:58:05 -0500 Subject: [PATCH 021/202] Update readme.md remove pedantic separation in readme --- readme.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/readme.md b/readme.md index f2a9b5b82..ed2ab6602 100644 --- a/readme.md +++ b/readme.md @@ -51,9 +51,6 @@ If you want to build from source: $ cd build $ cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF $ make -j$(nproc) - -install: - $ sudo make install ### macOS From 44c7cf5f27f545cd745b38b7351c991a7821703b Mon Sep 17 00:00:00 2001 From: majestrate Date: Mon, 13 Dec 2021 22:59:29 -0500 Subject: [PATCH 022/202] Update readme.md remove old parts about macos from readme --- readme.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/readme.md b/readme.md index ed2ab6602..525ac4bc3 100644 --- a/readme.md +++ b/readme.md @@ -119,13 +119,3 @@ This requires the binary to have the proper capabilities which is usually set by $ sudo setcap cap_net_admin,cap_net_bind_service=+eip /usr/local/bin/lokinet - -## Running on macOS/UNIX/BSD - -**YOU HAVE TO RUN AS ROOT**, run using sudo. Elevated privileges are needed to create the virtual tunnel interface. - -The macOS installer places the normal binaries (`lokinet` and `lokinet-bootstrap`) in `/usr/local/bin` which should be in your path, so you can easily use the binaries from your terminal. The installer also nukes your previous config and keys and sets up a fresh config and downloads the latest bootstrap seed. - -to run, after you create default config: - - $ sudo lokinet From 388fc53380083424fdc1d7d2ebc3c608a642ca8a Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 7 Dec 2021 11:17:20 -0500 Subject: [PATCH 023/202] match io loop event order on windows/apple to match linux. on win32/apple reading packets from the interface does not count as an io operation. manually trigger pump on win32/apple to pretend that it is an io event. add platform quark function MaybeWakeUpperLayers on vpn::Interface to manaully wake up the other components on platforms that need that (ones on which packet io is not done via io events). on non linux platforms, use uv_prepare_t instead of uv_check_t as the former triggers before blocking for io, instead of after. this better matches linux's order of operations in libuv. --- cmake/win32.cmake | 5 +++-- llarp/apple/vpn_interface.cpp | 16 ++++++++++++++-- llarp/apple/vpn_interface.hpp | 10 +++++++++- llarp/apple/vpn_platform.cpp | 5 +++-- llarp/apple/vpn_platform.hpp | 3 ++- llarp/config/config.cpp | 4 ++-- llarp/config/config.hpp | 2 +- llarp/ev/ev_libuv.cpp | 11 +++++++++-- llarp/ev/vpn.hpp | 9 +++++++-- llarp/handlers/exit.cpp | 2 +- llarp/handlers/tun.cpp | 2 +- llarp/net/sock_addr.hpp | 5 ----- llarp/router/router.cpp | 6 +----- llarp/vpn/android.hpp | 2 +- llarp/vpn/linux.hpp | 2 +- llarp/vpn/win32.hpp | 21 ++++++++++++++++----- pybind/llarp/config.cpp | 4 ++-- 17 files changed, 73 insertions(+), 36 deletions(-) diff --git a/cmake/win32.cmake b/cmake/win32.cmake index 2a4af72a0..73cda29b3 100644 --- a/cmake/win32.cmake +++ b/cmake/win32.cmake @@ -15,8 +15,9 @@ if(NOT MSVC_VERSION) # to .r[o]data section one after the other! add_compile_options(-fno-ident -Wa,-mbig-obj) link_libraries( -lws2_32 -lshlwapi -ldbghelp -luser32 -liphlpapi -lpsapi -luserenv) - # zmq requires windows xp or higher - add_definitions(-DWINVER=0x0501 -D_WIN32_WINNT=0x0501) + # the minimum windows version, set to 6 rn because supporting older windows is hell + set(_winver 0x0600) + add_definitions(-DWINVER=${_winver} -D_WIN32_WINNT=${_winver}) endif() if(EMBEDDED_CFG) diff --git a/llarp/apple/vpn_interface.cpp b/llarp/apple/vpn_interface.cpp index 079e24aa9..c54cef00a 100644 --- a/llarp/apple/vpn_interface.cpp +++ b/llarp/apple/vpn_interface.cpp @@ -1,12 +1,18 @@ #include "vpn_interface.hpp" #include "context.hpp" +#include namespace llarp::apple { VPNInterface::VPNInterface( - Context& ctx, packet_write_callback packet_writer, on_readable_callback on_readable) - : m_PacketWriter{std::move(packet_writer)}, m_OnReadable{std::move(on_readable)} + Context& ctx, + packet_write_callback packet_writer, + on_readable_callback on_readable, + AbstractRouter* router) + : m_PacketWriter{std::move(packet_writer)} + , m_OnReadable{std::move(on_readable)} + , _router{router} { ctx.loop->call_soon([this] { m_OnReadable(*this); }); } @@ -21,6 +27,12 @@ namespace llarp::apple return true; } + void + VPNInterface::MaybeWakeUpperLayers() const + { + _router->TriggerPump(); + } + int VPNInterface::PollFD() const { diff --git a/llarp/apple/vpn_interface.hpp b/llarp/apple/vpn_interface.hpp index c1dff8dbf..762227f91 100644 --- a/llarp/apple/vpn_interface.hpp +++ b/llarp/apple/vpn_interface.hpp @@ -17,7 +17,10 @@ namespace llarp::apple using on_readable_callback = std::function; explicit VPNInterface( - Context& ctx, packet_write_callback packet_writer, on_readable_callback on_readable); + Context& ctx, + packet_write_callback packet_writer, + on_readable_callback on_readable, + AbstractRouter* router); // Method to call when a packet has arrived to deliver the packet to lokinet bool @@ -35,6 +38,9 @@ namespace llarp::apple bool WritePacket(net::IPPacket pkt) override; + void + MaybeWakeUpperLayers() const override; + private: // Function for us to call when we have a packet to emit. Should return true if the packet was // handed off to the OS successfully. @@ -46,6 +52,8 @@ namespace llarp::apple static inline constexpr auto PacketQueueSize = 1024; thread::Queue m_ReadQueue{PacketQueueSize}; + + AbstractRouter* const _router; }; } // namespace llarp::apple diff --git a/llarp/apple/vpn_platform.cpp b/llarp/apple/vpn_platform.cpp index b11c0b05b..1d1eafb8e 100644 --- a/llarp/apple/vpn_platform.cpp +++ b/llarp/apple/vpn_platform.cpp @@ -15,8 +15,9 @@ namespace llarp::apple , m_OnReadable{std::move(on_readable)} {} - std::shared_ptr VPNPlatform::ObtainInterface(vpn::InterfaceInfo) + std::shared_ptr + VPNPlatform::ObtainInterface(vpn::InterfaceInfo, AbstractRouter* router) { - return std::make_shared(m_Context, m_PacketWriter, m_OnReadable); + return std::make_shared(m_Context, m_PacketWriter, m_OnReadable, router); } } // namespace llarp::apple diff --git a/llarp/apple/vpn_platform.hpp b/llarp/apple/vpn_platform.hpp index 04ce75646..0cf7f469b 100644 --- a/llarp/apple/vpn_platform.hpp +++ b/llarp/apple/vpn_platform.hpp @@ -16,7 +16,8 @@ namespace llarp::apple llarp_route_callbacks route_callbacks, void* callback_context); - std::shared_ptr ObtainInterface(vpn::InterfaceInfo) override; + std::shared_ptr + ObtainInterface(vpn::InterfaceInfo, AbstractRouter*) override; vpn::IRouteManager& RouteManager() override diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp index 3a2719343..047427631 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp @@ -798,12 +798,12 @@ namespace llarp const IpAddress addr{value}; if (not addr.hasPort()) throw std::invalid_argument("no port provided in link address"); - info.interface = addr.toHost(); + info.m_interface = addr.toHost(); info.port = *addr.getPort(); } else { - info.interface = std::string{name}; + info.m_interface = std::string{name}; std::vector splits = split(value, ","); for (std::string_view str : splits) diff --git a/llarp/config/config.hpp b/llarp/config/config.hpp index 03f7a000c..b738bdfb2 100644 --- a/llarp/config/config.hpp +++ b/llarp/config/config.hpp @@ -147,7 +147,7 @@ namespace llarp { struct LinkInfo { - std::string interface; + std::string m_interface; int addressFamily = -1; uint16_t port = -1; }; diff --git a/llarp/ev/ev_libuv.cpp b/llarp/ev/ev_libuv.cpp index b491f42b0..c7356f432 100644 --- a/llarp/ev/ev_libuv.cpp +++ b/llarp/ev/ev_libuv.cpp @@ -241,8 +241,11 @@ namespace llarp::uv using event_t = uvw::PollEvent; auto handle = m_Impl->resource(netif->PollFD()); #else - using event_t = uvw::CheckEvent; - auto handle = m_Impl->resource(); + // we use a uv_prepare_t because it fires before blocking for new io events unconditionally + // we want to match what linux does, using a uv_check_t does not suffice as the order of + // operations is not what we need. + using event_t = uvw::PrepareEvent; + auto handle = m_Impl->resource(); #endif if (!handle) return false; @@ -254,6 +257,10 @@ namespace llarp::uv LogDebug("got packet ", pkt.sz); if (handler) handler(std::move(pkt)); + // on windows/apple, vpn packet io does not happen as an io action that wakes up the event + // loop thus, we must manually wake up the event loop when we get a packet on our interface. + // on linux this is a nop + netif->MaybeWakeUpperLayers(); } }); diff --git a/llarp/ev/vpn.hpp b/llarp/ev/vpn.hpp index c3346fcdd..ede6b0acd 100644 --- a/llarp/ev/vpn.hpp +++ b/llarp/ev/vpn.hpp @@ -9,7 +9,8 @@ namespace llarp { struct Context; -} + struct AbstractRouter; +} // namespace llarp namespace llarp::vpn { @@ -59,6 +60,10 @@ namespace llarp::vpn /// returns false if we dropped it virtual bool WritePacket(net::IPPacket pkt) = 0; + + /// idempotently wake up the upper layers as needed (platform dependant) + virtual void + MaybeWakeUpperLayers() const {}; }; class IRouteManager @@ -112,7 +117,7 @@ namespace llarp::vpn /// get a new network interface fully configured given the interface info /// blocks until ready, throws on error virtual std::shared_ptr - ObtainInterface(InterfaceInfo info) = 0; + ObtainInterface(InterfaceInfo info, AbstractRouter* router) = 0; /// get owned ip route manager for managing routing table virtual IRouteManager& diff --git a/llarp/handlers/exit.cpp b/llarp/handlers/exit.cpp index 8c88c3753..7dc80f65a 100644 --- a/llarp/handlers/exit.cpp +++ b/llarp/handlers/exit.cpp @@ -443,7 +443,7 @@ namespace llarp info.ifname = m_ifname; info.addrs.emplace(m_OurRange); - m_NetIf = GetRouter()->GetVPNPlatform()->ObtainInterface(std::move(info)); + m_NetIf = GetRouter()->GetVPNPlatform()->ObtainInterface(std::move(info), m_Router); if (not m_NetIf) { llarp::LogError("Could not create interface"); diff --git a/llarp/handlers/tun.cpp b/llarp/handlers/tun.cpp index 5677fc12b..8736f2125 100644 --- a/llarp/handlers/tun.cpp +++ b/llarp/handlers/tun.cpp @@ -908,7 +908,7 @@ namespace llarp try { - m_NetIf = Router()->GetVPNPlatform()->ObtainInterface(std::move(info)); + m_NetIf = Router()->GetVPNPlatform()->ObtainInterface(std::move(info), Router()); } catch (std::exception& ex) { diff --git a/llarp/net/sock_addr.hpp b/llarp/net/sock_addr.hpp index 2e18451a2..3c25e8914 100644 --- a/llarp/net/sock_addr.hpp +++ b/llarp/net/sock_addr.hpp @@ -7,11 +7,6 @@ #include #include #include -extern "C" const char* -inet_ntop(int af, const void* src, char* dst, size_t size); -extern "C" int -inet_pton(int af, const char* src, void* dst); -#define inet_aton(x, y) inet_pton(AF_INET, x, y) #endif #include diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index 8729bf0a0..87816e7ae 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -709,7 +709,7 @@ namespace llarp util::memFn(&AbstractRouter::TriggerPump, this), util::memFn(&AbstractRouter::QueueWork, this)); - const std::string& key = serverConfig.interface; + const std::string& key = serverConfig.m_interface; int af = serverConfig.addressFamily; uint16_t port = serverConfig.port; if (!server->Configure(this, key, af, port)) @@ -1241,10 +1241,6 @@ namespace llarp LogInfo("have ", _nodedb->NumLoaded(), " routers"); -#ifdef _WIN32 - // windows uses proactor event loop so we need to constantly pump - _loop->add_ticker([this] { PumpLL(); }); -#endif _loop->call_every(ROUTER_TICK_INTERVAL, weak_from_this(), [this] { Tick(); }); _running.store(true); _startedAt = Now(); diff --git a/llarp/vpn/android.hpp b/llarp/vpn/android.hpp index c9792a009..043249b0d 100644 --- a/llarp/vpn/android.hpp +++ b/llarp/vpn/android.hpp @@ -92,7 +92,7 @@ namespace llarp::vpn {} std::shared_ptr - ObtainInterface(InterfaceInfo info) override + ObtainInterface(InterfaceInfo info, AbstractRouter*) override { return std::make_shared(std::move(info), fd); } diff --git a/llarp/vpn/linux.hpp b/llarp/vpn/linux.hpp index 0f47636e9..b682ae15b 100644 --- a/llarp/vpn/linux.hpp +++ b/llarp/vpn/linux.hpp @@ -448,7 +448,7 @@ namespace llarp::vpn public: std::shared_ptr - ObtainInterface(InterfaceInfo info) override + ObtainInterface(InterfaceInfo info, AbstractRouter*) override { return std::make_shared(std::move(info)); }; diff --git a/llarp/vpn/win32.hpp b/llarp/vpn/win32.hpp index 9ecd15e79..172fb0b94 100644 --- a/llarp/vpn/win32.hpp +++ b/llarp/vpn/win32.hpp @@ -6,6 +6,7 @@ #include #include #include +#include // DDK macros #define CTL_CODE(DeviceType, Function, Method, Access) \ @@ -177,6 +178,8 @@ namespace llarp::vpn InterfaceInfo m_Info; + AbstractRouter* const _router; + static std::wstring get_win_sys_path() { @@ -220,7 +223,8 @@ namespace llarp::vpn return converter.to_bytes(wcmd); } - Win32Interface(InterfaceInfo info) : m_ReadQueue{1024}, m_Info{std::move(info)} + Win32Interface(InterfaceInfo info, AbstractRouter* router) + : m_ReadQueue{1024}, m_Info{std::move(info)}, _router{router} { DWORD len; @@ -401,6 +405,12 @@ namespace llarp::vpn thread.join(); } + virtual void + MaybeWakeUpperLayers() const override + { + _router->TriggerPump(); + } + int PollFD() const override { @@ -541,8 +551,8 @@ namespace llarp::vpn Execute(RouteCommand() + " " + cmd + " c000::/2 " + ipv6.ToString()); ifname.back()++; - Execute(RouteCommand() + " " + cmd + " 0.0.0.0 MASK 128.0.0.0 " + ifname); - Execute(RouteCommand() + " " + cmd + " 128.0.0.0 MASK 128.0.0.0 " + ifname); + Execute(RouteCommand() + " " + cmd + " 0.0.0.0 MASK 128.0.0.0 " + ifname + " METRIC 2"); + Execute(RouteCommand() + " " + cmd + " 128.0.0.0 MASK 128.0.0.0 " + ifname + " METRIC 2"); } void @@ -629,12 +639,13 @@ namespace llarp::vpn public: std::shared_ptr - ObtainInterface(InterfaceInfo info) override + ObtainInterface(InterfaceInfo info, AbstractRouter* router) override { - auto netif = std::make_shared(std::move(info)); + auto netif = std::make_shared(std::move(info), router); netif->Start(); return netif; }; + IRouteManager& RouteManager() override { diff --git a/pybind/llarp/config.cpp b/pybind/llarp/config.cpp index 3ca6fe306..c0d15cdfe 100644 --- a/pybind/llarp/config.cpp +++ b/pybind/llarp/config.cpp @@ -77,7 +77,7 @@ namespace llarp "setOutboundLink", [](LinksConfig& self, std::string interface, int family, uint16_t port) { LinksConfig::LinkInfo info; - info.interface = std::move(interface); + info.m_interface = std::move(interface); info.addressFamily = family; info.port = port; self.m_OutboundLink = std::move(info); @@ -86,7 +86,7 @@ namespace llarp "addInboundLink", [](LinksConfig& self, std::string interface, int family, uint16_t port) { LinksConfig::LinkInfo info; - info.interface = std::move(interface); + info.m_interface = std::move(interface); info.addressFamily = family; info.port = port; self.m_InboundLinks.push_back(info); From 301b19bd0fb16d9bf10c999e06b35da722cc45b5 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 7 Dec 2021 11:22:18 -0500 Subject: [PATCH 024/202] do not send buggy reply as rpc --- llarp/rpc/rpc_server.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/llarp/rpc/rpc_server.cpp b/llarp/rpc/rpc_server.cpp index 5bb14e84e..4b15c1689 100644 --- a/llarp/rpc/rpc_server.cpp +++ b/llarp/rpc/rpc_server.cpp @@ -513,7 +513,7 @@ namespace llarp::rpc onGoodResult(result.reason); }); }, - 5s); + 2s); }; if (exit.has_value()) { @@ -564,8 +564,8 @@ namespace llarp::rpc { r->routePoker().Down(); ep->UnmapExitRange(range); + reply(CreateJSONResponse("OK")); } - reply(CreateJSONResponse("OK")); }); }); }) From 776e9227fd39bb458ee29b59ffb7ea459da24d13 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 7 Dec 2021 13:47:31 -0500 Subject: [PATCH 025/202] make serivce::Endpoint::EnsurePathToService have a default timeout as a named constant. --- llarp/rpc/rpc_server.cpp | 3 +-- llarp/service/endpoint.hpp | 7 ++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/llarp/rpc/rpc_server.cpp b/llarp/rpc/rpc_server.cpp index 4b15c1689..6c9fa866c 100644 --- a/llarp/rpc/rpc_server.cpp +++ b/llarp/rpc/rpc_server.cpp @@ -512,8 +512,7 @@ namespace llarp::rpc } onGoodResult(result.reason); }); - }, - 2s); + }); }; if (exit.has_value()) { diff --git a/llarp/service/endpoint.hpp b/llarp/service/endpoint.hpp index cfb5a2ef9..418f6899d 100644 --- a/llarp/service/endpoint.hpp +++ b/llarp/service/endpoint.hpp @@ -323,10 +323,15 @@ namespace llarp // nullptr if the path was not made before the timeout using PathEnsureHook = std::function; + static constexpr auto DefaultPathEnsureTimeout = 2s; + /// return false if we have already called this function before for this /// address bool - EnsurePathToService(const Address remote, PathEnsureHook h, llarp_time_t timeoutMS); + EnsurePathToService( + const Address remote, + PathEnsureHook h, + llarp_time_t timeoutMS = DefaultPathEnsureTimeout); using SNodeEnsureHook = std::function; From da887dc559086f0aac0ac87d84b340911b42a48d Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Wed, 1 Dec 2021 13:12:44 -0500 Subject: [PATCH 026/202] implement exit node pooling, allows users to use multiple exits for an address range. mappings per ip stick to the same exit, each new ip is mapped to a random exit in the specified pool. make exit-auth multi value --- llarp/config/config.cpp | 4 +- llarp/handlers/tun.cpp | 94 +++++++++++++++++++---------------------- llarp/handlers/tun.hpp | 13 ++++++ 3 files changed, 59 insertions(+), 52 deletions(-) diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp index 047427631..b625b9e78 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp @@ -454,6 +454,7 @@ namespace llarp "network", "exit-node", ClientOnly, + MultiValue, Comment{ "Specify a `.loki` address and an optional ip range to use as an exit broker.", "Example:", @@ -496,12 +497,13 @@ namespace llarp "network", "exit-auth", ClientOnly, + MultiValue, Comment{ "Specify an optional authentication code required to use a non-public exit node.", "For example:", " exit-auth=myfavouriteexit.loki:abc", "uses the authentication code `abc` whenever myfavouriteexit.loki is accessed.", - "Can be specified multiple time to store codes for different exit nodes.", + "Can be specified multiple times to store codes for different exit nodes.", }, [this](std::string arg) { if (arg.empty()) diff --git a/llarp/handlers/tun.cpp b/llarp/handlers/tun.cpp index 8736f2125..4a4ad117f 100644 --- a/llarp/handlers/tun.cpp +++ b/llarp/handlers/tun.cpp @@ -1,7 +1,5 @@ #include #include -// harmless on other platforms -#define __USE_MINGW_ANSI_STDIO 1 #include "tun.hpp" #include #ifndef _WIN32 @@ -1023,6 +1021,38 @@ namespace llarp return llarp::service::Endpoint::Stop(); } + std::optional + TunEndpoint::ObtainExitAddressFor( + huint128_t ip, + std::function)> exitSelectionStrat) + { + // is it already mapped? return the mapping + if (auto itr = m_ExitIPToExitAddress.find(ip); itr != m_ExitIPToExitAddress.end()) + return itr->second; + // build up our candidates to choose + std::unordered_set candidates; + for (const auto& entry : m_ExitMap.FindAllEntries(ip)) + { + // make sure it is allowed by the range if the ip is a bogon + if (not IsBogon(ip) or entry.first.BogonContains(ip)) + candidates.emplace(entry.second); + } + // no candidates? bail. + if (candidates.empty()) + return std::nullopt; + if (not exitSelectionStrat) + { + // default strat to random choice + exitSelectionStrat = [](auto candidates) { + auto itr = candidates.begin(); + std::advance(itr, llarp::randint() % candidates.size()); + return *itr; + }; + } + // map the exit and return the endpoint we mapped it to + return m_ExitIPToExitAddress.emplace(ip, exitSelectionStrat(candidates)).first->second; + } + void TunEndpoint::HandleGotUserPacket(net::IPPacket pkt) { @@ -1037,25 +1067,7 @@ namespace llarp dst = pkt.dstv6(); src = pkt.srcv6(); } - // this is for ipv6 slaac on ipv6 exits - /* - constexpr huint128_t ipv6_multicast_all_nodes = - huint128_t{uint128_t{0xff01'0000'0000'0000UL, 1UL}}; - constexpr huint128_t ipv6_multicast_all_routers = - huint128_t{uint128_t{0xff01'0000'0000'0000UL, 2UL}}; - if (dst == ipv6_multicast_all_nodes and m_state->m_ExitEnabled) - { - // send ipv6 multicast - for (const auto& [ip, addr] : m_IPToAddr) - { - (void)ip; - SendToOrQueue( - service::Address{addr.as_array()}, pkt.ConstBuffer(), service::ProtocolType::Exit); - } - return; - } - */ if (m_state->m_ExitEnabled) { dst = net::ExpandV4(net::TruncateV6(dst)); @@ -1063,28 +1075,18 @@ namespace llarp auto itr = m_IPToAddr.find(dst); if (itr == m_IPToAddr.end()) { - // find all ranges that match the destination ip - const auto exitEntries = m_ExitMap.FindAllEntries(dst); - if (exitEntries.empty()) + service::Address addr{}; + + if (auto maybe = ObtainExitAddressFor(dst)) + addr = *maybe; + else { // send icmp unreachable as we dont have any exits for this ip if (const auto icmp = pkt.MakeICMPUnreachable()) - { HandleWriteIPPacket(icmp->ConstBuffer(), dst, src, 0); - } + return; } - service::Address addr{}; - for (const auto& [range, exitAddr] : exitEntries) - { - if (not IsBogon(dst) or range.BogonContains(dst)) - { - addr = exitAddr; - } - // we do not permit bogons when they don't explicitly match a permitted bogon range - } - if (addr.IsZero()) // drop becase no exit was found that matches our rules - return; pkt.ZeroSourceAddress(); MarkAddressOutbound(addr); EnsurePathToService( @@ -1266,24 +1268,14 @@ namespace llarp } else // don't allow snode return false; - const auto mapped = m_ExitMap.FindAllEntries(src); - bool allow = false; - for (const auto& [range, exitAddr] : mapped) + // make sure the mapping matches + if (auto itr = m_ExitIPToExitAddress.find(src); itr != m_ExitIPToExitAddress.end()) { - if (not IsBogon(src) or range.BogonContains(src)) - { - // allow if this address matches the endpoint we think it should be - allow = exitAddr == fromAddr; - break; - } + if (itr->second != fromAddr) + return false; } - if (not allow) - { - var::visit( - [&](auto&& address) { LogWarn(Name(), " does not allow ", src, " from ", address); }, - addr); + else return false; - } } else { diff --git a/llarp/handlers/tun.hpp b/llarp/handlers/tun.hpp index a5173c17d..ee7a64b5f 100644 --- a/llarp/handlers/tun.hpp +++ b/llarp/handlers/tun.hpp @@ -222,7 +222,20 @@ namespace llarp /// a hidden service std::unordered_map, bool> m_SNodes; + /// maps ip address to an exit endpoint, useful when we have multiple exits on a range + std::unordered_map m_ExitIPToExitAddress; + private: + /// given an ip address that is not mapped locally find the address it shall be forwarded to + /// optionally provide a custom selection strategy, if none is provided it will choose a + /// random entry from the available choices + /// return std::nullopt if we cannot route this address to an exit + std::optional + ObtainExitAddressFor( + huint128_t ip, + std::function)> exitSelectionStrat = + nullptr); + template void SendDNSReply( From 14ffdb6639fb65001e4f279f7268f863abd46aa3 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Sat, 4 Dec 2021 10:08:18 -0500 Subject: [PATCH 027/202] configurable route poker this allows you to use exit nodes without forcing routes over the interface, useful for using lokinet with an exit and selectively routing over the lokinet interface using an external socks proxy or binding to device explicitly. * make route poker configurable, defaults to enabled but allows disabling it on runtime if desired * add config option [network]:auto-routing to enable/disable route poker --- llarp/config/config.cpp | 12 ++++++++++++ llarp/config/config.hpp | 2 ++ llarp/router/route_poker.cpp | 17 +++++++++++++---- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp index b625b9e78..84088b8aa 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp @@ -533,6 +533,18 @@ namespace llarp m_ExitAuths.emplace(exit, auth); }); + conf.defineOption( + "network", + "auto-routing", + ClientOnly, + Default{true}, + Comment{ + "enable / disable auto routing. When using an exit lokinet will add routes to " + "the OS to make traffic go over the network interface via lokinet.", + "enabled by default.", + }, + AssignmentAcceptor(m_EnableRoutePoker)); + conf.defineOption( "network", "ifname", diff --git a/llarp/config/config.hpp b/llarp/config/config.hpp index b738bdfb2..677a5ed2e 100644 --- a/llarp/config/config.hpp +++ b/llarp/config/config.hpp @@ -129,6 +129,8 @@ namespace llarp std::optional m_AddrMapPersistFile; + bool m_EnableRoutePoker; + void defineConfigOptions(ConfigDefinition& conf, const ConfigGenParameters& params); }; diff --git a/llarp/router/route_poker.cpp b/llarp/router/route_poker.cpp index 9017125bc..217814fe5 100644 --- a/llarp/router/route_poker.cpp +++ b/llarp/router/route_poker.cpp @@ -162,10 +162,13 @@ namespace llarp if (m_Enabled) return; - m_Enabling = true; - Update(); - m_Enabling = false; - m_Enabled = true; + if (m_Router->GetConfig()->network.m_EnableRoutePoker) + { + m_Enabling = true; + Update(); + m_Enabling = false; + m_Enabled = true; + } systemd_resolved_set_dns( m_Router->hiddenServiceContext().GetDefault()->GetIfName(), @@ -191,6 +194,9 @@ namespace llarp void RoutePoker::Up() { + if (not m_Router->GetConfig()->network.m_EnableRoutePoker) + return; + vpn::IRouteManager& route = m_Router->GetVPNPlatform()->RouteManager(); // black hole all routes by default @@ -207,6 +213,9 @@ namespace llarp void RoutePoker::Down() { + if (not m_Router->GetConfig()->network.m_EnableRoutePoker) + return; + // unpoke routes for first hops m_Router->ForEachPeer( [&](auto session, auto) mutable { DelRoute(session->GetRemoteEndpoint().asIPv4()); }, From 061aebc964177227c36a85a815403e668d059c44 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Sat, 4 Dec 2021 10:36:47 -0500 Subject: [PATCH 028/202] reword auto-routing config comment --- llarp/config/config.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp index 84088b8aa..00932520b 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp @@ -539,10 +539,11 @@ namespace llarp ClientOnly, Default{true}, Comment{ - "enable / disable auto routing. When using an exit lokinet will add routes to " - "the OS to make traffic go over the network interface via lokinet.", - "enabled by default.", - }, + "Enable / disable automatic route configuration.", + "When this is enabled and an exit is used Lokinet will automatically configure " + "operating system routes to route traffic through the exit node.", + "This is enabled by default, but can be disabled to perform advanced exit routing " + "configuration manually."}, AssignmentAcceptor(m_EnableRoutePoker)); conf.defineOption( From 409772f763bf443a61986303bbe2ab41dfe55bbd Mon Sep 17 00:00:00 2001 From: Benjamin Henrion Date: Sun, 19 Dec 2021 17:09:40 +0100 Subject: [PATCH 029/202] Add missing libssl-dev Add missing libssl-dev on Ubuntu to compile it from source. --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 525ac4bc3..3b75698ca 100644 --- a/readme.md +++ b/readme.md @@ -44,7 +44,7 @@ You can install these using: If you want to build from source: - $ 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 + $ 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 $ git clone --recursive https://github.com/oxen-io/lokinet $ cd lokinet $ mkdir build From 27ba3e044debf047baa242fd9f9a79e1db8bdeda Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 3 Jan 2022 10:16:08 -0500 Subject: [PATCH 030/202] prevent segfault in route poker if Init() is not called --- llarp/router/route_poker.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/llarp/router/route_poker.cpp b/llarp/router/route_poker.cpp index 217814fe5..e82abb5ec 100644 --- a/llarp/router/route_poker.cpp +++ b/llarp/router/route_poker.cpp @@ -87,6 +87,11 @@ namespace llarp RoutePoker::~RoutePoker() { + if (m_Router == nullptr) + return; + if (m_Router->GetVPNPlatform() == nullptr) + return; + vpn::IRouteManager& route = m_Router->GetVPNPlatform()->RouteManager(); for (const auto& [ip, gateway] : m_PokedRoutes) { From b442e8a43b63095b7d6c5b2eb2ce6d0a96f344f9 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Tue, 11 Jan 2022 12:13:24 -0400 Subject: [PATCH 031/202] readme: point to oxen docs; mention systemctl controls --- readme.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 3b75698ca..2c24cd3b2 100644 --- a/readme.md +++ b/readme.md @@ -14,6 +14,12 @@ A simple demo application that is lokinet "aware" can be found [here](https://gi [![Build Status](https://ci.oxen.rocks/api/badges/oxen-io/lokinet/status.svg?ref=refs/heads/dev)](https://ci.oxen.rocks/oxen-io/lokinet) +## Installing + +If you are simply looking to install Lokinet and don't want to compile it yourself the [Oxen Docs +Lokinet Guides](https://docs.oxen.io/products-built-on-oxen/lokinet/guides) for gentler +instructions to get up and running. + ## Building Build requirements: @@ -100,7 +106,9 @@ install (root): ### Debian / Ubuntu packages -When running from debian package the following steps are not needed as it is already ready to use. +When 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. ## Running on Linux (without debs) From 2c44ffe85b531f46d109c435d71c0c19231bf323 Mon Sep 17 00:00:00 2001 From: majestrate Date: Tue, 11 Jan 2022 12:44:05 -0500 Subject: [PATCH 032/202] Update llarp/router/route_poker.cpp Co-authored-by: Jason Rhinelander --- llarp/router/route_poker.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/llarp/router/route_poker.cpp b/llarp/router/route_poker.cpp index e82abb5ec..95b56d52b 100644 --- a/llarp/router/route_poker.cpp +++ b/llarp/router/route_poker.cpp @@ -87,9 +87,7 @@ namespace llarp RoutePoker::~RoutePoker() { - if (m_Router == nullptr) - return; - if (m_Router->GetVPNPlatform() == nullptr) + if (not m_Router or not m_Router->GetVPNPlatform()) return; vpn::IRouteManager& route = m_Router->GetVPNPlatform()->RouteManager(); From 205584acdf8856d2a0b36ea1847e18df972e5536 Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 13 Jan 2022 16:24:08 -0500 Subject: [PATCH 033/202] redo systemd status line to include number of paths and endpoint count. optionally warn about low path success. --- llarp/path/path_context.cpp | 16 +++++++++++++++- llarp/path/path_context.hpp | 4 ++++ llarp/router/router.cpp | 18 ++++++++++++------ llarp/service/endpoint.cpp | 6 ++++++ llarp/service/endpoint.hpp | 4 ++++ 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/llarp/path/path_context.cpp b/llarp/path/path_context.cpp index 028104696..f856ec3e8 100644 --- a/llarp/path/path_context.cpp +++ b/llarp/path/path_context.cpp @@ -319,10 +319,24 @@ namespace llarp PathContext::CurrentTransitPaths() { SyncTransitMap_t::Lock_t lock(m_TransitPaths.first); - auto& map = m_TransitPaths.second; + const auto& map = m_TransitPaths.second; return map.size() / 2; } + uint64_t + PathContext::CurrentOwnedPaths(path::PathStatus st) + { + uint64_t num{}; + util::Lock lock{m_OurPaths.first}; + auto& map = m_OurPaths.second; + for (auto itr = map.begin(); itr != map.end(); ++itr) + { + if (itr->second->Status() == st) + num++; + } + return num / 2; + } + void PathContext::PutTransitHop(std::shared_ptr hop) { diff --git a/llarp/path/path_context.hpp b/llarp/path/path_context.hpp index b53e11a06..af5b9e175 100644 --- a/llarp/path/path_context.hpp +++ b/llarp/path/path_context.hpp @@ -173,6 +173,10 @@ namespace llarp uint64_t CurrentTransitPaths(); + /// current number of paths we created in status + uint64_t + CurrentOwnedPaths(path::PathStatus status = path::PathStatus::ePathEstablished); + private: AbstractRouter* m_Router; SyncTransitMap_t m_TransitPaths; diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index 87816e7ae..2204e4766 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -822,12 +822,18 @@ namespace llarp else { ss << " client | known/connected: " << nodedb()->NumLoaded() << "/" - << NumberOfConnectedRouters() << " | path success: "; - hiddenServiceContext().ForEachService([&ss](const auto& name, const auto& ep) { - ss << " [" << name << " " << std::setprecision(4) - << (100.0 * ep->CurrentBuildStats().SuccessRatio()) << "%]"; - return true; - }); + << NumberOfConnectedRouters(); + if (auto ep = hiddenServiceContext().GetDefault()) + { + ss << " | paths/endpoints " << pathContext().CurrentOwnedPaths() << "/" + << ep->UniqueEndpoints(); + auto success_rate = ep->CurrentBuildStats().SuccessRatio(); + if (success_rate < 0.5) + { + ss << " [ !!! Low Build Success Rate (" << std::setprecision(4) + << (100.0 * success_rate) << "%) !!! ] "; + } + }; } const auto status = ss.str(); ::sd_notify(0, status.c_str()); diff --git a/llarp/service/endpoint.cpp b/llarp/service/endpoint.cpp index b513903aa..324f23eb8 100644 --- a/llarp/service/endpoint.cpp +++ b/llarp/service/endpoint.cpp @@ -695,6 +695,12 @@ namespace llarp } } + size_t + Endpoint::UniqueEndpoints() const + { + return m_state->m_RemoteSessions.size() + m_state->m_SNodeSessions.size(); + } + constexpr auto PublishIntrosetTimeout = 20s; bool diff --git a/llarp/service/endpoint.hpp b/llarp/service/endpoint.hpp index 418f6899d..b15add487 100644 --- a/llarp/service/endpoint.hpp +++ b/llarp/service/endpoint.hpp @@ -396,6 +396,10 @@ namespace llarp std::optional GetSeqNoForConvo(const ConvoTag& tag); + /// count unique endpoints we are talking to + size_t + UniqueEndpoints() const; + bool HasExit() const; From b3d9cd463f5266588bf1b69148a0d456a2b8e815 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 14 Jan 2022 13:03:01 -0500 Subject: [PATCH 034/202] route poker: allow not blackholing routes. allow runtime configuration to disable ip blackholing. --- llarp/config/config.cpp | 11 +++++++++++ llarp/config/config.hpp | 1 + llarp/router/route_poker.cpp | 6 ++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp index 00932520b..40b897ac6 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp @@ -546,6 +546,17 @@ namespace llarp "configuration manually."}, AssignmentAcceptor(m_EnableRoutePoker)); + conf.defineOption( + "network", + "blackhole-routes", + ClientOnly, + Default{true}, + Comment{ + "Enable / disable route configuration blackholes.", + "When enabled lokinet will drop ip4 and ip6 not included in exit config.", + "Enabled by default."}, + AssignmentAcceptor(m_BlackholeRoutes)); + conf.defineOption( "network", "ifname", diff --git a/llarp/config/config.hpp b/llarp/config/config.hpp index 677a5ed2e..4b518fc04 100644 --- a/llarp/config/config.hpp +++ b/llarp/config/config.hpp @@ -130,6 +130,7 @@ namespace llarp std::optional m_AddrMapPersistFile; bool m_EnableRoutePoker; + bool m_BlackholeRoutes; void defineConfigOptions(ConfigDefinition& conf, const ConfigGenParameters& params); diff --git a/llarp/router/route_poker.cpp b/llarp/router/route_poker.cpp index 95b56d52b..b24b88476 100644 --- a/llarp/router/route_poker.cpp +++ b/llarp/router/route_poker.cpp @@ -202,8 +202,10 @@ namespace llarp vpn::IRouteManager& route = m_Router->GetVPNPlatform()->RouteManager(); - // black hole all routes by default - route.AddBlackhole(); + // black hole all routes if enabled + if (m_Router->GetConfig()->network.m_BlackholeRoutes) + route.AddBlackhole(); + // explicit route pokes for first hops m_Router->ForEachPeer( [&](auto session, auto) mutable { AddRoute(session->GetRemoteEndpoint().asIPv4()); }, From 7e92f36b6af6df901c6ea7aa82b7c6496a1119d2 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 25 Jan 2022 13:13:20 -0500 Subject: [PATCH 035/202] disable tests by default make ci enable tests explicitly --- .drone.jsonnet | 2 +- CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index fed2b0709..465c2fc6b 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -72,7 +72,7 @@ local debian_pipeline(name, 'cmake .. -DWITH_SETCAP=OFF -DCMAKE_CXX_FLAGS=-fdiagnostics-color=always -DCMAKE_BUILD_TYPE=' + build_type + ' ' + (if werror then '-DWARNINGS_AS_ERRORS=ON ' else '') + '-DWITH_LTO=' + (if lto then 'ON ' else 'OFF ') + - (if tests then '' else '-DWITH_TESTS=OFF ') + + '-DWITH_TESTS=' + (if tests then 'ON ' else 'OFF ') + cmake_extra, 'VERBOSE=1 make -j' + jobs, ] diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d0244044..526fbb368 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -57,7 +57,7 @@ option(WITH_COVERAGE "generate coverage data" OFF) option(USE_SHELLHOOKS "enable shell hooks on compile time (dangerous)" OFF) option(WARNINGS_AS_ERRORS "treat all warnings as errors. turn off for development, on for release" OFF) option(TRACY_ROOT "include tracy profiler source" OFF) -option(WITH_TESTS "build unit tests" ON) +option(WITH_TESTS "build unit tests" OFF) option(WITH_HIVE "build simulation stubs" OFF) option(BUILD_PACKAGE "builds extra components for making an installer (with 'make package')" OFF) From 38c6d99375f925dea025378365f95129900710d5 Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 27 Jan 2022 09:57:43 -0500 Subject: [PATCH 036/202] wire up sigusr1 to trigger a network thaw on non win32 platforms --- llarp/context.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/llarp/context.cpp b/llarp/context.cpp index 59a54dc28..ecba8ee09 100644 --- a/llarp/context.cpp +++ b/llarp/context.cpp @@ -166,6 +166,14 @@ namespace llarp SigINT(); } #ifndef _WIN32 + if (sig == SIGUSR1) + { + if (router and not router->IsServiceNode()) + { + LogInfo("SIGUSR1: resetting network state"); + router->Thaw(); + } + } if (sig == SIGHUP) { Reload(); From 5fac6c84d857a19f8076957611229475b6ba7e4b Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 27 Jan 2022 10:59:04 -0500 Subject: [PATCH 037/202] detect timeskip and thaw network when we think it happened. --- llarp/router/router.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index 2204e4766..94110689b 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -807,6 +807,13 @@ namespace llarp return; // LogDebug("tick router"); const auto now = Now(); + constexpr auto TimeskipDetectedDuration = 1min; + if (const auto delta = now - _lastTick; _lastTick != 0s and delta > TimeskipDetectedDuration) + { + // we detected a time skip into the futre, thaw the network + LogWarn("Timeskip of ", delta, " detected. Resetting network state"); + Thaw(); + } #if defined(WITH_SYSTEMD) { From fc444741f1c179a12e1a380005c15253877d13b4 Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 27 Jan 2022 11:11:57 -0500 Subject: [PATCH 038/202] move constant to new header create llarp/constants/time.hpp for time/duration constants --- llarp/constants/time.hpp | 10 ++++++++++ llarp/router/router.cpp | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 llarp/constants/time.hpp diff --git a/llarp/constants/time.hpp b/llarp/constants/time.hpp new file mode 100644 index 000000000..69866e2c8 --- /dev/null +++ b/llarp/constants/time.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace llarp +{ + using namespace std::literals; + /// how big of a time skip before we reset network state + constexpr auto TimeskipDetectedDuration = 1min; +} // namespace llarp diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index 94110689b..835850e75 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -807,7 +808,6 @@ namespace llarp return; // LogDebug("tick router"); const auto now = Now(); - constexpr auto TimeskipDetectedDuration = 1min; if (const auto delta = now - _lastTick; _lastTick != 0s and delta > TimeskipDetectedDuration) { // we detected a time skip into the futre, thaw the network From 996de3d4c60bbff0e8c246dea6669e4a3e689c62 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 28 Jan 2022 11:10:03 -0500 Subject: [PATCH 039/202] make it so github sees the repo is gpl3 --- LICENSE.txt => LICENSE | 15 +-------------- cmake/installer.cmake | 2 +- contrib/mac.sh | 2 +- readme.md | 15 +++++++++++++++ 4 files changed, 18 insertions(+), 16 deletions(-) rename LICENSE.txt => LICENSE (98%) diff --git a/LICENSE.txt b/LICENSE similarity index 98% rename from LICENSE.txt rename to LICENSE index 49211c38d..e72bfddab 100644 --- a/LICENSE.txt +++ b/LICENSE @@ -1,16 +1,3 @@ -LokiNET is the reference implementation of LLARP (Low Latency Anonymous -Routing Protocol). - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -Copyright (c) 2018-2020 The Loki Project -Copyright (c) 2018-2020 Jeff Becker -Windows NT port and portions Copyright (c) 2018-2020 Rick V. - - GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 @@ -684,4 +671,4 @@ into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read -. +. \ No newline at end of file diff --git a/cmake/installer.cmake b/cmake/installer.cmake index b8c04e563..bdd4958fc 100644 --- a/cmake/installer.cmake +++ b/cmake/installer.cmake @@ -1,7 +1,7 @@ set(CPACK_PACKAGE_VENDOR "lokinet.org") set(CPACK_PACKAGE_HOMEPAGE_URL "https://lokinet.org/") set(CPACK_RESOURCE_FILE_README "${PROJECT_SOURCE_DIR}/contrib/readme-installer.txt") -set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE.txt") +set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") if(WIN32) include(cmake/win32_installer_deps.cmake) diff --git a/contrib/mac.sh b/contrib/mac.sh index 4719b40d2..0ddcbe3ff 100755 --- a/contrib/mac.sh +++ b/contrib/mac.sh @@ -9,7 +9,7 @@ set -e set +x -if ! [ -f LICENSE.txt ] || ! [ -d llarp ]; then +if ! [ -f LICENSE ] || ! [ -d llarp ]; then echo "You need to run this as ./contrib/mac.sh from the top-level lokinet project directory" fi diff --git a/readme.md b/readme.md index 2c24cd3b2..232a942ef 100644 --- a/readme.md +++ b/readme.md @@ -127,3 +127,18 @@ This requires the binary to have the proper capabilities which is usually set by $ sudo setcap cap_net_admin,cap_net_bind_service=+eip /usr/local/bin/lokinet + +---- + +# License + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +``` +Copyright © 2018-2022 The Oxen Project +Copyright © 2018-2022 Jeff Becker +Copyright © 2018-2020 Rick V. (Historical Windows NT port and portions) +``` From 185809907d256c591ddee0fc7f9e0a099a00ee2a Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 28 Jan 2022 11:21:11 -0500 Subject: [PATCH 040/202] fix typo in readme, use 3 hashes instead of 2 because it is a sub 3 section --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 232a942ef..f8df94448 100644 --- a/readme.md +++ b/readme.md @@ -110,7 +110,7 @@ When running from debian package the following steps are not needed as it is alr ready to use. You can stop/start/restart it using `systemctl start lokinet`, `systemctl stop lokinet`, etc. -## Running on Linux (without debs) +### Running on Linux (without debs) **DO NOT RUN AS ROOT**, run as normal user. From 5f49d3a49fbe8ff34dc72230e8375f7fbe0b21d2 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Sun, 19 Sep 2021 16:41:03 -0400 Subject: [PATCH 041/202] update header with notes --- include/lokinet/lokinet_udp.h | 50 +++++++++++++++-------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/include/lokinet/lokinet_udp.h b/include/lokinet/lokinet_udp.h index 27b00c3e7..25b240dba 100644 --- a/include/lokinet/lokinet_udp.h +++ b/include/lokinet/lokinet_udp.h @@ -59,46 +59,40 @@ extern "C" /// inbound listen udp socket /// expose udp port exposePort to the void - /// if srv is not NULL add an srv record for this port, the format being "thingservice" in which - /// will add a srv record "_udp.thingservice.ouraddress.tld" that advertises this port provide - /// localAddr to forward inbound udp packets to "ip:port" if localAddr is NULL then the resulting - /// socket MUST be drained by lokinet_udp_recvmmsg - /// + /// localAddr to forward inbound udp packets to "ip:port" /// returns 0 on success /// returns nonzero on error in which it is an errno value int EXPORT lokinet_udp_bind( int exposedPort, - char* srv, char* localAddr, struct lokinet_udp_listen_result* result, struct lokinet_context* ctx); - /// poll many udp sockets for activity - /// returns 0 on sucess - /// - /// returns non zero errno on error + /// get remote peer information about a local udp flow coming from localaddr + /// returns 0 on success + /// returns nonzero on error in which it is an errno value int EXPORT - lokinet_udp_poll( - const int* socket_ids, - size_t numsockets, - const struct timespec* timeout, - struct lokinet_context* ctx); + lokinet_udp_peername(char* localAddr, struct lokinet_udp_flow* flow, struct lokinet_context* ctx); - struct lokinet_udp_pkt - { - char remote_addr[256]; - int remote_port; - struct iovec pkt; - }; + /* + + Packet arrives for new flow: + - call "new_flow" callback, which can return: + - drop + - accept, returns (context pointer, flow timeout). + + If accepted then this packet and subsequent packets on the flow: + - call "new_packet" callback, given it the context pointer from accept. + + If no packets for (timeout) we call + - "flow_timeout" with the context pointer + + int new_flow(const struct remote_details* remote, void** user_ctx, int* flow_timeout); + void new_packet(const struct remote_details* remote, char* data, size_t len, void* user_ctx); + void flow_timeout(const struct remote_details* remote, void* user_ctx); - /// analog to recvmmsg - ssize_t EXPORT - lokinet_udp_recvmmsg( - int socket_id, - struct lokinet_udp_pkt* events, - size_t max_events, - struct lokient_context* ctx); + */ #ifdef __cplusplus } From 433febe5c636f9fcb20961b73179cd47dfc24436 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Mon, 20 Sep 2021 10:04:41 -0400 Subject: [PATCH 042/202] update liblokinet udp api header --- include/lokinet/lokinet_udp.h | 66 +++++++++++------------------------ 1 file changed, 21 insertions(+), 45 deletions(-) diff --git a/include/lokinet/lokinet_udp.h b/include/lokinet/lokinet_udp.h index 25b240dba..b68e685fb 100644 --- a/include/lokinet/lokinet_udp.h +++ b/include/lokinet/lokinet_udp.h @@ -21,7 +21,7 @@ extern "C" #endif /// information about a udp flow - struct lokinet_udp_flow + struct lokinet_udp_flowinfo { /// the socket id for this flow used for i/o purposes and closing this socket int socket_id; @@ -35,21 +35,6 @@ extern "C" int local_port; }; - /// establish an outbound udp flow - /// remoteHost is the remote .loki or .snode address conneting to - /// remotePort is either a string integer or an srv record name to lookup, e.g. thingservice in - /// which we do a srv lookup for _udp.thingservice.remotehost.tld and use the "best" port provided - /// localAddr is the local ip:port to bind our socket to, if localAddr is NULL then - /// lokinet_udp_sendmmsg MUST be used to send packets return 0 on success return nonzero on fail, - /// containing an errno value - int EXPORT - lokinet_udp_establish( - char* remoteHost, - char* remotePort, - char* localAddr, - struct lokinet_udp_flow* flow, - struct lokinet_context* ctx); - /// a result from a lokinet_udp_bind call struct lokinet_udp_bind_result { @@ -57,43 +42,34 @@ extern "C" int socket_id; }; + /// flow acceptor hook, return 0 success, return nonzero with errno on failure + typedef int (*lokinet_udp_flow_filter)(void*, const struct lokinet_udp_flowinfo*, void**, int*); + + /// hook function for handling packets + typedef void (*lokinet_udp_flow_recv_func)( + const struct lokinet_udp_flowinfo*, char*, size_t, void*); + + /// hook function for flow timeout + typedef void (*lokinet_udp_flow_timeout_func)(const lokinet_udp_flowinfo*, void*); + /// inbound listen udp socket /// expose udp port exposePort to the void - /// localAddr to forward inbound udp packets to "ip:port" - /// returns 0 on success - /// returns nonzero on error in which it is an errno value + /// filter MUST be non null, pointing to a flow filter for accepting new udp flows, called with + /// user data recv MUST be non null, pointing to a packet handler function for each flow, called + /// with per flow user data provided by filter function if accepted timeout MUST be non null, + /// pointing to a cleanup function to clean up a stale flow, staleness determined by the value + /// given by the filter function returns 0 on success returns nonzero on error in which it is an + /// errno value int EXPORT lokinet_udp_bind( int exposedPort, - char* localAddr, + lokinet_udp_flow_filter filter, + lokinet_udp_flow_recv_func recv, + lokinet_udp_flow_timeout_func timeout, + void* user, struct lokinet_udp_listen_result* result, struct lokinet_context* ctx); - /// get remote peer information about a local udp flow coming from localaddr - /// returns 0 on success - /// returns nonzero on error in which it is an errno value - int EXPORT - lokinet_udp_peername(char* localAddr, struct lokinet_udp_flow* flow, struct lokinet_context* ctx); - - /* - - Packet arrives for new flow: - - call "new_flow" callback, which can return: - - drop - - accept, returns (context pointer, flow timeout). - - If accepted then this packet and subsequent packets on the flow: - - call "new_packet" callback, given it the context pointer from accept. - - If no packets for (timeout) we call - - "flow_timeout" with the context pointer - - int new_flow(const struct remote_details* remote, void** user_ctx, int* flow_timeout); - void new_packet(const struct remote_details* remote, char* data, size_t len, void* user_ctx); - void flow_timeout(const struct remote_details* remote, void* user_ctx); - - */ - #ifdef __cplusplus } #endif From 50001da9a10e7a53c73979cfc150492721e30a01 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Mon, 20 Sep 2021 10:21:57 -0400 Subject: [PATCH 043/202] remove dead shit from header --- include/lokinet/lokinet_udp.h | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/include/lokinet/lokinet_udp.h b/include/lokinet/lokinet_udp.h index b68e685fb..8f21cb565 100644 --- a/include/lokinet/lokinet_udp.h +++ b/include/lokinet/lokinet_udp.h @@ -2,19 +2,6 @@ #include "lokinet_context.h" -#ifdef _WIN32 -extern "C" -{ - struct iovec - { - void* iov_base; - size_t iov_len; - }; -} -#else -#include -#endif - #ifdef __cplusplus extern "C" { From db7050cd2d580feac3fbe88da2ad32ec74523117 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Mon, 20 Sep 2021 10:50:58 -0400 Subject: [PATCH 044/202] update liblokinet udp header --- include/lokinet/lokinet_udp.h | 62 ++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 12 deletions(-) diff --git a/include/lokinet/lokinet_udp.h b/include/lokinet/lokinet_udp.h index 8f21cb565..82ffd0a3a 100644 --- a/include/lokinet/lokinet_udp.h +++ b/include/lokinet/lokinet_udp.h @@ -14,12 +14,8 @@ extern "C" int socket_id; /// remote endpoint's .loki or .snode address char remote_addr[256]; - /// local endpoint's ip address - char local_addr[64]; /// remote endpont's port int remote_port; - /// local endpoint's port - int local_port; }; /// a result from a lokinet_udp_bind call @@ -30,23 +26,37 @@ extern "C" }; /// flow acceptor hook, return 0 success, return nonzero with errno on failure - typedef int (*lokinet_udp_flow_filter)(void*, const struct lokinet_udp_flowinfo*, void**, int*); + typedef int (*lokinet_udp_flow_filter)( + void* /*user*/, + const struct lokinet_udp_flowinfo* /* remote address */, + void** /* flow-userdata */, + int* /* timeout */); /// hook function for handling packets typedef void (*lokinet_udp_flow_recv_func)( - const struct lokinet_udp_flowinfo*, char*, size_t, void*); + const struct lokinet_udp_flowinfo* /* remote address */, + char* /* data pointer */, + size_t /* data length */, + void* /* flow-userdata */); /// hook function for flow timeout - typedef void (*lokinet_udp_flow_timeout_func)(const lokinet_udp_flowinfo*, void*); + typedef void (*lokinet_udp_flow_timeout_func)( + const lokinet_udp_flowinfo* /* remote address */, void* /* flow-userdata */); /// inbound listen udp socket /// expose udp port exposePort to the void - /// filter MUST be non null, pointing to a flow filter for accepting new udp flows, called with - /// user data recv MUST be non null, pointing to a packet handler function for each flow, called - /// with per flow user data provided by filter function if accepted timeout MUST be non null, + //// + /// @param filter MUST be non null, pointing to a flow filter for accepting new udp flows, called + /// with user data + /// + /// @param recv MUST be non null, pointing to a packet handler function for each flow, called + /// with per flow user data provided by filter function if accepted + /// + /// @param timeout MUST be non null, /// pointing to a cleanup function to clean up a stale flow, staleness determined by the value - /// given by the filter function returns 0 on success returns nonzero on error in which it is an - /// errno value + /// given by the filter function returns 0 on success + /// + /// @returns nonzero on error in which it is an errno value int EXPORT lokinet_udp_bind( int exposedPort, @@ -57,6 +67,34 @@ extern "C" struct lokinet_udp_listen_result* result, struct lokinet_context* ctx); + /// @brief establish a udp flow to remote endpoint + /// + /// @param remote the remote address to establish to + /// + /// @param ctx the lokinet context to use + /// + /// @return 0 on success, non zero errno on fail + int EXPORT + lokinet_udp_establish(const struct lokinet_udp_flowinfo* remote, struct lokinet_context* ctx); + + /// @brief send on an established flow to remote endpoint + /// + /// @param flowinfo populated after call on success + /// + /// @param ptr pointer to data to send + /// + /// @param len the length of the data + /// + /// @param ctx the lokinet context to use + /// + /// @returns 0 on success and non zero errno on fail + int EXPORT + lokinet_udp_flow_send( + const struct lokinet_udp_flowinfo* remote, + const void* ptr, + size_t len, + struct lokinet_ctx* ctx); + #ifdef __cplusplus } #endif From 5b8ebb269c8e4045e5d447aa04dd05ed7cb931fb Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Mon, 20 Sep 2021 16:16:46 -0400 Subject: [PATCH 045/202] changes for liblokinet-ffi * cmake option BUILD_DAEMON for toggling building of daemon directory * when WITH_BOOTSTRAP is OFF dont build curl or cpr --- CMakeLists.txt | 8 +++++--- cmake/StaticBuild.cmake | 2 ++ external/CMakeLists.txt | 3 ++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 526fbb368..1c41c6e28 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,6 +60,7 @@ option(TRACY_ROOT "include tracy profiler source" OFF) option(WITH_TESTS "build unit tests" OFF) option(WITH_HIVE "build simulation stubs" OFF) option(BUILD_PACKAGE "builds extra components for making an installer (with 'make package')" OFF) +option(BUILD_DAEMON "build lokinet daemon and associated utils" ON) include(cmake/enable_lto.cmake) @@ -182,7 +183,7 @@ if(OXENMQ_FOUND) message(STATUS "Found system liboxenmq ${OXENMQ_VERSION}") else() message(STATUS "using oxenmq submodule") - add_subdirectory(${CMAKE_SOURCE_DIR}/external/oxen-mq) + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/external/oxen-mq) endif() @@ -327,8 +328,9 @@ endif() add_subdirectory(crypto) add_subdirectory(llarp) -add_subdirectory(daemon) - +if(BUILD_DAEMON) + add_subdirectory(daemon) +endif() if(WITH_HIVE) add_subdirectory(pybind) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 87ba00a81..d7546fd02 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -335,6 +335,7 @@ set_target_properties(libzmq PROPERTIES INTERFACE_LINK_LIBRARIES "${libzmq_link_libs}" INTERFACE_COMPILE_DEFINITIONS "ZMQ_STATIC") +if(WITH_BOOTSTRAP) set(curl_extra) if(WIN32) set(curl_ssl_opts --without-ssl --with-schannel) @@ -423,3 +424,4 @@ endif() set_target_properties(CURL::libcurl PROPERTIES INTERFACE_LINK_LIBRARIES "${libcurl_link_libs}" INTERFACE_COMPILE_DEFINITIONS "CURL_STATICLIB") +endif() diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 2ddd0e96f..47ea0b8ab 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -56,7 +56,7 @@ add_ngtcp2_lib() # cpr configuration. Ideally we'd just do this via add_subdirectory, but cpr's cmake requires # 3.15+, and we target lower than that (and this is fairly simple to build). - +if(WITH_BOOTSTRAP) if(NOT BUILD_STATIC_DEPS) find_package(CURL REQUIRED COMPONENTS HTTP HTTPS SSL) @@ -79,3 +79,4 @@ target_link_libraries(cpr PUBLIC CURL::libcurl) target_include_directories(cpr PUBLIC cpr/include) target_compile_definitions(cpr PUBLIC CPR_CURL_NOSIGNAL) add_library(cpr::cpr ALIAS cpr) +endif() From 00075f541bceb44484d0d378c82d4eb6b1992a89 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Mon, 20 Sep 2021 16:24:41 -0400 Subject: [PATCH 046/202] fix compile error --- include/lokinet/lokinet_udp.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/lokinet/lokinet_udp.h b/include/lokinet/lokinet_udp.h index 82ffd0a3a..b1909c4c3 100644 --- a/include/lokinet/lokinet_udp.h +++ b/include/lokinet/lokinet_udp.h @@ -41,7 +41,7 @@ extern "C" /// hook function for flow timeout typedef void (*lokinet_udp_flow_timeout_func)( - const lokinet_udp_flowinfo* /* remote address */, void* /* flow-userdata */); + const struct lokinet_udp_flowinfo* /* remote address */, void* /* flow-userdata */); /// inbound listen udp socket /// expose udp port exposePort to the void From c4b1a9c074da63798111742b90c45b73fd2636ad Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Mon, 20 Sep 2021 17:47:06 -0400 Subject: [PATCH 047/202] lokinet_add_bootstrap_rc * allow bootstrap lists to be passed in --- llarp/lokinet_shared.cpp | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index 7a4891032..86b7d3aa6 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -237,11 +238,26 @@ extern "C" auto lock = ctx->acquire(); // add a temp cryptography implementation here so rc.Verify works llarp::CryptoManager instance{new llarp::sodium::CryptoLibSodium{}}; - if (not rc.BDecode(&buf)) - return -1; - if (not rc.Verify(llarp::time_now_ms())) - return -2; - ctx->config->bootstrap.routers.insert(std::move(rc)); + if (data[0] == 'l') + { + llarp::BootstrapList routers{}; + if (not routers.BDecode(&buf)) + return -1; + for (const auto& rc : routers) + { + if (not rc.Verify(llarp::time_now_ms())) + return -2; + } + ctx->config->bootstrap.routers = std::move(routers); + } + else + { + if (not rc.BDecode(&buf)) + return -1; + if (not rc.Verify(llarp::time_now_ms())) + return -2; + ctx->config->bootstrap.routers.insert(std::move(rc)); + } return 0; } From 2428cc189e725252f783c15f41574b1b68da6f96 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Mon, 20 Sep 2021 17:55:07 -0400 Subject: [PATCH 048/202] llarp::BootstrapConfig update * make routers member a llarp::BootstrapList --- llarp/config/config.hpp | 3 ++- llarp/lokinet_shared.cpp | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/llarp/config/config.hpp b/llarp/config/config.hpp index 4b518fc04..ff7ca2e71 100644 --- a/llarp/config/config.hpp +++ b/llarp/config/config.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -196,7 +197,7 @@ namespace llarp struct BootstrapConfig { std::vector files; - std::set routers; + BootstrapList routers; bool seednode; void defineConfigOptions(ConfigDefinition& conf, const ConfigGenParameters& params); diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index 86b7d3aa6..16e66f718 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -240,15 +240,13 @@ extern "C" llarp::CryptoManager instance{new llarp::sodium::CryptoLibSodium{}}; if (data[0] == 'l') { - llarp::BootstrapList routers{}; - if (not routers.BDecode(&buf)) + if (not ctx->config->bootstrap.routers.BDecode(&buf)) return -1; - for (const auto& rc : routers) + for (const auto& rc : ctx->config.bootstrap.routers) { if (not rc.Verify(llarp::time_now_ms())) return -2; } - ctx->config->bootstrap.routers = std::move(routers); } else { From c5b5ff7810370ff71efc9736eee9d35cde27e0b9 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Mon, 20 Sep 2021 17:57:55 -0400 Subject: [PATCH 049/202] typo fix --- llarp/lokinet_shared.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index 16e66f718..145af9f03 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -242,7 +242,7 @@ extern "C" { if (not ctx->config->bootstrap.routers.BDecode(&buf)) return -1; - for (const auto& rc : ctx->config.bootstrap.routers) + for (const auto& rc : ctx->config->bootstrap.routers) { if (not rc.Verify(llarp::time_now_ms())) return -2; From 38d4cec7d1a7b2fbf2346f62b257bcca7a7d079c Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Mon, 20 Sep 2021 18:07:47 -0400 Subject: [PATCH 050/202] log errors on decoding --- llarp/lokinet_shared.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index 145af9f03..f893d00e6 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -8,9 +8,10 @@ #include #include #include -#include #include +#include + #include #ifdef _WIN32 @@ -241,7 +242,10 @@ extern "C" if (data[0] == 'l') { if (not ctx->config->bootstrap.routers.BDecode(&buf)) + { + LogError("Cannot decode bootstrap list: ", llarp::buffer_printer{buf}); return -1; + } for (const auto& rc : ctx->config->bootstrap.routers) { if (not rc.Verify(llarp::time_now_ms())) From 8c8f97adda0452871c49fabfb34e32f28cb90d60 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Mon, 20 Sep 2021 18:12:08 -0400 Subject: [PATCH 051/202] more logging --- llarp/lokinet_shared.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index f893d00e6..1b0bf0653 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -233,7 +233,6 @@ extern "C" lokinet_add_bootstrap_rc(const char* data, size_t datalen, struct lokinet_context* ctx) { llarp_buffer_t buf{data, datalen}; - llarp::RouterContact rc{}; if (ctx == nullptr) return -3; auto lock = ctx->acquire(); @@ -243,7 +242,7 @@ extern "C" { if (not ctx->config->bootstrap.routers.BDecode(&buf)) { - LogError("Cannot decode bootstrap list: ", llarp::buffer_printer{buf}); + llarp::LogError("Cannot decode bootstrap list: ", llarp::buffer_printer{buf}); return -1; } for (const auto& rc : ctx->config->bootstrap.routers) @@ -254,8 +253,12 @@ extern "C" } else { + llarp::RouterContact rc{}; if (not rc.BDecode(&buf)) + { + llarp::LogError("failed to decode signle RC: ", llarp::buffer_printer{buf}); return -1; + } if (not rc.Verify(llarp::time_now_ms())) return -2; ctx->config->bootstrap.routers.insert(std::move(rc)); From 66de6808843beadc20d5ebcce8841987dc51705e Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Mon, 20 Sep 2021 18:22:15 -0400 Subject: [PATCH 052/202] sanity check --- llarp/lokinet_shared.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index 1b0bf0653..0766be0f4 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -232,6 +232,8 @@ extern "C" int EXPORT lokinet_add_bootstrap_rc(const char* data, size_t datalen, struct lokinet_context* ctx) { + if (data == nullptr or datalen == 0) + return -3; llarp_buffer_t buf{data, datalen}; if (ctx == nullptr) return -3; From bbb082931abce87ef33ef04414db63470fc56b8b Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Mon, 20 Sep 2021 18:40:29 -0400 Subject: [PATCH 053/202] more logging --- llarp/bootstrap.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/llarp/bootstrap.cpp b/llarp/bootstrap.cpp index f21f3a025..32f273de4 100644 --- a/llarp/bootstrap.cpp +++ b/llarp/bootstrap.cpp @@ -1,5 +1,7 @@ #include "bootstrap.hpp" #include "util/bencode.hpp" +#include "util/logging/logger.hpp" +#include "util/logging/buffer.hpp" namespace llarp { @@ -16,9 +18,12 @@ namespace llarp [&](llarp_buffer_t* b, bool more) -> bool { if (more) { - RouterContact rc; + RouterContact rc{}; if (not rc.BDecode(b)) + { + LogError("invalid rc in bootstrap list: ", llarp::buffer_printer{*b}); return false; + } emplace(std::move(rc)); } return true; From 13c3786067da0cd6c7a18615d424d60b0d6c446d Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Mon, 20 Sep 2021 20:03:05 -0400 Subject: [PATCH 054/202] correct function names --- include/lokinet/lokinet_stream.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/include/lokinet/lokinet_stream.h b/include/lokinet/lokinet_stream.h index d5d97754b..77be691f5 100644 --- a/include/lokinet/lokinet_stream.h +++ b/include/lokinet/lokinet_stream.h @@ -53,6 +53,9 @@ extern "C" int EXPORT lokinet_inbound_stream(uint16_t port, struct lokinet_context* context); + void EXPORT + lokinet_close_stream(int stream_id, struct lokinet_context* context); + #ifdef __cplusplus } #endif From 9d069983b4cac45a3adacbad75101f4af29f4cd6 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Tue, 21 Sep 2021 09:07:28 -0400 Subject: [PATCH 055/202] add WITH_BOOTSTRAP option for toggling building lokinet-bootstrap --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c41c6e28..ff0a9f12d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,6 +61,7 @@ option(WITH_TESTS "build unit tests" OFF) option(WITH_HIVE "build simulation stubs" OFF) option(BUILD_PACKAGE "builds extra components for making an installer (with 'make package')" OFF) option(BUILD_DAEMON "build lokinet daemon and associated utils" ON) +option(WITH_BOOTSTRAP "build lokinet-bootstrap tool" ON) include(cmake/enable_lto.cmake) From 9fb11bf3da54c5de92b66c8838258419b575a4df Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Tue, 21 Sep 2021 10:32:50 -0400 Subject: [PATCH 056/202] typo fixes and clarify docs --- include/lokinet/lokinet_udp.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/include/lokinet/lokinet_udp.h b/include/lokinet/lokinet_udp.h index b1909c4c3..6142192df 100644 --- a/include/lokinet/lokinet_udp.h +++ b/include/lokinet/lokinet_udp.h @@ -10,12 +10,12 @@ extern "C" /// information about a udp flow struct lokinet_udp_flowinfo { - /// the socket id for this flow used for i/o purposes and closing this socket - int socket_id; /// remote endpoint's .loki or .snode address char remote_addr[256]; /// remote endpont's port int remote_port; + /// the socket id for this flow used for i/o purposes and closing this socket + int socket_id; }; /// a result from a lokinet_udp_bind call @@ -30,7 +30,7 @@ extern "C" void* /*user*/, const struct lokinet_udp_flowinfo* /* remote address */, void** /* flow-userdata */, - int* /* timeout */); + int* /* timeout seconds */); /// hook function for handling packets typedef void (*lokinet_udp_flow_recv_func)( @@ -64,7 +64,7 @@ extern "C" lokinet_udp_flow_recv_func recv, lokinet_udp_flow_timeout_func timeout, void* user, - struct lokinet_udp_listen_result* result, + struct lokinet_udp_bind_result* result, struct lokinet_context* ctx); /// @brief establish a udp flow to remote endpoint From 71364da9f4082be55d1c6159a7612512432dbe59 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Tue, 21 Sep 2021 10:58:02 -0400 Subject: [PATCH 057/202] fix typos add lokinet_udp_close --- include/lokinet/lokinet_udp.h | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/include/lokinet/lokinet_udp.h b/include/lokinet/lokinet_udp.h index 6142192df..54d7f0ed7 100644 --- a/include/lokinet/lokinet_udp.h +++ b/include/lokinet/lokinet_udp.h @@ -93,7 +93,16 @@ extern "C" const struct lokinet_udp_flowinfo* remote, const void* ptr, size_t len, - struct lokinet_ctx* ctx); + struct lokinet_context* ctx); + + /// @brief close a bound udp socket + /// closes all flows immediately + /// + /// @param socket_id the bound udp socket's id + /// + /// @param ctx lokinet context + void EXPORT + lokinet_udp_close(int socket_id, struct lokinet_context* ctx); #ifdef __cplusplus } From e11e736ea5e414549d29d788f2caea051be22cf2 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Tue, 21 Sep 2021 11:12:28 -0400 Subject: [PATCH 058/202] typofix --- include/lokinet/lokinet_udp.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/lokinet/lokinet_udp.h b/include/lokinet/lokinet_udp.h index 54d7f0ed7..1d4de8cd2 100644 --- a/include/lokinet/lokinet_udp.h +++ b/include/lokinet/lokinet_udp.h @@ -11,7 +11,7 @@ extern "C" struct lokinet_udp_flowinfo { /// remote endpoint's .loki or .snode address - char remote_addr[256]; + char remote_host[[256]; /// remote endpont's port int remote_port; /// the socket id for this flow used for i/o purposes and closing this socket From d3d07fe53e6abd1f3d0132e132fd0bdd2d170617 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Tue, 21 Sep 2021 11:12:59 -0400 Subject: [PATCH 059/202] typofix --- include/lokinet/lokinet_udp.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/lokinet/lokinet_udp.h b/include/lokinet/lokinet_udp.h index 1d4de8cd2..99d7cc83a 100644 --- a/include/lokinet/lokinet_udp.h +++ b/include/lokinet/lokinet_udp.h @@ -11,7 +11,7 @@ extern "C" struct lokinet_udp_flowinfo { /// remote endpoint's .loki or .snode address - char remote_host[[256]; + char remote_host[256]; /// remote endpont's port int remote_port; /// the socket id for this flow used for i/o purposes and closing this socket From 1c70b0f42f1ce95ffcfc6e7d669f415a91845fb7 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Tue, 21 Sep 2021 16:04:05 -0400 Subject: [PATCH 060/202] add lokinet_hex_to_base32z --- include/lokinet/lokinet_misc.h | 5 +++++ llarp/lokinet_shared.cpp | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/include/lokinet/lokinet_misc.h b/include/lokinet/lokinet_misc.h index dea41f9ba..fa20911e4 100644 --- a/include/lokinet/lokinet_misc.h +++ b/include/lokinet/lokinet_misc.h @@ -21,6 +21,11 @@ extern "C" int EXPORT lokinet_log_level(const char*); + /// @brief take in hex and turn it into base32z + /// @return value must be free()'d later + char* EXPORT + lokinet_hex_to_base32z(const char* hex); + #ifdef __cplusplus } #endif diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index 0766be0f4..3020889e2 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -12,6 +12,8 @@ #include +#include + #include #ifdef _WIN32 @@ -546,6 +548,13 @@ extern "C" return id; } + char* EXPORT + lokinet_hex_to_base32z(const char* hex) + { + const auto base32z = oxenmq::to_base32z(oxenmq::from_hex(std::string{hex})); + return strdup(base32z.c_str()); + } + void EXPORT lokinet_close_stream(int stream_id, struct lokinet_context* ctx) { From ba57ab04aa47c64b289ad48f799fa5d5f7dfd113 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Thu, 23 Sep 2021 14:01:04 -0400 Subject: [PATCH 061/202] wire up liblokient_udp_* --- include/lokinet/lokinet_udp.h | 20 +- llarp/CMakeLists.txt | 1 + llarp/handlers/null.hpp | 147 ++++++----- llarp/handlers/tun.hpp | 2 +- llarp/lokinet_shared.cpp | 392 +++++++++++++++++++++++++++++- llarp/net/ip_packet.cpp | 33 +++ llarp/net/ip_packet.hpp | 8 + llarp/service/endpoint.hpp | 8 + llarp/vpn/egres_packet_router.cpp | 101 ++++++++ llarp/vpn/egres_packet_router.hpp | 49 ++++ llarp/vpn/packet_router.hpp | 5 +- 11 files changed, 688 insertions(+), 78 deletions(-) create mode 100644 llarp/vpn/egres_packet_router.cpp create mode 100644 llarp/vpn/egres_packet_router.hpp diff --git a/include/lokinet/lokinet_udp.h b/include/lokinet/lokinet_udp.h index 99d7cc83a..7e5b6ca2b 100644 --- a/include/lokinet/lokinet_udp.h +++ b/include/lokinet/lokinet_udp.h @@ -13,7 +13,7 @@ extern "C" /// remote endpoint's .loki or .snode address char remote_host[256]; /// remote endpont's port - int remote_port; + uint16_t remote_port; /// the socket id for this flow used for i/o purposes and closing this socket int socket_id; }; @@ -32,10 +32,14 @@ extern "C" void** /* flow-userdata */, int* /* timeout seconds */); + /// callback to make a new outbound flow + typedef void(lokinet_udp_create_flow_func)( + void* /*userdata*/, void** /*flow userdata*/, int* /* flowtimeout */); + /// hook function for handling packets typedef void (*lokinet_udp_flow_recv_func)( const struct lokinet_udp_flowinfo* /* remote address */, - char* /* data pointer */, + const char* /* data pointer */, size_t /* data length */, void* /* flow-userdata */); @@ -59,7 +63,7 @@ extern "C" /// @returns nonzero on error in which it is an errno value int EXPORT lokinet_udp_bind( - int exposedPort, + uint16_t exposedPort, lokinet_udp_flow_filter filter, lokinet_udp_flow_recv_func recv, lokinet_udp_flow_timeout_func timeout, @@ -69,13 +73,21 @@ extern "C" /// @brief establish a udp flow to remote endpoint /// + /// @param create_flow the callback to create the new flow if we establish one + /// + /// @param user passed to new_flow as user data + /// /// @param remote the remote address to establish to /// /// @param ctx the lokinet context to use /// /// @return 0 on success, non zero errno on fail int EXPORT - lokinet_udp_establish(const struct lokinet_udp_flowinfo* remote, struct lokinet_context* ctx); + lokinet_udp_establish( + lokinet_udp_create_flow_func create_flow, + void* user, + const struct lokinet_udp_flowinfo* remote, + struct lokinet_context* ctx); /// @brief send on an established flow to remote endpoint /// diff --git a/llarp/CMakeLists.txt b/llarp/CMakeLists.txt index 05a60bbc3..2ccc1712c 100644 --- a/llarp/CMakeLists.txt +++ b/llarp/CMakeLists.txt @@ -54,6 +54,7 @@ add_library(lokinet-platform net/net_int.cpp net/sock_addr.cpp vpn/packet_router.cpp + vpn/egres_packet_router.cpp vpn/platform.cpp ) diff --git a/llarp/handlers/null.hpp b/llarp/handlers/null.hpp index e8f209dc6..174afb60d 100644 --- a/llarp/handlers/null.hpp +++ b/llarp/handlers/null.hpp @@ -5,87 +5,114 @@ #include #include #include +#include -namespace llarp +namespace llarp::handlers { - namespace handlers + struct NullEndpoint final : public llarp::service::Endpoint, + public std::enable_shared_from_this { - struct NullEndpoint final : public llarp::service::Endpoint, - public std::enable_shared_from_this + NullEndpoint(AbstractRouter* r, llarp::service::Context* parent) + : llarp::service::Endpoint{r, parent} + , m_PacketRouter{new vpn::EgresPacketRouter{[](auto, auto) {}}} { - NullEndpoint(AbstractRouter* r, llarp::service::Context* parent) - : llarp::service::Endpoint(r, parent) + r->loop()->add_ticker([this] { Pump(Now()); }); + } + + virtual bool + HandleInboundPacket( + const service::ConvoTag tag, + const llarp_buffer_t& buf, + service::ProtocolType t, + uint64_t) override + { + LogTrace("Inbound ", t, " packet (", buf.sz, "B) on convo ", tag); + if (t == service::ProtocolType::Control) { - r->loop()->add_ticker([this] { Pump(Now()); }); + return true; } - - virtual bool - HandleInboundPacket( - const service::ConvoTag tag, - const llarp_buffer_t& buf, - service::ProtocolType t, - uint64_t) override + if (t == service::ProtocolType::TrafficV4 or t == service::ProtocolType::TrafficV6) { - LogTrace("Inbound ", t, " packet (", buf.sz, "B) on convo ", tag); - if (t == service::ProtocolType::Control) + if (auto from = GetEndpointWithConvoTag(tag)) { + net::IPPacket pkt{}; + if (not pkt.Load(buf)) + { + LogWarn("invalid ip packet from remote T=", tag); + return false; + } + m_PacketRouter->HandleIPPacketFrom(std::move(*from), std::move(pkt)); return true; } - if (t != service::ProtocolType::QUIC) - return false; - - auto* quic = GetQUICTunnel(); - if (!quic) - { - LogWarn("incoming quic packet but this endpoint is not quic capable; dropping"); - return false; - } - if (buf.sz < 4) + else { - LogWarn("invalid incoming quic packet, dropping"); + LogWarn("did not handle packet, no endpoint with convotag T=", tag); return false; } - quic->receive_packet(tag, buf); - return true; } + if (t != service::ProtocolType::QUIC) + return false; - std::string - GetIfName() const override + auto* quic = GetQUICTunnel(); + if (!quic) { - return ""; + LogWarn("incoming quic packet but this endpoint is not quic capable; dropping"); + return false; } - - path::PathSet_ptr - GetSelf() override + if (buf.sz < 4) { - return shared_from_this(); + LogWarn("invalid incoming quic packet, dropping"); + return false; } + quic->receive_packet(tag, buf); + return true; + } - std::weak_ptr - GetWeak() override - { - return weak_from_this(); - } + std::string + GetIfName() const override + { + return ""; + } - bool - SupportsV6() const override - { - return false; - } + path::PathSet_ptr + GetSelf() override + { + return shared_from_this(); + } - void - SendPacketToRemote(const llarp_buffer_t&, service::ProtocolType) override{}; + std::weak_ptr + GetWeak() override + { + return weak_from_this(); + } - huint128_t ObtainIPForAddr(std::variant) override - { - return {0}; - } + bool + SupportsV6() const override + { + return false; + } - std::optional> ObtainAddrForIP( - huint128_t) const override - { - return std::nullopt; - } - }; - } // namespace handlers -} // namespace llarp + void + SendPacketToRemote(const llarp_buffer_t&, service::ProtocolType) override{}; + + huint128_t ObtainIPForAddr(std::variant) override + { + return {0}; + } + + std::optional> ObtainAddrForIP( + huint128_t) const override + { + return std::nullopt; + } + + vpn::EgresPacketRouter* + EgresPacketRouter() override + { + return m_PacketRouter.get(); + } + + private: + std::unique_ptr m_PacketRouter; + }; +} // namespace llarp::handlers diff --git a/llarp/handlers/tun.hpp b/llarp/handlers/tun.hpp index ee7a64b5f..37fe9f37d 100644 --- a/llarp/handlers/tun.hpp +++ b/llarp/handlers/tun.hpp @@ -15,7 +15,7 @@ #include #include #include -#include "service/protocol_type.hpp" +#include namespace llarp { diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index 3020889e2..982b1eef9 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -1,7 +1,5 @@ - - -#include "lokinet.h" -#include "llarp.hpp" +#include +#include #include #include @@ -15,6 +13,8 @@ #include #include +#include +#include #ifdef _WIN32 #define EHOSTDOWN ENETDOWN @@ -32,6 +32,165 @@ namespace return std::make_shared(); } }; + + struct UDPFlow + { + using Clock_t = std::chrono::steady_clock; + void* m_FlowUserData; + std::chrono::seconds m_FlowTimeout; + std::chrono::time_point m_ExpiresAt; + lokinet_udp_flowinfo m_FlowInfo; + lokinet_udp_flow_recv_func m_Recv; + + /// call timeout hook for this flow + void + TimedOut(lokinet_udp_flow_timeout_func timeout) + { + timeout(&m_FlowInfo, m_FlowUserData); + } + + /// mark this flow as active + /// updates the expires at timestamp + void + MarkActive() + { + m_ExpiresAt = Clock_t::now() + m_FlowTimeout; + } + + /// returns true if we think this flow is expired + bool + IsExpired() const + { + return Clock_t::now() >= m_ExpiresAt; + } + + void + HandlePacket(const llarp::net::IPPacket& pkt) + { + if (auto maybe = pkt.L4Data()) + { + MarkActive(); + m_Recv(&m_FlowInfo, maybe->first, maybe->second, m_FlowUserData); + } + } + }; + + struct UDPHandler + { + using AddressVariant_t = llarp::vpn::AddressVariant_t; + int m_SocketID; + llarp::nuint16_t m_LocalPort; + lokinet_udp_flow_filter m_Filter; + lokinet_udp_flow_recv_func m_Recv; + lokinet_udp_flow_timeout_func m_Timeout; + void* m_User; + std::weak_ptr m_Endpoint; + + std::unordered_map m_Flows; + + std::mutex m_Access; + + explicit UDPHandler( + int socketid, + llarp::nuint16_t localport, + lokinet_udp_flow_filter filter, + lokinet_udp_flow_recv_func recv, + lokinet_udp_flow_timeout_func timeout, + void* user, + std::weak_ptr ep) + : m_SocketID{socketid} + , m_LocalPort{localport} + , m_Filter{filter} + , m_Recv{recv} + , m_Timeout{timeout} + , m_User{user} + , m_Endpoint{ep} + {} + + void + KillAllFlows() + { + std::unique_lock lock{m_Access}; + for (auto& item : m_Flows) + { + item.second.TimedOut(m_Timeout); + } + m_Flows.clear(); + } + + void + AddFlow( + const AddressVariant_t& from, + const lokinet_udp_flowinfo& flow_addr, + void* flow_userdata, + int flow_timeoutseconds) + { + std::unique_lock lock{m_Access}; + auto& flow = m_Flows[from]; + flow.m_FlowInfo = flow_addr; + flow.m_FlowTimeout = std::chrono::seconds{flow_timeoutseconds}; + flow.m_FlowUserData = flow_userdata; + } + + void + ExpireOldFlows() + { + std::unique_lock lock{m_Access}; + for (auto itr = m_Flows.begin(); itr != m_Flows.end();) + { + if (itr->second.IsExpired()) + { + itr->second.TimedOut(m_Timeout); + itr = m_Flows.erase(itr); + } + else + ++itr; + } + } + + void + HandlePacketFrom(AddressVariant_t from, llarp::net::IPPacket pkt) + { + bool isNewFlow{false}; + { + std::unique_lock lock{m_Access}; + isNewFlow = m_Flows.count(from) == 0; + } + if (isNewFlow) + { + lokinet_udp_flowinfo flow_addr{}; + // set flow remote address + var::visit( + [&flow_addr](auto&& from) { + const auto addr = from.ToString(); + std::copy_n( + addr.data(), + std::min(addr.size(), sizeof(flow_addr.remote_host)), + flow_addr.remote_host); + }, + from); + // set socket id + flow_addr.socket_id = m_SocketID; + // get source port + if (auto srcport = pkt.SrcPort()) + { + flow_addr.remote_port = ToHost(*srcport).h; + } + else + return; // invalid data so we bail + void* flow_userdata = nullptr; + int flow_timeoutseconds{}; + // got a new flow, let's check if we want it + if (m_Filter(m_User, &flow_addr, &flow_userdata, &flow_timeoutseconds)) + return; + AddFlow(from, flow_addr, flow_userdata, flow_timeoutseconds); + } + { + std::unique_lock lock{m_Access}; + m_Flows[from].HandlePacket(pkt); + } + } + }; } // namespace struct lokinet_context @@ -43,7 +202,10 @@ struct lokinet_context std::unique_ptr runner; - lokinet_context() : impl{std::make_shared()}, config{llarp::Config::EmbeddedConfig()} + int _socket_id; + + lokinet_context() + : impl{std::make_shared()}, config{llarp::Config::EmbeddedConfig()}, _socket_id{0} {} ~lokinet_context() @@ -52,6 +214,69 @@ struct lokinet_context runner->join(); } + int + next_socket_id() + { + int id = ++_socket_id; + // handle overflow + if (id < 0) + { + _socket_id = 0; + id = ++_socket_id; + } + return id; + } + + /// make a udp handler and hold onto it + /// return its id + [[nodiscard]] int + make_udp_handler( + const std::shared_ptr& ep, + llarp::huint16_t exposePort, + lokinet_udp_flow_filter filter, + lokinet_udp_flow_recv_func recv, + lokinet_udp_flow_timeout_func timeout, + void* user) + { + if (udp_sockets.empty()) + { + // start udp flow expiration timer + impl->router->loop()->call_every(1s, std::make_shared(0), [this]() { + std::unique_lock lock{m_access}; + for (auto& item : udp_sockets) + { + item.second->ExpireOldFlows(); + } + }); + } + + auto udp = std::make_unique( + next_socket_id(), llarp::ToNet(exposePort), filter, recv, timeout, user, std::weak_ptr{ep}); + auto id = udp->m_SocketID; + auto pkt = ep->EgresPacketRouter(); + pkt->AddUDPHandler(exposePort, [udp = udp.get(), this](auto from, auto pkt) { + udp->HandlePacketFrom(std::move(from), std::move(pkt)); + }); + udp_sockets[udp->m_SocketID] = std::move(udp); + return id; + } + + void + remove_udp_handler(int socket_id) + { + std::unique_ptr udp; + { + std::unique_lock lock{m_access}; + if (auto itr = udp_sockets.find(socket_id); itr != udp_sockets.end()) + { + udp = std::move(itr->second); + udp_sockets.erase(itr); + } + } + if (udp) + udp->KillAllFlows(); + } + /// acquire mutex for accessing this context [[nodiscard]] auto acquire() @@ -66,6 +291,7 @@ struct lokinet_context } std::unordered_map streams; + std::unordered_map> udp_sockets; void inbound_stream(int id) @@ -82,8 +308,6 @@ struct lokinet_context namespace { - std::unique_ptr g_context; - void stream_error(lokinet_stream_result* result, int err) { @@ -359,11 +583,11 @@ extern "C" return; auto lock = ctx->acquire(); - if (not ctx->impl->IsStopping()) - { - ctx->impl->CloseAsync(); - ctx->impl->Wait(); - } + if (ctx->impl->IsStopping()) + return; + + ctx->impl->CloseAsync(); + ctx->impl->Wait(); if (ctx->runner) ctx->runner->join(); @@ -626,4 +850,148 @@ extern "C" delete result->internal; result->internal = nullptr; } + + int EXPORT + lokinet_udp_bind( + uint16_t exposedPort, + lokinet_udp_flow_filter filter, + lokinet_udp_flow_recv_func recv, + lokinet_udp_flow_timeout_func timeout, + void* user, + struct lokinet_udp_bind_result* result, + struct lokinet_context* ctx) + { + if (filter == nullptr or recv == nullptr or timeout == nullptr or result == nullptr + or ctx == nullptr) + return EINVAL; + + auto lock = ctx->acquire(); + if (auto ep = ctx->endpoint()) + { + result->socket_id = + ctx->make_udp_handler(ep, llarp::huint16_t{exposedPort}, filter, recv, timeout, user); + return 0; + } + else + return EINVAL; + } + + void EXPORT + lokinet_udp_close(int socket_id, struct lokinet_context* ctx) + { + if (ctx) + ctx->remove_udp_handler(socket_id); + } + + int EXPORT + lokinet_udp_flow_send( + const struct lokinet_udp_flowinfo* remote, + const void* ptr, + size_t len, + struct lokinet_context* ctx) + { + if (remote == nullptr or remote->remote_port == 0 or ptr == nullptr or len == 0 + or ctx == nullptr) + return EINVAL; + std::shared_ptr ep; + llarp::nuint16_t srcport{0}; + llarp::nuint16_t dstport{llarp::ToNet(llarp::huint16_t{remote->remote_port})}; + { + auto lock = ctx->acquire(); + if (auto itr = ctx->udp_sockets.find(remote->socket_id); itr != ctx->udp_sockets.end()) + { + ep = itr->second->m_Endpoint.lock(); + srcport = itr->second->m_LocalPort; + } + else + return EHOSTUNREACH; + } + if (auto maybe = llarp::service::ParseAddress(std::string{remote->remote_host})) + { + llarp::net::IPPacket pkt = llarp::net::IPPacket::UDP( + llarp::nuint32_t{0}, + srcport, + llarp::nuint32_t{0}, + dstport, + llarp_buffer_t{reinterpret_cast(ptr), len}); + + if (pkt.sz == 0) + return EINVAL; + std::promise ret; + ctx->impl->router->loop()->call_soon([addr = *maybe, pkt = std::move(pkt), ep, &ret]() { + if (auto tag = ep->GetBestConvoTagFor(addr)) + { + if (ep->SendToOrQueue(*tag, pkt.ConstBuffer(), llarp::service::ProtocolType::TrafficV4)) + { + ret.set_value(0); + return; + } + } + ret.set_value(ENETUNREACH); + }); + return ret.get_future().get(); + } + return EINVAL; + } + + int EXPORT + lokinet_udp_establish( + lokinet_udp_create_flow_func create_flow, + void* user, + const struct lokinet_udp_flowinfo* remote, + struct lokinet_context* ctx) + { + if (create_flow == nullptr or remote == nullptr or ctx == nullptr) + return EINVAL; + std::shared_ptr ep; + { + auto lock = ctx->acquire(); + if (auto itr = ctx->udp_sockets.find(remote->socket_id); itr != ctx->udp_sockets.end()) + { + ep = itr->second->m_Endpoint.lock(); + } + else + return EHOSTUNREACH; + } + if (auto maybe = llarp::service::ParseAddress(std::string{remote->remote_host})) + { + { + // check for pre existing flow + auto lock = ctx->acquire(); + if (auto itr = ctx->udp_sockets.find(remote->socket_id); itr != ctx->udp_sockets.end()) + { + auto& udp = itr->second; + if (udp->m_Flows.count(*maybe)) + { + // we already have a flow. + return EADDRINUSE; + } + } + } + std::promise gotten; + ctx->impl->router->loop()->call_soon([addr = *maybe, ep, &gotten]() { + ep->EnsurePathTo( + addr, [&gotten](auto result) { gotten.set_value(result.has_value()); }, 5s); + }); + if (gotten.get_future().get()) + { + void* flow_data{nullptr}; + int flow_timeoutseconds{}; + create_flow(user, &flow_data, &flow_timeoutseconds); + { + auto lock = ctx->acquire(); + if (auto itr = ctx->udp_sockets.find(remote->socket_id); itr != ctx->udp_sockets.end()) + { + itr->second->AddFlow(*maybe, *remote, flow_data, flow_timeoutseconds); + return 0; + } + else + return EADDRINUSE; + } + } + else + return ETIMEDOUT; + } + return EINVAL; + } } diff --git a/llarp/net/ip_packet.cpp b/llarp/net/ip_packet.cpp index c31c2332d..b1deaaeea 100644 --- a/llarp/net/ip_packet.cpp +++ b/llarp/net/ip_packet.cpp @@ -128,6 +128,19 @@ namespace llarp } } + std::optional + IPPacket::SrctPort() const + { + switch (IPProtocol{Header()->protocol}) + { + case IPProtocol::TCP: + case IPProtocol::UDP: + return nuint16_t{*reinterpret_cast(buf + (Header()->ihl * 4))}; + default: + return std::nullopt; + } + } + huint32_t IPPacket::srcv4() const { @@ -571,6 +584,26 @@ namespace llarp return std::nullopt; } + std::optional> + IPPacket::L4Data() const + { + const auto* hdr = Header(); + size_t l4_HeaderSize = 0; + if (hdr->protocol == 0x11) + { + l4_HeaderSize = 8; + } + else + return std::nullopt; + + // check for invalid size + if (sz < (hdr->ihl * 4) + l4_HeaderSize) + return std::nullopt; + + const uint8_t* ptr = buf + ((hdr->ihl * 4) + l4_HeaderSize); + return std::make_pair(reinterpret_cast(ptr), std::distance(ptr, buf + sz)); + } + IPPacket IPPacket::UDP( nuint32_t srcaddr, diff --git a/llarp/net/ip_packet.hpp b/llarp/net/ip_packet.hpp index 9f4b91fb6..0987633aa 100644 --- a/llarp/net/ip_packet.hpp +++ b/llarp/net/ip_packet.hpp @@ -293,6 +293,14 @@ namespace llarp std::optional DstPort() const; + /// get source port if applicable + std::optional + SrcPort() const; + + /// get pointer and size of layer 4 data + std::optional> + L4Data() const; + void UpdateIPv4Address(nuint32_t src, nuint32_t dst); diff --git a/llarp/service/endpoint.hpp b/llarp/service/endpoint.hpp index b15add487..48308a525 100644 --- a/llarp/service/endpoint.hpp +++ b/llarp/service/endpoint.hpp @@ -28,6 +28,8 @@ #include +#include + // minimum time between introset shifts #ifndef MIN_SHIFT_INTERVAL #define MIN_SHIFT_INTERVAL 5s @@ -168,6 +170,12 @@ namespace llarp void HandlePathDied(path::Path_ptr p) override; + virtual vpn::EgresPacketRouter* + EgresPacketRouter() + { + return nullptr; + }; + bool PublishIntroSet(const EncryptedIntroSet& i, AbstractRouter* r) override; diff --git a/llarp/vpn/egres_packet_router.cpp b/llarp/vpn/egres_packet_router.cpp new file mode 100644 index 000000000..f892f0832 --- /dev/null +++ b/llarp/vpn/egres_packet_router.cpp @@ -0,0 +1,101 @@ +#include "egres_packet_router.hpp" + +namespace llarp::vpn +{ + struct EgresUDPPacketHandler : public EgresLayer4Handler + { + EgresPacketHandlerFunc m_BaseHandler; + std::unordered_map m_LocalPorts; + + explicit EgresUDPPacketHandler(EgresPacketHandlerFunc baseHandler) + : m_BaseHandler{std::move(baseHandler)} + {} + + void + AddSubHandler(nuint16_t localport, EgresPacketHandlerFunc handler) override + { + m_LocalPorts.emplace(localport, std::move(handler)); + } + + void + RemoveSubHandler(nuint16_t localport) override + { + m_LocalPorts.erase(localport); + } + + void + HandleIPPacketFrom(AddressVariant_t from, net::IPPacket pkt) override + { + const uint8_t* ptr = pkt.buf + (pkt.Header()->ihl * 4) + 2; + const nuint16_t dstPort{*reinterpret_cast(ptr)}; + if (auto itr = m_LocalPorts.find(dstPort); itr != m_LocalPorts.end()) + { + itr->second(std::move(from), std::move(pkt)); + } + else + m_BaseHandler(std::move(from), std::move(pkt)); + } + }; + + struct EgresGenericLayer4Handler : public EgresLayer4Handler + { + EgresPacketHandlerFunc m_BaseHandler; + + explicit EgresGenericLayer4Handler(EgresPacketHandlerFunc baseHandler) + : m_BaseHandler{std::move(baseHandler)} + {} + + void + HandleIPPacketFrom(AddressVariant_t from, net::IPPacket pkt) override + { + m_BaseHandler(std::move(from), std::move(pkt)); + } + }; + + EgresPacketRouter::EgresPacketRouter(EgresPacketHandlerFunc baseHandler) + : m_BaseHandler{std::move(baseHandler)} + {} + + void + EgresPacketRouter::HandleIPPacketFrom(AddressVariant_t from, net::IPPacket pkt) + { + const auto proto = pkt.Header()->protocol; + if (const auto itr = m_IPProtoHandler.find(proto); itr != m_IPProtoHandler.end()) + { + itr->second->HandleIPPacketFrom(std::move(from), std::move(pkt)); + } + else + m_BaseHandler(std::move(from), std::move(pkt)); + } + + namespace + { + constexpr byte_t udp_proto = 0x11; + } + + void + EgresPacketRouter::AddUDPHandler(huint16_t localport, EgresPacketHandlerFunc func) + { + if (m_IPProtoHandler.find(udp_proto) == m_IPProtoHandler.end()) + { + m_IPProtoHandler.emplace(udp_proto, std::make_unique(m_BaseHandler)); + } + m_IPProtoHandler[udp_proto]->AddSubHandler(ToNet(localport), func); + } + + void + EgresPacketRouter::AddIProtoHandler(uint8_t proto, EgresPacketHandlerFunc func) + { + m_IPProtoHandler[proto] = std::make_unique(std::move(func)); + } + + void + EgresPacketRouter::RemoveUDPHandler(huint16_t localport) + { + if (auto itr = m_IPProtoHandler.find(udp_proto); itr != m_IPProtoHandler.end()) + { + itr->second->RemoveSubHandler(ToNet(localport)); + } + } + +} // namespace llarp::vpn diff --git a/llarp/vpn/egres_packet_router.hpp b/llarp/vpn/egres_packet_router.hpp new file mode 100644 index 000000000..8b074267d --- /dev/null +++ b/llarp/vpn/egres_packet_router.hpp @@ -0,0 +1,49 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace llarp::vpn +{ + using AddressVariant_t = llarp::EndpointBase::AddressVariant_t; + using EgresPacketHandlerFunc = std::function; + + struct EgresLayer4Handler + { + virtual ~EgresLayer4Handler() = default; + + virtual void + HandleIPPacketFrom(AddressVariant_t from, net::IPPacket pkt) = 0; + + virtual void AddSubHandler(nuint16_t, EgresPacketHandlerFunc){}; + virtual void RemoveSubHandler(nuint16_t){}; + }; + + class EgresPacketRouter + { + EgresPacketHandlerFunc m_BaseHandler; + std::unordered_map> m_IPProtoHandler; + + public: + /// baseHandler will be called if no other handlers matches a packet + explicit EgresPacketRouter(EgresPacketHandlerFunc baseHandler); + + /// feed in an ip packet for handling + void + HandleIPPacketFrom(AddressVariant_t, net::IPPacket pkt); + + /// add a non udp packet handler using ip protocol proto + void + AddIProtoHandler(uint8_t proto, EgresPacketHandlerFunc func); + + /// helper that adds a udp packet handler for UDP destinted for localport + void + AddUDPHandler(huint16_t localport, EgresPacketHandlerFunc func); + + /// remove a udp handler that is already set up by bound port + void + RemoveUDPHandler(huint16_t localport); + }; +} // namespace llarp::vpn diff --git a/llarp/vpn/packet_router.hpp b/llarp/vpn/packet_router.hpp index e84454eae..ee0721a05 100644 --- a/llarp/vpn/packet_router.hpp +++ b/llarp/vpn/packet_router.hpp @@ -17,7 +17,6 @@ namespace llarp::vpn virtual void AddSubHandler(nuint16_t, PacketHandlerFunc){}; }; - class PacketRouter { PacketHandlerFunc m_BaseHandler; @@ -38,5 +37,9 @@ namespace llarp::vpn /// helper that adds a udp packet handler for UDP destinted for localport void AddUDPHandler(huint16_t localport, PacketHandlerFunc func); + + /// remove a udp handler that is already set up by bound port + void + RemoveUDPHandler(huint16_t localport); }; } // namespace llarp::vpn From b225ec1043f2926edfac5e9d2600a3509035a5c3 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Thu, 23 Sep 2021 14:31:47 -0400 Subject: [PATCH 062/202] thread safety stuff --- llarp/lokinet_shared.cpp | 65 ++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index 982b1eef9..f97b04dad 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -229,7 +229,7 @@ struct lokinet_context /// make a udp handler and hold onto it /// return its id - [[nodiscard]] int + [[nodiscard]] std::optional make_udp_handler( const std::shared_ptr& ep, llarp::huint16_t exposePort, @@ -250,21 +250,38 @@ struct lokinet_context }); } - auto udp = std::make_unique( + auto udp = std::make_shared( next_socket_id(), llarp::ToNet(exposePort), filter, recv, timeout, user, std::weak_ptr{ep}); auto id = udp->m_SocketID; - auto pkt = ep->EgresPacketRouter(); - pkt->AddUDPHandler(exposePort, [udp = udp.get(), this](auto from, auto pkt) { - udp->HandlePacketFrom(std::move(from), std::move(pkt)); + std::promise result; + + impl->router->loop()->call([ep, &result, udp]() { + if (auto pkt = ep->GetEgresPacketRouter()) + { + pkt->AddUDPHandler(exposePort, [udp = std::weak_ptr{udp}](auto from, auto pkt) { + if (auto ptr = udp.lock()) + { + ptr->HandlePacketFrom(std::move(from), std::move(pkt)); + } + }); + result.set_value(true); + } + else + result.set_value(false); }); - udp_sockets[udp->m_SocketID] = std::move(udp); - return id; + + if (result.get_future().get()) + { + udp_sockets[udp->m_SocketID] = std::move(udp); + return id; + } + return std::nullopt; } void remove_udp_handler(int socket_id) { - std::unique_ptr udp; + std::shared_ptr udp; { std::unique_lock lock{m_access}; if (auto itr = udp_sockets.find(socket_id); itr != udp_sockets.end()) @@ -274,7 +291,14 @@ struct lokinet_context } } if (udp) + { udp->KillAllFlows(); + // remove packet handler + impl->router->loop()->call([ep = udp->m_Endpoint.lock(), locaport = udp->m_LocalPort]() { + if (auto pkt = ep->EgresPacketRouter()) + pkt->RemoveUDPHandler(localport); + }); + } } /// acquire mutex for accessing this context @@ -291,7 +315,7 @@ struct lokinet_context } std::unordered_map streams; - std::unordered_map> udp_sockets; + std::unordered_map> udp_sockets; void inbound_stream(int id) @@ -868,19 +892,23 @@ extern "C" auto lock = ctx->acquire(); if (auto ep = ctx->endpoint()) { - result->socket_id = - ctx->make_udp_handler(ep, llarp::huint16_t{exposedPort}, filter, recv, timeout, user); - return 0; + if (auto maybe = + ctx->make_udp_handler(ep, llarp::huint16_t{exposedPort}, filter, recv, timeout, user)) + { + result->socket_id = *maybe; + return 0; + } } - else - return EINVAL; + return EINVAL; } void EXPORT lokinet_udp_close(int socket_id, struct lokinet_context* ctx) { if (ctx) + { ctx->remove_udp_handler(socket_id); + } } int EXPORT @@ -918,7 +946,7 @@ extern "C" if (pkt.sz == 0) return EINVAL; std::promise ret; - ctx->impl->router->loop()->call_soon([addr = *maybe, pkt = std::move(pkt), ep, &ret]() { + ctx->impl->router->loop()->call([addr = *maybe, pkt = std::move(pkt), ep, &ret]() { if (auto tag = ep->GetBestConvoTagFor(addr)) { if (ep->SendToOrQueue(*tag, pkt.ConstBuffer(), llarp::service::ProtocolType::TrafficV4)) @@ -946,6 +974,11 @@ extern "C" std::shared_ptr ep; { auto lock = ctx->acquire(); + if (ctx->impl->router->loop()->inEventLoop()) + { + LogError("cannot call udp_establish from internal event loop"); + return EINVAL; + } if (auto itr = ctx->udp_sockets.find(remote->socket_id); itr != ctx->udp_sockets.end()) { ep = itr->second->m_Endpoint.lock(); @@ -969,7 +1002,7 @@ extern "C" } } std::promise gotten; - ctx->impl->router->loop()->call_soon([addr = *maybe, ep, &gotten]() { + ctx->impl->router->loop()->call([addr = *maybe, ep, &gotten]() { ep->EnsurePathTo( addr, [&gotten](auto result) { gotten.set_value(result.has_value()); }, 5s); }); From f5157c31da59d012da6b0357e930752ba8856fa7 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Fri, 24 Sep 2021 09:35:33 -0400 Subject: [PATCH 063/202] make it compile --- llarp/lokinet_shared.cpp | 13 +++++++------ llarp/net/ip_packet.cpp | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index f97b04dad..592b7e967 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -255,8 +255,8 @@ struct lokinet_context auto id = udp->m_SocketID; std::promise result; - impl->router->loop()->call([ep, &result, udp]() { - if (auto pkt = ep->GetEgresPacketRouter()) + impl->router->loop()->call([ep, &result, udp, exposePort]() { + if (auto pkt = ep->EgresPacketRouter()) { pkt->AddUDPHandler(exposePort, [udp = std::weak_ptr{udp}](auto from, auto pkt) { if (auto ptr = udp.lock()) @@ -294,10 +294,11 @@ struct lokinet_context { udp->KillAllFlows(); // remove packet handler - impl->router->loop()->call([ep = udp->m_Endpoint.lock(), locaport = udp->m_LocalPort]() { - if (auto pkt = ep->EgresPacketRouter()) - pkt->RemoveUDPHandler(localport); - }); + impl->router->loop()->call( + [ep = udp->m_Endpoint.lock(), localport = llarp::ToHost(udp->m_LocalPort)]() { + if (auto pkt = ep->EgresPacketRouter()) + pkt->RemoveUDPHandler(localport); + }); } } diff --git a/llarp/net/ip_packet.cpp b/llarp/net/ip_packet.cpp index b1deaaeea..deef93102 100644 --- a/llarp/net/ip_packet.cpp +++ b/llarp/net/ip_packet.cpp @@ -129,7 +129,7 @@ namespace llarp } std::optional - IPPacket::SrctPort() const + IPPacket::SrcPort() const { switch (IPProtocol{Header()->protocol}) { From f38bf2770dc9631f3b07e4b2dcd852d0bb001f8e Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Fri, 24 Sep 2021 10:10:08 -0400 Subject: [PATCH 064/202] move WITH_BOOTSTRAP option to root project CMakeLists.txt --- CMakeLists.txt | 8 +++++++- daemon/CMakeLists.txt | 14 ++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ff0a9f12d..97407324c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,12 @@ endif() list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") +set(DEFAULT_WITH_BOOTSTRAP ON) +if(APPLE) + set(DEFAULT_WITH_BOOTSTRAP OFF) +endif() + + # Core options option(USE_AVX2 "enable avx2 code" OFF) option(USE_NETNS "enable networking namespace support. Linux only" OFF) @@ -61,7 +67,7 @@ option(WITH_TESTS "build unit tests" OFF) option(WITH_HIVE "build simulation stubs" OFF) option(BUILD_PACKAGE "builds extra components for making an installer (with 'make package')" OFF) option(BUILD_DAEMON "build lokinet daemon and associated utils" ON) -option(WITH_BOOTSTRAP "build lokinet-bootstrap tool" ON) +option(WITH_BOOTSTRAP "build lokinet-bootstrap tool" ${DEFAULT_WITH_BOOTSTRAP}) include(cmake/enable_lto.cmake) diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt index bc7be3a1f..a565bf96f 100644 --- a/daemon/CMakeLists.txt +++ b/daemon/CMakeLists.txt @@ -1,8 +1,3 @@ -set(DEFAULT_WITH_BOOTSTRAP ON) -if(APPLE) - set(DEFAULT_WITH_BOOTSTRAP OFF) -endif() -option(WITH_BOOTSTRAP "build lokinet-bootstrap tool" ${DEFAULT_WITH_BOOTSTRAP}) add_executable(lokinet-vpn lokinet-vpn.cpp) if(APPLE) @@ -11,11 +6,10 @@ if(APPLE) else() add_executable(lokinet lokinet.cpp) enable_lto(lokinet lokinet-vpn) - - if(WITH_BOOTSTRAP) - add_executable(lokinet-bootstrap lokinet-bootstrap.cpp) - enable_lto(lokinet-bootstrap) - endif() +endif() +if(WITH_BOOTSTRAP) + add_executable(lokinet-bootstrap lokinet-bootstrap.cpp) + enable_lto(lokinet-bootstrap) endif() From 50b80564914f45daad44b69ecdf9b36c6d2c9f4e Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Fri, 24 Sep 2021 10:43:53 -0400 Subject: [PATCH 065/202] enable liblokinet on macos --- contrib/mac.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/mac.sh b/contrib/mac.sh index 0ddcbe3ff..0d338e579 100755 --- a/contrib/mac.sh +++ b/contrib/mac.sh @@ -21,7 +21,7 @@ cmake \ -DBUILD_PACKAGE=ON \ -DBUILD_SHARED_LIBS=OFF \ -DBUILD_TESTING=OFF \ - -DBUILD_LIBLOKINET=OFF \ + -DBUILD_LIBLOKINET=ON \ -DWITH_TESTS=OFF \ -DNATIVE_BUILD=OFF \ -DSTATIC_LINK=ON \ From 1feaec1169e8ce38fbc7e9f3a5cf7257f7fb0b26 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Fri, 24 Sep 2021 10:54:43 -0400 Subject: [PATCH 066/202] build liblokinet on macos --- daemon/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt index a565bf96f..c6780820a 100644 --- a/daemon/CMakeLists.txt +++ b/daemon/CMakeLists.txt @@ -110,7 +110,7 @@ if(APPLE) @ONLY) add_custom_target( sign - DEPENDS "${PROJECT_BINARY_DIR}/sign.sh" lokinet lokinet-extension + DEPENDS "${PROJECT_BINARY_DIR}/sign.sh" lokinet lokinet-extension lokinet-shared COMMAND "${PROJECT_BINARY_DIR}/sign.sh" ) else() From 51b1d41b12162a38ced5da8706ce77abc06bc2dc Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Sat, 25 Sep 2021 10:43:51 -0400 Subject: [PATCH 067/202] disable gost in static build --- cmake/StaticBuild.cmake | 1 + 1 file changed, 1 insertion(+) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index d7546fd02..73618ebdb 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -282,6 +282,7 @@ build_external(unbound DEPENDS openssl_external expat_external CONFIGURE_COMMAND ./configure ${cross_host} ${cross_rc} --prefix=${DEPS_DESTDIR} --disable-shared --enable-static --with-libunbound-only --with-pic + --disable-gost --$,enable,disable>-flto --with-ssl=${DEPS_DESTDIR} --with-libexpat=${DEPS_DESTDIR} "CC=${deps_cc}" "CFLAGS=${deps_CFLAGS}" From b31cac4b714113d2d734ac8e6cbb2ea6209e0a0c Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Sat, 25 Sep 2021 11:22:48 -0400 Subject: [PATCH 068/202] nodejs dipshittery --- cmake/StaticBuild.cmake | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 73618ebdb..8344d4315 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -277,12 +277,15 @@ build_external(expat ) add_static_target(expat expat_external libexpat.a) - +if(WIN32) + # fickleness from cross compile and some nodejs dipshittery causes this to be required + set(unbound_extra_opts --disable-gost --disable-ecdsa) +endif() build_external(unbound DEPENDS openssl_external expat_external CONFIGURE_COMMAND ./configure ${cross_host} ${cross_rc} --prefix=${DEPS_DESTDIR} --disable-shared --enable-static --with-libunbound-only --with-pic - --disable-gost + ${unbound_extra_opts} --$,enable,disable>-flto --with-ssl=${DEPS_DESTDIR} --with-libexpat=${DEPS_DESTDIR} "CC=${deps_cc}" "CFLAGS=${deps_CFLAGS}" From 65b29a1b703de95405f83cccb1e3be2ea414be03 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Sat, 25 Sep 2021 12:54:43 -0400 Subject: [PATCH 069/202] add liblokinet custom logger --- include/lokinet/lokinet_misc.h | 6 ++++++ llarp/lokinet_shared.cpp | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/include/lokinet/lokinet_misc.h b/include/lokinet/lokinet_misc.h index fa20911e4..b09b20e2c 100644 --- a/include/lokinet/lokinet_misc.h +++ b/include/lokinet/lokinet_misc.h @@ -21,6 +21,12 @@ extern "C" int EXPORT lokinet_log_level(const char*); + typedef void (*lokinet_logger_func)(const char*, void*); + + /// set a custom logger function + void EXPORT + lokinet_set_logger(lokinet_logger_func func, void* user); + /// @brief take in hex and turn it into base32z /// @return value must be free()'d later char* EXPORT diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index 592b7e967..6e793b915 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -22,6 +22,33 @@ namespace { + struct Logger : public llarp::ILogStream + { + lokinet_logger_func func; + void* user; + + explicit Logger(lokinet_logger_func _func, void* _user) : func{_func}, user{_user} + {} + + void + PreLog(std::stringstream&, llarp::LogLevel, const char*, int, const std::string&) const override + {} + + void + Print(llarp::LogLevel, const char*, const std::string& msg) override + { + func(msg.c_str(), user); + } + + void + PostLog(std::stringstream&) const override{}; + + void + ImmediateFlush() override{}; + + void Tick(llarp_time_t) override{}; + }; + struct Context : public llarp::Context { using llarp::Context::Context; @@ -1028,4 +1055,10 @@ extern "C" } return EINVAL; } + + void EXPORT + lokinet_set_logger(lokinet_logger_func func, void* user) + { + llarp::LogContext::Instance().logStream.reset(new Logger{func, user}); + } } From 8153edbf433b24e124eaa90d6c961aba06a71adc Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Sat, 25 Sep 2021 13:07:52 -0400 Subject: [PATCH 070/202] dont enable apple languages when not building daemon --- CMakeLists.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 97407324c..4c6bc125b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,8 +5,11 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Has to be set before `project()`, and ignored on non-macos: set(CMAKE_OSX_DEPLOYMENT_TARGET 10.12 CACHE STRING "macOS deployment target (Apple clang only)") +option(BUILD_DAEMON "build lokinet daemon and associated utils" ON) + + set(LANGS C CXX) -if(APPLE) +if(APPLE AND BUILD_DAEMON) set(LANGS ${LANGS} OBJC Swift) endif() @@ -66,7 +69,6 @@ option(TRACY_ROOT "include tracy profiler source" OFF) option(WITH_TESTS "build unit tests" OFF) option(WITH_HIVE "build simulation stubs" OFF) option(BUILD_PACKAGE "builds extra components for making an installer (with 'make package')" OFF) -option(BUILD_DAEMON "build lokinet daemon and associated utils" ON) option(WITH_BOOTSTRAP "build lokinet-bootstrap tool" ${DEFAULT_WITH_BOOTSTRAP}) include(cmake/enable_lto.cmake) From 82ffa2f02c6a11b8d81cf05ed858783e0f47c3b1 Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 30 Sep 2021 10:26:27 -0400 Subject: [PATCH 071/202] Update mac.sh disable liblokinet on mac by default in mac.sh --- contrib/mac.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/mac.sh b/contrib/mac.sh index 0d338e579..0ddcbe3ff 100755 --- a/contrib/mac.sh +++ b/contrib/mac.sh @@ -21,7 +21,7 @@ cmake \ -DBUILD_PACKAGE=ON \ -DBUILD_SHARED_LIBS=OFF \ -DBUILD_TESTING=OFF \ - -DBUILD_LIBLOKINET=ON \ + -DBUILD_LIBLOKINET=OFF \ -DWITH_TESTS=OFF \ -DNATIVE_BUILD=OFF \ -DSTATIC_LINK=ON \ From 97966976d09d08a27066c6896ea068435404f071 Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 30 Sep 2021 10:27:24 -0400 Subject: [PATCH 072/202] Update CMakeLists.txt dont depend on lokinet-shared target for sign target as we disabled it by default. --- daemon/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt index c6780820a..a565bf96f 100644 --- a/daemon/CMakeLists.txt +++ b/daemon/CMakeLists.txt @@ -110,7 +110,7 @@ if(APPLE) @ONLY) add_custom_target( sign - DEPENDS "${PROJECT_BINARY_DIR}/sign.sh" lokinet lokinet-extension lokinet-shared + DEPENDS "${PROJECT_BINARY_DIR}/sign.sh" lokinet lokinet-extension COMMAND "${PROJECT_BINARY_DIR}/sign.sh" ) else() From ef19111f885fd490066616b199d122e54aac245d Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Thu, 30 Sep 2021 10:32:38 -0400 Subject: [PATCH 073/202] dont pack struct becuase alignment --- include/lokinet/lokinet_stream.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/include/lokinet/lokinet_stream.h b/include/lokinet/lokinet_stream.h index 77be691f5..545049c9b 100644 --- a/include/lokinet/lokinet_stream.h +++ b/include/lokinet/lokinet_stream.h @@ -8,7 +8,6 @@ extern "C" #endif /// the result of a lokinet stream mapping attempt -#pragma pack(1) struct lokinet_stream_result { /// set to zero on success otherwise the error that happened @@ -23,7 +22,6 @@ extern "C" /// the id of the stream we created int stream_id; }; -#pragma pack() /// connect out to a remote endpoint /// remoteAddr is in the form of "name:port" From 94ce7a9af734912b28e96f25e7cb1db0fe87fda6 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Thu, 30 Sep 2021 10:36:20 -0400 Subject: [PATCH 074/202] make function pointer arguments named --- include/lokinet/lokinet_stream.h | 2 +- include/lokinet/lokinet_udp.h | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/include/lokinet/lokinet_stream.h b/include/lokinet/lokinet_stream.h index 545049c9b..385a67fc8 100644 --- a/include/lokinet/lokinet_stream.h +++ b/include/lokinet/lokinet_stream.h @@ -37,7 +37,7 @@ extern "C" /// return 0 to accept /// return -1 to explicitly reject /// return -2 to silently drop - typedef int (*lokinet_stream_filter)(const char* remote, uint16_t port, void*); + typedef int (*lokinet_stream_filter)(const char* remote, uint16_t port, void* userdata); /// set stream accepter filter /// passes user parameter into stream filter as void * diff --git a/include/lokinet/lokinet_udp.h b/include/lokinet/lokinet_udp.h index 7e5b6ca2b..15b503809 100644 --- a/include/lokinet/lokinet_udp.h +++ b/include/lokinet/lokinet_udp.h @@ -27,25 +27,25 @@ extern "C" /// flow acceptor hook, return 0 success, return nonzero with errno on failure typedef int (*lokinet_udp_flow_filter)( - void* /*user*/, - const struct lokinet_udp_flowinfo* /* remote address */, - void** /* flow-userdata */, - int* /* timeout seconds */); + void* userdata, + const struct lokinet_udp_flowinfo* remote_address, + void** flow_userdata, + int* timeout_seconds); /// callback to make a new outbound flow typedef void(lokinet_udp_create_flow_func)( - void* /*userdata*/, void** /*flow userdata*/, int* /* flowtimeout */); + void* userdata, void** flow_userdata, int* timeout_seconds); /// hook function for handling packets typedef void (*lokinet_udp_flow_recv_func)( - const struct lokinet_udp_flowinfo* /* remote address */, - const char* /* data pointer */, - size_t /* data length */, - void* /* flow-userdata */); + const struct lokinet_udp_flowinfo* remote_address, + const char* pkt_data, + size_t pkt_length, + void* flow_userdata); /// hook function for flow timeout typedef void (*lokinet_udp_flow_timeout_func)( - const struct lokinet_udp_flowinfo* /* remote address */, void* /* flow-userdata */); + const struct lokinet_udp_flowinfo* remote_address, void* flow_userdata); /// inbound listen udp socket /// expose udp port exposePort to the void From e2cd4d66ccdc8dd2ea9d274c449741f6a18b7b64 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Thu, 30 Sep 2021 10:40:54 -0400 Subject: [PATCH 075/202] docstring update --- include/lokinet/lokinet_udp.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/include/lokinet/lokinet_udp.h b/include/lokinet/lokinet_udp.h index 15b503809..9e520dcd3 100644 --- a/include/lokinet/lokinet_udp.h +++ b/include/lokinet/lokinet_udp.h @@ -90,8 +90,9 @@ extern "C" struct lokinet_context* ctx); /// @brief send on an established flow to remote endpoint + /// blocks until we have sent the packet /// - /// @param flowinfo populated after call on success + /// @param flowinfo remote flow to use for sending /// /// @param ptr pointer to data to send /// From b20e7bedf87f154fef8fcef3959f5bc6f0666563 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Thu, 30 Sep 2021 10:43:06 -0400 Subject: [PATCH 076/202] identify --- external/CMakeLists.txt | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 47ea0b8ab..8f7c49d84 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -57,26 +57,26 @@ add_ngtcp2_lib() # cpr configuration. Ideally we'd just do this via add_subdirectory, but cpr's cmake requires # 3.15+, and we target lower than that (and this is fairly simple to build). if(WITH_BOOTSTRAP) -if(NOT BUILD_STATIC_DEPS) - find_package(CURL REQUIRED COMPONENTS HTTP HTTPS SSL) + if(NOT BUILD_STATIC_DEPS) + find_package(CURL REQUIRED COMPONENTS HTTP HTTPS SSL) - # CURL::libcurl wasn't added to FindCURL until cmake 3.12, so add it if necessary - if (CMAKE_VERSION VERSION_LESS 3.12 AND NOT TARGET CURL::libcurl) - add_library(libcurl UNKNOWN IMPORTED GLOBAL) - set_target_properties(libcurl PROPERTIES - IMPORTED_LOCATION ${CURL_LIBRARIES} - INTERFACE_INCLUDE_DIRECTORIES "${CURL_INCLUDE_DIRS}") - add_library(CURL_libcurl INTERFACE) - target_link_libraries(CURL_libcurl INTERFACE libcurl) - add_library(CURL::libcurl ALIAS CURL_libcurl) + # CURL::libcurl wasn't added to FindCURL until cmake 3.12, so add it if necessary + if (CMAKE_VERSION VERSION_LESS 3.12 AND NOT TARGET CURL::libcurl) + add_library(libcurl UNKNOWN IMPORTED GLOBAL) + set_target_properties(libcurl PROPERTIES + IMPORTED_LOCATION ${CURL_LIBRARIES} + INTERFACE_INCLUDE_DIRECTORIES "${CURL_INCLUDE_DIRS}") + add_library(CURL_libcurl INTERFACE) + target_link_libraries(CURL_libcurl INTERFACE libcurl) + add_library(CURL::libcurl ALIAS CURL_libcurl) + endif() endif() -endif() -file(GLOB cpr_sources ${conf_depends} cpr/cpr/*.cpp) + file(GLOB cpr_sources ${conf_depends} cpr/cpr/*.cpp) -add_library(cpr STATIC EXCLUDE_FROM_ALL ${cpr_sources}) -target_link_libraries(cpr PUBLIC CURL::libcurl) -target_include_directories(cpr PUBLIC cpr/include) -target_compile_definitions(cpr PUBLIC CPR_CURL_NOSIGNAL) -add_library(cpr::cpr ALIAS cpr) + add_library(cpr STATIC EXCLUDE_FROM_ALL ${cpr_sources}) + target_link_libraries(cpr PUBLIC CURL::libcurl) + target_include_directories(cpr PUBLIC cpr/include) + target_compile_definitions(cpr PUBLIC CPR_CURL_NOSIGNAL) + add_library(cpr::cpr ALIAS cpr) endif() From 5286d442fb7dd411be25709ad1c0981eaf71d029 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Thu, 30 Sep 2021 13:17:06 -0400 Subject: [PATCH 077/202] updates: * add udptest example * fix up udp codepath in liblokinet --- contrib/liblokinet/CMakeLists.txt | 10 ++ contrib/liblokinet/readme.md | 13 ++ contrib/liblokinet/udptest.cpp | 239 ++++++++++++++++++++++++++++++ llarp/handlers/null.hpp | 8 +- llarp/lokinet_shared.cpp | 15 +- llarp/vpn/egres_packet_router.cpp | 17 ++- 6 files changed, 287 insertions(+), 15 deletions(-) create mode 100644 contrib/liblokinet/CMakeLists.txt create mode 100644 contrib/liblokinet/readme.md create mode 100644 contrib/liblokinet/udptest.cpp diff --git a/contrib/liblokinet/CMakeLists.txt b/contrib/liblokinet/CMakeLists.txt new file mode 100644 index 000000000..6985f741b --- /dev/null +++ b/contrib/liblokinet/CMakeLists.txt @@ -0,0 +1,10 @@ + +cmake_minimum_required(VERSION 3.10) + +project(udptest LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +add_executable(udptest udptest.cpp) +include_directories(../../include) +target_link_libraries(udptest PUBLIC lokinet) + diff --git a/contrib/liblokinet/readme.md b/contrib/liblokinet/readme.md new file mode 100644 index 000000000..e0f63d135 --- /dev/null +++ b/contrib/liblokinet/readme.md @@ -0,0 +1,13 @@ +# liblokinet examples + +building: + + $ mkdir -p build + $ cd build + $ cp /path/to/liblokinet.so . + $ cmake .. -DCMAKE_EXE_LINKER_FLAGS='-L.' + $ make + +running: + + $ ./udptest /path/to/bootstrap.signed diff --git a/contrib/liblokinet/udptest.cpp b/contrib/liblokinet/udptest.cpp new file mode 100644 index 000000000..9db9c399e --- /dev/null +++ b/contrib/liblokinet/udptest.cpp @@ -0,0 +1,239 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +bool _run{true}; + +using Lokinet_ptr = std::shared_ptr; + +[[nodiscard]] auto +MakeLokinet(const std::vector& bootstrap) +{ + auto ctx = std::shared_ptr(lokinet_context_new(), lokinet_context_free); + if (auto err = lokinet_add_bootstrap_rc(bootstrap.data(), bootstrap.size(), ctx.get())) + throw std::runtime_error{strerror(err)}; + if (lokinet_context_start(ctx.get())) + throw std::runtime_error{"could not start context"}; + return ctx; +} + +void +WaitForReady(const Lokinet_ptr& ctx) +{ + while (_run and lokinet_wait_for_ready(1000, ctx.get())) + { + std::cout << "waiting for context..." << std::endl; + } +} + +class Flow +{ + lokinet_udp_flowinfo const _info; + lokinet_context* const _ctx; + + public: + explicit Flow(const lokinet_udp_flowinfo* info, lokinet_context* ctx) : _info{*info}, _ctx{ctx} + {} + + lokinet_context* + Context() const + { + return _ctx; + } + + std::string + String() const + { + std::stringstream ss; + ss << std::string{_info.remote_host} << ":" << std::to_string(_info.remote_port) + << " on socket " << _info.socket_id; + return ss.str(); + } +}; + +struct ConnectJob +{ + lokinet_udp_flowinfo remote; + lokinet_context* ctx; +}; + +void +CreateOutboundFlow(void* user, void** flowdata, int* timeout) +{ + auto* job = static_cast(user); + Flow* flow = new Flow{&job->remote, job->ctx}; + *flowdata = flow; + *timeout = 30; + std::cout << "made outbound flow: " << flow->String() << std::endl; + ; +} + +int +ProcessNewInboundFlow(void* user, const lokinet_udp_flowinfo* remote, void** flowdata, int* timeout) +{ + auto* ctx = static_cast(user); + Flow* flow = new Flow{remote, ctx}; + std::cout << "new udp flow: " << flow->String() << std::endl; + *flowdata = flow; + *timeout = 30; + + return 0; +} + +void +DeleteFlow(const lokinet_udp_flowinfo* remote, void* flowdata) +{ + auto* flow = static_cast(flowdata); + std::cout << "udp flow from " << flow->String() << " timed out" << std::endl; + delete flow; +} + +void +HandleUDPPacket(const lokinet_udp_flowinfo* remote, const char* pkt, size_t len, void* flowdata) +{ + auto* flow = static_cast(flowdata); + std::cout << "we got " << len << " bytes of udp from " << flow->String() << std::endl; +} + +void +BounceUDPPacket(const lokinet_udp_flowinfo* remote, const char* pkt, size_t len, void* flowdata) +{ + auto* flow = static_cast(flowdata); + std::cout << "bounce " << len << " bytes of udp from " << flow->String() << std::endl; + if (auto err = lokinet_udp_flow_send(remote, pkt, len, flow->Context())) + { + std::cout << "bounce failed: " << strerror(err) << std::endl; + } +} + +Lokinet_ptr sender, recip; + +void +signal_handler(int) +{ + _run = false; +} + +int +main(int argc, char* argv[]) +{ + if (argc == 1) + { + std::cout << "usage: " << argv[0] << " bootstrap.signed" << std::endl; + return 1; + } + + /* + signal(SIGINT, signal_handler); + signal(SIGTERM, signal_handler); + */ + + std::vector bootstrap; + + // load bootstrap.signed + { + std::ifstream inf{argv[1], std::ifstream::ate | std::ifstream::binary}; + size_t len = inf.tellg(); + inf.seekg(0); + bootstrap.resize(len); + inf.read(bootstrap.data(), bootstrap.size()); + } + + if (auto* loglevel = getenv("LOKINET_LOG")) + lokinet_log_level(loglevel); + else + lokinet_log_level("none"); + + std::cout << "starting up" << std::endl; + + recip = MakeLokinet(bootstrap); + WaitForReady(recip); + + lokinet_udp_bind_result recipBindResult{}; + + const auto port = 10000; + + if (auto err = lokinet_udp_bind( + port, + ProcessNewInboundFlow, + BounceUDPPacket, + DeleteFlow, + recip.get(), + &recipBindResult, + recip.get())) + { + std::cout << "failed to bind recip udp socket " << strerror(err) << std::endl; + return 0; + } + + std::cout << "bound recip udp" << std::endl; + + sender = MakeLokinet(bootstrap); + WaitForReady(sender); + + std::string recipaddr{lokinet_address(recip.get())}; + + std::cout << "recip ready at " << recipaddr << std::endl; + + lokinet_udp_bind_result senderBindResult{}; + + if (auto err = lokinet_udp_bind( + port, + ProcessNewInboundFlow, + HandleUDPPacket, + DeleteFlow, + sender.get(), + &senderBindResult, + sender.get())) + { + std::cout << "failed to bind sender udp socket " << strerror(err) << std::endl; + return 0; + } + + ConnectJob connect{}; + connect.remote.socket_id = senderBindResult.socket_id; + connect.remote.remote_port = port; + std::copy_n(recipaddr.c_str(), recipaddr.size(), connect.remote.remote_host); + connect.ctx = sender.get(); + + std::cout << "bound sender udp" << std::endl; + + do + { + std::cout << "try establish to " << connect.remote.remote_host << std::endl; + if (auto err = + lokinet_udp_establish(CreateOutboundFlow, &connect, &connect.remote, sender.get())) + { + std::cout << "failed to establish to recip: " << strerror(err) << std::endl; + usleep(100000); + } + else + break; + } while (true); + std::cout << "sender established" << std::endl; + + const std::string buf{"liblokinet"}; + + const std::string senderAddr{lokinet_address(sender.get())}; + + do + { + std::cout << senderAddr << " send to remote: " << buf << std::endl; + if (auto err = lokinet_udp_flow_send(&connect.remote, buf.data(), buf.size(), sender.get())) + { + std::cout << "send failed: " << strerror(err) << std::endl; + } + usleep(100000); + } while (_run); + return 0; +} diff --git a/llarp/handlers/null.hpp b/llarp/handlers/null.hpp index 174afb60d..e0464b041 100644 --- a/llarp/handlers/null.hpp +++ b/llarp/handlers/null.hpp @@ -14,7 +14,13 @@ namespace llarp::handlers { NullEndpoint(AbstractRouter* r, llarp::service::Context* parent) : llarp::service::Endpoint{r, parent} - , m_PacketRouter{new vpn::EgresPacketRouter{[](auto, auto) {}}} + , m_PacketRouter{new vpn::EgresPacketRouter{[](auto from, auto pkt) { + var::visit( + [&pkt](auto&& from) { + LogError("unhandled traffic from: ", from, " of ", pkt.sz, " bytes"); + }, + from); + }}} { r->loop()->add_ticker([this] { Pump(Now()); }); } diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index 6e793b915..a1f73b3a9 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -157,6 +157,7 @@ namespace flow.m_FlowInfo = flow_addr; flow.m_FlowTimeout = std::chrono::seconds{flow_timeoutseconds}; flow.m_FlowUserData = flow_userdata; + flow.m_Recv = m_Recv; } void @@ -285,11 +286,8 @@ struct lokinet_context impl->router->loop()->call([ep, &result, udp, exposePort]() { if (auto pkt = ep->EgresPacketRouter()) { - pkt->AddUDPHandler(exposePort, [udp = std::weak_ptr{udp}](auto from, auto pkt) { - if (auto ptr = udp.lock()) - { - ptr->HandlePacketFrom(std::move(from), std::move(pkt)); - } + pkt->AddUDPHandler(exposePort, [udp](auto from, auto pkt) { + udp->HandlePacketFrom(std::move(from), std::move(pkt)); }); result.set_value(true); } @@ -1031,8 +1029,13 @@ extern "C" } std::promise gotten; ctx->impl->router->loop()->call([addr = *maybe, ep, &gotten]() { - ep->EnsurePathTo( + ep->MarkAddressOutbound(addr); + auto res = ep->EnsurePathTo( addr, [&gotten](auto result) { gotten.set_value(result.has_value()); }, 5s); + if (not res) + { + gotten.set_value(false); + } }); if (gotten.get_future().get()) { diff --git a/llarp/vpn/egres_packet_router.cpp b/llarp/vpn/egres_packet_router.cpp index f892f0832..655aaeb49 100644 --- a/llarp/vpn/egres_packet_router.cpp +++ b/llarp/vpn/egres_packet_router.cpp @@ -14,7 +14,7 @@ namespace llarp::vpn void AddSubHandler(nuint16_t localport, EgresPacketHandlerFunc handler) override { - m_LocalPorts.emplace(localport, std::move(handler)); + m_LocalPorts.emplace(std::move(localport), std::move(handler)); } void @@ -26,14 +26,15 @@ namespace llarp::vpn void HandleIPPacketFrom(AddressVariant_t from, net::IPPacket pkt) override { - const uint8_t* ptr = pkt.buf + (pkt.Header()->ihl * 4) + 2; - const nuint16_t dstPort{*reinterpret_cast(ptr)}; - if (auto itr = m_LocalPorts.find(dstPort); itr != m_LocalPorts.end()) + if (auto dstPort = pkt.DstPort()) { - itr->second(std::move(from), std::move(pkt)); + if (auto itr = m_LocalPorts.find(*dstPort); itr != m_LocalPorts.end()) + { + itr->second(std::move(from), std::move(pkt)); + return; + } } - else - m_BaseHandler(std::move(from), std::move(pkt)); + m_BaseHandler(std::move(from), std::move(pkt)); } }; @@ -80,7 +81,7 @@ namespace llarp::vpn { m_IPProtoHandler.emplace(udp_proto, std::make_unique(m_BaseHandler)); } - m_IPProtoHandler[udp_proto]->AddSubHandler(ToNet(localport), func); + m_IPProtoHandler[udp_proto]->AddSubHandler(ToNet(localport), std::move(func)); } void From bf6dfaaef837b353fc65a6d7b13b5d369e43f3d5 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Mon, 4 Oct 2021 10:32:26 -0400 Subject: [PATCH 078/202] cmake fixups --- cmake/StaticBuild.cmake | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 8344d4315..b266d68ee 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -277,15 +277,10 @@ build_external(expat ) add_static_target(expat expat_external libexpat.a) -if(WIN32) - # fickleness from cross compile and some nodejs dipshittery causes this to be required - set(unbound_extra_opts --disable-gost --disable-ecdsa) -endif() build_external(unbound DEPENDS openssl_external expat_external CONFIGURE_COMMAND ./configure ${cross_host} ${cross_rc} --prefix=${DEPS_DESTDIR} --disable-shared --enable-static --with-libunbound-only --with-pic - ${unbound_extra_opts} --$,enable,disable>-flto --with-ssl=${DEPS_DESTDIR} --with-libexpat=${DEPS_DESTDIR} "CC=${deps_cc}" "CFLAGS=${deps_CFLAGS}" @@ -339,7 +334,10 @@ set_target_properties(libzmq PROPERTIES INTERFACE_LINK_LIBRARIES "${libzmq_link_libs}" INTERFACE_COMPILE_DEFINITIONS "ZMQ_STATIC") -if(WITH_BOOTSTRAP) +if(NOT WITH_BOOTSTRAP) + return() +endif() + set(curl_extra) if(WIN32) set(curl_ssl_opts --without-ssl --with-schannel) @@ -428,4 +426,3 @@ endif() set_target_properties(CURL::libcurl PROPERTIES INTERFACE_LINK_LIBRARIES "${libcurl_link_libs}" INTERFACE_COMPILE_DEFINITIONS "CURL_STATICLIB") -endif() From f8768488ed8dba10c2aac48d4fed6325b4f5efac Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Mon, 4 Oct 2021 10:38:38 -0400 Subject: [PATCH 079/202] make pybind compile --- pybind/llarp/config.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pybind/llarp/config.cpp b/pybind/llarp/config.cpp index c0d15cdfe..42a46f2ac 100644 --- a/pybind/llarp/config.cpp +++ b/pybind/llarp/config.cpp @@ -75,18 +75,18 @@ namespace llarp .def(py::init<>()) .def( "setOutboundLink", - [](LinksConfig& self, std::string interface, int family, uint16_t port) { + [](LinksConfig& self, std::string _interface, int family, uint16_t port) { LinksConfig::LinkInfo info; - info.m_interface = std::move(interface); + info.m_interface = std::move(_interface); info.addressFamily = family; info.port = port; self.m_OutboundLink = std::move(info); }) .def( "addInboundLink", - [](LinksConfig& self, std::string interface, int family, uint16_t port) { + [](LinksConfig& self, std::string _interface, int family, uint16_t port) { LinksConfig::LinkInfo info; - info.m_interface = std::move(interface); + info.minterface = std::move(_interface); info.addressFamily = family; info.port = port; self.m_InboundLinks.push_back(info); From c655a21d68874ae0163ef86d9b308870d692d1f5 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Mon, 4 Oct 2021 10:53:54 -0400 Subject: [PATCH 080/202] typofix --- pybind/llarp/config.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pybind/llarp/config.cpp b/pybind/llarp/config.cpp index 42a46f2ac..0675f98e5 100644 --- a/pybind/llarp/config.cpp +++ b/pybind/llarp/config.cpp @@ -86,7 +86,7 @@ namespace llarp "addInboundLink", [](LinksConfig& self, std::string _interface, int family, uint16_t port) { LinksConfig::LinkInfo info; - info.minterface = std::move(_interface); + info.m_interface = std::move(_interface); info.addressFamily = family; info.port = port; self.m_InboundLinks.push_back(info); From 635f4bcd8c082b5e86ad59b3540df9fe07390094 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Wed, 13 Oct 2021 07:37:25 -0400 Subject: [PATCH 081/202] make it compile --- llarp/lokinet_shared.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index a1f73b3a9..e85df7389 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -31,11 +31,12 @@ namespace {} void - PreLog(std::stringstream&, llarp::LogLevel, const char*, int, const std::string&) const override + PreLog(std::stringstream&, llarp::LogLevel, std::string_view, int, const std::string&) + const override {} void - Print(llarp::LogLevel, const char*, const std::string& msg) override + Print(llarp::LogLevel, std::string_view, const std::string& msg) override { func(msg.c_str(), user); } @@ -1002,7 +1003,7 @@ extern "C" auto lock = ctx->acquire(); if (ctx->impl->router->loop()->inEventLoop()) { - LogError("cannot call udp_establish from internal event loop"); + llarp::LogError("cannot call udp_establish from internal event loop"); return EINVAL; } if (auto itr = ctx->udp_sockets.find(remote->socket_id); itr != ctx->udp_sockets.end()) From 743bc2433ae21be699d152d79846b18f8836fcce Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Wed, 13 Oct 2021 08:54:19 -0400 Subject: [PATCH 082/202] resolve race condition in udp flow and packet handling --- llarp/lokinet_shared.cpp | 60 +++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index e85df7389..a900b801e 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -151,7 +151,8 @@ namespace const AddressVariant_t& from, const lokinet_udp_flowinfo& flow_addr, void* flow_userdata, - int flow_timeoutseconds) + int flow_timeoutseconds, + std::optional firstPacket = std::nullopt) { std::unique_lock lock{m_Access}; auto& flow = m_Flows[from]; @@ -159,6 +160,8 @@ namespace flow.m_FlowTimeout = std::chrono::seconds{flow_timeoutseconds}; flow.m_FlowUserData = flow_userdata; flow.m_Recv = m_Recv; + if (firstPacket) + flow.HandlePacket(*firstPacket); } void @@ -180,44 +183,37 @@ namespace void HandlePacketFrom(AddressVariant_t from, llarp::net::IPPacket pkt) { - bool isNewFlow{false}; { std::unique_lock lock{m_Access}; - isNewFlow = m_Flows.count(from) == 0; - } - if (isNewFlow) - { - lokinet_udp_flowinfo flow_addr{}; - // set flow remote address - var::visit( - [&flow_addr](auto&& from) { - const auto addr = from.ToString(); - std::copy_n( - addr.data(), - std::min(addr.size(), sizeof(flow_addr.remote_host)), - flow_addr.remote_host); - }, - from); - // set socket id - flow_addr.socket_id = m_SocketID; - // get source port - if (auto srcport = pkt.SrcPort()) + if (m_Flows.count(from)) { - flow_addr.remote_port = ToHost(*srcport).h; - } - else - return; // invalid data so we bail - void* flow_userdata = nullptr; - int flow_timeoutseconds{}; - // got a new flow, let's check if we want it - if (m_Filter(m_User, &flow_addr, &flow_userdata, &flow_timeoutseconds)) + m_Flows[from].HandlePacket(pkt); return; - AddFlow(from, flow_addr, flow_userdata, flow_timeoutseconds); + } } + lokinet_udp_flowinfo flow_addr{}; + // set flow remote address + std::string addrstr = var::visit([&flow_addr](auto&& from) { return from.ToString(); }, from); + + std::copy_n( + addrstr.data(), + std::min(addrstr.size(), sizeof(flow_addr.remote_host)), + flow_addr.remote_host); + // set socket id + flow_addr.socket_id = m_SocketID; + // get source port + if (const auto srcport = pkt.SrcPort()) { - std::unique_lock lock{m_Access}; - m_Flows[from].HandlePacket(pkt); + flow_addr.remote_port = ToHost(*srcport).h; } + else + return; // invalid data so we bail + void* flow_userdata = nullptr; + int flow_timeoutseconds{}; + // got a new flow, let's check if we want it + if (m_Filter(m_User, &flow_addr, &flow_userdata, &flow_timeoutseconds)) + return; + AddFlow(from, flow_addr, flow_userdata, flow_timeoutseconds, pkt); } }; } // namespace From 04b23416ed1a2aadb0789eeb019b4f6cf372d5d3 Mon Sep 17 00:00:00 2001 From: Jeff Becker Date: Wed, 13 Oct 2021 08:54:45 -0400 Subject: [PATCH 083/202] do less allocations in lokinet_hex_tobase32z --- llarp/lokinet_shared.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index a900b801e..ac4652729 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -822,8 +822,22 @@ extern "C" char* EXPORT lokinet_hex_to_base32z(const char* hex) { - const auto base32z = oxenmq::to_base32z(oxenmq::from_hex(std::string{hex})); - return strdup(base32z.c_str()); + std::string_view hexview{hex}; + if (not oxenmq::is_hex(hexview)) + return nullptr; + + const size_t byte_len = hexview.size() / 2; + const size_t b32z_len = (byte_len * 8 + 4) / 5; // = ⌈N×8÷5⌉ because 5 bits per 32z char + auto buf = std::make_unique(b32z_len + 1); + char* end = buf.get() + b32z_len; + *end = 0; // null terminate + // Write the bytes into the *end* of the buffer so that when we rewrite the final b32z chars + // into the buffer we won't overwrite any byte values until after we've consumed them. + char* bytepos = end - byte_len; + oxenmq::from_hex(hexview.begin(), hexview.end(), bytepos); + // In-place conversion into the buffer + oxenmq::to_base32z(bytepos, end, buf.get()); + return buf.release(); // leak the buffer to the caller } void EXPORT From 199055a6aa8f26eee6cf5e797b4f2e1a3e0916ab Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 31 Jan 2022 11:51:26 -0500 Subject: [PATCH 084/202] chore: bump static deps versions --- cmake/StaticBuild.cmake | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 87ba00a81..439ad6259 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -5,32 +5,32 @@ set(LOCAL_MIRROR "" CACHE STRING "local mirror path/URL for lib downloads") -set(OPENSSL_VERSION 1.1.1l CACHE STRING "openssl version") +set(OPENSSL_VERSION 1.1.1m CACHE STRING "openssl version") set(OPENSSL_MIRROR ${LOCAL_MIRROR} https://www.openssl.org/source CACHE STRING "openssl download mirror(s)") set(OPENSSL_SOURCE openssl-${OPENSSL_VERSION}.tar.gz) -set(OPENSSL_HASH SHA256=0b7a3e5e59c34827fe0c3a74b7ec8baef302b98fa80088d7f9153aa16fa76bd1 +set(OPENSSL_HASH SHA256=f89199be8b23ca45fc7cb9f1d8d3ee67312318286ad030f5316aca6462db6c96 CACHE STRING "openssl source hash") -set(EXPAT_VERSION 2.3.0 CACHE STRING "expat version") +set(EXPAT_VERSION 2.4.4 CACHE STRING "expat version") string(REPLACE "." "_" EXPAT_TAG "R_${EXPAT_VERSION}") set(EXPAT_MIRROR ${LOCAL_MIRROR} https://github.com/libexpat/libexpat/releases/download/${EXPAT_TAG} CACHE STRING "expat download mirror(s)") set(EXPAT_SOURCE expat-${EXPAT_VERSION}.tar.xz) -set(EXPAT_HASH SHA512=dde8a9a094b18d795a0e86ca4aa68488b352dc67019e0d669e8b910ed149628de4c2a49bc3a5b832f624319336a01f9e4debe03433a43e1c420f36356d886820 +set(EXPAT_HASH SHA256=b5d25d6e373351c2ed19b562b4732d01d2589ac8c8e9e7962d8df1207cc311b8 CACHE STRING "expat source hash") -set(UNBOUND_VERSION 1.13.2 CACHE STRING "unbound version") +set(UNBOUND_VERSION 1.14.0 CACHE STRING "unbound version") set(UNBOUND_MIRROR ${LOCAL_MIRROR} https://nlnetlabs.nl/downloads/unbound CACHE STRING "unbound download mirror(s)") set(UNBOUND_SOURCE unbound-${UNBOUND_VERSION}.tar.gz) -set(UNBOUND_HASH SHA256=0a13b547f3b92a026b5ebd0423f54c991e5718037fd9f72445817f6a040e1a83 +set(UNBOUND_HASH SHA256=6ef91cbf02d5299eab39328c0857393de7b4885a2fe7233ddfe3c124ff5a89c8 CACHE STRING "unbound source hash") -set(SQLITE3_VERSION 3350500 CACHE STRING "sqlite3 version") -set(SQLITE3_MIRROR ${LOCAL_MIRROR} https://www.sqlite.org/2021 +set(SQLITE3_VERSION 3370200 CACHE STRING "sqlite3 version") +set(SQLITE3_MIRROR ${LOCAL_MIRROR} https://www.sqlite.org/2022 CACHE STRING "sqlite3 download mirror(s)") set(SQLITE3_SOURCE sqlite-autoconf-${SQLITE3_VERSION}.tar.gz) -set(SQLITE3_HASH SHA512=039af796f79fc4517be0bd5ba37886264d49da309e234ae6fccdb488ef0109ed2b917fc3e6c1fc7224dff4f736824c653aaf8f0a37550c5ebc14d035cb8ac737 - CACHE STRING "sqlite3 source hash") +set(SQLITE3_HASH SHA3_256=3764f471d188ef4e7a70a120f6cb80014dc50bb5fa53406b566508390a32e745 + CACHE STRING "sqlite3 source hash") set(SODIUM_VERSION 1.0.18 CACHE STRING "libsodium version") set(SODIUM_MIRROR ${LOCAL_MIRROR} @@ -48,11 +48,11 @@ set(ZMQ_SOURCE zeromq-${ZMQ_VERSION}.tar.gz) set(ZMQ_HASH SHA512=e198ef9f82d392754caadd547537666d4fba0afd7d027749b3adae450516bcf284d241d4616cad3cb4ad9af8c10373d456de92dc6d115b037941659f141e7c0e CACHE STRING "libzmq source hash") -set(LIBUV_VERSION 1.41.0 CACHE STRING "libuv version") +set(LIBUV_VERSION 1.43.0 CACHE STRING "libuv version") set(LIBUV_MIRROR ${LOCAL_MIRROR} https://dist.libuv.org/dist/v${LIBUV_VERSION} CACHE STRING "libuv mirror(s)") set(LIBUV_SOURCE libuv-v${LIBUV_VERSION}.tar.gz) -set(LIBUV_HASH SHA512=33613fa28e8136507300eba374351774849b6b39aab4e53c997a918d3bc1d1094c6123e0e509535095b14dc5daa885eadb1a67bed46622ad3cc79d62dc817e84 +set(LIBUV_HASH SHA256=90d72bb7ae18de2519d0cac70eb89c319351146b90cd3f91303a492707e693a4 CACHE STRING "libuv source hash") set(ZLIB_VERSION 1.2.11 CACHE STRING "zlib version") @@ -62,15 +62,13 @@ set(ZLIB_SOURCE zlib-${ZLIB_VERSION}.tar.gz) set(ZLIB_HASH SHA512=73fd3fff4adeccd4894084c15ddac89890cd10ef105dd5e1835e1e9bbb6a49ff229713bd197d203edfa17c2727700fce65a2a235f07568212d820dca88b528ae CACHE STRING "zlib source hash") -set(CURL_VERSION 7.76.1 CACHE STRING "curl version") +set(CURL_VERSION 7.81.0 CACHE STRING "curl version") set(CURL_MIRROR ${LOCAL_MIRROR} https://curl.haxx.se/download https://curl.askapache.com CACHE STRING "curl mirror(s)") set(CURL_SOURCE curl-${CURL_VERSION}.tar.xz) -set(CURL_HASH SHA256=64bb5288c39f0840c07d077e30d9052e1cbb9fa6c2dc52523824cc859e679145 +set(CURL_HASH SHA256=a067b688d1645183febc31309ec1f3cdce9213d02136b6a6de3d50f69c95a7d3 CACHE STRING "curl source hash") - - include(ExternalProject) set(DEPS_DESTDIR ${CMAKE_BINARY_DIR}/static-deps) From 3c983e2cd79436b11c99dfb0ed3285258026c518 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 31 Jan 2022 12:03:15 -0500 Subject: [PATCH 085/202] libcurl no long users --without-libmetalink so remove the flag --- cmake/StaticBuild.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 439ad6259..9dfd958ce 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -384,7 +384,7 @@ foreach(curl_arch ${curl_arches}) --enable-crypto-auth --disable-ntlm-wb --disable-tls-srp --disable-unix-sockets --disable-cookies --enable-http-auth --enable-doh --disable-mime --enable-dateparse --disable-netrc --without-libidn2 --disable-progress-meter --without-brotli --with-zlib=${DEPS_DESTDIR} ${curl_ssl_opts} - --without-libmetalink --without-librtmp --disable-versioned-symbols --enable-hidden-symbols + --without-librtmp --disable-versioned-symbols --enable-hidden-symbols --without-zsh-functions-dir --without-fish-functions-dir --without-nghttp3 --without-zstd "CC=${deps_cc}" "CFLAGS=${deps_noarch_CFLAGS}${cflags_extra}" ${curl_extra} From 95efe8f4e589aa7be5058124c1cfdad127f57294 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 31 Jan 2022 12:20:27 -0500 Subject: [PATCH 086/202] bump ngtcp2 to v0.1.0 tag --- .gitmodules | 1 + cmake/ngtcp2_lib.cmake | 5 +++-- external/ngtcp2 | 2 +- llarp/CMakeLists.txt | 2 +- llarp/quic/address.hpp | 4 ++-- llarp/quic/endpoint.cpp | 2 ++ 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.gitmodules b/.gitmodules index 2477762b8..9b49fa876 100644 --- a/.gitmodules +++ b/.gitmodules @@ -35,3 +35,4 @@ [submodule "external/ngtcp2"] path = external/ngtcp2 url = https://github.com/ngtcp2/ngtcp2.git + branch = v0.1.0 \ No newline at end of file diff --git a/cmake/ngtcp2_lib.cmake b/cmake/ngtcp2_lib.cmake index c6d06a42a..a14160b15 100644 --- a/cmake/ngtcp2_lib.cmake +++ b/cmake/ngtcp2_lib.cmake @@ -45,8 +45,9 @@ function(add_ngtcp2_lib) configure_file(ngtcp2/cmakeconfig.h.in ngtcp2/config.h) include_directories("${CMAKE_CURRENT_BINARY_DIR}/ngtcp2") # for config.h - + set(ENABLE_STATIC_LIB ON FORCE BOOL) + set(ENABLE_SHARED_LIB OFF FORCE BOOL) add_subdirectory(ngtcp2/lib EXCLUDE_FROM_ALL) - target_compile_definitions(ngtcp2 PRIVATE -DHAVE_CONFIG_H -D_GNU_SOURCE) + target_compile_definitions(ngtcp2_static PRIVATE -DHAVE_CONFIG_H -D_GNU_SOURCE) endfunction() diff --git a/external/ngtcp2 b/external/ngtcp2 index 15ba6021c..026b8434e 160000 --- a/external/ngtcp2 +++ b/external/ngtcp2 @@ -1 +1 @@ -Subproject commit 15ba6021ca352e2e60f9b43f4b96d2e97a42f60b +Subproject commit 026b8434ebcbeec48939d1c7671a0a4d5c75202b diff --git a/llarp/CMakeLists.txt b/llarp/CMakeLists.txt index 05a60bbc3..bab1c90ca 100644 --- a/llarp/CMakeLists.txt +++ b/llarp/CMakeLists.txt @@ -241,7 +241,7 @@ if(WITH_HIVE) ) endif() -target_link_libraries(liblokinet PUBLIC cxxopts lokinet-platform lokinet-util lokinet-cryptography sqlite_orm ngtcp2) +target_link_libraries(liblokinet PUBLIC cxxopts lokinet-platform lokinet-util lokinet-cryptography sqlite_orm ngtcp2_static) target_link_libraries(liblokinet PRIVATE libunbound) diff --git a/llarp/quic/address.hpp b/llarp/quic/address.hpp index 5f7bc3522..a6b56a243 100644 --- a/llarp/quic/address.hpp +++ b/llarp/quic/address.hpp @@ -21,7 +21,7 @@ namespace llarp::quic class Address { sockaddr_in6 saddr{}; - ngtcp2_addr a{sizeof(saddr), reinterpret_cast(&saddr)}; + ngtcp2_addr a{reinterpret_cast(&saddr), sizeof(saddr)}; public: Address() = default; @@ -102,7 +102,7 @@ namespace llarp::quic Address local_, remote_; public: - ngtcp2_path path{{local_.sockaddr_size(), local_}, {remote_.sockaddr_size(), remote_}, nullptr}; + ngtcp2_path path{{local_, local_.sockaddr_size()}, {remote_, remote_.sockaddr_size()}, nullptr}; // Public accessors are const: const Address& local = local_; diff --git a/llarp/quic/endpoint.cpp b/llarp/quic/endpoint.cpp index 5d57e9010..6fa7be772 100644 --- a/llarp/quic/endpoint.cpp +++ b/llarp/quic/endpoint.cpp @@ -245,6 +245,8 @@ namespace llarp::quic u8data(conn.conn_buffer), conn.conn_buffer.size(), code, + nullptr, // reason + 0, // reason length get_timestamp()); if (written <= 0) { From 582fdeda279f5185dff561ff6f65a4f54d0c0d53 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 31 Jan 2022 14:36:53 -0500 Subject: [PATCH 087/202] add close_reason parameter to quic::Endpoint::close_connection defaulting to emtpy string --- llarp/quic/endpoint.cpp | 7 ++++--- llarp/quic/endpoint.hpp | 12 +++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/llarp/quic/endpoint.cpp b/llarp/quic/endpoint.cpp index 6fa7be772..d53ab4880 100644 --- a/llarp/quic/endpoint.cpp +++ b/llarp/quic/endpoint.cpp @@ -225,7 +225,8 @@ namespace llarp::quic } void - Endpoint::close_connection(Connection& conn, uint64_t code, bool application) + Endpoint::close_connection( + Connection& conn, uint64_t code, bool application, std::string_view close_reason) { LogDebug("Closing connection ", conn.base_cid); if (!conn.closing) @@ -245,8 +246,8 @@ namespace llarp::quic u8data(conn.conn_buffer), conn.conn_buffer.size(), code, - nullptr, // reason - 0, // reason length + reinterpret_cast(close_reason.data()), + close_reason.size(), get_timestamp()); if (written <= 0) { diff --git a/llarp/quic/endpoint.hpp b/llarp/quic/endpoint.hpp index cfbf92c54..f49460593 100644 --- a/llarp/quic/endpoint.hpp +++ b/llarp/quic/endpoint.hpp @@ -200,10 +200,16 @@ namespace llarp::quic // // Takes the iterator to the connection pair from `conns` and optional error parameters: if // `application` is false (the default) then we do a hard connection close because of transport - // error, if true we do a graceful application close. For application closes the code is - // application-defined; for hard closes the code should be one of the NGTCP2_*_ERROR values. + // error, if true we do a graceful application close. `close_reason` can be provided for + // propagating reason for close to remote, defaults to empty string. For application closes the + // code is application-defined; for hard closes the code should be one of the NGTCP2_*_ERROR + // values. void - close_connection(Connection& conn, uint64_t code = NGTCP2_NO_ERROR, bool application = false); + close_connection( + Connection& conn, + uint64_t code = NGTCP2_NO_ERROR, + bool application = false, + std::string_view close_reason = ""sv); /// Puts a connection into draining mode (i.e. after getting a connection close). This will /// keep the connection registered for the recommended 3*Probe Timeout, during which we drop From 08a9e0ad39f49846bf9552abd67728a00f419789 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 2 Feb 2022 07:35:39 -0500 Subject: [PATCH 088/202] add cross compile helpers and update/add toolchains --- cmake/StaticBuild.cmake | 15 ++++++++- contrib/cross.sh | 45 +++++++++++++++++++++++++++ contrib/cross/aarch64.toolchain.cmake | 3 +- contrib/cross/armhf.toolchain.cmake | 2 +- contrib/cross/mips.toolchain.cmake | 13 ++++++++ contrib/cross/mips64.toolchain.cmake | 13 ++++++++ contrib/cross/mipsel.toolchain.cmake | 13 ++++++++ readme.md | 25 +++++++++++++-- 8 files changed, 123 insertions(+), 6 deletions(-) create mode 100755 contrib/cross.sh create mode 100644 contrib/cross/mips.toolchain.cmake create mode 100644 contrib/cross/mips64.toolchain.cmake create mode 100644 contrib/cross/mipsel.toolchain.cmake diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 9dfd958ce..7e63d55bd 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -234,6 +234,7 @@ add_static_target(zlib zlib_external libz.a) set(openssl_system_env "") +set(openssl_configure_command ./config) if(CMAKE_CROSSCOMPILING) if(ARCH_TRIPLET STREQUAL x86_64-w64-mingw32) set(openssl_system_env SYSTEM=MINGW64 RC=${CMAKE_RC_COMPILER} AR=${ARCH_TRIPLET}-ar RANLIB=${ARCH_TRIPLET}-ranlib) @@ -242,13 +243,25 @@ if(CMAKE_CROSSCOMPILING) elseif(ANDROID) set(openssl_system_env SYSTEM=Linux MACHINE=${android_machine} LD=${deps_ld} RANLIB=${deps_ranlib} AR=${deps_ar}) set(openssl_extra_opts no-asm) + elseif(ARCH_TRIPLET STREQUAL mips64-linux-gnuabi64) + set(openssl_system_env SYSTEM=Linux MACHINE=mips64) + set(openssl_configure_command ./Configure linux64-mips64) + elseif(ARCH_TRIPLET STREQUAL mips-linux-gnu) + set(openssl_system_env SYSTEM=Linux MACHINE=mips) + elseif(ARCH_TRIPLET STREQUAL mipsel-linux-gnu) + set(openssl_system_env SYSTEM=Linux MACHINE=mipsel) + elseif(ARCH_TRIPLET STREQUAL aarch64-linux-gnu) + # cross compile arm64 + set(openssl_system_env SYSTEM=Linux MACHINE=aarch64) endif() elseif(CMAKE_C_FLAGS MATCHES "-march=armv7") # Help openssl figure out that we're building from armv7 even if on armv8 hardware: set(openssl_system_env SYSTEM=Linux MACHINE=armv7) endif() + + build_external(openssl - CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env CC=${deps_cc} ${openssl_system_env} ./config + CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env CC=${deps_cc} ${openssl_system_env} ${openssl_configure_command} --prefix=${DEPS_DESTDIR} ${openssl_extra_opts} no-shared no-capieng no-dso no-dtls1 no-ec_nistp_64_gcc_128 no-gost no-heartbeats no-md2 no-rc5 no-rdrand no-rfc3779 no-sctp no-ssl-trace no-ssl2 no-ssl3 no-static-engine no-tests no-weak-ssl-ciphers no-zlib no-zlib-dynamic "CFLAGS=${deps_CFLAGS}" diff --git a/contrib/cross.sh b/contrib/cross.sh new file mode 100755 index 000000000..abdb13c34 --- /dev/null +++ b/contrib/cross.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# +# helper script for me for when i cross compile +# t. jeff +# +die() { + echo $@ + exit 1 +} + +root="$(readlink -e $(dirname $0)/../)" +cd $root +set -e +set +x +test $# = 0 && die no targets provided +mkdir -p build-cross +echo "all: $@" > build-cross/Makefile +for targ in $@ ; do + mkdir -p build-$targ + cd build-$targ + cmake \ + -G 'Unix Makefiles' \ + -DCMAKE_EXE_LINKER_FLAGS=-fstack-protector \ + -DCMAKE_CXX_FLAGS=-fdiagnostics-color=always\ + -DCMAKE_TOOLCHAIN_FILE=../contrib/cross/$targ.toolchain.cmake\ + -DBUILD_STATIC_DEPS=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_TESTING=OFF \ + -DBUILD_LIBLOKINET=OFF \ + -DWITH_TESTS=OFF \ + -DNATIVE_BUILD=OFF \ + -DSTATIC_LINK=ON \ + -DWITH_SYSTEMD=OFF \ + -DFORCE_OXENMQ_SUBMODULE=ON \ + -DSUBMODULE_CHECK=OFF \ + -DWITH_LTO=OFF \ + -DWITH_BOOTSTRAP=OFF \ + -DCMAKE_BUILD_TYPE=Release \ + .. + cd - + echo -ne "$targ:\n\t\$(MAKE) -C $root/build-$targ\n" >> build-cross/Makefile + +done + +make -j${JOBS:-$(nproc)} -C build-cross diff --git a/contrib/cross/aarch64.toolchain.cmake b/contrib/cross/aarch64.toolchain.cmake index 03357daf1..ada0993c3 100644 --- a/contrib/cross/aarch64.toolchain.cmake +++ b/contrib/cross/aarch64.toolchain.cmake @@ -1,6 +1,6 @@ set(CMAKE_SYSTEM_NAME Linux) set(TOOLCHAIN_PREFIX aarch64-linux-gnu) -#set(TOOLCHAIN_SUFFIX) +set(TOOLCHAIN_SUFFIX) set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) @@ -10,3 +10,4 @@ set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc${TOOLCHAIN_SUFFIX}) set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++${TOOLCHAIN_SUFFIX}) +set(ARCH_TRIPLET ${TOOLCHAIN_PREFIX}) diff --git a/contrib/cross/armhf.toolchain.cmake b/contrib/cross/armhf.toolchain.cmake index a61a270bb..aba32f0f2 100644 --- a/contrib/cross/armhf.toolchain.cmake +++ b/contrib/cross/armhf.toolchain.cmake @@ -1,6 +1,6 @@ set(CMAKE_SYSTEM_NAME Linux) set(TOOLCHAIN_PREFIX arm-linux-gnueabihf) -set(TOOLCHAIN_SUFFIX -8) +set(TOOLCHAIN_SUFFIX) set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) diff --git a/contrib/cross/mips.toolchain.cmake b/contrib/cross/mips.toolchain.cmake new file mode 100644 index 000000000..758fa4fb2 --- /dev/null +++ b/contrib/cross/mips.toolchain.cmake @@ -0,0 +1,13 @@ +set(CMAKE_SYSTEM_NAME Linux) +set(TOOLCHAIN_PREFIX mips-linux-gnu) +set(TOOLCHAIN_SUFFIX) + +set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc${TOOLCHAIN_SUFFIX}) +set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++${TOOLCHAIN_SUFFIX}) +set(ARCH_TRIPLET ${TOOLCHAIN_PREFIX}) diff --git a/contrib/cross/mips64.toolchain.cmake b/contrib/cross/mips64.toolchain.cmake new file mode 100644 index 000000000..e71867b68 --- /dev/null +++ b/contrib/cross/mips64.toolchain.cmake @@ -0,0 +1,13 @@ +set(CMAKE_SYSTEM_NAME Linux) +set(TOOLCHAIN_PREFIX mips64-linux-gnuabi64) +set(TOOLCHAIN_SUFFIX) + +set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc${TOOLCHAIN_SUFFIX}) +set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++${TOOLCHAIN_SUFFIX}) +set(ARCH_TRIPLET ${TOOLCHAIN_PREFIX}) diff --git a/contrib/cross/mipsel.toolchain.cmake b/contrib/cross/mipsel.toolchain.cmake new file mode 100644 index 000000000..142f96337 --- /dev/null +++ b/contrib/cross/mipsel.toolchain.cmake @@ -0,0 +1,13 @@ +set(CMAKE_SYSTEM_NAME Linux) +set(TOOLCHAIN_PREFIX mipsel-linux-gnu) +set(TOOLCHAIN_SUFFIX) + +set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc${TOOLCHAIN_SUFFIX}) +set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++${TOOLCHAIN_SUFFIX}) +set(ARCH_TRIPLET ${TOOLCHAIN_PREFIX}) diff --git a/readme.md b/readme.md index f8df94448..e2ce163f7 100644 --- a/readme.md +++ b/readme.md @@ -59,9 +59,28 @@ If you want to build from source: $ make -j$(nproc) $ sudo make install +#### Cross Compile + +supported cross targets: + +* aarch64 +* armhf +* mips +* mips64 +* mipsel +* ppc64le + +install the toolchain for `$arch` this example is `aarch64` + + $ sudo apt install g{cc,++}-aarch64-linux-gnu + +build 1 or many cross targets: + + $ ./contrib/cross.sh arch_1 arch_2 ... arch_n + ### macOS -Lokinet ~~is~~ will be available on the Apple App store. +Lokinet ~~is~~ will be available on the Apple App store. Source 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. If you find this disagreeable consider using a platform that permits compiling from source. @@ -112,11 +131,11 @@ lokinet`, etc. ### Running on Linux (without debs) -**DO NOT RUN AS ROOT**, run as normal user. +**DO NOT RUN AS ROOT**, run as normal user. set up the initial configs: - $ lokinet -g + $ lokinet -g $ lokinet-bootstrap after you create default config, run it: From 24811aff37573a5a39aadd94706b56f81e31b197 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 2 Feb 2022 07:40:52 -0500 Subject: [PATCH 089/202] fix up contrib/cross.sh to put everything in build-cross --- contrib/cross.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contrib/cross.sh b/contrib/cross.sh index abdb13c34..125a7c54e 100755 --- a/contrib/cross.sh +++ b/contrib/cross.sh @@ -11,18 +11,18 @@ die() { root="$(readlink -e $(dirname $0)/../)" cd $root set -e -set +x +set -x test $# = 0 && die no targets provided mkdir -p build-cross echo "all: $@" > build-cross/Makefile for targ in $@ ; do - mkdir -p build-$targ - cd build-$targ + mkdir -p $root/build-cross/build-$targ + cd $root/build-cross/build-$targ cmake \ -G 'Unix Makefiles' \ -DCMAKE_EXE_LINKER_FLAGS=-fstack-protector \ -DCMAKE_CXX_FLAGS=-fdiagnostics-color=always\ - -DCMAKE_TOOLCHAIN_FILE=../contrib/cross/$targ.toolchain.cmake\ + -DCMAKE_TOOLCHAIN_FILE=$root/contrib/cross/$targ.toolchain.cmake\ -DBUILD_STATIC_DEPS=ON \ -DBUILD_SHARED_LIBS=OFF \ -DBUILD_TESTING=OFF \ @@ -36,10 +36,10 @@ for targ in $@ ; do -DWITH_LTO=OFF \ -DWITH_BOOTSTRAP=OFF \ -DCMAKE_BUILD_TYPE=Release \ - .. - cd - - echo -ne "$targ:\n\t\$(MAKE) -C $root/build-$targ\n" >> build-cross/Makefile + $root + cd $root/build-cross + echo -ne "$targ:\n\t\$(MAKE) -C build-$targ\n" >> $root/build-cross/Makefile done - +cd $root make -j${JOBS:-$(nproc)} -C build-cross From f543f6962df0eef16335ddfdb3464ff638defdeb Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 2 Feb 2022 07:41:10 -0500 Subject: [PATCH 090/202] fix up ppc64le toolchain, remove suffix --- contrib/cross/ppc64le.toolchain.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/cross/ppc64le.toolchain.cmake b/contrib/cross/ppc64le.toolchain.cmake index c045b4939..d817c869a 100644 --- a/contrib/cross/ppc64le.toolchain.cmake +++ b/contrib/cross/ppc64le.toolchain.cmake @@ -1,6 +1,6 @@ set(CMAKE_SYSTEM_NAME Linux) set(TOOLCHAIN_PREFIX powerpc64le-linux-gnu) -set(TOOLCHAIN_SUFFIX -8) +set(TOOLCHAIN_SUFFIX) set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) From eae4d3cf749cae6c8f322f5af9b5479ea6fdd9bf Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 2 Feb 2022 08:09:43 -0500 Subject: [PATCH 091/202] fixup armhf target --- cmake/StaticBuild.cmake | 3 +++ contrib/cross/armhf.toolchain.cmake | 1 + 2 files changed, 4 insertions(+) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 7e63d55bd..4e0bdc06f 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -253,6 +253,9 @@ if(CMAKE_CROSSCOMPILING) elseif(ARCH_TRIPLET STREQUAL aarch64-linux-gnu) # cross compile arm64 set(openssl_system_env SYSTEM=Linux MACHINE=aarch64) + elseif(ARCH_TRIPLET MATCHES arm-linux) + # cross compile armhf + set(openssl_system_env SYSTEM=Linux MACHINE=armv4) endif() elseif(CMAKE_C_FLAGS MATCHES "-march=armv7") # Help openssl figure out that we're building from armv7 even if on armv8 hardware: diff --git a/contrib/cross/armhf.toolchain.cmake b/contrib/cross/armhf.toolchain.cmake index aba32f0f2..552975e6d 100644 --- a/contrib/cross/armhf.toolchain.cmake +++ b/contrib/cross/armhf.toolchain.cmake @@ -10,3 +10,4 @@ set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc${TOOLCHAIN_SUFFIX}) set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++${TOOLCHAIN_SUFFIX}) +set(ARCH_TRIPLET ${TOOLCHAIN_PREFIX}) From 48559bd282e047ec3b06b090eea08a80923a5923 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 2 Feb 2022 08:34:58 -0500 Subject: [PATCH 092/202] fix up ppc64le cross target --- cmake/StaticBuild.cmake | 3 +++ contrib/cross/ppc64le.toolchain.cmake | 1 + 2 files changed, 4 insertions(+) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 4e0bdc06f..fca5967f2 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -256,6 +256,9 @@ if(CMAKE_CROSSCOMPILING) elseif(ARCH_TRIPLET MATCHES arm-linux) # cross compile armhf set(openssl_system_env SYSTEM=Linux MACHINE=armv4) + elseif(ARCH_TRIPLET MATCHES powerpc64le) + # cross compile ppc64le + set(openssl_system_env SYSTEM=Linux MACHINE=ppc64le) endif() elseif(CMAKE_C_FLAGS MATCHES "-march=armv7") # Help openssl figure out that we're building from armv7 even if on armv8 hardware: diff --git a/contrib/cross/ppc64le.toolchain.cmake b/contrib/cross/ppc64le.toolchain.cmake index d817c869a..f0b956079 100644 --- a/contrib/cross/ppc64le.toolchain.cmake +++ b/contrib/cross/ppc64le.toolchain.cmake @@ -10,3 +10,4 @@ set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc${TOOLCHAIN_SUFFIX}) set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++${TOOLCHAIN_SUFFIX}) +set(ARCH_TRIPLET ${TOOLCHAIN_PREFIX}) From 810e3cc9c2203db9a2013da5539159424972a71b Mon Sep 17 00:00:00 2001 From: Jeff Date: Sun, 6 Feb 2022 13:27:19 -0500 Subject: [PATCH 093/202] dont capture flow_addr, results in compiler error on sid --- llarp/lokinet_shared.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index ac4652729..9e9c57ff9 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -193,7 +193,7 @@ namespace } lokinet_udp_flowinfo flow_addr{}; // set flow remote address - std::string addrstr = var::visit([&flow_addr](auto&& from) { return from.ToString(); }, from); + std::string addrstr = var::visit([](auto&& from) { return from.ToString(); }, from); std::copy_n( addrstr.data(), From 09e91cce1383be9085e7a82ab61ec01539425844 Mon Sep 17 00:00:00 2001 From: majestrate Date: Sat, 12 Feb 2022 14:50:43 -0500 Subject: [PATCH 094/202] fix up readme point install links to sections in readme.md --- readme.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/readme.md b/readme.md index e2ce163f7..8f43928fd 100644 --- a/readme.md +++ b/readme.md @@ -16,9 +16,11 @@ A simple demo application that is lokinet "aware" can be found [here](https://gi ## Installing -If you are simply looking to install Lokinet and don't want to compile it yourself the [Oxen Docs -Lokinet Guides](https://docs.oxen.io/products-built-on-oxen/lokinet/guides) for gentler -instructions to get up and running. +If you are simply looking to install Lokinet and don't want to compile it yourself we provide several options: + +* [Linux](./#linux-install) +* [Windows](./#windows-install) +* [macOS](./#mac-install) ## Building @@ -36,7 +38,7 @@ Build requirements: * cppzmq * sqlite3 -### Linux +### Linux You do not have to build from source if you are on debian or ubuntu as we have apt repositories with pre-built lokinet packages on `deb.oxen.io`. @@ -78,13 +80,13 @@ build 1 or many cross targets: $ ./contrib/cross.sh arch_1 arch_2 ... arch_n -### macOS +### macOS Lokinet ~~is~~ will be available on the Apple App store. Source 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. If you find this disagreeable consider using a platform that permits compiling from source. -### Windows +### Windows You can get the latest stable windows release from https://lokinet.org/ or check the releases page on github. From 101650d74250494685f05dcedc36b5292745be15 Mon Sep 17 00:00:00 2001 From: majestrate Date: Sat, 12 Feb 2022 14:51:20 -0500 Subject: [PATCH 095/202] Update readme.md typofix --- readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 8f43928fd..69a45cbff 100644 --- a/readme.md +++ b/readme.md @@ -18,9 +18,9 @@ A simple demo application that is lokinet "aware" can be found [here](https://gi If you are simply looking to install Lokinet and don't want to compile it yourself we provide several options: -* [Linux](./#linux-install) -* [Windows](./#windows-install) -* [macOS](./#mac-install) +* [Linux](#linux-install) +* [Windows](#windows-install) +* [macOS](#mac-install) ## Building From e61ca2934e2d9d376331fd453c88af7922f16115 Mon Sep 17 00:00:00 2001 From: majestrate Date: Sat, 12 Feb 2022 14:52:08 -0500 Subject: [PATCH 096/202] Update readme.md fix anchor --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 69a45cbff..5998ca141 100644 --- a/readme.md +++ b/readme.md @@ -80,7 +80,7 @@ build 1 or many cross targets: $ ./contrib/cross.sh arch_1 arch_2 ... arch_n -### macOS +### macOS Lokinet ~~is~~ will be available on the Apple App store. From 79662aaa9ae5d10940b0bcc8a97dc1830a118220 Mon Sep 17 00:00:00 2001 From: majestrate Date: Sat, 12 Feb 2022 19:05:34 -0500 Subject: [PATCH 097/202] spruce up README more remove dead links for docs link all support platforms add anchors in markdown --- readme.md | 67 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 27 deletions(-) diff --git a/readme.md b/readme.md index 5998ca141..0cc32d49d 100644 --- a/readme.md +++ b/readme.md @@ -4,23 +4,30 @@ Lokinet is the reference implementation of LLARP (low latency anonymous routing protocol), a layer 3 onion routing protocol. -You can learn more about the high level design of LLARP [here](docs/high-level.txt) - -And you can read the LLARP protocol specification [here](docs/proto_v0.txt) - -You can view documentation on how to get started [here](https://docs.oxen.io/products-built-on-oxen/lokinet) . - -A simple demo application that is lokinet "aware" can be found [here](https://github.com/majestrate/lokinet-aware-demos) +You can learn more about the high level design of LLARP [here](docs/) [![Build Status](https://ci.oxen.rocks/api/badges/oxen-io/lokinet/status.svg?ref=refs/heads/dev)](https://ci.oxen.rocks/oxen-io/lokinet) ## Installing -If you are simply looking to install Lokinet and don't want to compile it yourself we provide several options: +If you are simply looking to install Lokinet and don't want to compile it yourself we provide several options for platforms to run on: + +Tier 1: * [Linux](#linux-install) +* [Android](#apk-install) + +Tier 2: + * [Windows](#windows-install) -* [macOS](#mac-install) +* [MacOS](#mac-install) +* [FreeBSD](#freebsd-install) + +Currently Unsupproted Platforms: (maintainers welcome) + +* Apple iPhone +* Homebrew +* \[Insert Flavor of the Month windows package manager here\] ## Building @@ -40,9 +47,9 @@ Build requirements: ### Linux -You do not have to build from source if you are on debian or ubuntu as we have apt repositories with pre-built lokinet packages on `deb.oxen.io`. +You do not have to build from source if you are on debian or ubuntu as we have apt repositories with pre-built lokinet packages on `deb.oxen.io` or `rpm.oxen.io`. -You can install these using: +You can install debian packages using: $ sudo curl -so /etc/apt/trusted.gpg.d/oxen.gpg https://deb.oxen.io/pub.gpg $ echo "deb https://deb.oxen.io $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/oxen.list @@ -52,7 +59,7 @@ You can install these using: If you want to build from source: - $ 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 + $ 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 $ git clone --recursive https://github.com/oxen-io/lokinet $ cd lokinet $ mkdir build @@ -61,16 +68,7 @@ If you want to build from source: $ make -j$(nproc) $ sudo make install -#### Cross Compile - -supported cross targets: - -* aarch64 -* armhf -* mips -* mips64 -* mipsel -* ppc64le +#### Cross Compile For Linux install the toolchain for `$arch` this example is `aarch64` @@ -80,7 +78,7 @@ build 1 or many cross targets: $ ./contrib/cross.sh arch_1 arch_2 ... arch_n -### macOS +### MacOS Lokinet ~~is~~ will be available on the Apple App store. @@ -88,7 +86,11 @@ Source code compilation of Lokinet by end users is not supported or permitted by ### Windows -You can get the latest stable windows release from https://lokinet.org/ or check the releases page on github. +You can get the latest stable windows release from https://lokinet.org/ or check the [releases page on github](https://github.com/oxen-io/lokinet/releases). + +nightly builds for the brave or impatient can be found from our CI pipeline [here](https://oxen.rocks/oxen-io/lokinet/) + +#### Building For Windows windows builds are cross compiled from debian/ubuntu linux @@ -107,7 +109,9 @@ building: $ cd lokinet $ ./contrib/windows.sh -### FreeBSD +### FreeBSD + +Currently has no VPN Platform code, see #1513 build: @@ -122,16 +126,25 @@ build: install (root): # make install + +### Android + +We have an Android APK for lokinet VPN via android VPN API. + +Coming to F-Droid whenever that happens. [[issue]](https://github.com/oxen-io/lokinet-flutter-app/issues/8) + +* [source code](https://github.com/oxen-io/lokinet-flutter-app) +* [CI builds](https://oxen.rocks/oxen-io/lokinet/) ## Usage -### Debian / Ubuntu packages +### Debian / Ubuntu packages When 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. -### Running on Linux (without debs) +### Running on Linux (without debs) **DO NOT RUN AS ROOT**, run as normal user. From c0f47063cbcd8d85ca04a4c033b39590cab300d6 Mon Sep 17 00:00:00 2001 From: majestrate Date: Sat, 12 Feb 2022 19:08:01 -0500 Subject: [PATCH 098/202] Delete llarp.png remove unused image --- docs/llarp.png | Bin 424154 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 docs/llarp.png diff --git a/docs/llarp.png b/docs/llarp.png deleted file mode 100644 index 781db1124ddc2ec21b7f86344373dc2acf20cb5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 424154 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 A!T Date: Tue, 15 Feb 2022 16:40:11 -0500 Subject: [PATCH 099/202] use quad nine as default upstream dns --- llarp/config/config.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp index 40b897ac6..94b5ff15c 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp @@ -740,7 +740,7 @@ namespace llarp #endif // Default, but if we get any upstream (including upstream=, i.e. empty string) we clear it - constexpr Default DefaultUpstreamDNS{"1.1.1.1"}; + constexpr Default DefaultUpstreamDNS{"9.9.9.10"}; m_upstreamDNS.emplace_back(DefaultUpstreamDNS.val); if (!m_upstreamDNS.back().getPort()) m_upstreamDNS.back().setPort(53); From d9467f4dee3b79b2ede16285e65b0a6545006349 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 15 Feb 2022 16:29:37 +1100 Subject: [PATCH 100/202] fix numPaths value in Router::ExtractSummaryStatus --- llarp/router/router.cpp | 42 +++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index 835850e75..6c9a31989 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -139,30 +139,44 @@ namespace llarp // Compute all stats on all path builders on the default endpoint // Merge snodeSessions, remoteSessions and default into a single array std::vector builders; - auto snode_sessions = services["default"]["snodeSessions"]; + + const auto& serviceDefault = services.at("default"); + builders.push_back(serviceDefault); + + auto snode_sessions = serviceDefault.at("snodeSessions"); for (const auto& session : snode_sessions) - builders.push_back(session["buildStats"]); + builders.push_back(session); - auto remote_sessions = services["default"]["remoteSessions"]; + auto remote_sessions = serviceDefault.at("remoteSessions"); for (const auto& session : remote_sessions) - builders.push_back(session["buildStats"]); - - builders.push_back(services["default"]["buildStats"]); + builders.push_back(session); // Iterate over all items on this array to build the global pathStats - uint64_t paths = 0; + uint64_t pathsCount = 0; uint64_t success = 0; uint64_t attempts = 0; for (const auto& builder : builders) { if (builder.is_null()) continue; - if (builder["length"].is_number()) - paths += builder["length"].get(); - if (builder["success"].is_number()) - success += builder["success"].get(); - if (builder["attempts"].is_number()) - attempts += builder["attempts"].get(); + + const auto& paths = builder.at("paths"); + if (paths.is_array()) + { + for (const auto& [key, value] : paths.items()) + { + if (value.is_object() && value.at("status").is_string() + && value.at("status") == "established") + pathsCount++; + } + } + + const auto& buildStats = builder.at("buildStats"); + if (buildStats.is_null()) + continue; + + success += buildStats.at("success").get(); + attempts += buildStats.at("attempts").get(); } double ratio = static_cast(success) / (attempts + 1); @@ -171,7 +185,7 @@ namespace llarp {"authCodes", services["default"]["authCodes"]}, {"exitMap", services["default"]["exitMap"]}, {"lokiAddress", services["default"]["identity"]}, - {"numPathsBuilt", paths}, + {"numPathsBuilt", pathsCount}, {"numPeersConnected", peers}, {"numRoutersKnown", _nodedb->NumLoaded()}, {"ratio", ratio}, From d02558350a2be63789242a97a868e8a5faf06fba Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 17 Feb 2022 14:44:31 -0400 Subject: [PATCH 101/202] Crank oxen-mq to (1.2.)11; switch to oxen-encoding - Update oxen-mq submodule to latest stable - Add oxen-encoding submodule - Convert all oxenmq encoding usage to oxenc - Modernize cmake handling of oxenmq/oxenc --- .gitmodules | 5 ++++- CMakeLists.txt | 20 ++++++++++++++------ cmake/link_dep_libs.cmake | 13 ------------- external/CMakeLists.txt | 1 + external/oxen-encoding | 1 + external/oxen-mq | 2 +- llarp/crypto/types.cpp | 6 +++--- llarp/dht/messages/findname.cpp | 4 ++-- llarp/dht/messages/gotname.cpp | 6 +++--- llarp/dns/srv_data.cpp | 8 ++++---- llarp/endpoint_base.hpp | 2 +- llarp/ev/vpn.hpp | 2 +- llarp/handlers/tun.cpp | 4 ++-- llarp/iwp/session.cpp | 2 +- llarp/lokinet_shared.cpp | 8 ++++---- llarp/net/ip_range.cpp | 6 +++--- llarp/nodedb.cpp | 2 +- llarp/peerstats/types.cpp | 6 +++--- llarp/quic/client.cpp | 2 +- llarp/quic/connection.cpp | 14 +++++++------- llarp/quic/endpoint.cpp | 2 +- llarp/quic/server.cpp | 3 +-- llarp/quic/stream.hpp | 2 +- llarp/quic/tunnel.cpp | 2 +- llarp/router_contact.cpp | 6 +++--- llarp/router_contact.hpp | 6 +++--- llarp/router_id.cpp | 10 +++++----- llarp/rpc/lokid_rpc_client.cpp | 6 +++--- llarp/service/address.cpp | 8 ++++---- llarp/service/endpoint.hpp | 4 +--- llarp/service/endpoint_state.hpp | 2 +- llarp/service/intro_set.cpp | 8 ++++---- llarp/service/lns_tracker.hpp | 2 +- llarp/util/aligned.hpp | 12 ++++++------ llarp/vpn/linux.hpp | 4 ++-- pybind/llarp/router_id.cpp | 4 ++-- 36 files changed, 96 insertions(+), 99 deletions(-) delete mode 100644 cmake/link_dep_libs.cmake create mode 160000 external/oxen-encoding diff --git a/.gitmodules b/.gitmodules index 9b49fa876..ddf50410d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -35,4 +35,7 @@ [submodule "external/ngtcp2"] path = external/ngtcp2 url = https://github.com/ngtcp2/ngtcp2.git - branch = v0.1.0 \ No newline at end of file + branch = v0.1.0 +[submodule "external/oxen-encoding"] + path = external/oxen-encoding + url = https://github.com/oxen-io/oxen-encoding.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 4c6bc125b..8cdd74719 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,7 +105,6 @@ include(cmake/target_link_libraries_system.cmake) include(cmake/add_import_library.cmake) include(cmake/add_log_tag.cmake) include(cmake/libatomic.cmake) -include(cmake/link_dep_libs.cmake) if (STATIC_LINK) set(CMAKE_FIND_LIBRARY_SUFFIXES ${CMAKE_STATIC_LIBRARY_SUFFIX}) @@ -180,15 +179,24 @@ if(NOT TARGET sodium) export(TARGETS sodium NAMESPACE sodium:: FILE sodium-exports.cmake) endif() +option(FORCE_OXENC_SUBMODULE "force using oxen-encoding submodule" OFF) +if(NOT FORCE_OXENC_SUBMODULE) + pkg_check_modules(OXENC liboxenc>=1.0.1 IMPORTED_TARGET) +endif() +if(OXENC_FOUND) + add_library(oxenc::oxenc ALIAS PkgConfig::OXENC) + message(STATUS "Found system liboxenc ${OXENC_VERSION}") +else() + message(STATUS "using oxen-encoding submodule") + add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/external/oxen-encoding) +endif() + option(FORCE_OXENMQ_SUBMODULE "force using oxenmq submodule" OFF) if(NOT FORCE_OXENMQ_SUBMODULE) - pkg_check_modules(OXENMQ liboxenmq>=1.2.4) + pkg_check_modules(OXENMQ liboxenmq>=1.2.4 IMPORTED_TARGET) endif() if(OXENMQ_FOUND) - add_library(oxenmq INTERFACE) - link_dep_libs(oxenmq INTERFACE "${OXENMQ_LIBRARY_DIRS}" ${OXENMQ_LIBRARIES}) - target_include_directories(oxenmq INTERFACE ${OXENMQ_INCLUDE_DIRS}) - add_library(oxenmq::oxenmq ALIAS oxenmq) + add_library(oxenmq::oxenmq ALIAS PkgConfig::OXENMQ) message(STATUS "Found system liboxenmq ${OXENMQ_VERSION}") else() message(STATUS "using oxenmq submodule") diff --git a/cmake/link_dep_libs.cmake b/cmake/link_dep_libs.cmake deleted file mode 100644 index 14e4b4f16..000000000 --- a/cmake/link_dep_libs.cmake +++ /dev/null @@ -1,13 +0,0 @@ -# Properly links a target to a list of library names by finding the given libraries. Takes: -# - a target -# - a linktype (e.g. INTERFACE, PUBLIC, PRIVATE) -# - a library search path (or "" for defaults) -# - any number of library names -function(link_dep_libs target linktype libdirs) - foreach(lib ${ARGN}) - find_library(link_lib-${lib} NAMES ${lib} PATHS ${libdirs}) - if(link_lib-${lib}) - target_link_libraries(${target} ${linktype} ${link_lib-${lib}}) - endif() - endforeach() -endfunction() diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 8f7c49d84..0f935161b 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -22,6 +22,7 @@ if(SUBMODULE_CHECK) check_submodule(pybind11) check_submodule(sqlite_orm) check_submodule(oxen-mq) + check_submodule(oxen-encoding) check_submodule(uvw) check_submodule(cpr) check_submodule(ngtcp2) diff --git a/external/oxen-encoding b/external/oxen-encoding new file mode 160000 index 000000000..077cbff7a --- /dev/null +++ b/external/oxen-encoding @@ -0,0 +1 @@ +Subproject commit 077cbff7a38ce5c538d9653ffb119855151791c5 diff --git a/external/oxen-mq b/external/oxen-mq index 51754037e..5c72a57ec 160000 --- a/external/oxen-mq +++ b/external/oxen-mq @@ -1 +1 @@ -Subproject commit 51754037ea19204610751c2ea8ae72b7ed6c1818 +Subproject commit 5c72a57eca120750ecf557ce5a668fb38242956b diff --git a/llarp/crypto/types.cpp b/llarp/crypto/types.cpp index 3db017647..b2f190b83 100644 --- a/llarp/crypto/types.cpp +++ b/llarp/crypto/types.cpp @@ -7,7 +7,7 @@ #include -#include +#include #include #include @@ -20,14 +20,14 @@ namespace llarp { if (str.size() != 2 * size()) return false; - oxenmq::from_hex(str.begin(), str.end(), begin()); + oxenc::from_hex(str.begin(), str.end(), begin()); return true; } std::string PubKey::ToString() const { - return oxenmq::to_hex(begin(), end()); + return oxenc::to_hex(begin(), end()); } bool diff --git a/llarp/dht/messages/findname.cpp b/llarp/dht/messages/findname.cpp index 929db0ed7..049cc0724 100644 --- a/llarp/dht/messages/findname.cpp +++ b/llarp/dht/messages/findname.cpp @@ -1,5 +1,5 @@ #include "findname.hpp" -#include +#include #include #include "gotname.hpp" #include @@ -16,7 +16,7 @@ namespace llarp::dht bool FindNameMessage::BEncode(llarp_buffer_t* buf) const { - const auto data = oxenmq::bt_serialize(oxenmq::bt_dict{ + const auto data = oxenc::bt_serialize(oxenc::bt_dict{ {"A", "N"sv}, {"H", std::string_view{(char*)NameHash.data(), NameHash.size()}}, {"T", TxID}}); diff --git a/llarp/dht/messages/gotname.cpp b/llarp/dht/messages/gotname.cpp index 47efe5854..2f1720938 100644 --- a/llarp/dht/messages/gotname.cpp +++ b/llarp/dht/messages/gotname.cpp @@ -1,5 +1,5 @@ #include "gotname.hpp" -#include +#include #include #include #include @@ -19,8 +19,8 @@ namespace llarp::dht GotNameMessage::BEncode(llarp_buffer_t* buf) const { const std::string nonce((const char*)result.nonce.data(), result.nonce.size()); - const auto data = oxenmq::bt_serialize( - oxenmq::bt_dict{{"A", "M"sv}, {"D", result.ciphertext}, {"N", nonce}, {"T", TxID}}); + const auto data = oxenc::bt_serialize( + oxenc::bt_dict{{"A", "M"sv}, {"D", result.ciphertext}, {"N", nonce}, {"T", TxID}}); return buf->write(data.begin(), data.end()); } diff --git a/llarp/dns/srv_data.cpp b/llarp/dns/srv_data.cpp index 883d9c68c..e46b90732 100644 --- a/llarp/dns/srv_data.cpp +++ b/llarp/dns/srv_data.cpp @@ -4,7 +4,7 @@ #include -#include +#include #include "llarp/util/bencode.h" #include "llarp/util/types.hpp" @@ -106,7 +106,7 @@ namespace llarp::dns bool SRVData::BEncode(llarp_buffer_t* buf) const { - const std::string data = oxenmq::bt_serialize(toTuple()); + const std::string data = oxenc::bt_serialize(toTuple()); return buf->write(data.begin(), data.end()); } @@ -122,11 +122,11 @@ namespace llarp::dns try { SRVTuple tuple{}; - oxenmq::bt_deserialize(srvString, tuple); + oxenc::bt_deserialize(srvString, tuple); *this = fromTuple(std::move(tuple)); return IsValid(); } - catch (const oxenmq::bt_deserialize_invalid&) + catch (const oxenc::bt_deserialize_invalid&) { return false; }; diff --git a/llarp/endpoint_base.hpp b/llarp/endpoint_base.hpp index ce13786ac..e924ab523 100644 --- a/llarp/endpoint_base.hpp +++ b/llarp/endpoint_base.hpp @@ -14,7 +14,7 @@ #include #include #include -#include "oxenmq/variant.h" +#include "oxenc/variant.h" namespace llarp { diff --git a/llarp/ev/vpn.hpp b/llarp/ev/vpn.hpp index ede6b0acd..64da9f50d 100644 --- a/llarp/ev/vpn.hpp +++ b/llarp/ev/vpn.hpp @@ -4,7 +4,7 @@ #include #include -#include +#include namespace llarp { diff --git a/llarp/handlers/tun.cpp b/llarp/handlers/tun.cpp index 4a4ad117f..afaf9b5c3 100644 --- a/llarp/handlers/tun.cpp +++ b/llarp/handlers/tun.cpp @@ -274,7 +274,7 @@ namespace llarp { std::string_view bdata{data.data(), data.size()}; LogDebug(Name(), " parsing address map data: ", bdata); - const auto parsed = oxenmq::bt_deserialize(bdata); + const auto parsed = oxenc::bt_deserialize(bdata); for (const auto& [key, value] : parsed) { huint128_t ip{}; @@ -1011,7 +1011,7 @@ namespace llarp addrmap[ip.ToString()] = a.ToString(); } } - const auto data = oxenmq::bt_serialize(addrmap); + const auto data = oxenc::bt_serialize(addrmap); maybe->write(data.data(), data.size()); } } diff --git a/llarp/iwp/session.cpp b/llarp/iwp/session.cpp index 6eefcfc71..a58de4065 100644 --- a/llarp/iwp/session.cpp +++ b/llarp/iwp/session.cpp @@ -768,7 +768,7 @@ namespace llarp pos += sizeof(rxid); auto p2 = pos + ShortHash::SIZE; assert(p2 == data.data() + XMITOverhead); - LogTrace("rxid=", rxid, " sz=", sz, " h=", oxenmq::to_hex(pos, p2), " from ", m_RemoteAddr); + LogTrace("rxid=", rxid, " sz=", sz, " h=", oxenc::to_hex(pos, p2), " from ", m_RemoteAddr); m_LastRX = m_Parent->Now(); { // check for replay diff --git a/llarp/lokinet_shared.cpp b/llarp/lokinet_shared.cpp index 9e9c57ff9..2075ba421 100644 --- a/llarp/lokinet_shared.cpp +++ b/llarp/lokinet_shared.cpp @@ -10,7 +10,7 @@ #include -#include +#include #include #include @@ -823,7 +823,7 @@ extern "C" lokinet_hex_to_base32z(const char* hex) { std::string_view hexview{hex}; - if (not oxenmq::is_hex(hexview)) + if (not oxenc::is_hex(hexview)) return nullptr; const size_t byte_len = hexview.size() / 2; @@ -834,9 +834,9 @@ extern "C" // Write the bytes into the *end* of the buffer so that when we rewrite the final b32z chars // into the buffer we won't overwrite any byte values until after we've consumed them. char* bytepos = end - byte_len; - oxenmq::from_hex(hexview.begin(), hexview.end(), bytepos); + oxenc::from_hex(hexview.begin(), hexview.end(), bytepos); // In-place conversion into the buffer - oxenmq::to_base32z(bytepos, end, buf.get()); + oxenc::to_base32z(bytepos, end, buf.get()); return buf.release(); // leak the buffer to the caller } diff --git a/llarp/net/ip_range.cpp b/llarp/net/ip_range.cpp index 4b83e9d02..08d234584 100644 --- a/llarp/net/ip_range.cpp +++ b/llarp/net/ip_range.cpp @@ -1,6 +1,6 @@ #include "ip_range.hpp" -#include "oxenmq/bt_serialize.h" +#include "oxenc/bt_serialize.h" #include "llarp/util/bencode.h" @@ -9,7 +9,7 @@ namespace llarp bool IPRange::BEncode(llarp_buffer_t* buf) const { - const auto str = oxenmq::bt_serialize(ToString()); + const auto str = oxenc::bt_serialize(ToString()); return buf->write(str.begin(), str.end()); } @@ -24,7 +24,7 @@ namespace llarp std::string str; try { - oxenmq::bt_deserialize(data, str); + oxenc::bt_deserialize(data, str); } catch (std::exception&) { diff --git a/llarp/nodedb.cpp b/llarp/nodedb.cpp index 671ad2fc3..22feb80f3 100644 --- a/llarp/nodedb.cpp +++ b/llarp/nodedb.cpp @@ -93,7 +93,7 @@ namespace llarp fs::path NodeDB::GetPathForPubkey(RouterID pubkey) const { - std::string hexString = oxenmq::to_hex(pubkey.begin(), pubkey.end()); + std::string hexString = oxenc::to_hex(pubkey.begin(), pubkey.end()); std::string skiplistDir; const llarp::RouterID r{pubkey}; diff --git a/llarp/peerstats/types.cpp b/llarp/peerstats/types.cpp index e66918b57..89b0129cc 100644 --- a/llarp/peerstats/types.cpp +++ b/llarp/peerstats/types.cpp @@ -1,7 +1,7 @@ #include "types.hpp" #include -#include +#include #include namespace llarp @@ -103,7 +103,7 @@ namespace llarp { if (not buf) throw std::runtime_error("PeerStats: Can't use null buf"); - const oxenmq::bt_dict data = { + const oxenc::bt_dict data = { {NumConnectionAttemptsKey, numConnectionAttempts}, {NumConnectionSuccessesKey, numConnectionSuccesses}, {NumConnectionRejectionsKey, numConnectionRejections}, @@ -120,7 +120,7 @@ namespace llarp {LeastRCRemainingLifetimeKey, leastRCRemainingLifetime.count()}, {LastRCUpdatedKey, lastRCUpdated.count()}, }; - const auto serialized = oxenmq::bt_serialize(data); + const auto serialized = oxenc::bt_serialize(data); if (not buf->write(serialized.begin(), serialized.end())) throw std::runtime_error("PeerStats: buffer too small"); } diff --git a/llarp/quic/client.cpp b/llarp/quic/client.cpp index dd01e4dc0..a2a081edf 100644 --- a/llarp/quic/client.cpp +++ b/llarp/quic/client.cpp @@ -3,7 +3,7 @@ #include #include -#include +#include #include #include #include diff --git a/llarp/quic/connection.cpp b/llarp/quic/connection.cpp index 778908385..fed88401e 100644 --- a/llarp/quic/connection.cpp +++ b/llarp/quic/connection.cpp @@ -15,8 +15,8 @@ #include #include -#include -#include +#include +#include extern "C" { @@ -35,7 +35,7 @@ namespace llarp::quic std::ostream& operator<<(std::ostream& o, const ConnectionID& c) { - return o << oxenmq::to_hex(c.data, c.data + c.datalen); + return o << oxenc::to_hex(c.data, c.data + c.datalen); } ConnectionID @@ -297,7 +297,7 @@ namespace llarp::quic conn.endpoint.make_stateless_reset_token(cid, token); LogDebug( "make stateless reset token ", - oxenmq::to_hex(token, token + NGTCP2_STATELESS_RESET_TOKENLEN)); + oxenc::to_hex(token, token + NGTCP2_STATELESS_RESET_TOKENLEN)); return 0; } @@ -1093,7 +1093,7 @@ namespace llarp::quic uint16_t port; try { - oxenmq::bt_dict_consumer meta{lokinet_metadata}; + oxenc::bt_dict_consumer meta{lokinet_metadata}; // '#' contains the port the client wants us to forward to if (!meta.skip_until("#")) { @@ -1108,7 +1108,7 @@ namespace llarp::quic } LogDebug("decoded lokinet tunnel port = ", port); } - catch (const oxenmq::bt_deserialize_invalid& c) + catch (const oxenc::bt_deserialize_invalid& c) { LogWarn("transport params lokinet metadata is invalid: ", c.what()); return NGTCP2_ERR_TRANSPORT_PARAM; @@ -1191,7 +1191,7 @@ namespace llarp::quic // reserved field code that QUIC parsers must ignore); currently we only include the port in // here (from the client to tell the server what it's trying to reach, and reflected from // the server for the client to verify). - std::string lokinet_metadata = bt_serialize(oxenmq::bt_dict{ + std::string lokinet_metadata = bt_serialize(oxenc::bt_dict{ {"#", tunnel_port}, }); copy_and_advance(buf, lokinet_metadata_code); diff --git a/llarp/quic/endpoint.cpp b/llarp/quic/endpoint.cpp index d53ab4880..037011883 100644 --- a/llarp/quic/endpoint.cpp +++ b/llarp/quic/endpoint.cpp @@ -12,7 +12,7 @@ #include #include -#include +#include extern "C" { diff --git a/llarp/quic/server.cpp b/llarp/quic/server.cpp index 369426699..690a97fac 100644 --- a/llarp/quic/server.cpp +++ b/llarp/quic/server.cpp @@ -2,8 +2,7 @@ #include #include -#include -#include +#include #include #include diff --git a/llarp/quic/stream.hpp b/llarp/quic/stream.hpp index 20caa51c3..176a2ab84 100644 --- a/llarp/quic/stream.hpp +++ b/llarp/quic/stream.hpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/llarp/quic/tunnel.cpp b/llarp/quic/tunnel.cpp index fa5e22029..6954e20f4 100644 --- a/llarp/quic/tunnel.cpp +++ b/llarp/quic/tunnel.cpp @@ -170,7 +170,7 @@ namespace llarp::quic { LogWarn( "Remote connection returned invalid initial byte (0x", - oxenmq::to_hex(bdata.begin(), bdata.begin() + 1), + oxenc::to_hex(bdata.begin(), bdata.begin() + 1), "); dropping connection"); stream.close(tunnel::ERROR_BAD_INIT); client.close(); diff --git a/llarp/router_contact.cpp b/llarp/router_contact.cpp index a4c7ff1a9..dabbeae85 100644 --- a/llarp/router_contact.cpp +++ b/llarp/router_contact.cpp @@ -10,7 +10,7 @@ #include "util/printer.hpp" #include "util/time.hpp" -#include +#include #include #include "util/fs.hpp" @@ -267,7 +267,7 @@ namespace llarp try { std::string_view buf_view(reinterpret_cast(buf->cur), buf->size_left()); - oxenmq::bt_list_consumer btlist(buf_view); + oxenc::bt_list_consumer btlist(buf_view); uint64_t outer_version = btlist.consume_integer(); @@ -301,7 +301,7 @@ namespace llarp } bool - RouterContact::DecodeVersion_1(oxenmq::bt_list_consumer& btlist) + RouterContact::DecodeVersion_1(oxenc::bt_list_consumer& btlist) { auto signature_string = btlist.consume_string_view(); signed_bt_dict = btlist.consume_dict_data(); diff --git a/llarp/router_contact.hpp b/llarp/router_contact.hpp index bb500dfba..ccac70d67 100644 --- a/llarp/router_contact.hpp +++ b/llarp/router_contact.hpp @@ -18,10 +18,10 @@ #define MAX_RC_SIZE (1024) #define NICKLEN (32) -namespace oxenmq +namespace oxenc { class bt_list_consumer; -} // namespace oxenmq +} // namespace oxenc namespace llarp { @@ -226,7 +226,7 @@ namespace llarp DecodeVersion_0(llarp_buffer_t* buf); bool - DecodeVersion_1(oxenmq::bt_list_consumer& btlist); + DecodeVersion_1(oxenc::bt_list_consumer& btlist); }; inline std::ostream& diff --git a/llarp/router_id.cpp b/llarp/router_id.cpp index c6e3b3e33..987525137 100644 --- a/llarp/router_id.cpp +++ b/llarp/router_id.cpp @@ -1,5 +1,5 @@ #include "router_id.hpp" -#include +#include namespace llarp { @@ -8,7 +8,7 @@ namespace llarp std::string RouterID::ToString() const { - std::string b32 = oxenmq::to_base32z(begin(), end()); + std::string b32 = oxenc::to_base32z(begin(), end()); b32 += SNODE_TLD; return b32; } @@ -17,7 +17,7 @@ namespace llarp RouterID::ShortString() const { // 5 bytes produces exactly 8 base32z characters: - return oxenmq::to_base32z(begin(), begin() + 5); + return oxenc::to_base32z(begin(), begin() + 5); } util::StatusObject @@ -38,9 +38,9 @@ namespace llarp // - must end in a 1-bit value: 'o' or 'y' (i.e. 10000 or 00000) // - must have 51 preceeding base32z chars // - thus we get 51*5+1 = 256 bits = 32 bytes of output - if (str.size() != 52 || !oxenmq::is_base32z(str) || !(str.back() == 'o' || str.back() == 'y')) + if (str.size() != 52 || !oxenc::is_base32z(str) || !(str.back() == 'o' || str.back() == 'y')) return false; - oxenmq::from_base32z(str.begin(), str.end(), begin()); + oxenc::from_base32z(str.begin(), str.end(), begin()); return true; } } // namespace llarp diff --git a/llarp/rpc/lokid_rpc_client.cpp b/llarp/rpc/lokid_rpc_client.cpp index ea7122e79..ae76e93c4 100644 --- a/llarp/rpc/lokid_rpc_client.cpp +++ b/llarp/rpc/lokid_rpc_client.cpp @@ -334,8 +334,8 @@ namespace llarp { service::EncryptedName result; const auto j = nlohmann::json::parse(data[1]); - result.ciphertext = oxenmq::from_hex(j["encrypted_value"].get()); - const auto nonce = oxenmq::from_hex(j["nonce"].get()); + result.ciphertext = oxenc::from_hex(j["encrypted_value"].get()); + const auto nonce = oxenc::from_hex(j["nonce"].get()); if (nonce.size() != result.nonce.size()) { throw std::invalid_argument(stringify( @@ -392,7 +392,7 @@ namespace llarp } std::vector routerIdStrings; - oxenmq::bt_deserialize(msg.data[0], routerIdStrings); + oxenc::bt_deserialize(msg.data[0], routerIdStrings); std::vector routerIds; routerIds.reserve(routerIdStrings.size()); diff --git a/llarp/service/address.cpp b/llarp/service/address.cpp index 7166f1ca6..940658c28 100644 --- a/llarp/service/address.cpp +++ b/llarp/service/address.cpp @@ -1,6 +1,6 @@ #include "address.hpp" #include -#include +#include #include namespace llarp::service @@ -26,7 +26,7 @@ namespace llarp::service str = subdomain; str += '.'; } - str += oxenmq::to_base32z(begin(), end()); + str += oxenc::to_base32z(begin(), end()); str += tld; return str; } @@ -56,10 +56,10 @@ namespace llarp::service // - must end in a 1-bit value: 'o' or 'y' (i.e. 10000 or 00000) // - must have 51 preceeding base32z chars // - thus we get 51*5+1 = 256 bits = 32 bytes of output - if (str.size() != 52 || !oxenmq::is_base32z(str) || !(str.back() == 'o' || str.back() == 'y')) + if (str.size() != 52 || !oxenc::is_base32z(str) || !(str.back() == 'o' || str.back() == 'y')) return false; - oxenmq::from_base32z(str.begin(), str.end(), begin()); + oxenc::from_base32z(str.begin(), str.end(), begin()); return true; } diff --git a/llarp/service/endpoint.hpp b/llarp/service/endpoint.hpp index 48308a525..e7364d1f3 100644 --- a/llarp/service/endpoint.hpp +++ b/llarp/service/endpoint.hpp @@ -20,14 +20,12 @@ #include #include #include -#include +#include #include "endpoint_types.hpp" #include "llarp/endpoint_base.hpp" #include "auth.hpp" -#include - #include // minimum time between introset shifts diff --git a/llarp/service/endpoint_state.hpp b/llarp/service/endpoint_state.hpp index 704cba110..c256acf88 100644 --- a/llarp/service/endpoint_state.hpp +++ b/llarp/service/endpoint_state.hpp @@ -17,7 +17,7 @@ #include #include -#include +#include namespace llarp { diff --git a/llarp/service/intro_set.cpp b/llarp/service/intro_set.cpp index ca6e58414..e0deda9e4 100644 --- a/llarp/service/intro_set.cpp +++ b/llarp/service/intro_set.cpp @@ -2,7 +2,7 @@ #include #include -#include +#include namespace llarp::service { @@ -235,9 +235,9 @@ namespace llarp::service try { - oxenmq::bt_deserialize(srvString, SRVs); + oxenc::bt_deserialize(srvString, SRVs); } - catch (const oxenmq::bt_deserialize_invalid& err) + catch (const oxenc::bt_deserialize_invalid& err) { LogError("Error decoding SRV records from IntroSet: ", err.what()); return false; @@ -321,7 +321,7 @@ namespace llarp::service // srv records if (not SRVs.empty()) { - std::string serial = oxenmq::bt_serialize(SRVs); + std::string serial = oxenc::bt_serialize(SRVs); if (!bencode_write_bytestring(buf, "s", 1)) return false; if (!buf->write(serial.begin(), serial.end())) diff --git a/llarp/service/lns_tracker.hpp b/llarp/service/lns_tracker.hpp index 9aa65904f..aba31c509 100644 --- a/llarp/service/lns_tracker.hpp +++ b/llarp/service/lns_tracker.hpp @@ -8,7 +8,7 @@ #include "address.hpp" #include -#include +#include namespace llarp::service { diff --git a/llarp/util/aligned.hpp b/llarp/util/aligned.hpp index d588d154a..713220614 100644 --- a/llarp/util/aligned.hpp +++ b/llarp/util/aligned.hpp @@ -5,7 +5,7 @@ #include #include "printer.hpp" -#include +#include #include #include @@ -72,7 +72,7 @@ namespace llarp friend std::ostream& operator<<(std::ostream& out, const AlignedBuffer& self) { - return out << oxenmq::to_hex(self.begin(), self.end()); + return out << oxenc::to_hex(self.begin(), self.end()); } /// bitwise NOT @@ -270,21 +270,21 @@ namespace llarp std::string ToHex() const { - return oxenmq::to_hex(begin(), end()); + return oxenc::to_hex(begin(), end()); } std::string ShortHex() const { - return oxenmq::to_hex(begin(), begin() + 4); + return oxenc::to_hex(begin(), begin() + 4); } bool FromHex(std::string_view str) { - if (str.size() != 2 * size() || !oxenmq::is_hex(str)) + if (str.size() != 2 * size() || !oxenc::is_hex(str)) return false; - oxenmq::from_hex(str.begin(), str.end(), begin()); + oxenc::from_hex(str.begin(), str.end(), begin()); return true; } diff --git a/llarp/vpn/linux.hpp b/llarp/vpn/linux.hpp index b682ae15b..9f9ffbe0f 100644 --- a/llarp/vpn/linux.hpp +++ b/llarp/vpn/linux.hpp @@ -416,10 +416,10 @@ namespace llarp::vpn if (parts[1].find_first_not_of('0') == std::string::npos and parts[0] != ifname) { const auto& ip = parts[2]; - if ((ip.size() == sizeof(uint32_t) * 2) and oxenmq::is_hex(ip)) + if ((ip.size() == sizeof(uint32_t) * 2) and oxenc::is_hex(ip)) { huint32_t x{}; - oxenmq::from_hex(ip.begin(), ip.end(), reinterpret_cast(&x.h)); + oxenc::from_hex(ip.begin(), ip.end(), reinterpret_cast(&x.h)); gateways.emplace_back(x); } } diff --git a/pybind/llarp/router_id.cpp b/pybind/llarp/router_id.cpp index 2612c531f..1cb2c56b7 100644 --- a/pybind/llarp/router_id.cpp +++ b/pybind/llarp/router_id.cpp @@ -11,9 +11,9 @@ namespace llarp .def( "FromHex", [](RouterID* r, const std::string& hex) { - if (hex.size() != 2 * r->size() || !oxenmq::is_hex(hex)) + if (hex.size() != 2 * r->size() || !oxenc::is_hex(hex)) throw std::runtime_error("FromHex requires a 64-digit hex string"); - oxenmq::from_hex(hex.begin(), hex.end(), r->data()); + oxenc::from_hex(hex.begin(), hex.end(), r->data()); }) .def("__repr__", &RouterID::ToString) .def("__str__", &RouterID::ToString) From 97f4545fd51cdab972657ef2af1340052bfbb62d Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 4 Feb 2022 10:33:54 -0500 Subject: [PATCH 102/202] make CI pipline generate docs with doxygen, doxybook2 and mkdocs. --- .drone.jsonnet | 27 +++++ contrib/ci/drone-static-upload.sh | 4 + docs/CMakeLists.txt | 71 ++++++++--- docs/conf.py.in | 189 ------------------------------ docs/config.json | 9 ++ docs/index.md.in | 7 ++ docs/index.rst | 8 -- docs/mkdocs.yml | 7 ++ 8 files changed, 106 insertions(+), 216 deletions(-) delete mode 100644 docs/conf.py.in create mode 100644 docs/config.json create mode 100644 docs/index.md.in delete mode 100644 docs/index.rst create mode 100644 docs/mkdocs.yml diff --git a/.drone.jsonnet b/.drone.jsonnet index 465c2fc6b..e30202378 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -246,6 +246,28 @@ local mac_builder(name, ], }; +local docs_pipeline(name, image, extra_cmds=[], allow_fail=false) = { + kind: 'pipeline', + type: 'docker', + name: name, + platform: { arch: 'amd64' }, + trigger: { branch: { exclude: ['debian/*', 'ubuntu/*'] } }, + steps: [ + submodules, + { + name: 'build', + image: image, + pull: 'always', + [if allow_fail then 'failure']: 'ignore', + environment: { SSH_KEY: { from_secret: 'SSH_KEY' } }, + commands: [ + 'cmake -S . -B build-docs', + 'make -C build-docs doc', + ] + extra_cmds, + }, + ], +}; + [ { @@ -324,6 +346,11 @@ local mac_builder(name, ], jobs=4), + // documentation builder + docs_pipeline('Documentation', + docker_base + 'docbuilder', + extra_cmds=['UPLOAD_OS=docs ./contrib/ci/drone-static-upload.sh']), + // integration tests debian_pipeline('Router Hive', docker_base + 'ubuntu-lts', diff --git a/contrib/ci/drone-static-upload.sh b/contrib/ci/drone-static-upload.sh index f2b445ddb..1eb8d9989 100755 --- a/contrib/ci/drone-static-upload.sh +++ b/contrib/ci/drone-static-upload.sh @@ -43,6 +43,10 @@ elif [ -e lokinet.apk ] ; then # android af ngl archive="$base.apk" cp -av lokinet.apk "$archive" +elif [ -e build-docs ]; then + archive="$base.tar.xz" + cp -a build-docs/docs/markdown "$base" + tar cJvf "$archive" "$base" else cp -av daemon/lokinet daemon/lokinet-vpn "$base" cp -av ../contrib/bootstrap/mainnet.signed "$base/bootstrap.signed" diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt index c7b8a4fd5..85899e706 100644 --- a/docs/CMakeLists.txt +++ b/docs/CMakeLists.txt @@ -3,37 +3,70 @@ if (NOT DOXYGEN) message(STATUS "Documentation generation disabled (doxygen not found)") return() endif() -find_program(SPHINX_BUILD sphinx-build) -if (NOT SPHINX_BUILD) - message(STATUS "Documentation generation disabled (sphinx-build not found)") + +find_program(MKDOCS mkdocs) +if (NOT MKDOCS) + message(STATUS "Documentation generation disabled (mkdocs not found)") return() - endif() - +endif() + set(lokinet_doc_sources "${DOCS_SRC}") string(REPLACE ";" " " lokinet_doc_sources_spaced "${lokinet_doc_sources}") +add_custom_target(clean_xml COMMAND ${CMAKE_COMMAND} -E rm -rf doxyxml) +add_custom_target(clean_markdown COMMAND ${CMAKE_COMMAND} -E rm -rf markdown) + add_custom_command( OUTPUT doxyxml/index.xml COMMAND ${DOXYGEN} Doxyfile DEPENDS + clean_xml ${CMAKE_CURRENT_BINARY_DIR}/Doxyfile ${lokinet_doc_sources} ) + +if(NOT DOXYBOOK2_ZIP_URL) + set(DOXYBOOK2_VERSION v1.4.0 CACHE STRING "doxybook2 version") + set(DOXYBOOK2_ZIP_URL "https://github.com/matusnovak/doxybook2/releases/download/${DOXYBOOK2_VERSION}/doxybook2-linux-amd64-${DOXYBOOK2_VERSION}.zip") + set(DOXYBOOK2_ZIP_HASH_OPTS EXPECTED_HASH SHA256=bab9356f5daa550cbf21d8d9b554ea59c8be039716a2caf6e96dee52713fccb0) +endif() + +file(DOWNLOAD + ${DOXYBOOK2_ZIP_URL} + ${CMAKE_CURRENT_BINARY_DIR}/doxybook2.zip + ${DOXYBOOK2_ZIP_HASH_OPTS}) + +execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf ${CMAKE_CURRENT_BINARY_DIR}/doxybook2.zip + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + add_custom_command( - OUTPUT html/index.html - COMMAND ${SPHINX_BUILD} -j auto - -Dbreathe_projects.lokinet=${CMAKE_CURRENT_BINARY_DIR}/doxyxml - -Dversion=${lokinet_VERSION} -Drelease=${lokinet_VERSION} - -Aversion=${lokinet_VERSION} -Aversions=${lokinet_VERSION_MAJOR},${lokinet_VERSION_MINOR},${lokinet_VERSION_PATCH} - -b html - ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_BINARY_DIR}/html + OUTPUT markdown + COMMAND ${CMAKE_CURRENT_BINARY_DIR}/bin/doxybook2 --input ${CMAKE_CURRENT_BINARY_DIR}/doxyxml --output ${CMAKE_CURRENT_BINARY_DIR}/markdown --config config.json DEPENDS - ${CMAKE_CURRENT_BINARY_DIR}/index.rst - ${CMAKE_CURRENT_BINARY_DIR}/conf.py - ${CMAKE_CURRENT_BINARY_DIR}/doxyxml/index.xml -) -add_custom_target(doc DEPENDS html/index.html) -configure_file(conf.py.in conf.py @ONLY) + ${CMAKE_CURRENT_BINARY_DIR}/markdown/index.md + ${CMAKE_CURRENT_BINARY_DIR}/config.json + ${CMAKE_CURRENT_BINARY_DIR}/bin/doxybook2 + ${CMAKE_CURRENT_BINARY_DIR}/doxyxml/index.xml) + +add_custom_target(clean_html COMMAND ${CMAKE_COMMAND} -E rm -rf html) + +add_custom_command( + OUTPUT html + COMMAND ${MKDOCS} build + DEPENDS + clean_html + ${CMAKE_CURRENT_BINARY_DIR}/markdown) + +add_custom_target(doc DEPENDS html) + +# we seperate this step out so we force clean_markdown to run before markdown target +add_custom_command( + OUTPUT markdown/index.md + COMMAND ${CMAKE_COMMAND} -E copy index.md markdown/index.md + DEPENDS clean_markdown) + configure_file(Doxyfile.in Doxyfile @ONLY) -configure_file(index.rst index.rst COPYONLY) +configure_file(config.json config.json @ONLY) +configure_file(mkdocs.yml mkdocs.yml @ONLY) +configure_file(index.md.in index.md @ONLY) diff --git a/docs/conf.py.in b/docs/conf.py.in deleted file mode 100644 index e99ba91ec..000000000 --- a/docs/conf.py.in +++ /dev/null @@ -1,189 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Configuration file for the Sphinx documentation builder. -# -# This file does only contain a selection of the most common options. For a -# full list see the documentation: -# http://www.sphinx-doc.org/en/master/config - -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- Project information ----------------------------------------------------- - -project = 'lokinet' -copyright = '2020, Jeff Becker' -author = 'Jeff Becker' - -# The short X.Y version -version = '' -# The full version, including alpha/beta/rc tags -release = '' - - - -# -- General configuration --------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. - -extensions = ['breathe', 'exhale'] -breathe_projects = { "lokinet": "@CMAKE_CURRENT_BINARY_DIR@/doxyxml/" } -breathe_default_project = "lokinet" -breathe_domain_by_extension = {"h" : "cpp", "hpp": "cpp"} - -exhale_args = { - # These arguments are required - "containmentFolder": "./api", - "rootFileName": "lokinet.rst", - "rootFileTitle": "lokinet internals", - "doxygenStripFromPath": "..", - # Suggested optional arguments - "createTreeView": True, - # TIP: if using the sphinx-bootstrap-theme, you need - # "treeViewIsBootstrap": True, -} - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -source_suffix = ['.rst', '.md'] - -# The master toctree document. -master_doc = 'index' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns = [] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' -highlight_language = 'c++' -primary_domain = 'cpp' -default_role = 'cpp:any' -# -- Options for HTML output ------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'sphinx_rtd_theme' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# The default sidebars (for documents that don't match any pattern) are -# defined by theme itself. Builtin themes are using these templates by -# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', -# 'searchbox.html']``. -# -# html_sidebars = {} - - -# -- Options for HTMLHelp output --------------------------------------------- - -# Output file base name for HTML help builder. -htmlhelp_basename = 'lokinetdoc' - - -# -- Options for LaTeX output ------------------------------------------------ - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'lokinet.tex', 'lokinet Documentation', - 'Jeff Becker', 'manual'), -] - - -# -- Options for manual page output ------------------------------------------ - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'lokinet', 'lokinet Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ---------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'lokinet', 'lokinet Documentation', - author, 'lokinet', 'One line description of project.', - 'Miscellaneous'), -] - - -# -- Options for Epub output ------------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = project - -# The unique identifier of the text. This can be a ISBN number -# or the project homepage. -# -# epub_identifier = '' - -# A unique identification for the text. -# -# epub_uid = '' - -# A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] diff --git a/docs/config.json b/docs/config.json new file mode 100644 index 000000000..660ff8d8b --- /dev/null +++ b/docs/config.json @@ -0,0 +1,9 @@ +{ + "baseUrl": "/internals/", + "indexInFolders": false, + "linkSuffix": "/", + "mainPageInRoot": false, + "mainPageName": "index", + "linkLowercase": true, + "foldersToGenerate": ["files", "classes", "namespaces"] +} diff --git a/docs/index.md.in b/docs/index.md.in new file mode 100644 index 000000000..d7fe69d00 --- /dev/null +++ b/docs/index.md.in @@ -0,0 +1,7 @@ +# Lokinet @lokinet_VERSION@ (git rev: @GIT_VERSION@) + +summary goes here + +## Overview + +[code internals](index_namespaces.md) diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index a58c3ac24..000000000 --- a/docs/index.rst +++ /dev/null @@ -1,8 +0,0 @@ - -Welcome to Lokinet Internals -============================ - -.. toctree:: - :maxdepth: 2 - - api/lokinet diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 000000000..6a7e73926 --- /dev/null +++ b/docs/mkdocs.yml @@ -0,0 +1,7 @@ +site_name: Lokinet +theme: + name: 'readthedocs' +markdown_extensions: + - admonition +docs_dir: markdown +site_dir: html From 283eabd1a88f1c50627492feaed5f65981d7ae48 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 4 Feb 2022 14:31:16 -0500 Subject: [PATCH 103/202] use COPYONLY for the non templated configure_file targets --- docs/CMakeLists.txt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt index 85899e706..677de943c 100644 --- a/docs/CMakeLists.txt +++ b/docs/CMakeLists.txt @@ -60,13 +60,14 @@ add_custom_command( add_custom_target(doc DEPENDS html) +configure_file(Doxyfile.in Doxyfile @ONLY) +configure_file(index.md.in index.md @ONLY) + +configure_file(config.json config.json COPYONLY) +configure_file(mkdocs.yml mkdocs.yml COPYONLY) + # we seperate this step out so we force clean_markdown to run before markdown target add_custom_command( OUTPUT markdown/index.md COMMAND ${CMAKE_COMMAND} -E copy index.md markdown/index.md DEPENDS clean_markdown) - -configure_file(Doxyfile.in Doxyfile @ONLY) -configure_file(config.json config.json @ONLY) -configure_file(mkdocs.yml mkdocs.yml @ONLY) -configure_file(index.md.in index.md @ONLY) From c70f1866f8e53c51c2e331127458266a0be9ad47 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 4 Feb 2022 14:37:13 -0500 Subject: [PATCH 104/202] include mkdocs.yml in ci artifacts for docs --- contrib/ci/drone-static-upload.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/ci/drone-static-upload.sh b/contrib/ci/drone-static-upload.sh index 1eb8d9989..9449ff6dd 100755 --- a/contrib/ci/drone-static-upload.sh +++ b/contrib/ci/drone-static-upload.sh @@ -45,7 +45,7 @@ elif [ -e lokinet.apk ] ; then cp -av lokinet.apk "$archive" elif [ -e build-docs ]; then archive="$base.tar.xz" - cp -a build-docs/docs/markdown "$base" + cp -av build-docs/docs/mkdocs.yml build-docs/docs/markdown "$base" tar cJvf "$archive" "$base" else cp -av daemon/lokinet daemon/lokinet-vpn "$base" From bebfcbdba203d42fd4ad7520f2167b0200076ec1 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 4 Feb 2022 14:38:50 -0500 Subject: [PATCH 105/202] move documentation builder closer to the top of the ci jobs --- .drone.jsonnet | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index e30202378..443561cfd 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -287,6 +287,10 @@ local docs_pipeline(name, image, extra_cmds=[], allow_fail=false) = { ], }], }, + // documentation builder + docs_pipeline('Documentation', + docker_base + 'docbuilder', + extra_cmds=['UPLOAD_OS=docs ./contrib/ci/drone-static-upload.sh']), // Various debian builds debian_pipeline('Debian sid (amd64)', docker_base + 'debian-sid'), @@ -346,11 +350,6 @@ local docs_pipeline(name, image, extra_cmds=[], allow_fail=false) = { ], jobs=4), - // documentation builder - docs_pipeline('Documentation', - docker_base + 'docbuilder', - extra_cmds=['UPLOAD_OS=docs ./contrib/ci/drone-static-upload.sh']), - // integration tests debian_pipeline('Router Hive', docker_base + 'ubuntu-lts', From c39bd827d0e36a03a6d1eea8097d00938043ed04 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 4 Feb 2022 14:46:00 -0500 Subject: [PATCH 106/202] remove markdown extension that is not required --- docs/mkdocs.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 6a7e73926..4ba3d76cd 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -1,7 +1,5 @@ site_name: Lokinet theme: name: 'readthedocs' -markdown_extensions: - - admonition docs_dir: markdown site_dir: html From 6bb438ca332bd2125a4e84e9a73e12b96a797522 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 4 Feb 2022 15:00:10 -0500 Subject: [PATCH 107/202] make comments with colins in them not have them in it --- docs/config.json | 2 +- llarp/crypto/encrypted_frame.hpp | 1 - llarp/net/ip_range_map.hpp | 4 ++-- llarp/service/context.hpp | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/config.json b/docs/config.json index 660ff8d8b..06b4a34c9 100644 --- a/docs/config.json +++ b/docs/config.json @@ -1,5 +1,5 @@ { - "baseUrl": "/internals/", + "baseUrl": "/lokinet/", "indexInFolders": false, "linkSuffix": "/", "mainPageInRoot": false, diff --git a/llarp/crypto/encrypted_frame.hpp b/llarp/crypto/encrypted_frame.hpp index 7731d74e5..3bdd58b68 100644 --- a/llarp/crypto/encrypted_frame.hpp +++ b/llarp/crypto/encrypted_frame.hpp @@ -45,7 +45,6 @@ namespace llarp EncryptInPlace(const SecretKey& seckey, const PubKey& other); }; - /// TODO: can only handle 1 frame at a time template struct AsyncFrameDecrypter { diff --git a/llarp/net/ip_range_map.hpp b/llarp/net/ip_range_map.hpp index a5644e78c..bf659ae78 100644 --- a/llarp/net/ip_range_map.hpp +++ b/llarp/net/ip_range_map.hpp @@ -10,8 +10,8 @@ namespace llarp { /// a container that maps an ip range to a value that allows you to lookup /// key by range hit - /// TODO: do some kind of magic shit to ensure near constant time for - /// lookups + /// + /// in the future we should do some kind of magic shit to ensure near constant time for lookups template struct IPRangeMap { diff --git a/llarp/service/context.hpp b/llarp/service/context.hpp index a3ef35f3f..3694d9772 100644 --- a/llarp/service/context.hpp +++ b/llarp/service/context.hpp @@ -12,7 +12,7 @@ namespace llarp namespace service { /// holds all the hidden service endpoints we own - /// TODO: this should be refactored (removed entirely...?) now that lokinet + /// this should be refactored (removed entirely...?) now that lokinet /// only supports one endpoint per instance struct Context { From 54f431c9e802fc66fa2d6dc020c27693549202b0 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 4 Feb 2022 15:52:03 -0500 Subject: [PATCH 108/202] make doxybook2 run from PATH if installed on our system --- docs/CMakeLists.txt | 27 ++++++++++++++++----------- docs/config.json | 2 +- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt index 677de943c..7db7d921e 100644 --- a/docs/CMakeLists.txt +++ b/docs/CMakeLists.txt @@ -25,28 +25,33 @@ add_custom_command( ${lokinet_doc_sources} ) +# find doxybook2 +find_program(DOXYBOOK2 doxybook2) +if(NOT DOXYBOOK2) + if(NOT DOXYBOOK2_ZIP_URL) + set(DOXYBOOK2_VERSION v1.4.0 CACHE STRING "doxybook2 version") + set(DOXYBOOK2_ZIP_URL "https://github.com/matusnovak/doxybook2/releases/download/${DOXYBOOK2_VERSION}/doxybook2-linux-amd64-${DOXYBOOK2_VERSION}.zip") + set(DOXYBOOK2_ZIP_HASH_OPTS EXPECTED_HASH SHA256=bab9356f5daa550cbf21d8d9b554ea59c8be039716a2caf6e96dee52713fccb0) + endif() -if(NOT DOXYBOOK2_ZIP_URL) - set(DOXYBOOK2_VERSION v1.4.0 CACHE STRING "doxybook2 version") - set(DOXYBOOK2_ZIP_URL "https://github.com/matusnovak/doxybook2/releases/download/${DOXYBOOK2_VERSION}/doxybook2-linux-amd64-${DOXYBOOK2_VERSION}.zip") - set(DOXYBOOK2_ZIP_HASH_OPTS EXPECTED_HASH SHA256=bab9356f5daa550cbf21d8d9b554ea59c8be039716a2caf6e96dee52713fccb0) -endif() - -file(DOWNLOAD + file(DOWNLOAD ${DOXYBOOK2_ZIP_URL} ${CMAKE_CURRENT_BINARY_DIR}/doxybook2.zip ${DOXYBOOK2_ZIP_HASH_OPTS}) -execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf ${CMAKE_CURRENT_BINARY_DIR}/doxybook2.zip - WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + execute_process(COMMAND ${CMAKE_COMMAND} -E tar xf ${CMAKE_CURRENT_BINARY_DIR}/doxybook2.zip + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) + set(DOXYBOOK2 ${CMAKE_CURRENT_BINARY_DIR}/bin/doxybook2) + set(doxybook_localbin ${DOXYBOOK2}) +endif() add_custom_command( OUTPUT markdown - COMMAND ${CMAKE_CURRENT_BINARY_DIR}/bin/doxybook2 --input ${CMAKE_CURRENT_BINARY_DIR}/doxyxml --output ${CMAKE_CURRENT_BINARY_DIR}/markdown --config config.json + COMMAND ${DOXYBOOK2} --input ${CMAKE_CURRENT_BINARY_DIR}/doxyxml --output ${CMAKE_CURRENT_BINARY_DIR}/markdown --config config.json DEPENDS + ${doxybook_localbin} ${CMAKE_CURRENT_BINARY_DIR}/markdown/index.md ${CMAKE_CURRENT_BINARY_DIR}/config.json - ${CMAKE_CURRENT_BINARY_DIR}/bin/doxybook2 ${CMAKE_CURRENT_BINARY_DIR}/doxyxml/index.xml) add_custom_target(clean_html COMMAND ${CMAKE_COMMAND} -E rm -rf html) diff --git a/docs/config.json b/docs/config.json index 06b4a34c9..7123a48c6 100644 --- a/docs/config.json +++ b/docs/config.json @@ -1,5 +1,5 @@ { - "baseUrl": "/lokinet/", + "baseUrl": "", "indexInFolders": false, "linkSuffix": "/", "mainPageInRoot": false, From 0fe716e1dddc8b9fe172057b2b043b5e1b3901ef Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 19 Feb 2022 16:08:09 -0500 Subject: [PATCH 109/202] more docs --- docs/CMakeLists.txt | 18 ++++++++++++------ docs/config.json | 4 ++-- docs/fix-markdown.sh | 6 ++++++ 3 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 docs/fix-markdown.sh diff --git a/docs/CMakeLists.txt b/docs/CMakeLists.txt index 7db7d921e..4daf79b6c 100644 --- a/docs/CMakeLists.txt +++ b/docs/CMakeLists.txt @@ -46,16 +46,22 @@ if(NOT DOXYBOOK2) endif() add_custom_command( - OUTPUT markdown - COMMAND ${DOXYBOOK2} --input ${CMAKE_CURRENT_BINARY_DIR}/doxyxml --output ${CMAKE_CURRENT_BINARY_DIR}/markdown --config config.json + OUTPUT gen + COMMAND ${DOXYBOOK2} --input ${CMAKE_CURRENT_BINARY_DIR}/doxyxml --output ${CMAKE_CURRENT_BINARY_DIR}/gen --config config.json DEPENDS ${doxybook_localbin} - ${CMAKE_CURRENT_BINARY_DIR}/markdown/index.md + ${CMAKE_CURRENT_BINARY_DIR}/gen/index.md ${CMAKE_CURRENT_BINARY_DIR}/config.json ${CMAKE_CURRENT_BINARY_DIR}/doxyxml/index.xml) add_custom_target(clean_html COMMAND ${CMAKE_COMMAND} -E rm -rf html) +add_custom_command( + OUTPUT markdown + 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 + DEPENDS gen +) + add_custom_command( OUTPUT html COMMAND ${MKDOCS} build @@ -63,7 +69,7 @@ add_custom_command( clean_html ${CMAKE_CURRENT_BINARY_DIR}/markdown) -add_custom_target(doc DEPENDS html) +add_custom_target(doc DEPENDS markdown) configure_file(Doxyfile.in Doxyfile @ONLY) configure_file(index.md.in index.md @ONLY) @@ -73,6 +79,6 @@ configure_file(mkdocs.yml mkdocs.yml COPYONLY) # we seperate this step out so we force clean_markdown to run before markdown target add_custom_command( - OUTPUT markdown/index.md - COMMAND ${CMAKE_COMMAND} -E copy index.md markdown/index.md + OUTPUT gen/index.md + COMMAND ${CMAKE_COMMAND} -E copy index.md gen/index.md DEPENDS clean_markdown) diff --git a/docs/config.json b/docs/config.json index 7123a48c6..a160cbfba 100644 --- a/docs/config.json +++ b/docs/config.json @@ -1,9 +1,9 @@ { "baseUrl": "", "indexInFolders": false, - "linkSuffix": "/", + "linkSuffix": ".md", "mainPageInRoot": false, "mainPageName": "index", - "linkLowercase": true, + "linkLowercase": false, "foldersToGenerate": ["files", "classes", "namespaces"] } diff --git a/docs/fix-markdown.sh b/docs/fix-markdown.sh new file mode 100644 index 000000000..23f35a31d --- /dev/null +++ b/docs/fix-markdown.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# apply markdown file content quarks + + +# rewrite br tags +sed -i 's|
|
|g' $@ From ad6d206aa6a090c72b1172baa7e8f1a129e5636b Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Mon, 21 Feb 2022 10:31:44 +1100 Subject: [PATCH 110/202] add version and uptime to summary status endpoint --- llarp/router/router.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index 6c9a31989..ca981315f 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -182,6 +182,8 @@ namespace llarp return util::StatusObject{ {"running", true}, + {"version", llarp::VERSION_FULL}, + {"uptime", to_json(Uptime())}, {"authCodes", services["default"]["authCodes"]}, {"exitMap", services["default"]["exitMap"]}, {"lokiAddress", services["default"]["identity"]}, From 41405be612db6790b27e6248c22bf671318b85ec Mon Sep 17 00:00:00 2001 From: audric Date: Wed, 23 Feb 2022 14:21:47 +1100 Subject: [PATCH 111/202] summary status: no services means not running --- llarp/router/router.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index ca981315f..02862f00d 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -116,6 +116,9 @@ namespace llarp return util::StatusObject{{"running", false}}; auto services = _hiddenServiceContext.ExtractStatus(); + if (services.is_null()) + return util::StatusObject{{"running", false}}; + auto link_types = _linkManager.ExtractStatus(); uint64_t tx_rate = 0; From eeb93343c00a7331f16273714c85a28a66c8d431 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 23 Feb 2022 09:21:38 -0500 Subject: [PATCH 112/202] rpc fixes for lokinet gui only add stats for services when we have them --- llarp/router/router.cpp | 57 ++++++++++++++++++++++------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index 02862f00d..fea94dafb 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -95,18 +95,17 @@ namespace llarp util::StatusObject Router::ExtractStatus() const { - if (_running) - { - return util::StatusObject{ - {"running", true}, - {"numNodesKnown", _nodedb->NumLoaded()}, - {"dht", _dht->impl->ExtractStatus()}, - {"services", _hiddenServiceContext.ExtractStatus()}, - {"exit", _exitContext.ExtractStatus()}, - {"links", _linkManager.ExtractStatus()}, - {"outboundMessages", _outboundMessageHandler.ExtractStatus()}}; - } - return util::StatusObject{{"running", false}}; + if (not _running) + util::StatusObject{{"running", false}}; + + return util::StatusObject{ + {"running", true}, + {"numNodesKnown", _nodedb->NumLoaded()}, + {"dht", _dht->impl->ExtractStatus()}, + {"services", _hiddenServiceContext.ExtractStatus()}, + {"exit", _exitContext.ExtractStatus()}, + {"links", _linkManager.ExtractStatus()}, + {"outboundMessages", _outboundMessageHandler.ExtractStatus()}}; } util::StatusObject @@ -116,8 +115,6 @@ namespace llarp return util::StatusObject{{"running", false}}; auto services = _hiddenServiceContext.ExtractStatus(); - if (services.is_null()) - return util::StatusObject{{"running", false}}; auto link_types = _linkManager.ExtractStatus(); @@ -143,16 +140,19 @@ namespace llarp // Merge snodeSessions, remoteSessions and default into a single array std::vector builders; - const auto& serviceDefault = services.at("default"); - builders.push_back(serviceDefault); + if (services.is_object()) + { + const auto& serviceDefault = services.at("default"); + builders.push_back(serviceDefault); - auto snode_sessions = serviceDefault.at("snodeSessions"); - for (const auto& session : snode_sessions) - builders.push_back(session); + auto snode_sessions = serviceDefault.at("snodeSessions"); + for (const auto& session : snode_sessions) + builders.push_back(session); - auto remote_sessions = serviceDefault.at("remoteSessions"); - for (const auto& session : remote_sessions) - builders.push_back(session); + auto remote_sessions = serviceDefault.at("remoteSessions"); + for (const auto& session : remote_sessions) + builders.push_back(session); + } // Iterate over all items on this array to build the global pathStats uint64_t pathsCount = 0; @@ -183,13 +183,10 @@ namespace llarp } double ratio = static_cast(success) / (attempts + 1); - return util::StatusObject{ + util::StatusObject stats{ {"running", true}, {"version", llarp::VERSION_FULL}, {"uptime", to_json(Uptime())}, - {"authCodes", services["default"]["authCodes"]}, - {"exitMap", services["default"]["exitMap"]}, - {"lokiAddress", services["default"]["identity"]}, {"numPathsBuilt", pathsCount}, {"numPeersConnected", peers}, {"numRoutersKnown", _nodedb->NumLoaded()}, @@ -197,6 +194,14 @@ namespace llarp {"txRate", tx_rate}, {"rxRate", rx_rate}, }; + + if (services.is_object()) + { + stats["authCodes"] = services["default"]["authCodes"]; + stats["exitMap"] = services["default"]["exitMap"]; + stats["lokiAddress"] = services["default"]["identity"]; + } + return stats; } bool From ae96458f8ab1f1a34c44a811d3cdc502c11adae6 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sun, 27 Feb 2022 11:07:50 -0500 Subject: [PATCH 113/202] remove old dead code --- contrib/bootserv/.gitignore | 1 - .../config/lokinet-bootserv-nginx.conf | 29 -- contrib/bootserv/config/lokinet-bootserv.ini | 4 - contrib/bootserv/makefile | 20 -- contrib/bootserv/readme.md | 35 --- contrib/bootserv/src/cgi.cpp | 171 ----------- contrib/bootserv/src/handler.hpp | 43 --- contrib/bootserv/src/lokinet-cgi.hpp | 31 -- contrib/bootserv/src/lokinet-config.cpp | 132 --------- contrib/bootserv/src/lokinet-config.hpp | 47 --- contrib/bootserv/src/lokinet-cron.cpp | 37 --- contrib/bootserv/src/lokinet-cron.hpp | 25 -- contrib/bootserv/src/main.cpp | 60 ---- contrib/dtrace/lokinet.xml | 60 ---- contrib/dtrace/profiler.d | 18 -- contrib/freebsd/openrc/lokinet.rc | 20 -- contrib/munin/lokinet-munin.py | 79 ----- contrib/py/pylokinet/pylokinet/__init__.py | 0 contrib/py/pylokinet/pylokinet/__main__.py | 4 - contrib/py/pylokinet/pylokinet/bencode.py | 111 ------- contrib/py/pylokinet/pylokinet/bootserv.py | 278 ------------------ contrib/py/pylokinet/pylokinet/instance.py | 224 -------------- contrib/py/pylokinet/pylokinet/rc.py | 31 -- contrib/py/pylokinet/readme.md | 27 -- contrib/py/pylokinet/setup.py | 14 - contrib/py/vanity/.gitignore | 2 - contrib/py/vanity/bencode.py | 112 ------- contrib/py/vanity/lokinet-vanity.py | 138 --------- contrib/py/vanity/readme.md | 10 - contrib/py/vanity/requirements.txt | 1 - contrib/shadow/genconf.py | 141 --------- contrib/testnet/.gitignore | 1 - contrib/testnet/genconf.py | 157 ---------- contrib/testnet/readme.md | 23 -- contrib/testnet/requirements.txt | 2 - contrib/testnet/testnet.sh | 15 - 36 files changed, 2103 deletions(-) delete mode 100644 contrib/bootserv/.gitignore delete mode 100644 contrib/bootserv/config/lokinet-bootserv-nginx.conf delete mode 100644 contrib/bootserv/config/lokinet-bootserv.ini delete mode 100644 contrib/bootserv/makefile delete mode 100644 contrib/bootserv/readme.md delete mode 100644 contrib/bootserv/src/cgi.cpp delete mode 100644 contrib/bootserv/src/handler.hpp delete mode 100644 contrib/bootserv/src/lokinet-cgi.hpp delete mode 100644 contrib/bootserv/src/lokinet-config.cpp delete mode 100644 contrib/bootserv/src/lokinet-config.hpp delete mode 100644 contrib/bootserv/src/lokinet-cron.cpp delete mode 100644 contrib/bootserv/src/lokinet-cron.hpp delete mode 100644 contrib/bootserv/src/main.cpp delete mode 100644 contrib/dtrace/lokinet.xml delete mode 100644 contrib/dtrace/profiler.d delete mode 100644 contrib/freebsd/openrc/lokinet.rc delete mode 100644 contrib/munin/lokinet-munin.py delete mode 100644 contrib/py/pylokinet/pylokinet/__init__.py delete mode 100644 contrib/py/pylokinet/pylokinet/__main__.py delete mode 100644 contrib/py/pylokinet/pylokinet/bencode.py delete mode 100644 contrib/py/pylokinet/pylokinet/bootserv.py delete mode 100644 contrib/py/pylokinet/pylokinet/instance.py delete mode 100644 contrib/py/pylokinet/pylokinet/rc.py delete mode 100644 contrib/py/pylokinet/readme.md delete mode 100644 contrib/py/pylokinet/setup.py delete mode 100644 contrib/py/vanity/.gitignore delete mode 100644 contrib/py/vanity/bencode.py delete mode 100644 contrib/py/vanity/lokinet-vanity.py delete mode 100644 contrib/py/vanity/readme.md delete mode 100644 contrib/py/vanity/requirements.txt delete mode 100644 contrib/shadow/genconf.py delete mode 100644 contrib/testnet/.gitignore delete mode 100755 contrib/testnet/genconf.py delete mode 100644 contrib/testnet/readme.md delete mode 100644 contrib/testnet/requirements.txt delete mode 100755 contrib/testnet/testnet.sh diff --git a/contrib/bootserv/.gitignore b/contrib/bootserv/.gitignore deleted file mode 100644 index 0c119ef3e..000000000 --- a/contrib/bootserv/.gitignore +++ /dev/null @@ -1 +0,0 @@ -lokinet-bootserv \ No newline at end of file diff --git a/contrib/bootserv/config/lokinet-bootserv-nginx.conf b/contrib/bootserv/config/lokinet-bootserv-nginx.conf deleted file mode 100644 index 4fe24ee47..000000000 --- a/contrib/bootserv/config/lokinet-bootserv-nginx.conf +++ /dev/null @@ -1,29 +0,0 @@ -# replace your.server.tld with your server's fqdn - -server { - listen 80; - server_name your.server.tld; - location / { - return 302 https://your.server.tld$request_uri; - } - location /.well-known/acme-challenge { - root /var/www/letsencrypt; - } -} - -server { - listen 443 ssl; - server_name your.server.tld; - ssl_certificate /etc/letsencrypt/live/your.server.tld/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/your.server.tld/privkey.pem; - - location / { - root /var/www/lokinet-bootserv; - } - - location /bootstrap.signed { - include /etc/nginx/fastcgi_params; - fastcgi_param SCRIPT_FILENAME /usr/local/bin/lokinet-bootserv; - fastcgi_pass unix://tmp/cgi.sock; - } -} \ No newline at end of file diff --git a/contrib/bootserv/config/lokinet-bootserv.ini b/contrib/bootserv/config/lokinet-bootserv.ini deleted file mode 100644 index 1c1eda45b..000000000 --- a/contrib/bootserv/config/lokinet-bootserv.ini +++ /dev/null @@ -1,4 +0,0 @@ - -# set me to where the nodedb is for lokinet -#[nodedb] -#dir=/path/to/nodedb/ diff --git a/contrib/bootserv/makefile b/contrib/bootserv/makefile deleted file mode 100644 index 081016c40..000000000 --- a/contrib/bootserv/makefile +++ /dev/null @@ -1,20 +0,0 @@ - -SRC = $(sort $(wildcard src/*.cpp)) -OBJS = $(SRC:.cpp=.cpp.o) - -CGI_EXE = lokinet-bootserv - -CXX = clang++ - -all: build - -build: $(CGI_EXE) - -%.cpp.o: %.cpp - $(CXX) -g -std=c++17 -c -Wall -Werror -Wpedantic $^ -o $^.o - -$(CGI_EXE): $(OBJS) - $(CXX) -o $(CGI_EXE) $^ - -clean: - rm -f $(CGI_EXE) $(OBJS) diff --git a/contrib/bootserv/readme.md b/contrib/bootserv/readme.md deleted file mode 100644 index 572ba50f8..000000000 --- a/contrib/bootserv/readme.md +++ /dev/null @@ -1,35 +0,0 @@ -# lokinet-bootserv - -cgi executable for serving a random RC for bootstrap from a nodedb - -## configuring - -copy the example config (privileged) - - # cp configs/lokinet-bootserv.ini /usr/local/etc/lokinet-bootserv.ini - -edit config to have proper values, -specifically make sure the `[nodedb]` section has a `dir` value that points to a static copy of a healthy nodedb - -## building - -to build: - - $ make - -## installing (priviledged) - -install cgi binary: - - # cp lokinet-bootserv /usr/local/bin/lokinet-bootserv - -set up with nginx cgi: - - # cp configs/lokinet-bootserv-nginx.conf /etc/nginx/sites-available/lokinet-bootserv.conf - # ln -s /etc/nginx/sites-available/lokinet-bootserv.conf /etc/nginx/sites-enabled/ - -## maintainence - -add the following to crontab - - 0 0 * * * /usr/local/bin/lokinet-bootserv --cron diff --git a/contrib/bootserv/src/cgi.cpp b/contrib/bootserv/src/cgi.cpp deleted file mode 100644 index 46482ab7c..000000000 --- a/contrib/bootserv/src/cgi.cpp +++ /dev/null @@ -1,171 +0,0 @@ -#include "lokinet-cgi.hpp" -#include -#include -#include -#include - -namespace lokinet -{ - namespace bootserv - { - CGIHandler::CGIHandler(std::ostream& o) : Handler(o) - { - } - - CGIHandler::~CGIHandler() - { - } - - int - CGIHandler::Exec(const Config& conf) - { - const char* e = getenv("REQUEST_METHOD"); - if(e == nullptr) - return ReportError("$REQUEST_METHOD not set"); - std::string_view method(e); - - if(method != "GET") - { - out << "Content-Type: text/plain" << std::endl; - out << "Status: 405 Method Not Allowed" << std::endl << std::endl; - return 0; - } - - std::string fname; - if(!conf.VisitSection( - "nodedb", [&](const Config::Section_t& sect) -> bool { - auto itr = sect.find("dir"); - if(itr == sect.end()) - return false; - fname = PickRandomFileInDir( - std::string(itr->second.data(), itr->second.size())); - return true; - })) - - return ReportError("bad values in nodedb section of config"); - if(fname.empty()) - { - // no files in nodedb - out << "Content-Type: text/plain" << std::endl; - out << "Status: 404 Not Found" << std::endl << std::endl; - return 0; - } - return ServeFile(fname.c_str(), "application/octect-stream"); - } - - std::string - CGIHandler::PickRandomFileInDir(std::string dirname) const - { - // collect files - std::list< std::string > files; - { - DIR* d = opendir(dirname.c_str()); - if(d == nullptr) - { - return ""; - }; - std::list< std::string > subdirs; - dirent* ent = nullptr; - while((ent = readdir(d))) - { - std::string_view f = ent->d_name; - if(f != "." && f != "..") - { - std::stringstream ss; - ss << dirname; - ss << '/'; - ss << f; - subdirs.emplace_back(ss.str()); - } - } - closedir(d); - for(const auto& subdir : subdirs) - { - d = opendir(subdir.c_str()); - if(d) - { - while((ent = readdir(d))) - { - std::string_view f; - f = ent->d_name; - if(f != "." && f != ".." - && f.find_last_of(".signed") != std::string_view::npos) - { - std::stringstream ss; - ss << subdir << "/" << f; - files.emplace_back(ss.str()); - } - } - closedir(d); - } - } - } - uint32_t randint; - { - std::basic_ifstream< uint32_t > randf("/dev/urandom"); - if(!randf.is_open()) - return ""; - randf.read(&randint, 1); - } - auto itr = files.begin(); - if(files.size() > 1) - std::advance(itr, randint % files.size()); - return *itr; - } - - int - CGIHandler::ServeFile(const char* fname, const char* contentType) const - { - std::ifstream f(fname); - if(f.is_open()) - { - f.seekg(0, std::ios::end); - auto sz = f.tellg(); - f.seekg(0, std::ios::beg); - if(sz) - { - out << "Content-Type: " << contentType << std::endl; - out << "Status: 200 OK" << std::endl; - out << "Content-Length: " << std::to_string(sz) << std::endl - << std::endl; - char buf[512] = {0}; - size_t r = 0; - while((r = f.readsome(buf, sizeof(buf))) > 0) - out.write(buf, r); - out << std::flush; - } - else - { - out << "Content-Type: text/plain" << std::endl; - out << "Status: 500 Internal Server Error" << std::endl << std::endl; - out << "could not serve '" << fname << "' as it is an empty file" - << std::endl; - } - } - else - { - out << "Content-Type: text/plain" << std::endl; - out << "Status: 404 Not Found" << std::endl << std::endl; - out << "could not serve '" << fname - << "' as it does not exist on the filesystem" << std::endl; - } - return 0; - } - - int - CGIHandler::ReportError(const char* err) - { - out << "Content-Type: text/plain" << std::endl; - out << "Status: 500 Internal Server Error" << std::endl << std::endl; - out << err << std::endl; - return 0; - } - - Handler_ptr - NewCGIHandler(std::ostream& out) - { - return std::make_unique< CGIHandler >(out); - } - - } // namespace bootserv -} // namespace lokinet diff --git a/contrib/bootserv/src/handler.hpp b/contrib/bootserv/src/handler.hpp deleted file mode 100644 index 7327e900c..000000000 --- a/contrib/bootserv/src/handler.hpp +++ /dev/null @@ -1,43 +0,0 @@ -#ifndef LOKINET_BOOTSERV_HANDLER_HPP -#define LOKINET_BOOTSERV_HANDLER_HPP -#include -#include "lokinet-config.hpp" - -namespace lokinet -{ - namespace bootserv - { - struct Handler - { - Handler(std::ostream& o) : out(o){}; - - virtual ~Handler(){}; - - /// handle command - /// return exit code - virtual int - Exec(const Config& conf) = 0; - - /// report an error to system however that is done - /// return exit code - virtual int - ReportError(const char* err) = 0; - - protected: - std::ostream& out; - }; - - using Handler_ptr = std::unique_ptr< Handler >; - - /// create cgi handler - Handler_ptr - NewCGIHandler(std::ostream& out); - - /// create cron handler - Handler_ptr - NewCronHandler(std::ostream& out); - - } // namespace bootserv -} // namespace lokinet - -#endif diff --git a/contrib/bootserv/src/lokinet-cgi.hpp b/contrib/bootserv/src/lokinet-cgi.hpp deleted file mode 100644 index 032bf3d3d..000000000 --- a/contrib/bootserv/src/lokinet-cgi.hpp +++ /dev/null @@ -1,31 +0,0 @@ -#ifndef BOOTSERV_LOKINET_CRON_HPP -#define BOOTSERV_LOKINET_CRON_HPP - -#include "handler.hpp" - -namespace lokinet -{ - namespace bootserv - { - struct CGIHandler final : public Handler - { - CGIHandler(std::ostream& o); - ~CGIHandler(); - - int - Exec(const Config& conf) override; - - int - ReportError(const char* err) override; - - int - ServeFile(const char* fname, const char* mime) const; - - std::string - PickRandomFileInDir(std::string dirname) const; - }; - - } // namespace bootserv -} // namespace lokinet - -#endif diff --git a/contrib/bootserv/src/lokinet-config.cpp b/contrib/bootserv/src/lokinet-config.cpp deleted file mode 100644 index 155fb66f7..000000000 --- a/contrib/bootserv/src/lokinet-config.cpp +++ /dev/null @@ -1,132 +0,0 @@ -#include "lokinet-config.hpp" -#include -#include -#include - -namespace lokinet -{ - namespace bootserv - { - const char* Config::DefaultPath = "/usr/local/etc/lokinet-bootserv.ini"; - - bool - Config::LoadFile(const char* fname) - { - { - std::ifstream f(fname); - if(!f.is_open()) - return false; - f.seekg(0, std::ios::end); - m_Data.resize(f.tellg()); - f.seekg(0, std::ios::beg); - if(m_Data.size() == 0) - return false; - f.read(m_Data.data(), m_Data.size()); - } - return Parse(); - } - - void - Config::Clear() - { - m_Config.clear(); - m_Data.clear(); - } - - bool - Config::Parse() - { - std::list< String_t > lines; - { - auto itr = m_Data.begin(); - // split into lines - while(itr != m_Data.end()) - { - auto beg = itr; - while(itr != m_Data.end() && *itr != '\n' && *itr != '\r') - ++itr; - lines.emplace_back(std::addressof(*beg), (itr - beg)); - if(itr == m_Data.end()) - break; - ++itr; - } - } - - String_t sectName; - - for(const auto& line : lines) - { - String_t realLine; - auto comment = line.find_first_of(';'); - if(comment == String_t::npos) - comment = line.find_first_of('#'); - if(comment == String_t::npos) - realLine = line; - else - realLine = line.substr(0, comment); - // blank or commented line? - if(realLine.size() == 0) - continue; - // find delimiters - auto sectOpenPos = realLine.find_first_of('['); - auto sectClosPos = realLine.find_first_of(']'); - auto kvDelim = realLine.find_first_of('='); - if(sectOpenPos != String_t::npos && sectClosPos != String_t::npos - && kvDelim == String_t::npos) - { - // section header - - // clamp whitespaces - ++sectOpenPos; - while(std::isspace(realLine[sectOpenPos]) - && sectOpenPos != sectClosPos) - ++sectOpenPos; - --sectClosPos; - while(std::isspace(realLine[sectClosPos]) - && sectClosPos != sectOpenPos) - --sectClosPos; - // set section name - sectName = realLine.substr(sectOpenPos, sectClosPos); - } - else if(kvDelim != String_t::npos) - { - // key value pair - String_t::size_type k_start = 0; - String_t::size_type k_end = kvDelim; - String_t::size_type v_start = kvDelim + 1; - String_t::size_type v_end = realLine.size() - 1; - // clamp whitespaces - while(std::isspace(realLine[k_start]) && k_start != kvDelim) - ++k_start; - while(std::isspace(realLine[k_end]) && k_end != k_start) - --k_end; - while(std::isspace(realLine[v_start]) && v_start != v_end) - ++v_start; - while(std::isspace(realLine[v_end])) - --v_end; - - // sect.k = v - String_t k = realLine.substr(k_start, k_end); - String_t v = realLine.substr(v_start, v_end); - Section_t& sect = m_Config[sectName]; - sect[k] = v; - } - else // malformed? - return false; - } - return true; - } - - bool - Config::VisitSection( - const char* name, - std::function< bool(const Section_t& sect) > visit) const - { - auto itr = m_Config.find(name); - if(itr == m_Config.end()) - return false; - return visit(itr->second); - } - - } // namespace bootserv -} // namespace lokinet diff --git a/contrib/bootserv/src/lokinet-config.hpp b/contrib/bootserv/src/lokinet-config.hpp deleted file mode 100644 index caad9a2bc..000000000 --- a/contrib/bootserv/src/lokinet-config.hpp +++ /dev/null @@ -1,47 +0,0 @@ -#ifndef LOKINET_BOOTSERV_CONFIG_HPP -#define LOKINET_BOOTSERV_CONFIG_HPP -#include -#include -#include -#include -#include - -namespace lokinet -{ - namespace bootserv - { - struct Config - { - using String_t = std::string_view; - using Section_t = std::unordered_map< String_t, String_t >; - using Config_impl_t = std::unordered_map< String_t, Section_t >; - - static const char* DefaultPath; - - /// clear config - void - Clear(); - - /// load config file for bootserv - /// return true on success - /// return false on error - bool - LoadFile(const char* fname); - - /// visit a section in config read only by name - /// return false if no section or value propagated from visitor - bool - VisitSection(const char* name, - std::function< bool(const Section_t&) > visit) const; - - private: - bool - Parse(); - - std::vector< char > m_Data; - Config_impl_t m_Config; - }; - } // namespace bootserv -} // namespace lokinet - -#endif diff --git a/contrib/bootserv/src/lokinet-cron.cpp b/contrib/bootserv/src/lokinet-cron.cpp deleted file mode 100644 index dfca782a2..000000000 --- a/contrib/bootserv/src/lokinet-cron.cpp +++ /dev/null @@ -1,37 +0,0 @@ -#include "lokinet-cron.hpp" - -namespace lokinet -{ - namespace bootserv - { - CronHandler::CronHandler(std::ostream& o) : Handler(o) - { - } - - CronHandler::~CronHandler() - { - } - - int - CronHandler::Exec(const Config& conf) - { - // this runs the cron tasks - // TODO: implement me - return 0; - } - - int - CronHandler::ReportError(const char* err) - { - out << "error: " << err << std::endl; - return 1; - } - - Handler_ptr - NewCronHandler(std::ostream& out) - { - return std::make_unique< CronHandler >(out); - } - - } // namespace bootserv -} // namespace lokinet diff --git a/contrib/bootserv/src/lokinet-cron.hpp b/contrib/bootserv/src/lokinet-cron.hpp deleted file mode 100644 index 8bd9ba3c6..000000000 --- a/contrib/bootserv/src/lokinet-cron.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#ifndef BOOTSERV_LOKINET_CRON_HPP -#define BOOTSERV_LOKINET_CRON_HPP - -#include "handler.hpp" - -namespace lokinet -{ - namespace bootserv - { - struct CronHandler final : public Handler - { - CronHandler(std::ostream& o); - ~CronHandler(); - - int - Exec(const Config& conf) override; - - int - ReportError(const char* err) override; - }; - - } // namespace bootserv -} // namespace lokinet - -#endif diff --git a/contrib/bootserv/src/main.cpp b/contrib/bootserv/src/main.cpp deleted file mode 100644 index ad51d0f0b..000000000 --- a/contrib/bootserv/src/main.cpp +++ /dev/null @@ -1,60 +0,0 @@ -#include "handler.hpp" -#include "lokinet-config.hpp" - -#include -#include -#include - -static int -printhelp(const char* exe) -{ - std::cout << "usage: " << exe << " [--cron] [--conf /path/to/alt/config.ini]" - << std::endl; - return 1; -} - -int -main(int argc, char* argv[]) -{ - bool RunCron = false; - - const char* confFile = lokinet::bootserv::Config::DefaultPath; - lokinet::bootserv::Config config; - - lokinet::bootserv::Handler_ptr handler; - - option longopts[] = {{"cron", no_argument, 0, 'C'}, - {"help", no_argument, 0, 'h'}, - {"conf", required_argument, 0, 'c'}, - {0, 0, 0, 0}}; - - int c = 0; - int index = 0; - while((c = getopt_long(argc, argv, "hCc:", longopts, &index)) != -1) - { - switch(c) - { - case 'h': - return printhelp(argv[0]); - case 'C': - RunCron = true; - break; - case 'c': - confFile = optarg; - break; - } - } - if(RunCron) - handler = lokinet::bootserv::NewCronHandler(std::cout); - else - handler = lokinet::bootserv::NewCGIHandler(std::cout); - - if(!config.LoadFile(confFile)) - { - std::stringstream ss; - ss << "failed to load " << confFile; - return handler->ReportError(ss.str().c_str()); - } - else - return handler->Exec(config); -} diff --git a/contrib/dtrace/lokinet.xml b/contrib/dtrace/lokinet.xml deleted file mode 100644 index 82f46a6c9..000000000 --- a/contrib/dtrace/lokinet.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/contrib/dtrace/profiler.d b/contrib/dtrace/profiler.d deleted file mode 100644 index ce45a1ea8..000000000 --- a/contrib/dtrace/profiler.d +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/sbin/dtrace -s - -syscall:::entry -/pid == $target/ -{ - @calls[ustack(10), probefunc] = count(); -} - -profile:::tick-1sec -{ - /** print */ - printa(@calls); - /** clear */ - clear(@calls); - trunc(@calls, 15); -} - - diff --git a/contrib/freebsd/openrc/lokinet.rc b/contrib/freebsd/openrc/lokinet.rc deleted file mode 100644 index 67d8b336d..000000000 --- a/contrib/freebsd/openrc/lokinet.rc +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/sh - -. /etc/rc.subr - -name=lokinet -rcvar=lokinet_enable - -command="/usr/local/bin/${name}" -command_args="/usr/local/etc/${name}/daemon.ini > /dev/null 2>&1" - -pidfile="/usr/local/etc/${name}/lokinet.pid" - -required_files="/usr/local/etc/${name}/daemon.ini" - -sig_reload="HUP" - -start_precmd="${command} -g /usr/local/etc/${name}/daemon.ini" - -load_rc_config $name -run_rc_command "$1" \ No newline at end of file diff --git a/contrib/munin/lokinet-munin.py b/contrib/munin/lokinet-munin.py deleted file mode 100644 index f9c067319..000000000 --- a/contrib/munin/lokinet-munin.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python3 -# -# requires python3-requests -# -import requests -import json -import os -import sys - -from collections import defaultdict as Dict - -from requests.exceptions import RequestException - - -def jsonrpc(method, **args): - return requests.post('http://127.0.0.1:1190/', data=json.dumps( - {'method': method, 'params': args, 'id': 'munin'}), headers={'content-type': 'application/json'}).json() - - -def exit_sessions_main(): - if len(sys.argv) == 2 and sys.argv[1] == 'config': - print("graph_title lokinet exit sessions") - print("graph_vlabel sessions") - print("graph_category network") - print("graph_info This graph shows the number of exit sessions on a lokinet exit") - print("_exit_sessions.info Number of exit sessions") - print("_exit_sessions.label sessions") - else: - count = 0 - try: - j = jsonrpc("llarp.admin.exit.list") - count = len(j['result']) - except RequestException: - pass - print("_exit_sessions.value {}".format(count)) - - -def peers_main(): - if len(sys.argv) == 2 and sys.argv[1] == 'config': - print("graph_title lokinet peers") - print("graph_vlabel peers") - print("graph_category network") - print("graph_info This graph shows the number of node to node sessions of this lokinet router") - print("_peers_outbound.info Number of outbound lokinet peers") - print("_peers_inbound.info Number of inbound lokinet peers") - print("_peers_outbound.label outbound peers") - print("_peers_inbound.label inbound peers") - print("_peers_clients.info Number of lokinet client peers") - print("_peers_clients.label lokinet client peers") - else: - inbound = Dict(int) - outbound = Dict(int) - clients = Dict(int) - try: - j = jsonrpc("llarp.admin.link.neighboors") - for peer in j['result']: - if peer["svcnode"]: - if peer["outbound"]: - outbound[peer['ident']] += 1 - else: - inbound[peer['ident']] += 1 - else: - clients[peer['ident']] += 1 - except RequestException: - pass - - print("_peers_outbound.value {}".format(len(outbound))) - print("_peers_inbound.value {}".format(len(inbound))) - print("_peers_clients.value {}".format(len(clients))) - -if __name__ == '__main__': - exe = os.path.basename(sys.argv[0]).lower() - if exe == 'lokinet_peers': - peers_main() - elif exe == 'lokinet_exit': - exit_sessions_main() - else: - print( - 'please symlink this as `lokinet_peers` or `lokinet_exit` in munin plugins dir') diff --git a/contrib/py/pylokinet/pylokinet/__init__.py b/contrib/py/pylokinet/pylokinet/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/contrib/py/pylokinet/pylokinet/__main__.py b/contrib/py/pylokinet/pylokinet/__main__.py deleted file mode 100644 index 710fd7900..000000000 --- a/contrib/py/pylokinet/pylokinet/__main__.py +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env python3 - -from pylokinet.instance import main -main() \ No newline at end of file diff --git a/contrib/py/pylokinet/pylokinet/bencode.py b/contrib/py/pylokinet/pylokinet/bencode.py deleted file mode 100644 index 2f6963209..000000000 --- a/contrib/py/pylokinet/pylokinet/bencode.py +++ /dev/null @@ -1,111 +0,0 @@ -# -# super freaking dead simple wicked awesome bencode library -# -from io import BytesIO - -class BCodec: - encoding = 'utf-8' - def __init__(self, fd): - self._fd = fd - - def _write_bytestring(self, bs): - self._fd.write('{}:'.format(len(bs)).encode('ascii')) - self._fd.write(bs) - - def _write_list(self, l): - self._fd.write(b'l') - for item in l: - self.encode(item) - self._fd.write(b'e') - - def _write_dict(self, d): - self._fd.write(b'd') - keys = list(d.keys()) - keys.sort() - for k in keys: - if isinstance(k, str): - self._write_bytestring(k.encode(self.encoding)) - elif isinstance(k, bytes): - self._write_bytestring(k) - else: - self._write_bytestring('{}'.format(k).encode(self.encoding)) - self.encode(d[k]) - self._fd.write(b'e') - - def _write_int(self, i): - self._fd.write('i{}e'.format(i).encode(self.encoding)) - - def encode(self, obj): - if isinstance(obj, dict): - self._write_dict(obj) - elif isinstance(obj, list): - self._write_list(obj) - elif isinstance(obj, int): - self._write_int(obj) - elif isinstance(obj, str): - self._write_bytestring(obj.encode(self.encoding)) - elif isinstance(obj, bytes): - self._write_bytestring(obj) - elif hasattr(obj, bencode): - obj.bencode(self._fd) - else: - raise ValueError("invalid object type") - - def _readuntil(self, delim): - b = bytes() - while True: - ch = self._fd.read(1) - if ch == delim: - return b - b += ch - - def _decode_list(self): - l = list() - while True: - b = self._fd.read(1) - if b == b'e': - return l - l.append(self._decode(b)) - - def _decode_dict(self): - d = dict() - while True: - ch = self._fd.read(1) - if ch == b'e': - return d - k = self._decode_bytestring(ch) - d[k] = self.decode() - - def _decode_int(self): - return int(self._readuntil(b'e'), 10) - - def _decode_bytestring(self, ch): - ch += self._readuntil(b':') - l = int(ch, base=10) - return self._fd.read(l) - - def _decode(self, ch): - if ch == b'd': - return self._decode_dict() - elif ch == b'l': - return self._decode_list() - elif ch == b'i': - return self._decode_int() - elif ch in [b'0',b'1',b'2',b'3',b'4',b'5',b'6',b'7',b'8',b'9']: - return self._decode_bytestring(ch) - else: - raise ValueError(ch) - - def decode(self): - return self._decode(self._fd.read(1)) - - -def bencode(obj): - buf = BytesIO() - b = BCodec(buf) - b.encode(obj) - return buf.getvalue() - -def bdecode(bytestring): - buf = BytesIO(bytestring) - return BCodec(buf).decode() \ No newline at end of file diff --git a/contrib/py/pylokinet/pylokinet/bootserv.py b/contrib/py/pylokinet/pylokinet/bootserv.py deleted file mode 100644 index a196c507b..000000000 --- a/contrib/py/pylokinet/pylokinet/bootserv.py +++ /dev/null @@ -1,278 +0,0 @@ -#!/usr/bin/env python3 -# -# python wsgi application for managing many lokinet instances -# - -__doc__ = """lokinet bootserv wsgi app -also handles webhooks for CI -run me with via gunicorn pylokinet.bootserv:app -""" - -import os - -from pylokinet import rc -import json - -import random -import time -from datetime import datetime -from email.utils import parsedate, format_datetime -from dateutil.parser import parse as date_parse -import requests - - -root = './lokinet' - -def _compare_dates(left, right): - """ - return true if left timestamp is bigger than right - """ - return date_parse(left) > date_parse(right) - -class TokenHolder: - - _dir = root - _token = None - - def __init__(self, f="token"): - if not os.path.exists(self._dir): - os.mkdir(self._dir, 0o700) - f = os.path.join(self._dir, f) - if os.path.exists(f): - with open(f) as fd: - self._token = fd.read().replace("\n", "") - - def verify(self, token): - """ - return true if token matches - """ - if self._token is None: - return False - return self._token == token - -class BinHolder: - """ - serves a binary file in a dir - """ - _dir = os.path.join(root, 'bin') - - def __init__(self, f): - if not os.path.exists(self._dir): - os.mkdir(self._dir, 0o700) - self._fpath = os.path.join(self._dir, f) - - def put(self, r): - """ - put a new file into the place that is held - """ - with open(self._fpath, "wb") as f: - for chunk in r.iter_content(chunk_size=1024): - f.write(chunk) - - - def is_new(self, date): - """ - return true if last modified timestamp is fresher than current - """ - t = date_parse('{}'.format(date)) - if not t: - return False - if os.path.exists(self._fpath): - st = os.stat(self._fpath) - return st.st_mtime < t.timestamp() - return True - - - def serve(self, last_modified, respond): - """ - serve file with caching - """ - t = parsedate(last_modified) - if t: - t = time.mktime(t) - if t is None: - t = 0 - if not os.path.exists(self._fpath): - respond("404 Not Found", []) - return [] - st = os.stat(self._fpath) - if st.st_mtime < t: - respond("304 Not Modified", [("Last-Modified", format_datetime(st.st_mtime)) ]) - return [] - with open(self._fpath, "rb") as f: - data = f.read() - respond("200 OK", [("Content-Type", "application/octect-stream"), - ("Last-Modified", format_datetime(datetime.fromtimestamp(int(st.st_mtime)))),("Content-Length", "{}".format(st.st_size))]) - return [data] - - -class RCHolder: - - _dir = os.path.join(root, 'nodedb') - - _rc_files = list() - - def __init__(self): - if os.path.exists(self._dir): - for root, _, files in os.walk(self._dir): - for f in files: - self._add_rc(os.path.join(root, f)) - else: - os.mkdir(self._dir, 0o700) - - def prune(self): - """ - remove invalid entries - """ - delfiles = [] - for p in self._rc_files: - with open(p, 'rb') as f: - if not rc.validate(f.read()): - delfiles.append(p) - for f in delfiles: - os.remove(f) - - def validate_then_put(self, body): - if not rc.validate(body): - return False - k = rc.get_pubkey(body) - print(k) - if k is None: - return False - with open(os.path.join(self._dir, k), "wb") as f: - f.write(body) - return True - - def _add_rc(self, fpath): - self._rc_files.append(fpath) - - def serve_random(self): - with open(random.choice(self._rc_files), 'rb') as f: - return f.read() - - def empty(self): - return len(self._rc_files) == 0 - - -def handle_rc_upload(body, respond): - holder = RCHolder() - if holder.validate_then_put(body): - respond("200 OK", [("Content-Type", "text/plain")]) - return ["rc accepted".encode('ascii')] - else: - respond("400 Bad Request", [("Content-Type", "text/plain")]) - return ["bad rc".encode('ascii')] - - -def serve_random_rc(): - holder = RCHolder() - if holder.empty(): - return None - else: - return holder.serve_random() - -def response(status, msg, respond): - respond(status, [("Content-Type", "text/plain"), ("Content-Length", "{}".format(len(msg)))]) - return [msg.encode("utf-8")] - -def handle_serve_lokinet(modified_since, respond): - l = BinHolder('lokinet.zip') - return l.serve(modified_since, respond) - - -def fetch_lokinet(j, ref="staging", name="build:linux"): - holder = BinHolder("lokinet.zip") - if 'builds' not in j: - return False - selected = None - attrs = dict() - if 'object_attributes' in j: - attrs = j['object_attributes'] - if 'ref' not in attrs or attrs["ref"] != ref: - return True - - for build in j['builds']: - if 'name' not in build or build['name'] != name: - continue - if 'status' not in build or build['status'] != 'success': - continue - if 'finished_at' not in build or build['finished_at'] is None: - continue - if holder.is_new(build['finished_at']): - if selected is None or _compare_dates(build["finished_at"], selected["finished_at"]): - selected = build - if selected and 'id' in selected: - url = 'https://gitlab.com/lokiproject/loki-network/-/jobs/{}/artifacts/download'.format(selected['id']) - r = requests.get(url) - if r.status_code == 200: - holder.put(r) - return True - - #if 'artifacts_file' not in selected: - # return False - #f = selected["artifacts_file"] - #return True - -def handle_webhook(j, token, event, respond): - """ - handle CI webhook - """ - t = TokenHolder() - if not t.verify(token): - respond("403 Forbidden", []) - return [] - event = event.lower() - if event == 'pipeline hook': - if fetch_lokinet(j): - respond("200 OK", []) - return [] - else: - respond("500 Internal Server Error", []) - return [] - else: - respond("404 Not Found", []) - return [] - - -def app(environ, start_response): - request_body_size = int(environ.get("CONTENT_LENGTH", 0)) - method = environ.get("REQUEST_METHOD") - if method.upper() == "PUT" and request_body_size > 0: - rcbody = environ.get("wsgi.input").read(request_body_size) - return handle_rc_upload(rcbody, start_response) - elif method.upper() == "POST": - if environ.get("PATH_INFO") == "/": - j = json.loads(environ.get("wsgi.input").read(request_body_size)) - token = environ.get("HTTP_X_GITLAB_TOKEN") - return handle_webhook(j, token, environ.get("HTTP_X_GITLAB_EVENT"), start_response) - else: - return response("404 Not Found", 'bad url', start_response) - elif method.upper() == "GET": - if environ.get("PATH_INFO") == "/bootstrap.signed": - resp = serve_random_rc() - if resp is not None: - start_response('200 OK', [("Content-Type", "application/octet-stream")]) - return [resp] - else: - return response('404 Not Found', 'no RCs', start_response) - elif environ.get("PATH_INFO") == "/ping": - return response('200 OK', 'pong', start_response) - elif environ.get("PATH_INFO") == "/lokinet.zip": - return handle_serve_lokinet(environ.get("HTTP_IF_MODIFIED_SINCE"),start_response) - elif environ.get("PATH_INFO") == "/": - return response("200 OK", "lokinet bootserv", start_response) - else: - return response('404 Not Found', 'Not found', start_response) - else: - return response('405 Method Not Allowed', 'method not allowed', start_response) - - -def main(): - """ - run as cron job - """ - h = RCHolder() - h.prune() - -if __name__ == '__main__': - main() diff --git a/contrib/py/pylokinet/pylokinet/instance.py b/contrib/py/pylokinet/pylokinet/instance.py deleted file mode 100644 index 4639ffec8..000000000 --- a/contrib/py/pylokinet/pylokinet/instance.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env python3 -# -# lokinet runtime wrapper -# - -from ctypes import * -import configparser -import signal -import time -import threading -import os -import sys -import requests - -from pylokinet import rc - -lib_file = os.path.join(os.path.realpath('.'), 'liblokinet-shared.so') - - -def log(msg): - sys.stderr.write("lokinet: {}\n".format(msg)) - sys.stderr.flush() - - -class LokiNET(threading.Thread): - - lib = None - ctx = 0 - failed = False - up = False - - asRouter = True - - def configure(self, lib, conf, ip=None, port=None, ifname=None, seedfile=None, lokid_host=None, lokid_port=None): - log("configure lib={} conf={}".format(lib, conf)) - if not os.path.exists(os.path.dirname(conf)): - os.mkdir(os.path.dirname(conf)) - try: - self.lib = CDLL(lib) - except OSError as ex: - log("failed to load library: {}".format(ex)) - return False - if self.lib.llarp_ensure_config(conf.encode('utf-8'), os.path.dirname(conf).encode('utf-8'), True, self.asRouter): - config = configparser.ConfigParser() - config.read(conf) - log('overwrite ip="{}" port="{}" ifname="{}" seedfile="{}" lokid=("{}", "{}")'.format( - ip, port, ifname, seedfile, lokid_host, lokid_port)) - if seedfile and lokid_host and lokid_port: - if not os.path.exists(seedfile): - log('cannot access service node seed at "{}"'.format(seedfile)) - return False - config['lokid'] = { - 'service-node-seed': seedfile, - 'enabled': "true", - 'jsonrpc': "{}:{}".format(lokid_host, lokid_port) - } - if ip: - config['router']['public-address'] = '{}'.format(ip) - if port: - config['router']['public-port'] = '{}'.format(port) - if ifname and port: - config['bind'] = { - ifname: '{}'.format(port) - } - with open(conf, "w") as f: - config.write(f) - self.ctx = self.lib.llarp_main_init(conf.encode('utf-8')) - else: - return False - return self.lib.llarp_main_setup(self.ctx, False) == 0 - - def inform_fail(self): - """ - inform lokinet crashed - """ - self.failed = True - self._inform() - - def inform_up(self): - self.up = True - self._inform() - - def _inform(self): - """ - inform waiter - """ - - def wait_for_up(self, timeout): - """ - wait for lokinet to go up for :timeout: seconds - :return True if we are up and running otherwise False: - """ - # return self._up.wait(timeout) - - def signal(self, sig): - if self.ctx and self.lib: - self.lib.llarp_main_signal(self.ctx, int(sig)) - - def run(self): - # self._up.acquire() - self.up = True - code = self.lib.llarp_main_run(self.ctx) - log("llarp_main_run exited with status {}".format(code)) - if code: - self.inform_fail() - self.up = False - # self._up.release() - - def close(self): - if self.lib and self.ctx: - self.lib.llarp_main_free(self.ctx) - - -def getconf(name, fallback=None): - return name in os.environ and os.environ[name] or fallback - - -def run_main(args): - seedfile = getconf("LOKI_SEED_FILE") - if seedfile is None: - print("LOKI_SEED_FILE was not set") - return - - lokid_host = getconf("LOKI_RPC_HOST", "127.0.0.1") - lokid_port = getconf("LOKI_RPC_PORT", "22023") - - root = getconf("LOKINET_ROOT") - if root is None: - print("LOKINET_ROOT was not set") - return - - rc_callback = getconf("LOKINET_SUBMIT_URL") - if rc_callback is None: - print("LOKINET_SUBMIT_URL was not set") - return - - bootstrap = getconf("LOKINET_BOOTSTRAP_URL") - if bootstrap is None: - print("LOKINET_BOOTSTRAP_URL was not set") - - lib = getconf("LOKINET_LIB", lib_file) - if not os.path.exists(lib): - lib = "liblokinet-shared.so" - timeout = int(getconf("LOKINET_TIMEOUT", "5")) - ping_interval = int(getconf("LOKINET_PING_INTERVAL", "60")) - ping_callback = getconf("LOKINET_PING_URL") - ip = getconf("LOKINET_IP") - port = getconf("LOKINET_PORT") - ifname = getconf("LOKINET_IFNAME") - if ping_callback is None: - print("LOKINET_PING_URL was not set") - return - conf = os.path.join(root, "daemon.ini") - log("going up") - loki = LokiNET() - log("bootstrapping...") - try: - r = requests.get(bootstrap) - if r.status_code == 404: - log("bootstrap gave no RCs, we are probably the seed node") - elif r.status_code != 200: - raise Exception("http {}".format(r.status_code)) - else: - data = r.content - if rc.validate(data): - log("valid RC obtained") - with open(os.path.join(root, "bootstrap.signed"), "wb") as f: - f.write(data) - else: - raise Exception("invalid RC") - except Exception as ex: - log("failed to bootstrap: {}".format(ex)) - loki.close() - return - if loki.configure(lib, conf, ip, port, ifname, seedfile, lokid_host, lokid_port): - log("configured") - - loki.start() - try: - log("waiting for spawn") - while timeout > 0: - time.sleep(1) - if loki.failed: - log("failed") - break - log("waiting {}".format(timeout)) - timeout -= 1 - if loki.up: - log("submitting rc") - try: - with open(os.path.join(root, 'self.signed'), 'rb') as f: - r = requests.put(rc_callback, data=f.read(), headers={ - "content-type": "application/octect-stream"}) - log('submit rc reply: HTTP {}'.format(r.status_code)) - except Exception as ex: - log("failed to submit rc: {}".format(ex)) - loki.signal(signal.SIGINT) - time.sleep(2) - else: - while loki.up: - time.sleep(ping_interval) - try: - r = requests.get(ping_callback) - log("ping reply: HTTP {}".format(r.status_code)) - except Exception as ex: - log("failed to submit ping: {}".format(ex)) - else: - log("failed to go up") - loki.signal(signal.SIGINT) - except KeyboardInterrupt: - loki.signal(signal.SIGINT) - time.sleep(2) - finally: - loki.close() - else: - loki.close() - - -def main(): - run_main(sys.argv[1:]) - - -if __name__ == "__main__": - main() diff --git a/contrib/py/pylokinet/pylokinet/rc.py b/contrib/py/pylokinet/pylokinet/rc.py deleted file mode 100644 index 8a8a9347c..000000000 --- a/contrib/py/pylokinet/pylokinet/rc.py +++ /dev/null @@ -1,31 +0,0 @@ -from pylokinet import bencode -import pysodium -import binascii -import time - -def _expired(ts, lifetime=84600000): - """ - return True if a timestamp is considered expired - lifetime is default 23.5 hours - """ - return (int(time.time()) * 1000) - ts >= lifetime - -def validate(data): - rc = bencode.bdecode(data) - if b'z' not in rc or b'k' not in rc: - return False - sig = rc[b'z'] - rc[b'z'] = b'\x00' * 64 - buf = bencode.bencode(rc) - try: - k = rc[b'k'] - pysodium.crypto_sign_verify_detached(sig, buf, k) - except: - return False - else: - return not _expired(rc[b't']) - -def get_pubkey(data): - rc = bencode.bdecode(data) - if b'k' in rc: - return binascii.hexlify(rc[b'k']).decode('ascii') \ No newline at end of file diff --git a/contrib/py/pylokinet/readme.md b/contrib/py/pylokinet/readme.md deleted file mode 100644 index 45b56103e..000000000 --- a/contrib/py/pylokinet/readme.md +++ /dev/null @@ -1,27 +0,0 @@ -# pylokinet - -lokinet with python 3 - - # python3 setup.py install - -## bootserv - -bootserv is a bootstrap server for accepting and serving RCs - - $ gunicorn -b 0.0.0.0:8000 pylokinet.bootserv:app - -## pylokinet instance - -obtain `liblokinet-shared.so` from a lokinet build - -run (root): - - # export LOKINET_ROOT=/tmp/lokinet-instance/ - # export LOKINET_LIB=/path/to/liblokinet-shared.so - # export LOKINET_BOOTSTRAP_URL=http://bootserv.ip.address.here:8000/bootstrap.signed - # export LOKINET_PING_URL=http://bootserv.ip.address.here:8000/ping - # export LOKINET_SUBMIT_URL=http://bootserv.ip.address.here:8000/ - # export LOKINET_IP=public.ip.goes.here - # export LOKINET_PORT=1090 - # export LOKINET_IFNAME=eth0 - # python3 -m pylokinet \ No newline at end of file diff --git a/contrib/py/pylokinet/setup.py b/contrib/py/pylokinet/setup.py deleted file mode 100644 index f20b3a8f4..000000000 --- a/contrib/py/pylokinet/setup.py +++ /dev/null @@ -1,14 +0,0 @@ -from setuptools import setup, find_packages - - - -setup( - name="pylokinet", - version="0.0.1", - license="ZLIB", - author="jeff", - author_email="jeff@i2p.rocks", - description="lokinet python bindings", - url="https://github.com/loki-project/loki-network", - install_requires=["pysodium", "requests", "python-dateutil"], - packages=find_packages()) \ No newline at end of file diff --git a/contrib/py/vanity/.gitignore b/contrib/py/vanity/.gitignore deleted file mode 100644 index 8e5737fb0..000000000 --- a/contrib/py/vanity/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -__pycache__ -*.private \ No newline at end of file diff --git a/contrib/py/vanity/bencode.py b/contrib/py/vanity/bencode.py deleted file mode 100644 index 5c131fc9b..000000000 --- a/contrib/py/vanity/bencode.py +++ /dev/null @@ -1,112 +0,0 @@ -# -# super freaking dead simple wicked awesome bencode library -# -from io import BytesIO - -class BCodec: - encoding = 'utf-8' - def __init__(self, fd): - self._fd = fd - - def _write_bytestring(self, bs): - self._fd.write('{}:'.format(len(bs)).encode('ascii')) - self._fd.write(bs) - - def _write_list(self, l): - self._fd.write(b'l') - for item in l: - self.encode(item) - self._fd.write(b'e') - - def _write_dict(self, d): - self._fd.write(b'd') - keys = list(d.keys()) - keys.sort() - for k in keys: - if isinstance(k, str): - self._write_bytestring(k.encode(self.encoding)) - elif isinstance(k, bytes): - self._write_bytestring(k) - else: - self._write_bytestring('{}'.format(k).encode(self.encoding)) - self.encode(d[k]) - self._fd.write(b'e') - - def _write_int(self, i): - self._fd.write('i{}e'.format(i).encode(self.encoding)) - - def encode(self, obj): - if isinstance(obj, dict): - self._write_dict(obj) - elif isinstance(obj, list): - self._write_list(obj) - elif isinstance(obj, int): - self._write_int(obj) - elif isinstance(obj, str): - self._write_bytestring(obj.encode(self.encoding)) - elif isinstance(obj, bytes): - self._write_bytestring(obj) - elif hasattr(obj, bencode): - obj.bencode(self._fd) - else: - raise ValueError("invalid object type") - - def _readuntil(self, delim): - b = bytes() - while True: - ch = self._fd.read(1) - if ch == delim: - return b - b += ch - - def _decode_list(self): - l = list() - while True: - b = self._fd.read(1) - if b == b'e': - return l - l.append(self._decode(b)) - - def _decode_dict(self): - d = dict() - while True: - ch = self._fd.read(1) - if ch == b'e': - return d - k = self._decode_bytestring(ch) - d[k] = self.decode() - - def _decode_int(self): - return int(self._readuntil(b'e'), 10) - - def _decode_bytestring(self, ch): - ch += self._readuntil(b':') - l = int(ch, base=10) - return self._fd.read(l) - - def _decode(self, ch): - if ch == b'd': - return self._decode_dict() - elif ch == b'l': - return self._decode_list() - elif ch == b'i': - return self._decode_int() - elif ch in [b'0',b'1',b'2',b'3',b'4',b'5',b'6',b'7',b'8',b'9']: - return self._decode_bytestring(ch) - else: - raise ValueError(ch) - - def decode(self): - return self._decode(self._fd.read(1)) - - -def bencode(obj): - buf = BytesIO() - b = BCodec(buf) - b.encode(obj) - return buf.bytes() - -def bdecode(bytestring): - buf = BytesIO() - buf.write(bytestring) - return BCodec(buf).decode() \ No newline at end of file diff --git a/contrib/py/vanity/lokinet-vanity.py b/contrib/py/vanity/lokinet-vanity.py deleted file mode 100644 index 5fc1119dd..000000000 --- a/contrib/py/vanity/lokinet-vanity.py +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env python3 -import bencode -import sys -import libnacl -import struct -from io import BytesIO -import time -from multiprocessing import Process, Array, Value - - -def print_help(): - print('usage: {} keyfile.private prefix numthreads'.format(sys.argv[0])) - return 1 - - -_zalpha = ['y', 'b', 'n', 'd', 'r', 'f', 'g', '8', - 'e', 'j', 'k', 'm', 'c', 'p', 'q', 'x', - 'o', 't', '1', 'u', 'w', 'i', 's', 'z', - 'a', '3', '4', '5', 'h', '7', '6', '9'] - - -def zb32_encode(buf): - s = str() - bits = 0 - l = len(buf) - idx = 0 - tmp = buf[idx] - while bits > 0 or idx < l: - if bits < 5: - if idx < l: - tmp <<= 8 - tmp |= buf[idx] & 0xff - idx += 1 - bits += 8 - else: - tmp <<= 5 - bits - bits = 5 - bits -= 5 - s += _zalpha[(tmp >> bits) & 0x1f] - return s - - -def _gen_si(keys): - e = keys[b'e'][32:] - s = keys[b's'][32:] - v = keys[b'v'] - return {'e': e, 's': s, 'v': v} - - -class AddrGen: - - def __init__(self, threads, keys, prefix): - self._inc = threads - self._keys = keys - self._c = Value('i') - self.sync = Array('i', 3) - self._procs = [] - self.prefix = prefix - - def runit(self): - for ch in self.prefix: - if ch not in _zalpha: - print("invalid prefix, {} not a valid character".format(ch)) - return None, None - print("find ^{}.loki".format(self.prefix)) - i = self._inc - while i > 0: - p = Process(target=self._gen_addr_tick, args=(self.prefix, abs( - libnacl.randombytes_random()), abs(libnacl.randombytes_random()), _gen_si(self._keys))) - p.start() - self._procs.append(p) - i -= 1 - return self._runner() - - def _gen_addr_tick(self, prefix, lo, hi, si): - print(prefix) - fd = BytesIO() - addr = '' - enc = bencode.BCodec(fd) - while self.sync[2] == 0: - si['x'] = struct.pack('>QQ', lo, hi) - fd.seek(0, 0) - enc.encode(si) - pub = bytes(fd.getbuffer()) - addr = zb32_encode(libnacl.crypto_generichash(pub)) - if addr.startswith(prefix): - self.sync[2] = 1 - self.sync[0] = hi - self.sync[1] = lo - return - hi += self._inc - if hi == 0: - lo += 1 - self._c.value += 1 - - def _print_stats(self): - print('{} H/s'.format(self._c.value)) - self._c.value = 0 - - def _joinall(self): - for p in self._procs: - p.join() - - def _runner(self): - while self.sync[2] == 0: - time.sleep(1) - self._print_stats() - self._joinall() - fd = BytesIO() - enc = bencode.BCodec(fd) - hi = self.sync[0] - lo = self.sync[1] - si = _gen_si(self._keys) - si['x'] = struct.pack('>QQ', lo, hi) - enc.encode(si) - pub = bytes(fd.getbuffer()) - addr = zb32_encode(libnacl.crypto_generichash(pub)) - return si['x'], addr - - -def main(args): - if len(args) != 3: - return print_help() - keys = None - with open(args[0], 'rb') as fd: - dec = bencode.BCodec(fd) - keys = dec.decode() - runner = AddrGen(int(args[2]), keys, args[1]) - keys[b'x'], addr = runner.runit() - if addr: - print("found {}.loki".format(addr)) - with open(args[0], 'wb') as fd: - enc = bencode.BCodec(fd) - enc.encode(keys) - - -if __name__ == '__main__': - main(sys.argv[1:]) diff --git a/contrib/py/vanity/readme.md b/contrib/py/vanity/readme.md deleted file mode 100644 index f8161176d..000000000 --- a/contrib/py/vanity/readme.md +++ /dev/null @@ -1,10 +0,0 @@ -# lokinet vanity address generator - -installing deps: - - sudo apt install libsodium-dev - pip3 install --user -r requirements.txt - -to generate a nonce with a prefix `^7oki` using 8 cpu threads: - - python3 lokinet-vanity.py keyfile.private 7oki 8 diff --git a/contrib/py/vanity/requirements.txt b/contrib/py/vanity/requirements.txt deleted file mode 100644 index 15d991e7b..000000000 --- a/contrib/py/vanity/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -libnacl \ No newline at end of file diff --git a/contrib/shadow/genconf.py b/contrib/shadow/genconf.py deleted file mode 100644 index 0a4c0794a..000000000 --- a/contrib/shadow/genconf.py +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env python3 - -import configparser -import sys -import os - -from xml.etree import ElementTree as etree - - -def getSetting(s, name, fallback): return name in s and s[name] or fallback - - -shadowRoot = getSetting(os.environ, "SHADOW_ROOT", - os.path.join(os.environ['HOME'], '.shadow')) - -libpath = 'libshadow-plugin-lokinet.so' - - -def nodeconf(conf, baseDir, name, ifname=None, port=None): - conf['netdb'] = {'dir': 'tmp-nodes'} - conf['router'] = {} - conf['router']['contact-file'] = os.path.join( - baseDir, '{}.signed'.format(name)) - conf['router']['ident-privkey'] = os.path.join( - baseDir, '{}-ident.key'.format(name)) - conf['router']['transport-privkey'] = os.path.join( - baseDir, '{}-transport.key'.format(name)) - if ifname and port: - conf['bind'] = {ifname: port} - conf['connect'] = {} - - -def addPeer(conf, baseDir, peer): - conf['connect'][peer] = os.path.join(baseDir, '{}.signed'.format(peer)) - - -def createNode(pluginName, root, peer, life=600): - node = etree.SubElement(root, 'node') - node.attrib['id'] = peer['name'] - node.attrib['interfacebuffer'] = '{}'.format(1024 * 1024 * 100) - app = etree.SubElement(node, 'process') - app.attrib['plugin'] = pluginName - app.attrib['time'] = '{}'.format(life) - app.attrib['arguments'] = peer['configfile'] - - -def makeBase(settings, name, id): - return { - 'id': id, - 'name': name, - 'contact-file': os.path.join(getSetting(settings, 'baseDir', 'tmp'), '{}.signed'.format(name)), - 'configfile': os.path.join(getSetting(settings, 'baseDir', 'tmp'), '{}.ini'.format(name)), - 'config': configparser.ConfigParser() - } - - -def makeClient(settings, name, id): - peer = makeBase(settings, name, id) - basedir = getSetting(settings, 'baseDir', 'tmp') - nodeconf(peer['config'], basedir, name) - peer['config']['network'] = { - 'type': 'null', - 'tag': 'test', - 'prefetch-tag': 'test' - } - return peer - - -def makeSVCNode(settings, name, id, port): - peer = makeBase(settings, name, id) - nodeconf(peer['config'], getSetting( - settings, 'baseDir', 'tmp'), name, 'eth0', port) - peer['config']['network'] = { - 'type': 'null' - } - return peer - - -def genconf(settings, outf): - root = etree.Element('shadow') - root.attrib["environment"] = 'LLARP_SHADOW=1' - topology = etree.SubElement(root, 'topology') - topology.attrib['path'] = getSetting(settings, 'topology', os.path.join( - shadowRoot, 'share', 'topology.graphml.xml')) - - pluginName = getSetting(settings, 'name', 'lokinet-shared') - - kill = etree.SubElement(root, 'kill') - kill.attrib['time'] = getSetting(settings, 'runFor', '600') - - baseDir = getSetting(settings, 'baseDir', - os.path.join('/tmp', 'lokinet-shadow')) - - if not os.path.exists(baseDir): - os.mkdir(baseDir) - - plugin = etree.SubElement(root, "plugin") - plugin.attrib['id'] = pluginName - plugin.attrib['path'] = libpath - basePort = getSetting(settings, 'svc-base-port', 19000) - svcNodeCount = getSetting(settings, 'service-nodes', 80) - peers = list() - for nodeid in range(svcNodeCount): - peers.append(makeSVCNode( - settings, 'svc-node-{}'.format(nodeid), str(nodeid), basePort + 1)) - basePort += 1 - - # make all service nodes know each other - for peer in peers: - for nodeid in range(svcNodeCount): - if str(nodeid) != peer['id']: - addPeer(peer['config'], baseDir, 'svc-node-{}'.format(nodeid)) - - # add client nodes - for nodeid in range(getSetting(settings, 'client-nodes', 200)): - peer = makeClient( - settings, 'client-node-{}'.format(nodeid), str(nodeid)) - peers.append(peer) - for p in range(getSetting(settings, 'client-connect-to', 10)): - addPeer(peer['config'], baseDir, - 'svc-node-{}'.format((p + nodeid) % svcNodeCount)) - - # generate xml and settings files - for peer in peers: - createNode(pluginName, root, peer) - - with open(peer['configfile'], 'w') as f: - peer['config'].write(f) - - # render - outf.write(etree.tostring(root).decode('utf-8')) - - -if __name__ == '__main__': - settings = { - 'baseDir': os.path.join("/tmp", "lokinet-shadow"), - 'topology': os.path.join(shadowRoot, 'share', 'topology.graphml.xml'), - 'runFor': '{}'.format(60 * 10 * 10) - } - with open(sys.argv[1], 'w') as f: - genconf(settings, f) diff --git a/contrib/testnet/.gitignore b/contrib/testnet/.gitignore deleted file mode 100644 index 6f53c74ca..000000000 --- a/contrib/testnet/.gitignore +++ /dev/null @@ -1 +0,0 @@ -v/ \ No newline at end of file diff --git a/contrib/testnet/genconf.py b/contrib/testnet/genconf.py deleted file mode 100755 index 49fa5c876..000000000 --- a/contrib/testnet/genconf.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env python -# -# this script generate supervisord configs for running a test network on loopback -# - - -from argparse import ArgumentParser as AP -from configparser import ConfigParser as CP - -import os - - -def svcNodeName(id): return 'svc-node-%03d' % id - - -def clientNodeName(id): return 'client-node-%03d' % id - - -def main(): - ap = AP() - ap.add_argument('--valgrind', type=bool, default=False) - ap.add_argument('--dir', type=str, default='testnet_tmp') - ap.add_argument('--svc', type=int, default=20, - help='number of service nodes') - ap.add_argument('--baseport', type=int, default=19000) - ap.add_argument('--clients', type=int, default=200, - help='number of client nodes') - ap.add_argument('--bin', type=str, required=True) - ap.add_argument('--out', type=str, required=True) - ap.add_argument('--connect', type=int, default=10) - ap.add_argument('--ip', type=str, default=None) - ap.add_argument('--ifname', type=str, default='lo') - ap.add_argument('--netid', type=str, default=None) - ap.add_argument('--loglevel', type=str, default='debug') - args = ap.parse_args() - - if args.valgrind: - exe = 'valgrind {}'.format(args.bin) - else: - exe = '{} -v'.format(args.bin) - basedir = os.path.abspath(args.dir) - - for nodeid in range(args.svc): - config = CP() - config['router'] = { - 'data-dir': '.', - 'net-threads': '1', - 'worker-threads': '4', - 'nickname': svcNodeName(nodeid), - 'min-connections': "{}".format(args.connect) - } - if args.netid: - config['router']['netid'] = args.netid - - if args.ip: - config['router']['public-ip'] = args.ip - config['router']['public-port'] = str(args.baseport + nodeid) - - config['bind'] = { - args.ifname: str(args.baseport + nodeid) - } - config["logging"] = { - "level": args.loglevel - } - config['netdb'] = { - 'dir': 'netdb' - } - config['network'] = { - 'type' : 'null', - 'save-profiles': 'false' - } - config['api'] = { - 'enabled': 'false' - } - config['lokid'] = { - 'enabled': 'false', - } - config["logging"] = { - "level": args.loglevel - } - d = os.path.join(args.dir, svcNodeName(nodeid)) - if not os.path.exists(d): - os.mkdir(d) - fp = os.path.join(d, 'daemon.ini') - with open(fp, 'w') as f: - config.write(f) - for n in [0]: - if nodeid is not 0: - f.write("[bootstrap]\nadd-node={}\n".format(os.path.join(basedir,svcNodeName(n), 'self.signed'))) - else: - f.write("[bootstrap]\nseed-node=true\n") - - for nodeid in range(args.clients): - config = CP() - - config['router'] = { - 'data-dir': '.', - 'net-threads': '1', - 'worker-threads': '2', - 'nickname': clientNodeName(nodeid) - } - if args.netid: - config['router']['netid'] = args.netid - - config["logging"] = { - "level": args.loglevel - } - - config['netdb'] = { - 'dir': 'netdb' - } - config['api'] = { - 'enabled': 'false' - } - config['network'] = { - 'type' : 'null' - } - d = os.path.join(args.dir, clientNodeName(nodeid)) - if not os.path.exists(d): - os.mkdir(d) - fp = os.path.join(d, 'client.ini') - with open(fp, 'w') as f: - config.write(f) - for n in [0]: - otherID = (n + nodeid) % args.svc - f.write("[bootstrap]\nadd-node={}\n".format(os.path.join(basedir,svcNodeName(otherID), 'self.signed'))) - - with open(args.out, 'w') as f: - basedir = os.path.join(args.dir, 'svc-node-%(process_num)03d') - f.write('''[program:svc-node] -directory = {} -command = {} -r {}/daemon.ini -autorestart=true -redirect_stderr=true -#stdout_logfile=/dev/fd/1 -stdout_logfile={}/svc-node-%(process_num)03d-log.txt -stdout_logfile_maxbytes=0 -process_name = svc-node-%(process_num)03d -numprocs = {} -'''.format(basedir, exe, basedir, args.dir, args.svc)) - basedir = os.path.join(args.dir, 'client-node-%(process_num)03d') - f.write('''[program:Client-node] -directory = {} -command = bash -c "sleep 5 && {} {}/client.ini" -autorestart=true -redirect_stderr=true -#stdout_logfile=/dev/fd/1 -stdout_logfile={}/client-node-%(process_num)03d-log.txt -stdout_logfile_maxbytes=0 -process_name = client-node-%(process_num)03d -numprocs = {} -'''.format(basedir, exe, basedir, args.dir, args.clients)) - f.write('[supervisord]\ndirectory=.\n') - - -if __name__ == '__main__': - main() diff --git a/contrib/testnet/readme.md b/contrib/testnet/readme.md deleted file mode 100644 index cb95b3334..000000000 --- a/contrib/testnet/readme.md +++ /dev/null @@ -1,23 +0,0 @@ -loopback testnet scripts - -requirements: - -* bash -* python3 -* supervisord - - -setup: - -make a testnet compatable lokinet build: - - $ cmake -DWITH_TESTNET=ON -B build-testnet -S . - $ make -C build-testnet lokinet - -usage: - -from root of repo run: - - $ ./contrib/testnet/testnet.sh build-testnet/daemon/lokinet 20 200 - -this will spin up 20 service nodes and 200 clients diff --git a/contrib/testnet/requirements.txt b/contrib/testnet/requirements.txt deleted file mode 100644 index 010599389..000000000 --- a/contrib/testnet/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -flask -pynacl diff --git a/contrib/testnet/testnet.sh b/contrib/testnet/testnet.sh deleted file mode 100755 index 86d55e832..000000000 --- a/contrib/testnet/testnet.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -for arg in "$1" "$2" "$3" ; do - test x = "x$arg" && echo "usage: $0 path/to/lokinet num_svc num_clients" && exit 1; -done - -script_root=$(dirname $(readlink -e $0)) -testnet_dir=/tmp/lokinet-testnet - -mkdir -p $testnet_dir - -set -x - -$script_root/genconf.py --bin $1 --netid=testnet --out=$testnet_dir/testnet.ini --svc $2 --dir=$testnet_dir --clients $3 || exit 1 -supervisord -n -c $testnet_dir/testnet.ini From 6629e8c8814d1ae62949269b97e0bcf6df246347 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 28 Feb 2022 10:43:41 -0500 Subject: [PATCH 114/202] cmake workarround for quark in older cmake where pkg config produces non existing targets --- CMakeLists.txt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8cdd74719..cd2d50dac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -183,8 +183,15 @@ option(FORCE_OXENC_SUBMODULE "force using oxen-encoding submodule" OFF) if(NOT FORCE_OXENC_SUBMODULE) pkg_check_modules(OXENC liboxenc>=1.0.1 IMPORTED_TARGET) endif() + if(OXENC_FOUND) - add_library(oxenc::oxenc ALIAS PkgConfig::OXENC) + if(NOT TARGET PkgConfig::OXENC AND CMAKE_VERSION VERSION_LESS "3.21") + # Work around cmake bug 22180 (PkgConfig::OXENC not set if no flags needed): + add_library(_empty_oxenc INTERFACE) + add_library(oxenc::oxenc ALIAS _empty_oxenc) + else() + add_library(oxenc::oxenc ALIAS PkgConfig::OXENC) + endif() message(STATUS "Found system liboxenc ${OXENC_VERSION}") else() message(STATUS "using oxen-encoding submodule") From b4c4c3977a0b3826901b482d788f27a06e975bf8 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 26 Feb 2022 14:10:27 -0500 Subject: [PATCH 115/202] oxenc --- llarp/handlers/tun.cpp | 2 +- llarp/rpc/lokid_rpc_client.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/llarp/handlers/tun.cpp b/llarp/handlers/tun.cpp index afaf9b5c3..96f63ee12 100644 --- a/llarp/handlers/tun.cpp +++ b/llarp/handlers/tun.cpp @@ -27,7 +27,7 @@ #include #include - +#include namespace llarp { namespace handlers diff --git a/llarp/rpc/lokid_rpc_client.cpp b/llarp/rpc/lokid_rpc_client.cpp index ae76e93c4..0fc6ddb7b 100644 --- a/llarp/rpc/lokid_rpc_client.cpp +++ b/llarp/rpc/lokid_rpc_client.cpp @@ -6,7 +6,7 @@ #include #include - +#include #include namespace llarp From a76acd49560f9845c2ad9a839364f2834afc7756 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sat, 5 Mar 2022 20:04:39 -0500 Subject: [PATCH 116/202] fix wire protocol race condition only send close packet once, before we were sending a close after we got a close causing excess log spam. include handshake phase when checking for connection timeouts. when we change our rc make sure to put it into nodedb too when we are a service node to prevent weirdness in dht lookups. --- llarp/iwp/session.cpp | 5 ++++- llarp/iwp/session.hpp | 1 + llarp/router/router.cpp | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/llarp/iwp/session.cpp b/llarp/iwp/session.cpp index a58de4065..0607ff1eb 100644 --- a/llarp/iwp/session.cpp +++ b/llarp/iwp/session.cpp @@ -174,7 +174,10 @@ namespace llarp if (m_State == State::Ready) m_Parent->UnmapAddr(m_RemoteAddr); m_State = State::Closed; + if (m_SentClosed.test_and_set()) + return; EncryptAndSend(std::move(close_msg)); + LogInfo(m_Parent->PrintableName(), " closing connection to ", m_RemoteAddr); } @@ -338,7 +341,7 @@ namespace llarp bool Session::TimedOut(llarp_time_t now) const { - if (m_State == State::Ready || m_State == State::LinkIntro) + if (m_State == State::Ready) { return now > m_LastRX && now - m_LastRX diff --git a/llarp/iwp/session.hpp b/llarp/iwp/session.hpp index 460063005..dc429f5de 100644 --- a/llarp/iwp/session.hpp +++ b/llarp/iwp/session.hpp @@ -203,6 +203,7 @@ namespace llarp std::atomic_flag m_PlaintextEmpty; llarp::thread::Queue m_PlaintextRecv; + std::atomic_flag m_SentClosed; void EncryptWorker(CryptoQueue_t msgs); diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index fea94dafb..d5a1994ad 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -443,6 +443,8 @@ namespace llarp LogError("RC is invalid, not saving"); return false; } + if (m_isServiceNode) + _nodedb->Put(_rc); QueueDiskIO([&]() { HandleSaveRC(); }); return true; } From 1a74abca313b3726c3738a0dc046e8d085fb255d Mon Sep 17 00:00:00 2001 From: Pebu Date: Mon, 14 Mar 2022 08:04:58 -0400 Subject: [PATCH 117/202] Fix spelling changes --- readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/readme.md b/readme.md index 0cc32d49d..c4e19ecc2 100644 --- a/readme.md +++ b/readme.md @@ -23,7 +23,7 @@ Tier 2: * [MacOS](#mac-install) * [FreeBSD](#freebsd-install) -Currently Unsupproted Platforms: (maintainers welcome) +Currently Unsupported Platforms: (maintainers welcome) * Apple iPhone * Homebrew From 12044af04e17d728526b30265b2d2ccafa80f97b Mon Sep 17 00:00:00 2001 From: majestrate Date: Tue, 15 Mar 2022 21:37:55 -0400 Subject: [PATCH 118/202] Update readme.md add note about updating alternatives in windows cross build --- readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/readme.md b/readme.md index c4e19ecc2..e6fdaede5 100644 --- a/readme.md +++ b/readme.md @@ -102,6 +102,8 @@ additional build requirements: setup: $ sudo apt install build-essential cmake git pkg-config mingw-w64 nsis cpack automake libtool + $ sudo update-alternatives --set x86_64-w64-mingw32-gcc /usr/bin/x86_64-w64-mingw32-gcc-posix + $ sudo update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix building: From 56492c88acfdc40a86d6597c81fe26d6ebe31a23 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 2 Feb 2022 09:07:47 -0500 Subject: [PATCH 119/202] dry cross compile --- CMakeLists.txt | 3 +++ contrib/cross.sh | 7 +++++-- contrib/cross/aarch64.toolchain.cmake | 13 ------------- contrib/cross/armhf.toolchain.cmake | 13 ------------- ...{mips.toolchain.cmake => cross.toolchain.cmake} | 6 ++---- contrib/cross/mips64.toolchain.cmake | 13 ------------- contrib/cross/mipsel.toolchain.cmake | 13 ------------- contrib/cross/ppc64le.toolchain.cmake | 13 ------------- readme.md | 14 +++++++++++++- 9 files changed, 23 insertions(+), 72 deletions(-) delete mode 100644 contrib/cross/aarch64.toolchain.cmake delete mode 100644 contrib/cross/armhf.toolchain.cmake rename contrib/cross/{mips.toolchain.cmake => cross.toolchain.cmake} (80%) delete mode 100644 contrib/cross/mips64.toolchain.cmake delete mode 100644 contrib/cross/mipsel.toolchain.cmake delete mode 100644 contrib/cross/ppc64le.toolchain.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index cd2d50dac..f393ee96d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -73,6 +73,9 @@ option(WITH_BOOTSTRAP "build lokinet-bootstrap tool" ${DEFAULT_WITH_BOOTSTRAP}) include(cmake/enable_lto.cmake) +option(CROSS_PLATFORM "cross compiler platform" "Linux") +option(CROSS_PREFIX "toolchain cross compiler prefix" "") + option(BUILD_STATIC_DEPS "Download, build, and statically link against core dependencies" OFF) option(STATIC_LINK "link statically against dependencies" ${BUILD_STATIC_DEPS}) if(BUILD_STATIC_DEPS AND NOT STATIC_LINK) diff --git a/contrib/cross.sh b/contrib/cross.sh index 125a7c54e..81d40edae 100755 --- a/contrib/cross.sh +++ b/contrib/cross.sh @@ -8,10 +8,11 @@ die() { exit 1 } +platform=${PLATFORM:-Linux} root="$(readlink -e $(dirname $0)/../)" cd $root set -e -set -x +set +x test $# = 0 && die no targets provided mkdir -p build-cross echo "all: $@" > build-cross/Makefile @@ -20,9 +21,11 @@ for targ in $@ ; do cd $root/build-cross/build-$targ cmake \ -G 'Unix Makefiles' \ + -DCROSS_PLATFORM=$platform \ + -DCROSS_PREFIX=$targ \ -DCMAKE_EXE_LINKER_FLAGS=-fstack-protector \ -DCMAKE_CXX_FLAGS=-fdiagnostics-color=always\ - -DCMAKE_TOOLCHAIN_FILE=$root/contrib/cross/$targ.toolchain.cmake\ + -DCMAKE_TOOLCHAIN_FILE=$root/contrib/cross/cross.toolchain.cmake\ -DBUILD_STATIC_DEPS=ON \ -DBUILD_SHARED_LIBS=OFF \ -DBUILD_TESTING=OFF \ diff --git a/contrib/cross/aarch64.toolchain.cmake b/contrib/cross/aarch64.toolchain.cmake deleted file mode 100644 index ada0993c3..000000000 --- a/contrib/cross/aarch64.toolchain.cmake +++ /dev/null @@ -1,13 +0,0 @@ -set(CMAKE_SYSTEM_NAME Linux) -set(TOOLCHAIN_PREFIX aarch64-linux-gnu) -set(TOOLCHAIN_SUFFIX) - -set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) - -set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) -set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) -set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) - -set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc${TOOLCHAIN_SUFFIX}) -set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++${TOOLCHAIN_SUFFIX}) -set(ARCH_TRIPLET ${TOOLCHAIN_PREFIX}) diff --git a/contrib/cross/armhf.toolchain.cmake b/contrib/cross/armhf.toolchain.cmake deleted file mode 100644 index 552975e6d..000000000 --- a/contrib/cross/armhf.toolchain.cmake +++ /dev/null @@ -1,13 +0,0 @@ -set(CMAKE_SYSTEM_NAME Linux) -set(TOOLCHAIN_PREFIX arm-linux-gnueabihf) -set(TOOLCHAIN_SUFFIX) - -set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) - -set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) -set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) -set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) - -set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc${TOOLCHAIN_SUFFIX}) -set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++${TOOLCHAIN_SUFFIX}) -set(ARCH_TRIPLET ${TOOLCHAIN_PREFIX}) diff --git a/contrib/cross/mips.toolchain.cmake b/contrib/cross/cross.toolchain.cmake similarity index 80% rename from contrib/cross/mips.toolchain.cmake rename to contrib/cross/cross.toolchain.cmake index 758fa4fb2..2a7cde131 100644 --- a/contrib/cross/mips.toolchain.cmake +++ b/contrib/cross/cross.toolchain.cmake @@ -1,7 +1,5 @@ -set(CMAKE_SYSTEM_NAME Linux) -set(TOOLCHAIN_PREFIX mips-linux-gnu) -set(TOOLCHAIN_SUFFIX) - +set(CMAKE_SYSTEM_NAME ${CROSS_PLATFORM}) +set(TOOLCHAIN_PREFIX ${CROSS_PREFIX}) set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) diff --git a/contrib/cross/mips64.toolchain.cmake b/contrib/cross/mips64.toolchain.cmake deleted file mode 100644 index e71867b68..000000000 --- a/contrib/cross/mips64.toolchain.cmake +++ /dev/null @@ -1,13 +0,0 @@ -set(CMAKE_SYSTEM_NAME Linux) -set(TOOLCHAIN_PREFIX mips64-linux-gnuabi64) -set(TOOLCHAIN_SUFFIX) - -set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) - -set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) -set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) -set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) - -set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc${TOOLCHAIN_SUFFIX}) -set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++${TOOLCHAIN_SUFFIX}) -set(ARCH_TRIPLET ${TOOLCHAIN_PREFIX}) diff --git a/contrib/cross/mipsel.toolchain.cmake b/contrib/cross/mipsel.toolchain.cmake deleted file mode 100644 index 142f96337..000000000 --- a/contrib/cross/mipsel.toolchain.cmake +++ /dev/null @@ -1,13 +0,0 @@ -set(CMAKE_SYSTEM_NAME Linux) -set(TOOLCHAIN_PREFIX mipsel-linux-gnu) -set(TOOLCHAIN_SUFFIX) - -set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) - -set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) -set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) -set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) - -set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc${TOOLCHAIN_SUFFIX}) -set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++${TOOLCHAIN_SUFFIX}) -set(ARCH_TRIPLET ${TOOLCHAIN_PREFIX}) diff --git a/contrib/cross/ppc64le.toolchain.cmake b/contrib/cross/ppc64le.toolchain.cmake deleted file mode 100644 index f0b956079..000000000 --- a/contrib/cross/ppc64le.toolchain.cmake +++ /dev/null @@ -1,13 +0,0 @@ -set(CMAKE_SYSTEM_NAME Linux) -set(TOOLCHAIN_PREFIX powerpc64le-linux-gnu) -set(TOOLCHAIN_SUFFIX) - -set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX}) - -set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) -set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) -set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) - -set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc${TOOLCHAIN_SUFFIX}) -set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++${TOOLCHAIN_SUFFIX}) -set(ARCH_TRIPLET ${TOOLCHAIN_PREFIX}) diff --git a/readme.md b/readme.md index e6fdaede5..be57d75f3 100644 --- a/readme.md +++ b/readme.md @@ -68,9 +68,21 @@ If you want to build from source: $ make -j$(nproc) $ sudo make install + #### Cross Compile For Linux -install the toolchain for `$arch` this example is `aarch64` +current cross targets: + +* aarch64-linux-gnu +* arm-linux-gnueabihf +* mips-linux-gnu +* mips64-linux-gnuabi64 +* mipsel-linux-gnu +* powerpc64le-linux-gnu + + + +install the toolchain (this one is for `aarch64-linux-gnu`) $ sudo apt install g{cc,++}-aarch64-linux-gnu From 66b7c3698edc114491d91405d1e9439fcd64d3f5 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 2 Feb 2022 09:23:12 -0500 Subject: [PATCH 120/202] remove unneeded line from contrib/cross.sh --- contrib/cross.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/contrib/cross.sh b/contrib/cross.sh index 81d40edae..40b79024a 100755 --- a/contrib/cross.sh +++ b/contrib/cross.sh @@ -12,7 +12,6 @@ platform=${PLATFORM:-Linux} root="$(readlink -e $(dirname $0)/../)" cd $root set -e -set +x test $# = 0 && die no targets provided mkdir -p build-cross echo "all: $@" > build-cross/Makefile From 7251944adce5f07c46970911ecfcf8f332a0444c Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 2 Feb 2022 10:23:07 -0500 Subject: [PATCH 121/202] redo contrib/cross.sh to take cmake arguments --- contrib/cross.sh | 64 +++++++++++++++++++++++++++++++----------------- readme.md | 4 +-- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/contrib/cross.sh b/contrib/cross.sh index 40b79024a..0b7d66328 100755 --- a/contrib/cross.sh +++ b/contrib/cross.sh @@ -3,6 +3,8 @@ # helper script for me for when i cross compile # t. jeff # +set -e + die() { echo $@ exit 1 @@ -11,36 +13,52 @@ die() { platform=${PLATFORM:-Linux} root="$(readlink -e $(dirname $0)/../)" cd $root -set -e -test $# = 0 && die no targets provided mkdir -p build-cross -echo "all: $@" > build-cross/Makefile -for targ in $@ ; do - mkdir -p $root/build-cross/build-$targ - cd $root/build-cross/build-$targ + +cmake_opts="-DBUILD_STATIC_DEPS=ON \ + -DSTATIC_LINK=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DBUILD_TESTING=OFF \ + -DBUILD_LIBLOKINET=OFF \ + -DWITH_TESTS=OFF \ + -DNATIVE_BUILD=OFF \ + -DSTATIC_LINK=ON \ + -DWITH_SYSTEMD=OFF \ + -DFORCE_OXENMQ_SUBMODULE=ON \ + -DSUBMODULE_CHECK=OFF \ + -DWITH_LTO=OFF \ + -DWITH_BOOTSTRAP=OFF \ + -DCMAKE_BUILD_TYPE=RelWithDeb" + +targets=() + +while [ "$#" -gt 0 ]; do + if [ "$1" = "--" ]; then + shift + cmake_opts=$@ + break + fi + targets+=("$1") + shift +done +test ${#targets[@]} = 0 && die no targets provided + +archs="${targets[@]}" +echo "all: $archs" > build-cross/Makefile +for arch in $archs ; do + mkdir -p $root/build-cross/build-$arch + cd $root/build-cross/build-$arch cmake \ -G 'Unix Makefiles' \ -DCROSS_PLATFORM=$platform \ - -DCROSS_PREFIX=$targ \ + -DCROSS_PREFIX=$arch \ -DCMAKE_EXE_LINKER_FLAGS=-fstack-protector \ - -DCMAKE_CXX_FLAGS=-fdiagnostics-color=always\ - -DCMAKE_TOOLCHAIN_FILE=$root/contrib/cross/cross.toolchain.cmake\ - -DBUILD_STATIC_DEPS=ON \ - -DBUILD_SHARED_LIBS=OFF \ - -DBUILD_TESTING=OFF \ - -DBUILD_LIBLOKINET=OFF \ - -DWITH_TESTS=OFF \ - -DNATIVE_BUILD=OFF \ - -DSTATIC_LINK=ON \ - -DWITH_SYSTEMD=OFF \ - -DFORCE_OXENMQ_SUBMODULE=ON \ - -DSUBMODULE_CHECK=OFF \ - -DWITH_LTO=OFF \ - -DWITH_BOOTSTRAP=OFF \ - -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_CXX_FLAGS=-fdiagnostics-color=always \ + -DCMAKE_TOOLCHAIN_FILE=$root/contrib/cross/cross.toolchain.cmake \ + $cmake_opts \ $root cd $root/build-cross - echo -ne "$targ:\n\t\$(MAKE) -C build-$targ\n" >> $root/build-cross/Makefile + echo -ne "$arch:\n\t\$(MAKE) -C build-$arch\n" >> $root/build-cross/Makefile done cd $root diff --git a/readme.md b/readme.md index be57d75f3..2f35d1dcb 100644 --- a/readme.md +++ b/readme.md @@ -80,9 +80,7 @@ current cross targets: * mipsel-linux-gnu * powerpc64le-linux-gnu - - -install the toolchain (this one is for `aarch64-linux-gnu`) +install the toolchain (this one is for `aarch64-linux-gnu`, you can provide your own toolchain if you want) $ sudo apt install g{cc,++}-aarch64-linux-gnu From 7265a6c80e01a75b9ff3d9a07d14af063532aa01 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 2 Feb 2022 10:22:42 -0500 Subject: [PATCH 122/202] ci pipeline for cross compile --- .drone.jsonnet | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/.drone.jsonnet b/.drone.jsonnet index 465c2fc6b..b80c393e9 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -146,6 +146,36 @@ local windows_cross_pipeline(name, ], }; +// linux cross compile on debian +local linux_cross_pipeline(name, + cross_targets, + arch='amd64', + build_type='Release', + cmake_extra='', + extra_cmds=[], + jobs=6, + allow_fail=false) = { + kind: 'pipeline', + type: 'docker', + name: name, + platform: { arch: arch }, + trigger: { branch: { exclude: ['debian/*', 'ubuntu/*'] } }, + steps: [ + submodules, + { + name: 'build', + image: docker_base + 'debian-stable-cross', + pull: 'always', + [if allow_fail then 'failure']: 'ignore', + environment: { SSH_KEY: { from_secret: 'SSH_KEY' }, CROSS_TARGETS: std.join(':', cross_targets) }, + commands: [ + 'echo "Building on ${DRONE_STAGE_MACHINE}"', + 'VERBOSE=1 JOBS=' + jobs + ' ./contrib/cross.sh ' + std.join(' ', cross_targets) + (if std.length(cmake_extra) > 0 then ' -- ' + cmake_extra else ''), + ] + extra_cmds, + }, + ], +}; + // Builds a snapshot .deb on a debian-like system by merging into the debian/* or ubuntu/* branch local deb_builder(image, distro, distro_branch, arch='amd64', loki_repo=true) = { kind: 'pipeline', @@ -284,6 +314,12 @@ local mac_builder(name, // ARM builds (ARM64 and armhf) debian_pipeline('Debian sid (ARM64)', docker_base + 'debian-sid', arch='arm64', jobs=4), debian_pipeline('Debian stable (armhf)', docker_base + 'debian-stable/arm32v7', arch='arm64', jobs=4), + + // cross compile targets + linux_cross_pipeline('Cross Compile (mips)', cross_targets=['mips-linux-gnu', 'mipsel-linux-gnu']), + linux_cross_pipeline('Cross Compile (arm/arm64)', cross_targets=['arm-linux-gnueabihf', 'aarch64-linux-gnu']), + linux_cross_pipeline('Cross Compile (ppc64le)', cross_targets=['powerpc64le-linux-gnu']), + // android apk builder apk_builder('android apk', docker_base + 'flutter', extra_cmds=['UPLOAD_OS=android ./contrib/ci/drone-static-upload.sh']), From e2a94b44bb67dd5b2c11c151d4572c17e6fa3bd7 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 29 Mar 2022 08:39:15 -0400 Subject: [PATCH 123/202] bump zlib hashpin --- cmake/StaticBuild.cmake | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 2fe4671b9..df95f1eb4 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -55,12 +55,12 @@ set(LIBUV_SOURCE libuv-v${LIBUV_VERSION}.tar.gz) set(LIBUV_HASH SHA256=90d72bb7ae18de2519d0cac70eb89c319351146b90cd3f91303a492707e693a4 CACHE STRING "libuv source hash") -set(ZLIB_VERSION 1.2.11 CACHE STRING "zlib version") +set(ZLIB_VERSION 1.2.12 CACHE STRING "zlib version") set(ZLIB_MIRROR ${LOCAL_MIRROR} https://zlib.net CACHE STRING "zlib mirror(s)") set(ZLIB_SOURCE zlib-${ZLIB_VERSION}.tar.gz) -set(ZLIB_HASH SHA512=73fd3fff4adeccd4894084c15ddac89890cd10ef105dd5e1835e1e9bbb6a49ff229713bd197d203edfa17c2727700fce65a2a235f07568212d820dca88b528ae - CACHE STRING "zlib source hash") +set(ZLIB_HASH SHA256=91844808532e5ce316b3c010929493c0244f3d37593afd6de04f71821d5136d9 + CACHE STRING "zlib source hash") set(CURL_VERSION 7.81.0 CACHE STRING "curl version") set(CURL_MIRROR ${LOCAL_MIRROR} https://curl.haxx.se/download https://curl.askapache.com From 38a157808ec7d97a3ef8e01f931f28e296b70fba Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 28 Mar 2022 18:11:47 -0400 Subject: [PATCH 124/202] Cache best paths determined by GetPathByRouter to reduce cpu usage --- llarp/path/pathbuilder.cpp | 7 ++++--- llarp/path/pathset.cpp | 27 ++++++++++++++++++++++++++- llarp/path/pathset.hpp | 5 ++++- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/llarp/path/pathbuilder.cpp b/llarp/path/pathbuilder.cpp index 7800a12d4..2b7f4be39 100644 --- a/llarp/path/pathbuilder.cpp +++ b/llarp/path/pathbuilder.cpp @@ -191,10 +191,11 @@ namespace llarp lastBuild = 0s; } - void Builder::Tick(llarp_time_t) + void + Builder::Tick(llarp_time_t now) { - const auto now = llarp::time_now_ms(); - + PathSet::Tick(now); + now = llarp::time_now_ms(); m_router->pathBuildLimiter().Decay(now); ExpirePaths(now, m_router); diff --git a/llarp/path/pathset.cpp b/llarp/path/pathset.cpp index 75cecff39..50a40ef75 100644 --- a/llarp/path/pathset.cpp +++ b/llarp/path/pathset.cpp @@ -66,13 +66,31 @@ namespace llarp PathSet::TickPaths(AbstractRouter* r) { const auto now = llarp::time_now_ms(); - Lock_t l(m_PathsMutex); + Lock_t l{m_PathsMutex}; for (auto& item : m_Paths) { item.second->Tick(now, r); } } + void PathSet::Tick(llarp_time_t) + { + std::unordered_set endpoints; + for (auto& item : m_Paths) + { + endpoints.emplace(item.second->Endpoint()); + } + + m_PathCache.clear(); + for (const auto& ep : endpoints) + { + if (auto path = GetPathByRouter(ep)) + { + m_PathCache[ep] = path->weak_from_this(); + } + } + } + void PathSet::ExpirePaths(llarp_time_t now, AbstractRouter* router) { @@ -150,6 +168,13 @@ namespace llarp { Lock_t l(m_PathsMutex); Path_ptr chosen = nullptr; + if (roles == ePathRoleAny) + { + if (auto itr = m_PathCache.find(id); itr != m_PathCache.end()) + { + return itr->second.lock(); + } + } auto itr = m_Paths.begin(); while (itr != m_Paths.end()) { diff --git a/llarp/path/pathset.hpp b/llarp/path/pathset.hpp index 8457441b2..5bef0a643 100644 --- a/llarp/path/pathset.hpp +++ b/llarp/path/pathset.hpp @@ -130,7 +130,7 @@ namespace llarp /// tick owned paths virtual void - Tick(llarp_time_t now) = 0; + Tick(llarp_time_t now); /// count the number of paths that will exist at this timestamp in future size_t @@ -320,6 +320,9 @@ namespace llarp using PathMap_t = std::unordered_map, Path_ptr>; mutable Mtx_t m_PathsMutex; PathMap_t m_Paths; + + private: + std::unordered_map> m_PathCache; }; } // namespace path From 77bf2f4af96165bf645d7e268e2c38d90bee6a8e Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 30 Mar 2022 17:35:45 -0400 Subject: [PATCH 125/202] disable building fat liblokinet.so on bionic because lto is broken on bionoic still --- .drone.jsonnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index ef062f1e2..2bb81c2cc 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -367,7 +367,7 @@ local docs_pipeline(name, image, extra_cmds=[], allow_fail=false) = { '-DCMAKE_C_COMPILER=gcc-8 -DCMAKE_CXX_COMPILER=g++-8 ' + '-DCMAKE_CXX_FLAGS="-march=x86-64 -mtune=haswell" ' + '-DCMAKE_C_FLAGS="-march=x86-64 -mtune=haswell" ' + - '-DNATIVE_BUILD=OFF -DWITH_SYSTEMD=OFF -DWITH_BOOTSTRAP=OFF', + '-DNATIVE_BUILD=OFF -DWITH_SYSTEMD=OFF -DWITH_BOOTSTRAP=OFF -DBUILD_LIBLOKINET=OFF', extra_cmds=[ '../contrib/ci/drone-check-static-libs.sh', '../contrib/ci/drone-static-upload.sh', From 17687e300efb4d4c6f750b59d5aa85b118c2d060 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 30 Mar 2022 19:38:41 -0400 Subject: [PATCH 126/202] drone-ci image name fix, use -builder images --- .drone.jsonnet | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 2bb81c2cc..afbf6dc0e 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -393,11 +393,11 @@ local docs_pipeline(name, image, extra_cmds=[], allow_fail=false) = { cmake_extra='-DWITH_HIVE=ON'), // Deb builds: - deb_builder(docker_base + 'debian-sid-debhelper', 'sid', 'debian/sid'), - deb_builder(docker_base + 'debian-bullseye-debhelper', 'bullseye', 'debian/bullseye'), - deb_builder(docker_base + 'ubuntu-impish-debhelper', 'impish', 'ubuntu/impish'), - deb_builder(docker_base + 'ubuntu-focal-debhelper', 'focal', 'ubuntu/focal'), - deb_builder(docker_base + 'debian-sid-debhelper', 'sid', 'debian/sid', arch='arm64'), + deb_builder(docker_base + 'debian-sid-builder', 'sid', 'debian/sid'), + deb_builder(docker_base + 'debian-bullseye-builder', 'bullseye', 'debian/bullseye'), + deb_builder(docker_base + 'ubuntu-impish-builder', 'impish', 'ubuntu/impish'), + deb_builder(docker_base + 'ubuntu-focal-builder', 'focal', 'ubuntu/focal'), + deb_builder(docker_base + 'debian-sid-builder', 'sid', 'debian/sid', arch='arm64'), // Macos builds: mac_builder('macOS (Release)'), From af041cfee48d12fa1dc6a39c25247a1e231bfd71 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 1 Apr 2022 00:10:02 -0400 Subject: [PATCH 127/202] try not to spam builds --- llarp/service/outbound_context.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/llarp/service/outbound_context.cpp b/llarp/service/outbound_context.cpp index e2ac2cb6e..2f296207f 100644 --- a/llarp/service/outbound_context.cpp +++ b/llarp/service/outbound_context.cpp @@ -479,7 +479,8 @@ namespace llarp { if (markedBad or path::Builder::BuildCooldownHit(now)) return false; - if (NumInStatus(path::ePathBuilding) >= numDesiredPaths) + + if (NumInStatus(path::ePathBuilding) >= std::max(numDesiredPaths / size_t{2}, size_t{1})) return false; size_t numValidPaths = 0; From ffadcb2e93696d4529207a7b59704398d8aa5d8e Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 1 Apr 2022 10:16:43 -0400 Subject: [PATCH 128/202] clean up paths that are ignored and inactive --- llarp/path/path.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/llarp/path/path.cpp b/llarp/path/path.cpp index d0b5ea72f..e031b845b 100644 --- a/llarp/path/path.cpp +++ b/llarp/path/path.cpp @@ -472,6 +472,11 @@ namespace llarp EnterState(ePathTimeout, now); } } + if (_status == ePathIgnore and now - m_LastRecvMessage >= path::alive_timeout) + { + // clean up this path as we dont use it anymore + EnterState(ePathExpired, now); + } } void From f702aacc389f34ddeabaaeebafb8c87f7e611637 Mon Sep 17 00:00:00 2001 From: majestrate Date: Tue, 5 Apr 2022 19:13:11 -0400 Subject: [PATCH 129/202] add arch linux section to readme cite current ongoing discussion thread on the aur. --- readme.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/readme.md b/readme.md index 2f35d1dcb..cbded271b 100644 --- a/readme.md +++ b/readme.md @@ -68,6 +68,9 @@ If you want to build from source: $ make -j$(nproc) $ sudo make install +#### Arch Linux + +Due 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). #### Cross Compile For Linux From 3fbddac464bef3c2dd7386fb8686ed5571c362e9 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 12 Apr 2022 11:41:37 -0400 Subject: [PATCH 130/202] idempotent flush queues on path builds --- llarp/messages/relay_commit.cpp | 2 ++ llarp/messages/relay_status.cpp | 33 ++++++++++++++++++++++----------- llarp/messages/relay_status.hpp | 5 ++++- 3 files changed, 28 insertions(+), 12 deletions(-) diff --git a/llarp/messages/relay_commit.cpp b/llarp/messages/relay_commit.cpp index 22fd0e33a..cdcc9f57b 100644 --- a/llarp/messages/relay_commit.cpp +++ b/llarp/messages/relay_commit.cpp @@ -456,6 +456,8 @@ namespace llarp self->decrypter = nullptr; }); } + // trigger idempotent pump to ensure that the build messages propagate + self->context->Router()->TriggerPump(); } }; diff --git a/llarp/messages/relay_status.cpp b/llarp/messages/relay_status.cpp index 9ea58a55b..ceee052e9 100644 --- a/llarp/messages/relay_status.cpp +++ b/llarp/messages/relay_status.cpp @@ -221,24 +221,35 @@ namespace llarp std::shared_ptr hop) { router->loop()->call([router, nextHop, msg = std::move(msg), hop = std::move(hop)] { - SendMessage(router, nextHop, msg); - // destroy hop as needed - if ((msg->status & LR_StatusRecord::SUCCESS) != LR_StatusRecord::SUCCESS) - { - hop->QueueDestroySelf(router); - } + SendMessage(router, nextHop, msg, hop); }); } void LR_StatusMessage::SendMessage( - AbstractRouter* router, const RouterID nextHop, std::shared_ptr msg) + AbstractRouter* router, + const RouterID nextHop, + std::shared_ptr msg, + std::shared_ptr hop) { llarp::LogDebug("Attempting to send LR_Status message to (", nextHop, ")"); - if (not router->SendToOrQueue(nextHop, *msg)) - { - llarp::LogError("Sending LR_Status message, SendToOrQueue to ", nextHop, " failed"); - } + + auto resultCallback = [hop, router, msg, nextHop](auto status) { + if ((msg->status & LR_StatusRecord::SUCCESS) != LR_StatusRecord::SUCCESS + or status != SendStatus::Success) + { + llarp::LogError("Failed to propagate LR_Status message to ", nextHop); + hop->QueueDestroySelf(router); + } + }; + + // send the status message to previous hop + // if it fails we are hitting a failure case we can't cope with so ... drop. + if (not router->SendToOrQueue(nextHop, *msg, resultCallback)) + resultCallback(SendStatus::Congestion); + + // trigger idempotent pump to make sure stuff gets sent + router->TriggerPump(); } bool diff --git a/llarp/messages/relay_status.hpp b/llarp/messages/relay_status.hpp index b331cd622..265ce6a08 100644 --- a/llarp/messages/relay_status.hpp +++ b/llarp/messages/relay_status.hpp @@ -105,7 +105,10 @@ namespace llarp static void SendMessage( - AbstractRouter* router, const RouterID nextHop, std::shared_ptr msg); + AbstractRouter* router, + const RouterID nextHop, + std::shared_ptr msg, + std::shared_ptr hop); const char* Name() const override From 768ed30c057d01238ea87e1a7111f5d1882a25a3 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 13 Apr 2022 22:49:24 -0400 Subject: [PATCH 131/202] add public key in rpc ping --- llarp/rpc/lokid_rpc_client.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/llarp/rpc/lokid_rpc_client.cpp b/llarp/rpc/lokid_rpc_client.cpp index 0fc6ddb7b..44e5c9520 100644 --- a/llarp/rpc/lokid_rpc_client.cpp +++ b/llarp/rpc/lokid_rpc_client.cpp @@ -7,6 +7,7 @@ #include #include +#include #include namespace llarp @@ -147,7 +148,12 @@ namespace llarp constexpr auto PingInterval = 30s; auto makePingRequest = [self = shared_from_this()]() { // send a ping - nlohmann::json payload = {{"version", {VERSION[0], VERSION[1], VERSION[2]}}}; + PubKey pk{}; + if (auto r = self->m_Router.lock()) + pk = r->pubkey(); + nlohmann::json payload = { + {"pubkey_ed25519", oxenc::to_hex(pk.begin(), pk.end())}, + {"version", {VERSION[0], VERSION[1], VERSION[2]}}}; self->Request( "admin.lokinet_ping", [](bool success, std::vector data) { From 56d22b25d82e571d70bc3f9a3de3c4bc43b410db Mon Sep 17 00:00:00 2001 From: necro-nemesis Date: Fri, 15 Apr 2022 08:44:17 -0400 Subject: [PATCH 132/202] Update to F36 container --- .drone.jsonnet | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 97fd42718..b1c6222a6 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -1,6 +1,6 @@ -local distro = "fedora-35"; -local distro_name = 'Fedora 35'; -local distro_docker = 'fedora:35'; +local distro = "fedora-36"; +local distro_name = 'Fedora 36'; +local distro_docker = 'fedora:36'; local submodules = { name: 'submodules', From 699591b76e8822c08884bc1a54b9b2027d07818a Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 15 Apr 2022 17:46:44 -0400 Subject: [PATCH 133/202] remove bitrotten files --- lokinet-docker.ini | 82 ---------------------------------------------- release.md | 55 ------------------------------- release_es.md | 55 ------------------------------- release_ru.md | 53 ------------------------------ 4 files changed, 245 deletions(-) delete mode 100644 lokinet-docker.ini delete mode 100644 release.md delete mode 100644 release_es.md delete mode 100644 release_ru.md diff --git a/lokinet-docker.ini b/lokinet-docker.ini deleted file mode 100644 index c55480eec..000000000 --- a/lokinet-docker.ini +++ /dev/null @@ -1,82 +0,0 @@ -# this configuration was auto generated with 'sane' defaults -# change these values as desired - - -[router] -# number of crypto worker threads -threads=4 -# path to store signed RC -contact-file=/root/.lokinet/self.signed -# path to store transport private key -transport-privkey=/root/.lokinet/transport.private -# path to store identity signing key -ident-privkey=/root/.lokinet/identity.private -# encryption key for onion routing -encryption-privkey=/root/.lokinet/encryption.private - -# uncomment following line to set router nickname to 'lokinet' -#nickname=lokinet - - -[logging] -level=info -# uncomment for logging to file -#type=file -#file=/path/to/logfile -# uncomment for syslog logging -#type=syslog - -[metrics] -json-metrics-path=/root/.lokinet/metrics.json - -# admin api (disabled by default) -[api] -enabled=true -#authkey=insertpubkey1here -#authkey=insertpubkey2here -#authkey=insertpubkey3here -bind=127.0.0.1:1190 - -# system settings for privileges and such -[system] -user=lokinet -group=lokinet -pidfile=/root/.lokinet/lokinet.pid - -# dns provider configuration section -[dns] -# resolver -upstream=1.1.1.1 -bind=127.3.2.1:53 - -# network database settings block -[netdb] -# directory for network database skiplist storage -dir=/netdb - -# bootstrap settings -[bootstrap] -# add a bootstrap node's signed identity to the list of nodes we want to bootstrap from -# if we don't have any peers we connect to this router -add-node=/root/.lokinet/bootstrap.signed - -# lokid settings (disabled by default) -[lokid] -enabled=false -jsonrpc=127.0.0.1:22023 -#service-node-seed=/path/to/servicenode/seed - -# network settings -[network] -profiles=/root/.lokinet/profiles.dat -enabled=true -exit=false -#exit-blacklist=tcp:25 -#exit-whitelist=tcp:* -#exit-whitelist=udp:* -ifaddr=10.200.0.1/8 -ifname=loki-docker0 - -# ROUTERS ONLY: publish network interfaces for handling inbound traffic -[bind] -eth0=1090 diff --git a/release.md b/release.md deleted file mode 100644 index 1ff06b51b..000000000 --- a/release.md +++ /dev/null @@ -1,55 +0,0 @@ - -The windows release is signed by rick, his public key is: - - -``` - ------BEGIN PGP PUBLIC KEY BLOCK----- - -mDMEXBhAthYJKwYBBAHaRw8BAQdACj8fXcXB+ktPL/gNRBGZajE9ycsQOiMPXigH -0uP6BCW0G1JpY2sgViA8cmlja0Bzbm93bGlnaHQubmV0PoiQBBMWCAA4FiEEsbLw -yIc/Y1IT8TNLwO3Icj/cNGUFAlwYQLYCGyMFCwkIBwIGFQoJCAsCBBYCAwECHgEC -F4AACgkQwO3Icj/cNGUeCwEAuyFfehigul3So0xOuRIxldiHoqLJfSEp4kjU+8b5 -NjsBAIOC4KFpdv8CTPa/aQgRIx/UlOjJ8vMnS94XPSs2vRcDuDgEXBhAthIKKwYB -BAGXVQEFAQEHQKT2GHP2O+q5vgXd6D4IiOu8rI+kcGllVY/0DEqGesJYAwEIB4h4 -BBgWCAAgFiEEsbLwyIc/Y1IT8TNLwO3Icj/cNGUFAlwYQLYCGwwACgkQwO3Icj/c -NGX/tgD9GES37acIhovhMzDj0u9oU/1HqNyx4A45EQ90dP8KMN4BALBRzWXgB23t -9r6g3ZWHQJEpF4RnmcbDbR0SxdyoCkQG -=RdBx ------END PGP PUBLIC KEY BLOCK----- - -``` - -The linux and macos releases are signed by jeff, his public key is: - -``` - ------BEGIN PGP PUBLIC KEY BLOCK----- - -mDMEWZx2ERYJKwYBBAHaRw8BAQdAKxsq4dGzYzKJqU8Vin5d8vJF10/NG4Hziw+f -WTbM8nC0MEplZmYgQmVja2VyIChwcm9iYWJseSBub3QgZXZpbCkgPGplZmZAaTJw -LnJvY2tzPoh5BBMWCAAhBQJZnHYRAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheA -AAoJEPNXs7Qvb5sFP2MBAIcL8KOd/RupEtSMyb2f4OBsaE8oFU+NsvfevW0XrBBQ -AQDhjax9f2D0k30pj4uYBJRb/L0JJFfbzI+uwgTtgRp1DLg4BFmcdhESCisGAQQB -l1UBBQEBB0BJOuegxPmX1Ma/nv4O2lZp0rA89EazPgtUrR3e1846DQMBCAeIYQQY -FggACQUCWZx2EQIbDAAKCRDzV7O0L2+bBUgkAPsEeiiut+gGECP/63m7NyTwruNP -oVZUYE1m8XXbHr28UgEA4nXGIAHDRuIUY4sRcVQz2Um9O6kaCdQHH0eSPE48VQ8= -=gFkp ------END PGP PUBLIC KEY BLOCK----- - -``` - -To verify the releases first import those keys into gpg key database - -run the following, copy paste both keys and press `^D` (control - D) to finish - - $ gpg --import - -Alternatively you can get jeff's key off a key server: - - $ gpg --recv-key 67EF6BA68E7B0B0D6EB4F7D4F357B3B42F6F9B05 # jeff's key - -then verify the signatures, make sure that the `.sig` file and the release file -are in the same directory. - - $ gpg --verify release-file.exe.sig diff --git a/release_es.md b/release_es.md deleted file mode 100644 index 5d0a9bca1..000000000 --- a/release_es.md +++ /dev/null @@ -1,55 +0,0 @@ - -La version lanzada para windows esta firmada por rick, su llave plublica es: - - -``` - ------BEGIN PGP PUBLIC KEY BLOCK----- - -mDMEXBhAthYJKwYBBAHaRw8BAQdACj8fXcXB+ktPL/gNRBGZajE9ycsQOiMPXigH -0uP6BCW0G1JpY2sgViA8cmlja0Bzbm93bGlnaHQubmV0PoiQBBMWCAA4FiEEsbLw -yIc/Y1IT8TNLwO3Icj/cNGUFAlwYQLYCGyMFCwkIBwIGFQoJCAsCBBYCAwECHgEC -F4AACgkQwO3Icj/cNGUeCwEAuyFfehigul3So0xOuRIxldiHoqLJfSEp4kjU+8b5 -NjsBAIOC4KFpdv8CTPa/aQgRIx/UlOjJ8vMnS94XPSs2vRcDuDgEXBhAthIKKwYB -BAGXVQEFAQEHQKT2GHP2O+q5vgXd6D4IiOu8rI+kcGllVY/0DEqGesJYAwEIB4h4 -BBgWCAAgFiEEsbLwyIc/Y1IT8TNLwO3Icj/cNGUFAlwYQLYCGwwACgkQwO3Icj/c -NGX/tgD9GES37acIhovhMzDj0u9oU/1HqNyx4A45EQ90dP8KMN4BALBRzWXgB23t -9r6g3ZWHQJEpF4RnmcbDbR0SxdyoCkQG -=RdBx ------END PGP PUBLIC KEY BLOCK----- - -``` - -La version lanzada para linux y macos estan firmadas por jeff, su llave publica es: - -``` - ------BEGIN PGP PUBLIC KEY BLOCK----- - -mDMEWZx2ERYJKwYBBAHaRw8BAQdAKxsq4dGzYzKJqU8Vin5d8vJF10/NG4Hziw+f -WTbM8nC0MEplZmYgQmVja2VyIChwcm9iYWJseSBub3QgZXZpbCkgPGplZmZAaTJw -LnJvY2tzPoh5BBMWCAAhBQJZnHYRAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheA -AAoJEPNXs7Qvb5sFP2MBAIcL8KOd/RupEtSMyb2f4OBsaE8oFU+NsvfevW0XrBBQ -AQDhjax9f2D0k30pj4uYBJRb/L0JJFfbzI+uwgTtgRp1DLg4BFmcdhESCisGAQQB -l1UBBQEBB0BJOuegxPmX1Ma/nv4O2lZp0rA89EazPgtUrR3e1846DQMBCAeIYQQY -FggACQUCWZx2EQIbDAAKCRDzV7O0L2+bBUgkAPsEeiiut+gGECP/63m7NyTwruNP -oVZUYE1m8XXbHr28UgEA4nXGIAHDRuIUY4sRcVQz2Um9O6kaCdQHH0eSPE48VQ8= -=gFkp ------END PGP PUBLIC KEY BLOCK----- - -``` - -Para verificar las versiones lanzada, primero importar esas llaves en la base de datos de llaves de gpg - -correr lo proximo, copie y pegue ambas llaves y presione `^D` (control - D) para completar - - $ gpg --import - -De forma alternativa puede obtener la llave de jeff desde un servidor de llaves: - - $ gpg --recv-key 67EF6BA68E7B0B0D6EB4F7D4F357B3B42F6F9B05 # jeff's key - -luego verifique la llave, este seguro de que el archivo `.sig` y el archivo de la version lanzada esta -en el mismo directorio. - - $ gpg --verify release-file.exe.sig diff --git a/release_ru.md b/release_ru.md deleted file mode 100644 index 804d97ddd..000000000 --- a/release_ru.md +++ /dev/null @@ -1,53 +0,0 @@ - -Релиз Ð´Ð»Ñ Windows подпиÑан Риком, его открытый ключ: - - -``` - ------BEGIN PGP PUBLIC KEY BLOCK----- - -mDMEXBhAthYJKwYBBAHaRw8BAQdACj8fXcXB+ktPL/gNRBGZajE9ycsQOiMPXigH -0uP6BCW0G1JpY2sgViA8cmlja0Bzbm93bGlnaHQubmV0PoiQBBMWCAA4FiEEsbLw -yIc/Y1IT8TNLwO3Icj/cNGUFAlwYQLYCGyMFCwkIBwIGFQoJCAsCBBYCAwECHgEC -F4AACgkQwO3Icj/cNGUeCwEAuyFfehigul3So0xOuRIxldiHoqLJfSEp4kjU+8b5 -NjsBAIOC4KFpdv8CTPa/aQgRIx/UlOjJ8vMnS94XPSs2vRcDuDgEXBhAthIKKwYB -BAGXVQEFAQEHQKT2GHP2O+q5vgXd6D4IiOu8rI+kcGllVY/0DEqGesJYAwEIB4h4 -BBgWCAAgFiEEsbLwyIc/Y1IT8TNLwO3Icj/cNGUFAlwYQLYCGwwACgkQwO3Icj/c -NGX/tgD9GES37acIhovhMzDj0u9oU/1HqNyx4A45EQ90dP8KMN4BALBRzWXgB23t -9r6g3ZWHQJEpF4RnmcbDbR0SxdyoCkQG -=RdBx ------END PGP PUBLIC KEY BLOCK----- - -``` -Релизы linux и macos подпиÑаны Джеффом, его открытый ключ: - -``` - ------BEGIN PGP PUBLIC KEY BLOCK----- - -mDMEWZx2ERYJKwYBBAHaRw8BAQdAKxsq4dGzYzKJqU8Vin5d8vJF10/NG4Hziw+f -WTbM8nC0MEplZmYgQmVja2VyIChwcm9iYWJseSBub3QgZXZpbCkgPGplZmZAaTJw -LnJvY2tzPoh5BBMWCAAhBQJZnHYRAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheA -AAoJEPNXs7Qvb5sFP2MBAIcL8KOd/RupEtSMyb2f4OBsaE8oFU+NsvfevW0XrBBQ -AQDhjax9f2D0k30pj4uYBJRb/L0JJFfbzI+uwgTtgRp1DLg4BFmcdhESCisGAQQB -l1UBBQEBB0BJOuegxPmX1Ma/nv4O2lZp0rA89EazPgtUrR3e1846DQMBCAeIYQQY -FggACQUCWZx2EQIbDAAKCRDzV7O0L2+bBUgkAPsEeiiut+gGECP/63m7NyTwruNP -oVZUYE1m8XXbHr28UgEA4nXGIAHDRuIUY4sRcVQz2Um9O6kaCdQHH0eSPE48VQ8= -=gFkp ------END PGP PUBLIC KEY BLOCK----- - -``` - -Чтобы проверить релизы, Ñначала импортируйте Ñти ключи в базу данных ключей gpg. - -выполните Ñледующее, Ñкопируйте и вÑтавьте оба ключа и нажмите `^D` (control - D), чтобы закончить - - $ gpg --import - -Ð’ качеÑтве альтернативы вы можете получить ключ Джеффа на Ñервере ключей: - - $ gpg --recv-key 67EF6BA68E7B0B0D6EB4F7D4F357B3B42F6F9B05 # jeff's key - -затем проверьте подпиÑи, убедитеÑÑŒ, что файл `.sig` и файл релиза находÑÑ‚ÑÑ Ð² том же каталоге. - - $ gpg --verify release-file.exe.sig From 9765eeee7e86d28f0376bdd1027891c494953382 Mon Sep 17 00:00:00 2001 From: Jeff Date: Sun, 17 Apr 2022 21:22:53 -0400 Subject: [PATCH 134/202] handle edge case better when our path is fine but the recipiant's path on the pivot router isn't we should pivot to another router --- llarp/service/outbound_context.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/llarp/service/outbound_context.cpp b/llarp/service/outbound_context.cpp index 2f296207f..a67b70fe7 100644 --- a/llarp/service/outbound_context.cpp +++ b/llarp/service/outbound_context.cpp @@ -45,11 +45,10 @@ namespace llarp if (dst == remoteIntro.pathID && remoteIntro.router == p->Endpoint()) { LogWarn(Name(), " message ", seq, " dropped by endpoint ", p->Endpoint(), " via ", dst); + markedBad = remoteIntro.IsExpired(Now()); MarkCurrentIntroBad(Now()); - ShiftIntroduction(false); + ShiftIntroRouter(p->Endpoint()); UpdateIntroSet(); - SwapIntros(); - markedBad = remoteIntro.IsExpired(Now()); } return true; } From d05518be571477a28d8d339a924ab1e9d0196edb Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 18 Apr 2022 15:56:54 -0400 Subject: [PATCH 135/202] use the new electron gui in windows build --- cmake/win32_installer_deps.cmake | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmake/win32_installer_deps.cmake b/cmake/win32_installer_deps.cmake index 95ea1aedf..81ea1eac2 100644 --- a/cmake/win32_installer_deps.cmake +++ b/cmake/win32_installer_deps.cmake @@ -1,6 +1,6 @@ if(NOT GUI_ZIP_URL) - set(GUI_ZIP_URL "https://oxen.rocks/oxen-io/loki-network-control-panel/lokinet-gui-windows-32bit-v0.3.8.zip") - set(GUI_ZIP_HASH_OPTS EXPECTED_HASH SHA256=60c2b738cf997e5684f307e5222498fd09143d495a932924105a49bf59ded8bb) + set(GUI_ZIP_URL "https://oxen.rocks/oxen-io/lokinet-gui/dev/lokinet-windows-x64-20220331T180338Z-569f90ad8.zip") + set(GUI_ZIP_HASH_OPTS EXPECTED_HASH SHA256=316f10489f5907bfa9c74b21f8ef2fdd7b7c7e6a0f5bcedaed2ee5f4004eab52) endif() set(TUNTAP_URL "https://build.openvpn.net/downloads/releases/latest/tap-windows-latest-stable.exe") From 1a254a4301b497f32a4e500498b50d8058217046 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 18 Apr 2022 16:23:21 -0400 Subject: [PATCH 136/202] kill/restore ipv6 with powershell because windows is vile --- llarp/vpn/win32.hpp | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/llarp/vpn/win32.hpp b/llarp/vpn/win32.hpp index 172fb0b94..55d4d9f93 100644 --- a/llarp/vpn/win32.hpp +++ b/llarp/vpn/win32.hpp @@ -169,18 +169,9 @@ namespace llarp::vpn return ret; } - class Win32Interface final : public NetworkInterface + namespace { - std::atomic m_Run; - HANDLE m_Device, m_IOCP; - std::vector m_Threads; - thread::Queue m_ReadQueue; - - InterfaceInfo m_Info; - - AbstractRouter* const _router; - - static std::wstring + std::wstring get_win_sys_path() { wchar_t win_sys_path[MAX_PATH] = {0}; @@ -193,6 +184,18 @@ namespace llarp::vpn } return win_sys_path; } + } // namespace + + class Win32Interface final : public NetworkInterface + { + std::atomic m_Run; + HANDLE m_Device, m_IOCP; + std::vector m_Threads; + thread::Queue m_ReadQueue; + + InterfaceInfo m_Info; + + AbstractRouter* const _router; static std::string NetSHCommand() @@ -514,6 +517,17 @@ namespace llarp::vpn ::system(cmd.c_str()); } + static std::string + PowerShell() + { + std::wstring wcmd = + get_win_sys_path() + L"\\WindowsPowerShell\\v1.0\\powershell.exe -Command "; + + using convert_type = std::codecvt_utf8; + std::wstring_convert converter; + return converter.to_bytes(wcmd); + } + static std::string RouteCommand() { @@ -623,12 +637,16 @@ namespace llarp::vpn void AddDefaultRouteViaInterface(std::string ifname) override { + // kill ipv6 + Execute(PowerShell() + R"(Disable-NetAdapterBinding -Name "*" -ComponentID ms_tcpip6)"); DefaultRouteViaInterface(ifname, "ADD"); } void DelDefaultRouteViaInterface(std::string ifname) override { + // restore ipv6 + Execute(PowerShell() + R"(Enable-NetAdapterBinding -Name "*" -ComponentID ms_tcpip6)"); DefaultRouteViaInterface(ifname, "DELETE"); } }; From 9a6bfe6013f8a9fd56aef06cb3dd27d8fb55cc1d Mon Sep 17 00:00:00 2001 From: Jeff Date: Sun, 26 Dec 2021 16:48:27 -0500 Subject: [PATCH 137/202] static endpoint auth codes --- llarp/config/config.cpp | 10 +++++++++ llarp/config/config.hpp | 1 + llarp/handlers/tun.cpp | 7 +++++- llarp/rpc/endpoint_rpc.cpp | 46 ++++++++++++++++++++++++++------------ llarp/rpc/endpoint_rpc.hpp | 4 +++- 5 files changed, 52 insertions(+), 16 deletions(-) diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp index 94b5ff15c..f6c09f881 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp @@ -366,6 +366,16 @@ namespace llarp m_AuthWhitelist.emplace(std::move(addr)); }); + conf.defineOption( + "network", + "auth-static", + ClientOnly, + MultiValue, + Comment{ + "manually add a static auth code to accept for endpoint auth", + }, + [this](std::string arg) { m_AuthStaticTokens.emplace(std::move(arg)); }); + conf.defineOption( "network", "reachable", diff --git a/llarp/config/config.hpp b/llarp/config/config.hpp index ff7ca2e71..bf2e61b8b 100644 --- a/llarp/config/config.hpp +++ b/llarp/config/config.hpp @@ -118,6 +118,7 @@ namespace llarp std::optional m_AuthUrl; std::optional m_AuthMethod; std::unordered_set m_AuthWhitelist; + std::unordered_set m_AuthStaticTokens; std::vector m_SRVRecords; diff --git a/llarp/handlers/tun.cpp b/llarp/handlers/tun.cpp index 96f63ee12..050cac60c 100644 --- a/llarp/handlers/tun.cpp +++ b/llarp/handlers/tun.cpp @@ -183,7 +183,12 @@ namespace llarp method = *conf.m_AuthMethod; } auto auth = std::make_shared( - url, method, conf.m_AuthWhitelist, Router()->lmq(), shared_from_this()); + url, + method, + conf.m_AuthWhitelist, + conf.m_AuthStaticTokens, + Router()->lmq(), + shared_from_this()); auth->Start(); m_AuthPolicy = std::move(auth); } diff --git a/llarp/rpc/endpoint_rpc.cpp b/llarp/rpc/endpoint_rpc.cpp index 302f14d65..8fa90bf95 100644 --- a/llarp/rpc/endpoint_rpc.cpp +++ b/llarp/rpc/endpoint_rpc.cpp @@ -6,14 +6,16 @@ namespace llarp::rpc EndpointAuthRPC::EndpointAuthRPC( std::string url, std::string method, - Whitelist_t whitelist, + Whitelist_t whitelist_addrs, + std::unordered_set whitelist_tokens, LMQ_ptr lmq, Endpoint_ptr endpoint) - : m_AuthURL(std::move(url)) - , m_AuthMethod(std::move(method)) - , m_AuthWhitelist(std::move(whitelist)) - , m_LMQ(std::move(lmq)) - , m_Endpoint(std::move(endpoint)) + : m_AuthURL{std::move(url)} + , m_AuthMethod{std::move(method)} + , m_AuthWhitelist{std::move(whitelist_addrs)} + , m_AuthStaticTokens{std::move(whitelist_tokens)} + , m_LMQ{std::move(lmq)} + , m_Endpoint{std::move(endpoint)} {} void @@ -57,13 +59,6 @@ namespace llarp::rpc reply(service::AuthResult{service::AuthResultCode::eAuthAccepted, "explicitly whitelisted"}); return; } - if (not m_Conn.has_value()) - { - // we don't have a connection to the backend so it's failed - reply(service::AuthResult{ - service::AuthResultCode::eAuthFailed, "remote has no connection to auth backend"}); - return; - } if (msg->proto != llarp::service::ProtocolType::Auth) { @@ -72,9 +67,32 @@ namespace llarp::rpc return; } + std::string payload{(char*)msg->payload.data(), msg->payload.size()}; + + if (m_AuthStaticTokens.count(payload)) + { + reply(service::AuthResult{service::AuthResultCode::eAuthAccepted, "explicitly whitelisted"}); + return; + } + + if (not m_Conn.has_value()) + { + if (m_AuthStaticTokens.empty()) + { + // we don't have a connection to the backend so it's failed + reply(service::AuthResult{ + service::AuthResultCode::eAuthFailed, "remote has no connection to auth backend"}); + } + else + { + // static auth mode + reply(service::AuthResult{service::AuthResultCode::eAuthRejected, "access not permitted"}); + } + return; + } + const auto authinfo = msg->EncodeAuthInfo(); std::string_view metainfo{authinfo.data(), authinfo.size()}; - std::string_view payload{(char*)msg->payload.data(), msg->payload.size()}; // call method with 2 parameters: metainfo and userdata m_LMQ->request( *m_Conn, diff --git a/llarp/rpc/endpoint_rpc.hpp b/llarp/rpc/endpoint_rpc.hpp index c84cf6183..02e573d6d 100644 --- a/llarp/rpc/endpoint_rpc.hpp +++ b/llarp/rpc/endpoint_rpc.hpp @@ -20,7 +20,8 @@ namespace llarp::rpc explicit EndpointAuthRPC( std::string url, std::string method, - Whitelist_t whitelist, + Whitelist_t addr_whitelist, + std::unordered_set token_whitelist, LMQ_ptr lmq, Endpoint_ptr endpoint); virtual ~EndpointAuthRPC() = default; @@ -40,6 +41,7 @@ namespace llarp::rpc const std::string m_AuthURL; const std::string m_AuthMethod; const Whitelist_t m_AuthWhitelist; + const std::unordered_set m_AuthStaticTokens; LMQ_ptr m_LMQ; Endpoint_ptr m_Endpoint; std::optional m_Conn; From a082ba4e7706ab38ff8407cbb735ee032753eb22 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 17 Jan 2022 07:57:08 -0500 Subject: [PATCH 138/202] add file auth for tokens --- llarp/config/config.cpp | 21 +++++++++- llarp/config/config.hpp | 1 + llarp/handlers/tun.cpp | 6 ++- llarp/service/auth.cpp | 92 +++++++++++++++++++++++++++++++++++++++++ llarp/service/auth.hpp | 8 +++- 5 files changed, 124 insertions(+), 4 deletions(-) diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp index f6c09f881..1e640373a 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp @@ -317,7 +317,7 @@ namespace llarp ClientOnly, Comment{ "Set the endpoint authentication mechanism.", - "none/whitelist/lmq", + "none/whitelist/lmq/file", }, [this](std::string arg) { if (arg.empty()) @@ -366,13 +366,30 @@ namespace llarp m_AuthWhitelist.emplace(std::move(addr)); }); + conf.defineOption( + "network", + "auth-file", + ClientOnly, + MultiValue, + Comment{ + "Read auth tokens from file to accept endpoint auth", + "Can be provided multiple times", + }, + [this](fs::path arg) { + if (not fs::exists(arg)) + throw std::invalid_argument{ + stringify("cannot load auth file ", arg, " as it does not seem to exist")}; + m_AuthFiles.emplace(std::move(arg)); + }); + conf.defineOption( "network", "auth-static", ClientOnly, MultiValue, Comment{ - "manually add a static auth code to accept for endpoint auth", + "Manually add a static auth code to accept for endpoint auth", + "Can be provided multiple times", }, [this](std::string arg) { m_AuthStaticTokens.emplace(std::move(arg)); }); diff --git a/llarp/config/config.hpp b/llarp/config/config.hpp index bf2e61b8b..6f6d82ef2 100644 --- a/llarp/config/config.hpp +++ b/llarp/config/config.hpp @@ -119,6 +119,7 @@ namespace llarp std::optional m_AuthMethod; std::unordered_set m_AuthWhitelist; std::unordered_set m_AuthStaticTokens; + std::set m_AuthFiles; std::vector m_SRVRecords; diff --git a/llarp/handlers/tun.cpp b/llarp/handlers/tun.cpp index 050cac60c..6f7f27103 100644 --- a/llarp/handlers/tun.cpp +++ b/llarp/handlers/tun.cpp @@ -174,7 +174,11 @@ namespace llarp LogInfo(Name(), " setting to be not reachable by default"); } - if (conf.m_AuthType != service::AuthType::eAuthTypeNone) + if (conf.m_AuthType == service::AuthType::eAuthTypeFile) + { + m_AuthPolicy = service::MakeFileAuthPolicy(m_router, conf.m_AuthFiles); + } + else if (conf.m_AuthType != service::AuthType::eAuthTypeNone) { std::string url, method; if (conf.m_AuthUrl.has_value() and conf.m_AuthMethod.has_value()) diff --git a/llarp/service/auth.cpp b/llarp/service/auth.cpp index 2f631f3fb..a5e4518a0 100644 --- a/llarp/service/auth.cpp +++ b/llarp/service/auth.cpp @@ -1,6 +1,11 @@ #include "auth.hpp" #include +#include +#include "protocol.hpp" +#include +#include + namespace llarp::service { /// maybe get auth result from string @@ -22,6 +27,7 @@ namespace llarp::service ParseAuthType(std::string data) { std::unordered_map values = { + {"file", AuthType::eAuthTypeFile}, {"lmq", AuthType::eAuthTypeLMQ}, {"whitelist", AuthType::eAuthTypeWhitelist}, {"none", AuthType::eAuthTypeNone}}; @@ -58,4 +64,90 @@ namespace llarp::service } } + class FileAuthPolicy : public IAuthPolicy, public std::enable_shared_from_this + { + const std::set m_Files; + AbstractRouter* const m_Router; + mutable util::Mutex m_Access; + std::unordered_set m_Pending; + /// returns an auth result for a auth info challange, opens every file until it finds a token + /// matching it + /// this is expected to be done in the IO thread + AuthResult + CheckFiles(const AuthInfo& info) const + { + for (const auto& f : m_Files) + { + fs::ifstream i{f}; + std::string line{}; + while (std::getline(i, line)) + { + // split off comments + const auto parts = split_any(line, "#;", true); + if (auto part = parts[0]; not parts.empty() and not parts[0].empty()) + { + // split off whitespaces + if (TrimWhitespace(part) == info.token) + return AuthResult{AuthResultCode::eAuthAccepted, "accepted by whitelist"}; + } + } + } + return AuthResult{AuthResultCode::eAuthRejected, "rejected by whitelist"}; + } + + public: + FileAuthPolicy(AbstractRouter* r, std::set files) + : m_Files{std::move(files)}, m_Router{r} + {} + + void + AuthenticateAsync( + std::shared_ptr msg, std::function hook) override + { + auto reply = m_Router->loop()->make_caller( + [tag = msg->tag, hook, self = shared_from_this()](AuthResult result) { + { + util::Lock _lock{self->m_Access}; + self->m_Pending.erase(tag); + } + hook(result); + }); + { + util::Lock _lock{m_Access}; + m_Pending.emplace(msg->tag); + } + if (msg->proto == ProtocolType::Auth) + { + m_Router->QueueDiskIO( + [self = shared_from_this(), + auth = AuthInfo{std::string{ + reinterpret_cast(msg->payload.data()), msg->payload.size()}}, + reply]() { + try + { + reply(self->CheckFiles(auth)); + } + catch (std::exception& ex) + { + reply(AuthResult{AuthResultCode::eAuthFailed, ex.what()}); + } + }); + } + else + reply(AuthResult{AuthResultCode::eAuthRejected, "protocol error"}); + } + bool + AsyncAuthPending(ConvoTag tag) const override + { + util::Lock _lock{m_Access}; + return m_Pending.count(tag); + } + }; + + std::shared_ptr + MakeFileAuthPolicy(AbstractRouter* r, std::set files) + { + return std::make_shared(r, std::move(files)); + } + } // namespace llarp::service diff --git a/llarp/service/auth.hpp b/llarp/service/auth.hpp index 98e515e02..fd094f475 100644 --- a/llarp/service/auth.hpp +++ b/llarp/service/auth.hpp @@ -71,7 +71,9 @@ namespace llarp::service /// manual whitelist eAuthTypeWhitelist, /// LMQ server - eAuthTypeLMQ + eAuthTypeLMQ, + /// plain file + eAuthTypeFile, }; /// get an auth type from a string @@ -79,4 +81,8 @@ namespace llarp::service AuthType ParseAuthType(std::string arg); + /// make an IAuthPolicy that reads out of a static file + std::shared_ptr + MakeFileAuthPolicy(AbstractRouter*, std::set files); + } // namespace llarp::service From a51576d1ea1e0969013d4b3e796adfa6886802ab Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 31 Jan 2022 16:02:30 -0500 Subject: [PATCH 139/202] make destructor virtual --- llarp/service/auth.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llarp/service/auth.hpp b/llarp/service/auth.hpp index fd094f475..acfaadebf 100644 --- a/llarp/service/auth.hpp +++ b/llarp/service/auth.hpp @@ -44,7 +44,7 @@ namespace llarp::service struct IAuthPolicy { - ~IAuthPolicy() = default; + virtual ~IAuthPolicy() = default; /// asynchronously determine if we accept new convotag from remote service, call hook with /// result later From 5050cd029902e03d34ffe3d77413fe200417c88a Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 1 Apr 2022 12:52:25 -0400 Subject: [PATCH 140/202] add hashed password capability to endpoint auth by file --- llarp/CMakeLists.txt | 2 +- llarp/config/config.cpp | 9 +++++++ llarp/config/config.hpp | 1 + llarp/crypto/crypto.hpp | 4 +++ llarp/crypto/crypto_libsodium.cpp | 18 +++++++++++++ llarp/crypto/crypto_libsodium.hpp | 3 +++ llarp/handlers/tun.cpp | 2 +- llarp/service/auth.cpp | 43 ++++++++++++++++++++++++++----- llarp/service/auth.hpp | 16 ++++++++++-- test/crypto/test_llarp_crypto.cpp | 41 +++++++++++++++++++++++++++++ 10 files changed, 129 insertions(+), 10 deletions(-) diff --git a/llarp/CMakeLists.txt b/llarp/CMakeLists.txt index d328bc489..7fc9aecb4 100644 --- a/llarp/CMakeLists.txt +++ b/llarp/CMakeLists.txt @@ -243,7 +243,7 @@ if(WITH_HIVE) endif() target_link_libraries(liblokinet PUBLIC cxxopts lokinet-platform lokinet-util lokinet-cryptography sqlite_orm ngtcp2_static) -target_link_libraries(liblokinet PRIVATE libunbound) +target_link_libraries(liblokinet PRIVATE libunbound crypt) if(BUILD_LIBLOKINET) diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp index 1e640373a..5e6a53115 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp @@ -381,6 +381,15 @@ namespace llarp stringify("cannot load auth file ", arg, " as it does not seem to exist")}; m_AuthFiles.emplace(std::move(arg)); }); + conf.defineOption( + "network", + "auth-file-type", + ClientOnly, + Comment{ + "How to interpret the contents of an auth file.", + "Possible values: hashes, plaintext", + }, + [this](std::string arg) { m_AuthFileType = service::ParseAuthFileType(std::move(arg)); }); conf.defineOption( "network", diff --git a/llarp/config/config.hpp b/llarp/config/config.hpp index 6f6d82ef2..09eb1febb 100644 --- a/llarp/config/config.hpp +++ b/llarp/config/config.hpp @@ -115,6 +115,7 @@ namespace llarp std::unordered_map m_mapAddrs; service::AuthType m_AuthType = service::AuthType::eAuthTypeNone; + service::AuthFileType m_AuthFileType = service::AuthFileType::eAuthFileHashes; std::optional m_AuthUrl; std::optional m_AuthMethod; std::unordered_set m_AuthWhitelist; diff --git a/llarp/crypto/crypto.hpp b/llarp/crypto/crypto.hpp index 56cc6c60f..1af34e6b4 100644 --- a/llarp/crypto/crypto.hpp +++ b/llarp/crypto/crypto.hpp @@ -100,6 +100,10 @@ namespace llarp virtual bool check_identity_privkey(const SecretKey&) = 0; + + /// check if a password hash string matches the challenge + virtual bool + check_passwd_hash(std::string pwhash, std::string challenge) = 0; }; inline Crypto::~Crypto() = default; diff --git a/llarp/crypto/crypto_libsodium.cpp b/llarp/crypto/crypto_libsodium.cpp index 8a14f66af..d901b976e 100644 --- a/llarp/crypto/crypto_libsodium.cpp +++ b/llarp/crypto/crypto_libsodium.cpp @@ -13,6 +13,9 @@ #include #include #include +#include + +#include extern "C" { @@ -463,6 +466,21 @@ namespace llarp auto d = keypair.data(); crypto_kem_keypair(d + PQ_SECRETKEYSIZE, d); } + + bool + CryptoLibSodium::check_passwd_hash(std::string pwhash, std::string challenge) + { + bool ret = false; + auto pos = pwhash.find_last_of('$'); + auto settings = pwhash.substr(0, pos); + crypt_data data{}; + if (char* ptr = crypt_r(challenge.c_str(), settings.c_str(), &data)) + { + ret = ptr == pwhash; + } + sodium_memzero(&data, sizeof(data)); + return ret; + } } // namespace sodium const byte_t* diff --git a/llarp/crypto/crypto_libsodium.hpp b/llarp/crypto/crypto_libsodium.hpp index f085aee1e..6248e9484 100644 --- a/llarp/crypto/crypto_libsodium.hpp +++ b/llarp/crypto/crypto_libsodium.hpp @@ -104,6 +104,9 @@ namespace llarp bool check_identity_privkey(const SecretKey&) override; + + bool + check_passwd_hash(std::string pwhash, std::string challenge) override; }; } // namespace sodium diff --git a/llarp/handlers/tun.cpp b/llarp/handlers/tun.cpp index 6f7f27103..d4d76c647 100644 --- a/llarp/handlers/tun.cpp +++ b/llarp/handlers/tun.cpp @@ -176,7 +176,7 @@ namespace llarp if (conf.m_AuthType == service::AuthType::eAuthTypeFile) { - m_AuthPolicy = service::MakeFileAuthPolicy(m_router, conf.m_AuthFiles); + m_AuthPolicy = service::MakeFileAuthPolicy(m_router, conf.m_AuthFiles, conf.m_AuthFileType); } else if (conf.m_AuthType != service::AuthType::eAuthTypeNone) { diff --git a/llarp/service/auth.cpp b/llarp/service/auth.cpp index a5e4518a0..c765ac7d2 100644 --- a/llarp/service/auth.cpp +++ b/llarp/service/auth.cpp @@ -37,6 +37,21 @@ namespace llarp::service return itr->second; } + AuthFileType + ParseAuthFileType(std::string data) + { + std::unordered_map values = { + {"plain", AuthFileType::eAuthFilePlain}, + {"plaintext", AuthFileType::eAuthFilePlain}, + {"hashed", AuthFileType::eAuthFileHashes}, + {"hashes", AuthFileType::eAuthFileHashes}, + {"hash", AuthFileType::eAuthFileHashes}}; + const auto itr = values.find(data); + if (itr == values.end()) + throw std::invalid_argument("no such auth file type: " + data); + return itr->second; + } + /// turn an auth result code into an int uint64_t AuthResultCodeAsInt(AuthResultCode code) @@ -67,6 +82,7 @@ namespace llarp::service class FileAuthPolicy : public IAuthPolicy, public std::enable_shared_from_this { const std::set m_Files; + const AuthFileType m_Type; AbstractRouter* const m_Router; mutable util::Mutex m_Access; std::unordered_set m_Pending; @@ -86,8 +102,8 @@ namespace llarp::service const auto parts = split_any(line, "#;", true); if (auto part = parts[0]; not parts.empty() and not parts[0].empty()) { - // split off whitespaces - if (TrimWhitespace(part) == info.token) + // split off whitespaces and check password + if (CheckPasswd(std::string{TrimWhitespace(part)}, info.token)) return AuthResult{AuthResultCode::eAuthAccepted, "accepted by whitelist"}; } } @@ -95,9 +111,24 @@ namespace llarp::service return AuthResult{AuthResultCode::eAuthRejected, "rejected by whitelist"}; } + bool + CheckPasswd(std::string hash, std::string challenge) const + { + switch (m_Type) + { + case AuthFileType::eAuthFilePlain: + return hash == challenge; + case AuthFileType::eAuthFileHashes: + return CryptoManager::instance()->check_passwd_hash( + std::move(hash), std::move(challenge)); + default: + return false; + } + } + public: - FileAuthPolicy(AbstractRouter* r, std::set files) - : m_Files{std::move(files)}, m_Router{r} + FileAuthPolicy(AbstractRouter* r, std::set files, AuthFileType filetype) + : m_Files{std::move(files)}, m_Type{filetype}, m_Router{r} {} void @@ -145,9 +176,9 @@ namespace llarp::service }; std::shared_ptr - MakeFileAuthPolicy(AbstractRouter* r, std::set files) + MakeFileAuthPolicy(AbstractRouter* r, std::set files, AuthFileType filetype) { - return std::make_shared(r, std::move(files)); + return std::make_shared(r, std::move(files), filetype); } } // namespace llarp::service diff --git a/llarp/service/auth.hpp b/llarp/service/auth.hpp index acfaadebf..b87f9eab7 100644 --- a/llarp/service/auth.hpp +++ b/llarp/service/auth.hpp @@ -72,17 +72,29 @@ namespace llarp::service eAuthTypeWhitelist, /// LMQ server eAuthTypeLMQ, - /// plain file + /// static file eAuthTypeFile, }; + /// how to interpret an file for auth + enum class AuthFileType + { + eAuthFilePlain, + eAuthFileHashes, + }; + /// get an auth type from a string /// throws std::invalid_argument if arg is invalid AuthType ParseAuthType(std::string arg); + /// get an auth file type from a string + /// throws std::invalid_argument if arg is invalid + AuthFileType + ParseAuthFileType(std::string arg); + /// make an IAuthPolicy that reads out of a static file std::shared_ptr - MakeFileAuthPolicy(AbstractRouter*, std::set files); + MakeFileAuthPolicy(AbstractRouter*, std::set files, AuthFileType fileType); } // namespace llarp::service diff --git a/test/crypto/test_llarp_crypto.cpp b/test/crypto/test_llarp_crypto.cpp index 34747e53d..cef34b931 100644 --- a/test/crypto/test_llarp_crypto.cpp +++ b/test/crypto/test_llarp_crypto.cpp @@ -47,3 +47,44 @@ TEST_CASE("PQ crypto") REQUIRE(c->pqe_decrypt(block, otherShared, pq_keypair_to_secret(keys))); REQUIRE(otherShared == shared); } + +TEST_CASE("passwd hash valid") +{ + llarp::sodium::CryptoLibSodium crypto; + + // poggers password hashes + std::set valid_hashes; + // UNIX DES + valid_hashes.emplace("CVu85Ms694POo"); + // sha256 salted + valid_hashes.emplace( + "$5$cIghotiBGjfPC7Fu$" + "TXXxPhpUcEiF9tMnjhEVJFi9AlNDSkNRQFTrXPQTKS9"); + // sha512 salted + valid_hashes.emplace( + "$6$qB77ms3wCIo.xVKP$Hl0RLuDgWNmIW4s." + "5KUbFmnauoTfrWSPJzDCD8ZTSSfwRbMgqgG6F9y3K.YEYVij8g/" + "Js0DRT2RhgXoX0sHGb."); + + for (const auto& hash : valid_hashes) + { + // make sure it is poggers ... + REQUIRE(crypto.check_passwd_hash(hash, "poggers")); + // ... and not inscrutible + REQUIRE(not crypto.check_passwd_hash(hash, "inscrutible")); + } +} + +TEST_CASE("passwd hash malformed") +{ + llarp::sodium::CryptoLibSodium crypto; + + std::set invalid_hashes = { + "stevejobs", + "$JKEDbzgzym1N6", // crypt() for "stevejobs" with a $ at the begining + "$0$zero$AAAAAAAAAAA", + "$$$AAAAAAAAAAAA", + "$LIGMA$BALLS$LMAOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO."}; + for (const auto& hash : invalid_hashes) + REQUIRE(not crypto.check_passwd_hash(hash, "stevejobs")); +} From ee12ba51d544ae9436595038d1bdfa7f7c22de18 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 1 Apr 2022 13:18:18 -0400 Subject: [PATCH 141/202] disable hashed auth on windows --- llarp/CMakeLists.txt | 8 +++++++- llarp/crypto/crypto_libsodium.cpp | 8 ++++++++ llarp/service/auth.cpp | 4 ++++ test/crypto/test_llarp_crypto.cpp | 4 ++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/llarp/CMakeLists.txt b/llarp/CMakeLists.txt index 7fc9aecb4..33474aca0 100644 --- a/llarp/CMakeLists.txt +++ b/llarp/CMakeLists.txt @@ -243,7 +243,13 @@ if(WITH_HIVE) endif() target_link_libraries(liblokinet PUBLIC cxxopts lokinet-platform lokinet-util lokinet-cryptography sqlite_orm ngtcp2_static) -target_link_libraries(liblokinet PRIVATE libunbound crypt) +target_link_libraries(liblokinet PRIVATE libunbound) +if(NOT WIN32) + pkg_check_modules(CRYPT libcrypt REQUIRED IMPORTED_TARGET) + add_library(libcrypt INTERFACE) + target_link_libraries(libcrypt INTERFACE PkgConfig::CRYPT) + target_link_libraries(liblokinet PRIVATE libcrypt) +endif() if(BUILD_LIBLOKINET) diff --git a/llarp/crypto/crypto_libsodium.cpp b/llarp/crypto/crypto_libsodium.cpp index d901b976e..9c51f8443 100644 --- a/llarp/crypto/crypto_libsodium.cpp +++ b/llarp/crypto/crypto_libsodium.cpp @@ -13,7 +13,9 @@ #include #include #include +#ifndef _WIN32 #include +#endif #include @@ -470,6 +472,11 @@ namespace llarp bool CryptoLibSodium::check_passwd_hash(std::string pwhash, std::string challenge) { +#ifdef _WIN32 + (void)pwhash; + (void)challenge; + return false; +#else bool ret = false; auto pos = pwhash.find_last_of('$'); auto settings = pwhash.substr(0, pos); @@ -480,6 +487,7 @@ namespace llarp } sodium_memzero(&data, sizeof(data)); return ret; +#endif } } // namespace sodium diff --git a/llarp/service/auth.cpp b/llarp/service/auth.cpp index c765ac7d2..af52361d4 100644 --- a/llarp/service/auth.cpp +++ b/llarp/service/auth.cpp @@ -49,6 +49,10 @@ namespace llarp::service const auto itr = values.find(data); if (itr == values.end()) throw std::invalid_argument("no such auth file type: " + data); +#ifdef _WIN32 + if (itr->second == AuthFileType::eAuthFileHashes) + throw std::invalid_argument("unsupported auth file type: " + data); +#endif return itr->second; } diff --git a/test/crypto/test_llarp_crypto.cpp b/test/crypto/test_llarp_crypto.cpp index cef34b931..5d9cf707b 100644 --- a/test/crypto/test_llarp_crypto.cpp +++ b/test/crypto/test_llarp_crypto.cpp @@ -48,6 +48,8 @@ TEST_CASE("PQ crypto") REQUIRE(otherShared == shared); } +#ifndef _WIN32 + TEST_CASE("passwd hash valid") { llarp::sodium::CryptoLibSodium crypto; @@ -88,3 +90,5 @@ TEST_CASE("passwd hash malformed") for (const auto& hash : invalid_hashes) REQUIRE(not crypto.check_passwd_hash(hash, "stevejobs")); } + +#endif From 7a8410b375833e9fdaf9b5904aa381be52eca147 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 1 Apr 2022 14:31:20 -0400 Subject: [PATCH 142/202] make more platforms ignore libcrypt --- llarp/CMakeLists.txt | 6 ++++-- llarp/crypto/crypto_libsodium.cpp | 8 +++----- llarp/service/auth.cpp | 2 +- test/crypto/test_llarp_crypto.cpp | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/llarp/CMakeLists.txt b/llarp/CMakeLists.txt index 33474aca0..5dc983885 100644 --- a/llarp/CMakeLists.txt +++ b/llarp/CMakeLists.txt @@ -244,11 +244,13 @@ endif() target_link_libraries(liblokinet PUBLIC cxxopts lokinet-platform lokinet-util lokinet-cryptography sqlite_orm ngtcp2_static) target_link_libraries(liblokinet PRIVATE libunbound) -if(NOT WIN32) - pkg_check_modules(CRYPT libcrypt REQUIRED IMPORTED_TARGET) +pkg_check_modules(CRYPT libcrypt IMPORTED_TARGET) +if(CRYPT_FOUND AND NOT WIN32 AND NOT ANDROID) + add_definitions(-DHAVE_CRYPT) add_library(libcrypt INTERFACE) target_link_libraries(libcrypt INTERFACE PkgConfig::CRYPT) target_link_libraries(liblokinet PRIVATE libcrypt) + message(STATUS "using libcrypt ${CRYPT_VERSION}") endif() diff --git a/llarp/crypto/crypto_libsodium.cpp b/llarp/crypto/crypto_libsodium.cpp index 9c51f8443..60f2e5513 100644 --- a/llarp/crypto/crypto_libsodium.cpp +++ b/llarp/crypto/crypto_libsodium.cpp @@ -13,7 +13,7 @@ #include #include #include -#ifndef _WIN32 +#ifdef HAVE_CRYPT #include #endif @@ -472,12 +472,10 @@ namespace llarp bool CryptoLibSodium::check_passwd_hash(std::string pwhash, std::string challenge) { -#ifdef _WIN32 (void)pwhash; (void)challenge; - return false; -#else bool ret = false; +#ifdef HAVE_CRYPT auto pos = pwhash.find_last_of('$'); auto settings = pwhash.substr(0, pos); crypt_data data{}; @@ -486,8 +484,8 @@ namespace llarp ret = ptr == pwhash; } sodium_memzero(&data, sizeof(data)); - return ret; #endif + return ret; } } // namespace sodium diff --git a/llarp/service/auth.cpp b/llarp/service/auth.cpp index af52361d4..1c6ce91fb 100644 --- a/llarp/service/auth.cpp +++ b/llarp/service/auth.cpp @@ -49,7 +49,7 @@ namespace llarp::service const auto itr = values.find(data); if (itr == values.end()) throw std::invalid_argument("no such auth file type: " + data); -#ifdef _WIN32 +#ifndef HAVE_CRYPT if (itr->second == AuthFileType::eAuthFileHashes) throw std::invalid_argument("unsupported auth file type: " + data); #endif diff --git a/test/crypto/test_llarp_crypto.cpp b/test/crypto/test_llarp_crypto.cpp index 5d9cf707b..13105a87e 100644 --- a/test/crypto/test_llarp_crypto.cpp +++ b/test/crypto/test_llarp_crypto.cpp @@ -48,7 +48,7 @@ TEST_CASE("PQ crypto") REQUIRE(otherShared == shared); } -#ifndef _WIN32 +#ifdef HAVE_CRYPT TEST_CASE("passwd hash valid") { From 64684d4dd425be00100f43d23ac1eb1ef7314290 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 18 Apr 2022 13:59:14 -0400 Subject: [PATCH 143/202] dont require libcrypt for cross compiled builds --- llarp/CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llarp/CMakeLists.txt b/llarp/CMakeLists.txt index 5dc983885..d8a67086e 100644 --- a/llarp/CMakeLists.txt +++ b/llarp/CMakeLists.txt @@ -245,7 +245,7 @@ endif() target_link_libraries(liblokinet PUBLIC cxxopts lokinet-platform lokinet-util lokinet-cryptography sqlite_orm ngtcp2_static) target_link_libraries(liblokinet PRIVATE libunbound) pkg_check_modules(CRYPT libcrypt IMPORTED_TARGET) -if(CRYPT_FOUND AND NOT WIN32 AND NOT ANDROID) +if(CRYPT_FOUND AND NOT CMAKE_CROSSCOMPILING) add_definitions(-DHAVE_CRYPT) add_library(libcrypt INTERFACE) target_link_libraries(libcrypt INTERFACE PkgConfig::CRYPT) From b09298e211aa92e9fb152e24284230bfdcb2f8eb Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 28 Apr 2022 12:09:51 -0300 Subject: [PATCH 144/202] Replace llarp/util/endian.hpp with oxenc/endian.h --- llarp/crypto/crypto_libsodium.cpp | 4 +- llarp/dns/message.cpp | 4 +- llarp/handlers/tun.cpp | 5 +- llarp/iwp/message_buffer.cpp | 11 +- llarp/iwp/session.cpp | 19 +- llarp/net/ip_packet.cpp | 11 +- llarp/net/ip_packet.hpp | 5 +- llarp/net/net_int.cpp | 4 +- llarp/net/net_int.hpp | 1 - llarp/net/uint128.hpp | 9 +- llarp/path/path.cpp | 5 +- llarp/path/transit_hop.cpp | 5 +- llarp/routing/transfer_traffic_message.cpp | 5 +- llarp/util/buffer.cpp | 64 +++---- llarp/util/endian.hpp | 203 --------------------- llarp/vpn/linux.hpp | 6 +- 16 files changed, 84 insertions(+), 277 deletions(-) delete mode 100644 llarp/util/endian.hpp diff --git a/llarp/crypto/crypto_libsodium.cpp b/llarp/crypto/crypto_libsodium.cpp index 60f2e5513..a5671a151 100644 --- a/llarp/crypto/crypto_libsodium.cpp +++ b/llarp/crypto/crypto_libsodium.cpp @@ -8,8 +8,8 @@ #include #include #include +#include #include -#include #include #include #include @@ -295,7 +295,7 @@ namespace llarp std::array buf; std::copy(derived_key_hash_str, derived_key_hash_str + 160, buf.begin()); std::copy(k.begin(), k.end(), buf.begin() + 160); - htole64buf(buf.data() + 160 + K::SIZE, i); + oxenc::write_host_as_little(i, buf.data() + 160 + K::SIZE); // n = H(b) // h = make_point(n) ShortHash n; diff --git a/llarp/dns/message.cpp b/llarp/dns/message.cpp index 0d78d1fdb..65b30c78d 100644 --- a/llarp/dns/message.cpp +++ b/llarp/dns/message.cpp @@ -1,9 +1,9 @@ #include "message.hpp" +#include #include "dns.hpp" #include "srv_data.hpp" #include -#include #include #include #include @@ -197,7 +197,7 @@ namespace llarp const auto addr = net::TruncateV6(ip); rec.rr_type = qTypeA; rec.rData.resize(4); - htobe32buf(rec.rData.data(), addr.h); + oxenc::write_host_as_big(addr.h, rec.rData.data()); } answers.emplace_back(std::move(rec)); } diff --git a/llarp/handlers/tun.cpp b/llarp/handlers/tun.cpp index d4d76c647..c31a300e6 100644 --- a/llarp/handlers/tun.cpp +++ b/llarp/handlers/tun.cpp @@ -22,12 +22,11 @@ #include #include #include - #include -#include - #include + #include + namespace llarp { namespace handlers diff --git a/llarp/iwp/message_buffer.cpp b/llarp/iwp/message_buffer.cpp index 495fb076b..e5098f656 100644 --- a/llarp/iwp/message_buffer.cpp +++ b/llarp/iwp/message_buffer.cpp @@ -27,8 +27,9 @@ namespace llarp { size_t extra = std::min(m_Data.size(), FragmentSize); auto xmit = CreatePacket(Command::eXMIT, 10 + 32 + extra, 0, 0); - htobe16buf(xmit.data() + CommandOverhead + PacketOverhead, m_Data.size()); - htobe64buf(xmit.data() + 2 + CommandOverhead + PacketOverhead, m_MsgID); + oxenc::write_host_as_big( + static_cast(m_Data.size()), xmit.data() + CommandOverhead + PacketOverhead); + oxenc::write_host_as_big(m_MsgID, xmit.data() + 2 + CommandOverhead + PacketOverhead); std::copy_n( m_Digest.begin(), m_Digest.size(), xmit.data() + 10 + CommandOverhead + PacketOverhead); std::copy_n(m_Data.data(), extra, xmit.data() + 10 + CommandOverhead + PacketOverhead + 32); @@ -71,8 +72,8 @@ namespace llarp { const size_t fragsz = idx + FragmentSize < datasz ? FragmentSize : datasz - idx; auto frag = CreatePacket(Command::eDATA, fragsz + Overhead, 0, 0); - htobe16buf(frag.data() + 2 + PacketOverhead, idx); - htobe64buf(frag.data() + 4 + PacketOverhead, m_MsgID); + oxenc::write_host_as_big(idx, frag.data() + 2 + PacketOverhead); + oxenc::write_host_as_big(m_MsgID, frag.data() + 4 + PacketOverhead); std::copy( m_Data.begin() + idx, m_Data.begin() + idx + fragsz, @@ -136,7 +137,7 @@ namespace llarp InboundMessage::ACKS() const { auto acks = CreatePacket(Command::eACKS, 9); - htobe64buf(acks.data() + CommandOverhead + PacketOverhead, m_MsgID); + oxenc::write_host_as_big(m_MsgID, acks.data() + CommandOverhead + PacketOverhead); acks[PacketOverhead + 10] = AcksBitmask(); return acks; } diff --git a/llarp/iwp/session.cpp b/llarp/iwp/session.cpp index 0607ff1eb..fcd4097d3 100644 --- a/llarp/iwp/session.cpp +++ b/llarp/iwp/session.cpp @@ -223,7 +223,7 @@ namespace llarp const auto& itr = m_SendMACKs.top(); while (numAcks > 0) { - htobe64buf(ptr, itr); + oxenc::write_host_as_big(itr, ptr); m_SendMACKs.pop(); numAcks--; ptr += sizeof(uint64_t); @@ -716,7 +716,7 @@ namespace llarp byte_t* ptr = data.data() + CommandOverhead + PacketOverhead + 1; while (numAcks > 0) { - uint64_t acked = bufbe64toh(ptr); + auto acked = oxenc::load_big_to_host(ptr); LogTrace("mack containing txid=", acked, " from ", m_RemoteAddr); auto itr = m_TXMsgs.find(acked); if (itr != m_TXMsgs.end()) @@ -743,7 +743,7 @@ namespace llarp LogError("short nack from ", m_RemoteAddr); return; } - uint64_t txid = bufbe64toh(data.data() + CommandOverhead + PacketOverhead); + auto txid = oxenc::load_big_to_host(data.data() + CommandOverhead + PacketOverhead); LogTrace("got nack on ", txid, " from ", m_RemoteAddr); auto itr = m_TXMsgs.find(txid); if (itr != m_TXMsgs.end()) @@ -765,9 +765,9 @@ namespace llarp return; } auto* pos = data.data() + CommandOverhead + PacketOverhead; - uint16_t sz = bufbe16toh(pos); + auto sz = oxenc::load_big_to_host(pos); pos += sizeof(sz); - uint64_t rxid = bufbe64toh(pos); + auto rxid = oxenc::load_big_to_host(pos); pos += sizeof(rxid); auto p2 = pos + ShortHash::SIZE; assert(p2 == data.data() + XMITOverhead); @@ -826,8 +826,9 @@ namespace llarp return; } m_LastRX = m_Parent->Now(); - uint16_t sz = bufbe16toh(data.data() + CommandOverhead + PacketOverhead); - uint64_t rxid = bufbe64toh(data.data() + CommandOverhead + sizeof(uint16_t) + PacketOverhead); + auto sz = oxenc::load_big_to_host(data.data() + CommandOverhead + PacketOverhead); + auto rxid = oxenc::load_big_to_host( + data.data() + CommandOverhead + sizeof(uint16_t) + PacketOverhead); auto itr = m_RXMsgs.find(rxid); if (itr == m_RXMsgs.end()) { @@ -835,7 +836,7 @@ namespace llarp { LogTrace("no rxid=", rxid, " for ", m_RemoteAddr); auto nack = CreatePacket(Command::eNACK, 8); - htobe64buf(nack.data() + PacketOverhead + CommandOverhead, rxid); + oxenc::write_host_as_big(rxid, nack.data() + PacketOverhead + CommandOverhead); EncryptAndSend(std::move(nack)); } else @@ -888,7 +889,7 @@ namespace llarp } const auto now = m_Parent->Now(); m_LastRX = now; - uint64_t txid = bufbe64toh(data.data() + 2 + PacketOverhead); + auto txid = oxenc::load_big_to_host(data.data() + 2 + PacketOverhead); auto itr = m_TXMsgs.find(txid); if (itr == m_TXMsgs.end()) { diff --git a/llarp/net/ip_packet.cpp b/llarp/net/ip_packet.cpp index deef93102..5e287b6cd 100644 --- a/llarp/net/ip_packet.cpp +++ b/llarp/net/ip_packet.cpp @@ -2,13 +2,14 @@ #include "ip.hpp" #include -#include #include #include #ifndef _WIN32 #include #endif +#include + #include #include @@ -564,11 +565,11 @@ namespace llarp // code 'Destination host unknown error' *itr++ = 7; // checksum + unused - htobe32buf(itr, 0); + oxenc::write_host_as_big(0, itr); checksum = (uint16_t*)itr; itr += 4; // next hop mtu is ignored but let's put something here anyways just in case tm - htobe16buf(itr, 1500); + oxenc::write_host_as_big(1500, itr); itr += 2; // copy ip header and first 8 bytes of datagram for icmp rject std::copy_n(buf, l4_PacketSize + l3_HeaderSize, itr); @@ -637,9 +638,9 @@ namespace llarp ptr += 2; std::memcpy(ptr, &dstport.n, 2); ptr += 2; - htobe16buf(ptr, static_cast(buf.sz + 8)); + oxenc::write_host_as_big(static_cast(buf.sz + 8), ptr); ptr += 2; - htobe16buf(ptr, uint16_t{0}); // checksum + oxenc::write_host_as_big(uint16_t{0}, ptr); // checksum ptr += 2; std::copy_n(buf.base, buf.sz, ptr); diff --git a/llarp/net/ip_packet.hpp b/llarp/net/ip_packet.hpp index 0987633aa..b9e3c4ddd 100644 --- a/llarp/net/ip_packet.hpp +++ b/llarp/net/ip_packet.hpp @@ -4,6 +4,7 @@ #include "net.hpp" #include #include +#include // Guarantees __{LITTLE,BIG}_ENDIAN__ defines #ifndef _WIN32 // unix, linux @@ -16,11 +17,9 @@ struct ip_header #ifdef __LITTLE_ENDIAN__ unsigned int ihl : 4; unsigned int version : 4; -#elif defined(__BIG_ENDIAN__) +#else unsigned int version : 4; unsigned int ihl : 4; -#else -#error "Please fix " #endif #if defined(__linux__) diff --git a/llarp/net/net_int.cpp b/llarp/net/net_int.cpp index 973d6ed4e..566e11b70 100644 --- a/llarp/net/net_int.cpp +++ b/llarp/net/net_int.cpp @@ -2,6 +2,8 @@ #include "ip.hpp" #include +#include + namespace llarp { huint16_t @@ -46,7 +48,7 @@ namespace llarp { c.resize(16); std::fill(c.begin(), c.end(), 0); - htobe32buf(c.data() + 12, h); + oxenc::write_host_as_big(h, c.data() + 12); c[11] = 0xff; c[10] = 0xff; } diff --git a/llarp/net/net_int.hpp b/llarp/net/net_int.hpp index 616e912d8..b5ec0e7cb 100644 --- a/llarp/net/net_int.hpp +++ b/llarp/net/net_int.hpp @@ -15,7 +15,6 @@ #include // for itoa #include -#include #include #include "uint128.hpp" diff --git a/llarp/net/uint128.hpp b/llarp/net/uint128.hpp index aa1c96d08..8340000b2 100644 --- a/llarp/net/uint128.hpp +++ b/llarp/net/uint128.hpp @@ -5,7 +5,8 @@ #include #include "../util/meta/traits.hpp" -#include "../util/endian.hpp" + +#include namespace llarp { @@ -309,9 +310,9 @@ ntoh128(llarp::uint128_t i) #ifdef __BIG_ENDIAN__ return i; #else - const auto loSwapped = htobe64(i.lower); - const auto hiSwapped = htobe64(i.upper); - return {loSwapped, hiSwapped}; + const auto loSwapped = oxenc::big_to_host(i.lower); + const auto hiSwapped = oxenc::big_to_host(i.upper); + return {/*upper=*/loSwapped, /*lower=*/hiSwapped}; #endif } diff --git a/llarp/path/path.cpp b/llarp/path/path.cpp index e031b845b..c61d9098b 100644 --- a/llarp/path/path.cpp +++ b/llarp/path/path.cpp @@ -14,9 +14,10 @@ #include #include #include -#include #include +#include + #include #include @@ -894,7 +895,7 @@ namespace llarp { if (pkt.size() <= 8) return false; - uint64_t counter = bufbe64toh(pkt.data()); + auto counter = oxenc::load_big_to_host(pkt.data()); if (m_ExitTrafficHandler( self, llarp_buffer_t(pkt.data() + 8, pkt.size() - 8), counter, msg.protocol)) { diff --git a/llarp/path/transit_hop.cpp b/llarp/path/transit_hop.cpp index f2cd2d1aa..e52251996 100644 --- a/llarp/path/transit_hop.cpp +++ b/llarp/path/transit_hop.cpp @@ -14,7 +14,8 @@ #include #include #include -#include + +#include namespace llarp { @@ -396,7 +397,7 @@ namespace llarp // check short packet buffer if (pkt.size() <= 8) continue; - uint64_t counter = bufbe64toh(pkt.data()); + auto counter = oxenc::load_big_to_host(pkt.data()); sent &= endpoint->QueueOutboundTraffic( info.rxID, ManagedBuffer(llarp_buffer_t(pkt.data() + 8, pkt.size() - 8)), diff --git a/llarp/routing/transfer_traffic_message.cpp b/llarp/routing/transfer_traffic_message.cpp index b3b66e207..b1a7beeba 100644 --- a/llarp/routing/transfer_traffic_message.cpp +++ b/llarp/routing/transfer_traffic_message.cpp @@ -2,7 +2,8 @@ #include "handler.hpp" #include -#include + +#include namespace llarp { @@ -15,7 +16,7 @@ namespace llarp return false; X.emplace_back(buf.sz + 8); byte_t* ptr = X.back().data(); - htobe64buf(ptr, counter); + oxenc::write_host_as_big(counter, ptr); ptr += 8; memcpy(ptr, buf.base, buf.sz); // 8 bytes encoding overhead and 8 bytes counter diff --git a/llarp/util/buffer.cpp b/llarp/util/buffer.cpp index 2967024eb..f185e6347 100644 --- a/llarp/util/buffer.cpp +++ b/llarp/util/buffer.cpp @@ -1,5 +1,5 @@ #include "buffer.hpp" -#include "endian.hpp" +#include #include #include @@ -33,64 +33,66 @@ llarp_buffer_t::writef(const char* fmt, ...) return true; } +namespace +{ + template + bool + put(llarp_buffer_t& buf, UInt i) + { + if (buf.size_left() < sizeof(UInt)) + return false; + oxenc::write_host_as_big(i, buf.cur); + buf.cur += sizeof(UInt); + return true; + } + + template + bool + read(llarp_buffer_t& buf, UInt& i) + { + if (buf.size_left() < sizeof(UInt)) + return false; + i = oxenc::load_big_to_host(buf.cur); + buf.cur += sizeof(UInt); + return true; + } + +} // namespace + bool llarp_buffer_t::put_uint16(uint16_t i) { - if (size_left() < sizeof(uint16_t)) - return false; - htobe16buf(cur, i); - cur += sizeof(uint16_t); - return true; + return put(*this, i); } bool llarp_buffer_t::put_uint64(uint64_t i) { - if (size_left() < sizeof(uint64_t)) - return false; - htobe64buf(cur, i); - cur += sizeof(uint64_t); - return true; + return put(*this, i); } bool llarp_buffer_t::put_uint32(uint32_t i) { - if (size_left() < sizeof(uint32_t)) - return false; - htobe32buf(cur, i); - cur += sizeof(uint32_t); - return true; + return put(*this, i); } bool llarp_buffer_t::read_uint16(uint16_t& i) { - if (size_left() < sizeof(uint16_t)) - return false; - i = bufbe16toh(cur); - cur += sizeof(uint16_t); - return true; + return read(*this, i); } bool llarp_buffer_t::read_uint32(uint32_t& i) { - if (size_left() < sizeof(uint32_t)) - return false; - i = bufbe32toh(cur); - cur += sizeof(uint32_t); - return true; + return read(*this, i); } bool llarp_buffer_t::read_uint64(uint64_t& i) { - if (size_left() < sizeof(uint64_t)) - return false; - i = bufbe64toh(cur); - cur += sizeof(uint64_t); - return true; + return read(*this, i); } size_t diff --git a/llarp/util/endian.hpp b/llarp/util/endian.hpp deleted file mode 100644 index eb2b534c7..000000000 --- a/llarp/util/endian.hpp +++ /dev/null @@ -1,203 +0,0 @@ -#pragma once - -// adapted from libi2pd - -#include -#include - -#if defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) -#include -#elif defined(__sun) -#include -#include -#define htobe16(x) htons(x) -#define htole16(x) (x) -#define be16toh(x) ntohs(x) -#define le16toh(x) (x) - -#define htobe32(x) htonl(x) -#define htole32(x) (x) -#define be32toh(x) ntohl(x) -#define le32toh(x) (x) - -#define htobe64(x) \ - (((uint64_t)htonl(((uint32_t)(((uint64_t)(x)) >> 32)))) \ - | (((uint64_t)htonl(((uint32_t)(x)))) << 32)) -#define htole64(x) (x) -#define be64toh(x) \ - (((uint64_t)ntohl(((uint32_t)(((uint64_t)(x)) >> 32)))) \ - | (((uint64_t)ntohl(((uint32_t)(x)))) << 32)) -#define le64toh(x) (x) -#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__GLIBC__) -#include -#elif defined(__APPLE__) && defined(__MACH__) -#include -#define htobe16(x) OSSwapHostToBigInt16(x) -#define htole16(x) OSSwapHostToLittleInt16(x) -#define be16toh(x) OSSwapBigToHostInt16(x) -#define le16toh(x) OSSwapLittleToHostInt16(x) - -#define htobe32(x) OSSwapHostToBigInt32(x) -#define htole32(x) OSSwapHostToLittleInt32(x) -#define be32toh(x) OSSwapBigToHostInt32(x) -#define le32toh(x) OSSwapLittleToHostInt32(x) - -#define htobe64(x) OSSwapHostToBigInt64(x) -#define htole64(x) OSSwapHostToLittleInt64(x) -#define be64toh(x) OSSwapBigToHostInt64(x) -#define le64toh(x) OSSwapLittleToHostInt64(x) -#elif defined(_WIN32) -#include -#ifndef __LITTLE_ENDIAN__ -#define __LITTLE_ENDIAN__ -#endif -#define htobe16(x) htons(x) -#define htole16(x) (x) -#define be16toh(x) ntohs(x) -#define le16toh(x) (x) - -#define htobe32(x) htonl(x) -#define htole32(x) (x) -#define be32toh(x) ntohl(x) -#define le32toh(x) (x) - -#define htobe64(x) \ - (((uint64_t)htonl(((uint32_t)(((uint64_t)(x)) >> 32)))) \ - | (((uint64_t)htonl(((uint32_t)(x)))) << 32)) -#define htole64(x) (x) -#define be64toh(x) \ - (((uint64_t)ntohl(((uint32_t)(((uint64_t)(x)) >> 32)))) \ - | (((uint64_t)ntohl(((uint32_t)(x)))) << 32)) -#define le64toh(x) (x) -#else -#define NEEDS_LOCAL_ENDIAN -#include -uint16_t -htobe16(uint16_t int16); -uint32_t -htobe32(uint32_t int32); -uint64_t -htobe64(uint64_t int64); - -uint16_t -be16toh(uint16_t big16); -uint32_t -be32toh(uint32_t big32); -uint64_t -be64toh(uint64_t big64); - -// assume LittleEndine -#define htole16 -#define htole32 -#define htole64 -#define le16toh -#define le32toh -#define le64toh - -#endif - -#if !defined(__LITTLE_ENDIAN__) && defined(__BYTE_ORDER) && defined(__LITTLE_ENDIAN) \ - && __BYTE_ORDER == __LITTLE_ENDIAN -#define __LITTLE_ENDIAN__ -#elif !defined(__BIG_ENDIAN__) && defined(__BYTE_ORDER) && defined(__BIG_ENDIAN) \ - && __BYTE_ORDER == __BIG_ENDIAN -#define __BIG_ENDIAN__ -#elif !defined(__LITTLE_ENDIAN__) && !defined(__BIG_ENDIAN__) -#error "Error: don't know which endian this is" -#endif - -inline uint16_t -buf16toh(const void* buf) -{ - uint16_t b16; - memcpy(&b16, buf, sizeof(uint16_t)); - return b16; -} - -inline uint32_t -buf32toh(const void* buf) -{ - uint32_t b32; - memcpy(&b32, buf, sizeof(uint32_t)); - return b32; -} - -inline uint64_t -buf64toh(const void* buf) -{ - uint64_t b64; - memcpy(&b64, buf, sizeof(uint64_t)); - return b64; -} - -inline uint16_t -bufbe16toh(const void* buf) -{ - return be16toh(buf16toh(buf)); -} - -inline uint32_t -bufbe32toh(const void* buf) -{ - return be32toh(buf32toh(buf)); -} - -inline uint64_t -bufbe64toh(const void* buf) -{ - return be64toh(buf64toh(buf)); -} - -inline void -htobuf16(void* buf, uint16_t b16) -{ - memcpy(buf, &b16, sizeof(uint16_t)); -} - -inline void -htobuf32(void* buf, uint32_t b32) -{ - memcpy(buf, &b32, sizeof(uint32_t)); -} - -inline void -htobuf64(void* buf, uint64_t b64) -{ - memcpy(buf, &b64, sizeof(uint64_t)); -} - -inline void -htobe16buf(void* buf, uint16_t big16) -{ - htobuf16(buf, htobe16(big16)); -} - -inline void -htobe32buf(void* buf, uint32_t big32) -{ - htobuf32(buf, htobe32(big32)); -} - -inline void -htobe64buf(void* buf, uint64_t big64) -{ - htobuf64(buf, htobe64(big64)); -} - -inline void -htole16buf(void* buf, uint16_t big16) -{ - htobuf16(buf, htole16(big16)); -} - -inline void -htole32buf(void* buf, uint32_t big32) -{ - htobuf32(buf, htole32(big32)); -} - -inline void -htole64buf(void* buf, uint64_t big64) -{ - htobuf64(buf, htole64(big64)); -} diff --git a/llarp/vpn/linux.hpp b/llarp/vpn/linux.hpp index 9f9ffbe0f..cad48fe91 100644 --- a/llarp/vpn/linux.hpp +++ b/llarp/vpn/linux.hpp @@ -9,7 +9,7 @@ #include #include -#include +#include #include #include #include @@ -19,6 +19,8 @@ #include #include +#include + namespace llarp::vpn { struct in6_ifreq @@ -181,7 +183,7 @@ namespace llarp::vpn { family = AF_INET; bitlen = bits; - htobe32buf(data, addr.h); + oxenc::write_host_as_big(addr.h, data); } _inet_addr(huint128_t addr, size_t bits = 128) From c2f8a618e086753f3fca106746c68d41f9628ecc Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Thu, 28 Apr 2022 14:23:52 -0300 Subject: [PATCH 145/202] Update to oxenc 1.0.2 --- CMakeLists.txt | 2 +- external/oxen-encoding | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f393ee96d..ed45e5b70 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -184,7 +184,7 @@ endif() option(FORCE_OXENC_SUBMODULE "force using oxen-encoding submodule" OFF) if(NOT FORCE_OXENC_SUBMODULE) - pkg_check_modules(OXENC liboxenc>=1.0.1 IMPORTED_TARGET) + pkg_check_modules(OXENC liboxenc>=1.0.2 IMPORTED_TARGET) endif() if(OXENC_FOUND) diff --git a/external/oxen-encoding b/external/oxen-encoding index 077cbff7a..02ded69fa 160000 --- a/external/oxen-encoding +++ b/external/oxen-encoding @@ -1 +1 @@ -Subproject commit 077cbff7a38ce5c538d9653ffb119855151791c5 +Subproject commit 02ded69fa18640d1f2455f7a70c5d705d8895db3 From 926074f7c48d876a017289c94c69067cdcdc1b36 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 2 May 2022 08:21:09 -0400 Subject: [PATCH 146/202] add idempotent pump after sending lrcm to very make sure that it is pumped --- llarp/messages/relay_commit.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/llarp/messages/relay_commit.cpp b/llarp/messages/relay_commit.cpp index cdcc9f57b..900971ecb 100644 --- a/llarp/messages/relay_commit.cpp +++ b/llarp/messages/relay_commit.cpp @@ -319,6 +319,8 @@ namespace llarp self->hop = nullptr; }; self->context->ForwardLRCM(self->hop->info.upstream, self->frames, func); + // trigger idempotent pump to ensure that the build messages propagate + self->context->Router()->TriggerPump(); } // this is called from the logic thread From 8aa465d0ed0c1a17a872e64ace18b0d4f909edef Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 2 May 2022 08:20:08 -0400 Subject: [PATCH 147/202] randomize all frames in lrsm to prevent info leak about hop length --- llarp/messages/relay_status.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/llarp/messages/relay_status.cpp b/llarp/messages/relay_status.cpp index ceee052e9..0ae3358f0 100644 --- a/llarp/messages/relay_status.cpp +++ b/llarp/messages/relay_status.cpp @@ -145,8 +145,8 @@ namespace llarp void LR_StatusMessage::SetDummyFrames() { - // TODO - return; + for (auto& f : frames) + f.Randomize(); } // call this from a worker thread From 8960ca08f3444ffa8a9e6a29b65fba66ae023b19 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 30 Mar 2022 16:21:57 -0400 Subject: [PATCH 148/202] propagate link layer message priority to link layer so it can order retransmissions with that in mind --- llarp/iwp/message_buffer.cpp | 4 +- llarp/iwp/message_buffer.hpp | 12 +++- llarp/iwp/session.cpp | 26 ++++--- llarp/iwp/session.hpp | 5 +- llarp/link/i_link_manager.hpp | 3 +- llarp/link/link_manager.cpp | 9 ++- llarp/link/link_manager.hpp | 3 +- llarp/link/server.cpp | 7 +- llarp/link/server.hpp | 3 +- llarp/link/session.hpp | 2 +- llarp/router/outbound_message_handler.cpp | 84 +++++++++++------------ llarp/router/outbound_message_handler.hpp | 12 ++-- llarp/router/rc_gossiper.cpp | 2 +- 13 files changed, 99 insertions(+), 73 deletions(-) diff --git a/llarp/iwp/message_buffer.cpp b/llarp/iwp/message_buffer.cpp index e5098f656..000f80a6a 100644 --- a/llarp/iwp/message_buffer.cpp +++ b/llarp/iwp/message_buffer.cpp @@ -10,12 +10,14 @@ namespace llarp uint64_t msgid, ILinkSession::Message_t msg, llarp_time_t now, - ILinkSession::CompletionHandler handler) + ILinkSession::CompletionHandler handler, + uint16_t priority) : m_Data{std::move(msg)} , m_MsgID{msgid} , m_Completed{handler} , m_LastFlush{now} , m_StartedAt{now} + , m_ResendPriority{priority} { const llarp_buffer_t buf(m_Data); CryptoManager::instance()->shorthash(m_Digest, buf); diff --git a/llarp/iwp/message_buffer.hpp b/llarp/iwp/message_buffer.hpp index e760a8ac5..3ea352064 100644 --- a/llarp/iwp/message_buffer.hpp +++ b/llarp/iwp/message_buffer.hpp @@ -40,7 +40,8 @@ namespace llarp uint64_t msgid, ILinkSession::Message_t data, llarp_time_t now, - ILinkSession::CompletionHandler handler); + ILinkSession::CompletionHandler handler, + uint16_t priority); ILinkSession::Message_t m_Data; uint64_t m_MsgID = 0; @@ -49,6 +50,15 @@ namespace llarp llarp_time_t m_LastFlush = 0s; ShortHash m_Digest; llarp_time_t m_StartedAt = 0s; + uint16_t m_ResendPriority; + + bool + operator<(const OutboundMessage& msg) const + { + // yes, the first order is reversed as higher means more important + // second part is for queue order + return msg.m_ResendPriority < m_ResendPriority or m_MsgID < msg.m_MsgID; + } ILinkSession::Packet_t XMIT() const; diff --git a/llarp/iwp/session.cpp b/llarp/iwp/session.cpp index fcd4097d3..200fa7130 100644 --- a/llarp/iwp/session.cpp +++ b/llarp/iwp/session.cpp @@ -5,6 +5,8 @@ #include #include +#include + namespace llarp { namespace iwp @@ -127,7 +129,7 @@ namespace llarp { LogError("failed to encode LIM for ", m_RemoteAddr); } - if (!SendMessageBuffer(std::move(data), h)) + if (not SendMessageBuffer(std::move(data), h)) { LogError("failed to send LIM to ", m_RemoteAddr); } @@ -183,7 +185,7 @@ namespace llarp bool Session::SendMessageBuffer( - ILinkSession::Message_t buf, ILinkSession::CompletionHandler completed) + ILinkSession::Message_t buf, ILinkSession::CompletionHandler completed, uint16_t priority) { if (m_TXMsgs.size() >= MaxSendQueueSize) { @@ -194,8 +196,9 @@ namespace llarp const auto now = m_Parent->Now(); const auto msgid = m_TXID++; const auto bufsz = buf.size(); - auto& msg = m_TXMsgs.emplace(msgid, OutboundMessage{msgid, std::move(buf), now, completed}) - .first->second; + auto& msg = + m_TXMsgs.emplace(msgid, OutboundMessage{msgid, std::move(buf), now, completed, priority}) + .first->second; TriggerPump(); EncryptAndSend(msg.XMIT()); if (bufsz > FragmentSize) @@ -253,15 +256,22 @@ namespace llarp msg.SendACKS(util::memFn(&Session::EncryptAndSend, this), now); } } + std::priority_queue< + OutboundMessage*, + std::vector, + ComparePtr> + resend; for (auto& [id, msg] : m_TXMsgs) { if (msg.ShouldFlush(now)) - { - msg.FlushUnAcked(util::memFn(&Session::EncryptAndSend, this), now); - } + resend.push(&msg); + } + if (not resend.empty()) + { + for (auto& msg = resend.top(); not resend.empty(); resend.pop()) + msg->FlushUnAcked(util::memFn(&Session::EncryptAndSend, this), now); } } - assert(shared_from_this().use_count() > 1); if (not m_EncryptNext.empty()) { m_Parent->QueueWork( diff --git a/llarp/iwp/session.hpp b/llarp/iwp/session.hpp index dc429f5de..212b32812 100644 --- a/llarp/iwp/session.hpp +++ b/llarp/iwp/session.hpp @@ -60,7 +60,10 @@ namespace llarp Tick(llarp_time_t now) override; bool - SendMessageBuffer(ILinkSession::Message_t msg, CompletionHandler resultHandler) override; + SendMessageBuffer( + ILinkSession::Message_t msg, + CompletionHandler resultHandler, + uint16_t priority = 0) override; void Send_LL(const byte_t* buf, size_t sz); diff --git a/llarp/link/i_link_manager.hpp b/llarp/link/i_link_manager.hpp index abf8b8081..48ffd3d94 100644 --- a/llarp/link/i_link_manager.hpp +++ b/llarp/link/i_link_manager.hpp @@ -30,7 +30,8 @@ namespace llarp SendTo( const RouterID& remote, const llarp_buffer_t& buf, - ILinkSession::CompletionHandler completed) = 0; + ILinkSession::CompletionHandler completed, + uint16_t priority = 0) = 0; virtual bool HasSessionTo(const RouterID& remote) const = 0; diff --git a/llarp/link/link_manager.cpp b/llarp/link/link_manager.cpp index a5682ebdd..116bfb392 100644 --- a/llarp/link/link_manager.cpp +++ b/llarp/link/link_manager.cpp @@ -19,7 +19,7 @@ namespace llarp // TODO: may want to add some memory of session failures for a given // router on a given link and not return that link here for a // duration - if (!link->IsCompatable(rc)) + if (not link->IsCompatable(rc)) continue; return link; @@ -36,7 +36,10 @@ namespace llarp bool LinkManager::SendTo( - const RouterID& remote, const llarp_buffer_t& buf, ILinkSession::CompletionHandler completed) + const RouterID& remote, + const llarp_buffer_t& buf, + ILinkSession::CompletionHandler completed, + uint16_t priority) { if (stopping) return false; @@ -51,7 +54,7 @@ namespace llarp return false; } - return link->SendTo(remote, buf, completed); + return link->SendTo(remote, buf, completed, priority); } bool diff --git a/llarp/link/link_manager.hpp b/llarp/link/link_manager.hpp index 0f8eeac69..77179b713 100644 --- a/llarp/link/link_manager.hpp +++ b/llarp/link/link_manager.hpp @@ -28,7 +28,8 @@ namespace llarp SendTo( const RouterID& remote, const llarp_buffer_t& buf, - ILinkSession::CompletionHandler completed) override; + ILinkSession::CompletionHandler completed, + uint16_t priority) override; bool HasSessionTo(const RouterID& remote) const override; diff --git a/llarp/link/server.cpp b/llarp/link/server.cpp index 083bd71ea..ddc21da76 100644 --- a/llarp/link/server.cpp +++ b/llarp/link/server.cpp @@ -440,7 +440,10 @@ namespace llarp bool ILinkLayer::SendTo( - const RouterID& remote, const llarp_buffer_t& buf, ILinkSession::CompletionHandler completed) + const RouterID& remote, + const llarp_buffer_t& buf, + ILinkSession::CompletionHandler completed, + uint16_t priority) { std::shared_ptr s; { @@ -459,7 +462,7 @@ namespace llarp } ILinkSession::Message_t pkt(buf.sz); std::copy_n(buf.base, buf.sz, pkt.begin()); - return s && s->SendMessageBuffer(std::move(pkt), completed); + return s && s->SendMessageBuffer(std::move(pkt), completed, priority); } bool diff --git a/llarp/link/server.hpp b/llarp/link/server.hpp index 803120454..a976dbbc2 100644 --- a/llarp/link/server.hpp +++ b/llarp/link/server.hpp @@ -148,7 +148,8 @@ namespace llarp SendTo( const RouterID& remote, const llarp_buffer_t& buf, - ILinkSession::CompletionHandler completed); + ILinkSession::CompletionHandler completed, + uint16_t priority); virtual bool GetOurAddressInfo(AddressInfo& addr) const; diff --git a/llarp/link/session.hpp b/llarp/link/session.hpp index ab8b5d4f5..fc8df2414 100644 --- a/llarp/link/session.hpp +++ b/llarp/link/session.hpp @@ -57,7 +57,7 @@ namespace llarp /// send a message buffer to the remote endpoint virtual bool - SendMessageBuffer(Message_t, CompletionHandler handler) = 0; + SendMessageBuffer(Message_t, CompletionHandler handler, uint16_t priority) = 0; /// start the connection virtual void diff --git a/llarp/router/outbound_message_handler.cpp b/llarp/router/outbound_message_handler.cpp index 2a8639d09..2e56591ea 100644 --- a/llarp/router/outbound_message_handler.cpp +++ b/llarp/router/outbound_message_handler.cpp @@ -30,25 +30,28 @@ namespace llarp DoCallback(callback, SendStatus::InvalidRouter); return true; } - const uint16_t priority = msg.Priority(); + MessageQueueEntry ent; + ent.router = remote; + ent.inform = std::move(callback); + ent.pathid = msg.pathid; + ent.priority = msg.Priority(); + std::array linkmsg_buffer; - llarp_buffer_t buf(linkmsg_buffer); + llarp_buffer_t buf{linkmsg_buffer}; if (!EncodeBuffer(msg, buf)) { return false; } - Message message; - message.first.resize(buf.sz); - message.second = callback; + ent.message.resize(buf.sz); - std::copy_n(buf.base, buf.sz, message.first.data()); + std::copy_n(buf.base, buf.sz, ent.message.data()); // if we have a session to the destination, queue the message and return if (_router->linkManager().HasSessionTo(remote)) { - QueueOutboundMessage(remote, std::move(message), msg.pathid, priority); + QueueOutboundMessage(std::move(ent)); return true; } @@ -58,16 +61,11 @@ namespace llarp // in progress. bool shouldCreateSession = false; { - util::Lock l(_mutex); + util::Lock l{_mutex}; // create queue for if it doesn't exist, and get iterator auto [queue_itr, is_new] = pendingSessionMessageQueues.emplace(remote, MessageQueue()); - - MessageQueueEntry entry; - entry.priority = priority; - entry.message = message; - entry.router = remote; - queue_itr->second.push(std::move(entry)); + queue_itr->second.push(std::move(ent)); shouldCreateSession = is_new; } @@ -86,7 +84,7 @@ namespace llarp m_Killer.TryAccess([this]() { recentlyRemovedPaths.Decay(); ProcessOutboundQueue(); - if (/*bool more = */ SendRoundRobin()) + if (SendRoundRobin()) _router->TriggerPump(); }); } @@ -190,52 +188,48 @@ namespace llarp } bool - OutboundMessageHandler::Send(const RouterID& remote, const Message& msg) + OutboundMessageHandler::Send(const MessageQueueEntry& ent) { - const llarp_buffer_t buf(msg.first); - auto callback = msg.second; + const llarp_buffer_t buf{ent.message}; m_queueStats.sent++; - return _router->linkManager().SendTo(remote, buf, [=](ILinkSession::DeliveryStatus status) { - if (status == ILinkSession::DeliveryStatus::eDeliverySuccess) - DoCallback(callback, SendStatus::Success); - else - { - DoCallback(callback, SendStatus::Congestion); - } - }); + SendStatusHandler callback = ent.inform; + return _router->linkManager().SendTo( + ent.router, + buf, + [this, callback](ILinkSession::DeliveryStatus status) { + if (status == ILinkSession::DeliveryStatus::eDeliverySuccess) + DoCallback(callback, SendStatus::Success); + else + { + DoCallback(callback, SendStatus::Congestion); + } + }, + ent.priority); } bool - OutboundMessageHandler::SendIfSession(const RouterID& remote, const Message& msg) + OutboundMessageHandler::SendIfSession(const MessageQueueEntry& ent) { - if (_router->linkManager().HasSessionTo(remote)) + if (_router->linkManager().HasSessionTo(ent.router)) { - return Send(remote, msg); + return Send(ent); } return false; } bool - OutboundMessageHandler::QueueOutboundMessage( - const RouterID& remote, Message&& msg, const PathID_t& pathid, uint16_t priority) + OutboundMessageHandler::QueueOutboundMessage(MessageQueueEntry entry) { - MessageQueueEntry entry; - entry.message = std::move(msg); - // copy callback in case we need to call it, so we can std::move(entry) - auto callback_copy = entry.message.second; - entry.router = remote; - entry.pathid = pathid; - entry.priority = priority; + auto callback = entry.inform; if (outboundQueue.tryPushBack(std::move(entry)) != llarp::thread::QueueReturn::Success) { m_queueStats.dropped++; - DoCallback(callback_copy, SendStatus::Congestion); + DoCallback(callback, SendStatus::Congestion); } else { m_queueStats.queued++; - uint32_t queueSize = outboundQueue.size(); m_queueStats.queueWatermark = std::max(queueSize, m_queueStats.queueWatermark); } @@ -272,7 +266,7 @@ namespace llarp } else { - DoCallback(entry.message.second, SendStatus::Congestion); + DoCallback(entry.inform, SendStatus::Congestion); m_queueStats.dropped++; } } @@ -288,7 +282,7 @@ namespace llarp while (not routing_mq.empty()) { const MessageQueueEntry& entry = routing_mq.top(); - Send(entry.router, entry.message); + Send(entry); routing_mq.pop(); } @@ -331,7 +325,7 @@ namespace llarp { const MessageQueueEntry& entry = message_queue.top(); - Send(entry.router, entry.message); + Send(entry); message_queue.pop(); consecutive_empty = 0; @@ -380,11 +374,11 @@ namespace llarp if (status == SendStatus::Success) { - Send(entry.router, entry.message); + Send(entry); } else { - DoCallback(entry.message.second, status); + DoCallback(entry.inform, status); } movedMessages.pop(); } diff --git a/llarp/router/outbound_message_handler.hpp b/llarp/router/outbound_message_handler.hpp index 3edb6d917..9e4c7fcdc 100644 --- a/llarp/router/outbound_message_handler.hpp +++ b/llarp/router/outbound_message_handler.hpp @@ -74,15 +74,14 @@ namespace llarp Init(AbstractRouter* router); private: - using Message = std::pair, SendStatusHandler>; - /* A message that has been queued for sending, but not yet * processed into an individual path's message queue. */ struct MessageQueueEntry { uint16_t priority; - Message message; + std::vector message; + SendStatusHandler inform; PathID_t pathid; RouterID router; @@ -131,14 +130,14 @@ namespace llarp * returns the result of the call to LinkManager::SendTo() */ bool - Send(const RouterID& remote, const Message& msg); + Send(const MessageQueueEntry& ent); /* Sends the message along to the link layer if we have a session to the remote * * returns the result of the Send() call, or false if no session. */ bool - SendIfSession(const RouterID& remote, const Message& msg); + SendIfSession(const MessageQueueEntry& ent); /* queues a message to the shared outbound message queue. * @@ -149,8 +148,7 @@ namespace llarp * are placed in their paths' respective individual queues. */ bool - QueueOutboundMessage( - const RouterID& remote, Message&& msg, const PathID_t& pathid, uint16_t priority = 0); + QueueOutboundMessage(MessageQueueEntry entry); /* Processes messages on the shared message queue into their paths' respective * individual queues. diff --git a/llarp/router/rc_gossiper.cpp b/llarp/router/rc_gossiper.cpp index 7eb8aa88a..70493523b 100644 --- a/llarp/router/rc_gossiper.cpp +++ b/llarp/router/rc_gossiper.cpp @@ -116,7 +116,7 @@ namespace llarp m_router->NotifyRouterEvent(m_router->pubkey(), rc); // send message - peerSession->SendMessageBuffer(std::move(msg), nullptr); + peerSession->SendMessageBuffer(std::move(msg), nullptr, gossip.Priority()); }); return true; } From 14d75cc654b2ffac1b13ac5b3e6754acc981d52a Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 4 Apr 2022 17:50:20 -0400 Subject: [PATCH 149/202] add util::ascending_priority_queue type --- llarp/service/endpoint.cpp | 3 ++- llarp/service/protocol.hpp | 4 ++-- llarp/util/priority_queue.hpp | 13 +++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 llarp/util/priority_queue.hpp diff --git a/llarp/service/endpoint.cpp b/llarp/service/endpoint.cpp index 324f23eb8..18e7876df 100644 --- a/llarp/service/endpoint.cpp +++ b/llarp/service/endpoint.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include @@ -1629,7 +1630,7 @@ namespace llarp session->FlushDownstream(); // handle inbound traffic sorted - std::priority_queue queue; + util::ascending_priority_queue queue; while (not m_InboundTrafficQueue.empty()) { // succ it out diff --git a/llarp/service/protocol.hpp b/llarp/service/protocol.hpp index f2485c3af..1776abba4 100644 --- a/llarp/service/protocol.hpp +++ b/llarp/service/protocol.hpp @@ -64,9 +64,9 @@ namespace llarp ProcessAsync(path::Path_ptr p, PathID_t from, std::shared_ptr self); bool - operator<(const ProtocolMessage& other) const + operator>(const ProtocolMessage& other) const { - return other.seqno < seqno; + return seqno > other.seqno; } }; diff --git a/llarp/util/priority_queue.hpp b/llarp/util/priority_queue.hpp new file mode 100644 index 000000000..19ef98f55 --- /dev/null +++ b/llarp/util/priority_queue.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +namespace llarp::util +{ + /// priority queue that uses operator > instead of operator < + template > + using ascending_priority_queue = + std::priority_queue>; + +} // namespace llarp::util From 26c8063fc90c28b3db0d903c54c2c4b940b39fba Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 4 Apr 2022 18:12:27 -0400 Subject: [PATCH 150/202] convert priority queues --- llarp/handlers/tun.hpp | 10 ++++++---- llarp/iwp/session.cpp | 8 ++++---- llarp/iwp/session.hpp | 4 ++-- llarp/router/outbound_message_handler.hpp | 8 ++++---- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/llarp/handlers/tun.hpp b/llarp/handlers/tun.hpp index 37fe9f37d..fe93dfdae 100644 --- a/llarp/handlers/tun.hpp +++ b/llarp/handlers/tun.hpp @@ -12,10 +12,12 @@ #include #include -#include + #include #include + #include +#include namespace llarp { @@ -185,14 +187,14 @@ namespace llarp net::IPPacket pkt; bool - operator<(const WritePacket& other) const + operator>(const WritePacket& other) const { - return other.seqno < seqno; + return seqno > other.seqno; } }; /// queue for sending packets to user from network - std::priority_queue m_NetworkToUserPktQueue; + util::ascending_priority_queue m_NetworkToUserPktQueue; void Pump(llarp_time_t now) override; diff --git a/llarp/iwp/session.cpp b/llarp/iwp/session.cpp index 200fa7130..604a9eaf0 100644 --- a/llarp/iwp/session.cpp +++ b/llarp/iwp/session.cpp @@ -260,15 +260,15 @@ namespace llarp OutboundMessage*, std::vector, ComparePtr> - resend; + to_resend; for (auto& [id, msg] : m_TXMsgs) { if (msg.ShouldFlush(now)) - resend.push(&msg); + to_resend.push(&msg); } - if (not resend.empty()) + if (not to_resend.empty()) { - for (auto& msg = resend.top(); not resend.empty(); resend.pop()) + for (auto& msg = to_resend.top(); not to_resend.empty(); to_resend.pop()) msg->FlushUnAcked(util::memFn(&Session::EncryptAndSend, this), now); } } diff --git a/llarp/iwp/session.hpp b/llarp/iwp/session.hpp index 212b32812..210a37a1e 100644 --- a/llarp/iwp/session.hpp +++ b/llarp/iwp/session.hpp @@ -8,8 +8,8 @@ #include #include #include -#include +#include #include namespace llarp @@ -197,7 +197,7 @@ namespace llarp /// maps rxid to time recieved std::unordered_map m_ReplayFilter; /// rx messages to send in next round of multiacks - std::priority_queue, std::greater<>> m_SendMACKs; + util::ascending_priority_queue m_SendMACKs; using CryptoQueue_t = std::vector; diff --git a/llarp/router/outbound_message_handler.hpp b/llarp/router/outbound_message_handler.hpp index 9e4c7fcdc..232568f64 100644 --- a/llarp/router/outbound_message_handler.hpp +++ b/llarp/router/outbound_message_handler.hpp @@ -6,12 +6,12 @@ #include #include #include +#include #include #include #include #include -#include struct llarp_buffer_t; @@ -86,9 +86,9 @@ namespace llarp RouterID router; bool - operator<(const MessageQueueEntry& other) const + operator>(const MessageQueueEntry& other) const { - return other.priority < priority; + return priority > other.priority; } }; @@ -103,7 +103,7 @@ namespace llarp uint32_t numTicks = 0; }; - using MessageQueue = std::priority_queue; + using MessageQueue = util::ascending_priority_queue; /* If a session is not yet created with the destination router for a message, * a special queue is created for that router and an attempt is made to From 238c33f565ee2e6afaf5c6a8d1726a4218f7d502 Mon Sep 17 00:00:00 2001 From: majestrate Date: Mon, 2 May 2022 15:15:00 -0400 Subject: [PATCH 151/202] Update llarp/iwp/session.cpp return Co-authored-by: Thomas Winget --- llarp/iwp/session.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/llarp/iwp/session.cpp b/llarp/iwp/session.cpp index 604a9eaf0..09ea9ec6b 100644 --- a/llarp/iwp/session.cpp +++ b/llarp/iwp/session.cpp @@ -128,10 +128,12 @@ namespace llarp if (not msg.BEncode(&buf)) { LogError("failed to encode LIM for ", m_RemoteAddr); + return; } if (not SendMessageBuffer(std::move(data), h)) { LogError("failed to send LIM to ", m_RemoteAddr); + return; } LogTrace("sent LIM to ", m_RemoteAddr); } From 5b0ece3f9e3f1cce4b2b6e85e7f6654fb98edb2b Mon Sep 17 00:00:00 2001 From: majestrate Date: Mon, 2 May 2022 16:12:27 -0400 Subject: [PATCH 152/202] Update llarp/router/outbound_message_handler.cpp add comment Co-authored-by: Thomas Winget --- llarp/router/outbound_message_handler.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/llarp/router/outbound_message_handler.cpp b/llarp/router/outbound_message_handler.cpp index 2e56591ea..1abbb8c05 100644 --- a/llarp/router/outbound_message_handler.cpp +++ b/llarp/router/outbound_message_handler.cpp @@ -84,7 +84,10 @@ namespace llarp m_Killer.TryAccess([this]() { recentlyRemovedPaths.Decay(); ProcessOutboundQueue(); - if (SendRoundRobin()) + // TODO: this probably shouldn't be pumping, as it defeats the purpose + // of having a limit on sends per tick, but chaning it is potentially bad + // and requires testing so it should be changed later. + if (/*bool more = */ SendRoundRobin()) _router->TriggerPump(); }); } From 546aede5289659408a24f302729929fbb9ca3236 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 2 May 2022 16:32:45 -0400 Subject: [PATCH 153/202] add mirror for cmake static deps --- .drone.jsonnet | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index afbf6dc0e..38f3eb1f1 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -23,6 +23,9 @@ local submodules = { commands: submodule_commands, }; +// cmake options for static deps mirror +local ci_mirror_opts = '-DLOCAL_MIRROR=https://oxen.rocks/deps'; + local apt_get_quiet = 'apt-get -o=Dpkg::Use-Pty=0 -q'; // Regular build on a debian-like system: @@ -140,7 +143,7 @@ local windows_cross_pipeline(name, 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y build-essential cmake git pkg-config ccache g++-mingw-w64-x86-64-posix nsis zip automake libtool', 'update-alternatives --set x86_64-w64-mingw32-gcc /usr/bin/x86_64-w64-mingw32-gcc-posix', 'update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix', - 'VERBOSE=1 JOBS=' + jobs + ' ./contrib/windows.sh', + 'VERBOSE=1 JOBS=' + jobs + ' ./contrib/windows.sh ' + ci_mirror_opts, ] + extra_cmds, }, ], @@ -270,7 +273,7 @@ local mac_builder(name, // basic system headers. WTF apple: 'export SDKROOT="$(xcrun --sdk macosx --show-sdk-path)"', 'ulimit -n 1024', // because macos sets ulimit to 256 for some reason yeah idk - './contrib/mac.sh', + './contrib/mac.sh ' + ci_mirror_opts, ] + extra_cmds, }, ], From 8efac67c0ab84e7f434c556a2b65a296689227fc Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 2 May 2022 16:52:32 -0400 Subject: [PATCH 154/202] add static deps mirror to ci --- .drone.jsonnet | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 38f3eb1f1..9f2aefdcf 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -24,7 +24,7 @@ local submodules = { }; // cmake options for static deps mirror -local ci_mirror_opts = '-DLOCAL_MIRROR=https://oxen.rocks/deps'; +local ci_mirror_opts = '-DLOCAL_MIRROR=https://oxen.rocks/deps '; local apt_get_quiet = 'apt-get -o=Dpkg::Use-Pty=0 -q'; @@ -173,8 +173,7 @@ local linux_cross_pipeline(name, environment: { SSH_KEY: { from_secret: 'SSH_KEY' }, CROSS_TARGETS: std.join(':', cross_targets) }, commands: [ 'echo "Building on ${DRONE_STAGE_MACHINE}"', - 'VERBOSE=1 JOBS=' + jobs + ' ./contrib/cross.sh ' + std.join(' ', cross_targets) + (if std.length(cmake_extra) > 0 then ' -- ' + cmake_extra else ''), - ] + extra_cmds, + 'VERBOSE=1 JOBS=' + jobs + ' ./contrib/cross.sh ' + std.join(' ', cross_targets) + ' -- ' + ci_mirror_opts + cmake_extra, }, ], }; From d85d20815649bbea3f83160d61fa10fb9d3a5268 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 2 May 2022 16:52:56 -0400 Subject: [PATCH 155/202] bump libuv version with hashpin --- .drone.jsonnet | 3 ++- cmake/StaticBuild.cmake | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 9f2aefdcf..ffc968653 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -173,7 +173,8 @@ local linux_cross_pipeline(name, environment: { SSH_KEY: { from_secret: 'SSH_KEY' }, CROSS_TARGETS: std.join(':', cross_targets) }, commands: [ 'echo "Building on ${DRONE_STAGE_MACHINE}"', - 'VERBOSE=1 JOBS=' + jobs + ' ./contrib/cross.sh ' + std.join(' ', cross_targets) + ' -- ' + ci_mirror_opts + cmake_extra, + 'VERBOSE=1 JOBS=' + jobs + ' ./contrib/cross.sh ' + std.join(' ', cross_targets) + (if std.length(cmake_extra) > 0 then ' -- ' + cmake_extra else ''), + ], }, ], }; diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index df95f1eb4..826a62385 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -48,11 +48,11 @@ set(ZMQ_SOURCE zeromq-${ZMQ_VERSION}.tar.gz) set(ZMQ_HASH SHA512=e198ef9f82d392754caadd547537666d4fba0afd7d027749b3adae450516bcf284d241d4616cad3cb4ad9af8c10373d456de92dc6d115b037941659f141e7c0e CACHE STRING "libzmq source hash") -set(LIBUV_VERSION 1.43.0 CACHE STRING "libuv version") +set(LIBUV_VERSION 1.44.1 CACHE STRING "libuv version") set(LIBUV_MIRROR ${LOCAL_MIRROR} https://dist.libuv.org/dist/v${LIBUV_VERSION} CACHE STRING "libuv mirror(s)") set(LIBUV_SOURCE libuv-v${LIBUV_VERSION}.tar.gz) -set(LIBUV_HASH SHA256=90d72bb7ae18de2519d0cac70eb89c319351146b90cd3f91303a492707e693a4 +set(LIBUV_HASH SHA512=b4f8944e2c79e3a6a31ded6cccbe4c0eeada50db6bc8a448d7015642795012a4b80ffeef7ca455bb093c59a8950d0e1430566c3c2fa87b73f82699098162d834) CACHE STRING "libuv source hash") set(ZLIB_VERSION 1.2.12 CACHE STRING "zlib version") From 3c925688818a140c61724e36c39550525e2cd157 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 2 May 2022 17:48:46 -0400 Subject: [PATCH 156/202] typo --- cmake/StaticBuild.cmake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index 826a62385..c92c733b9 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -52,7 +52,7 @@ set(LIBUV_VERSION 1.44.1 CACHE STRING "libuv version") set(LIBUV_MIRROR ${LOCAL_MIRROR} https://dist.libuv.org/dist/v${LIBUV_VERSION} CACHE STRING "libuv mirror(s)") set(LIBUV_SOURCE libuv-v${LIBUV_VERSION}.tar.gz) -set(LIBUV_HASH SHA512=b4f8944e2c79e3a6a31ded6cccbe4c0eeada50db6bc8a448d7015642795012a4b80ffeef7ca455bb093c59a8950d0e1430566c3c2fa87b73f82699098162d834) +set(LIBUV_HASH SHA512=b4f8944e2c79e3a6a31ded6cccbe4c0eeada50db6bc8a448d7015642795012a4b80ffeef7ca455bb093c59a8950d0e1430566c3c2fa87b73f82699098162d834 CACHE STRING "libuv source hash") set(ZLIB_VERSION 1.2.12 CACHE STRING "zlib version") From 9556741f6a3dce9eae6414fb919d5c408757497a Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 10 May 2022 09:47:54 -0400 Subject: [PATCH 157/202] split up link order for oxenmq and oxenc * lokinet uberlib needs oxenc and oxenmq * lokinet-util only needs oxenc fixes #1911 --- CMakeLists.txt | 1 + llarp/CMakeLists.txt | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ed45e5b70..7a36f117e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -199,6 +199,7 @@ if(OXENC_FOUND) else() message(STATUS "using oxen-encoding submodule") add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/external/oxen-encoding) + add_library(oxenc::oxenc ALIAS oxenc) endif() option(FORCE_OXENMQ_SUBMODULE "force using oxenmq submodule" OFF) diff --git a/llarp/CMakeLists.txt b/llarp/CMakeLists.txt index d8a67086e..40786322b 100644 --- a/llarp/CMakeLists.txt +++ b/llarp/CMakeLists.txt @@ -34,7 +34,7 @@ target_link_libraries(lokinet-util PUBLIC nlohmann_json::nlohmann_json filesystem date::date - oxenmq::oxenmq + oxenc::oxenc ) if(ANDROID) @@ -59,7 +59,7 @@ add_library(lokinet-platform ) target_link_libraries(lokinet-platform PUBLIC lokinet-cryptography lokinet-util Threads::Threads base_libs uvw) - +target_link_libraries(lokinet-platform PRIVATE oxenmq::oxenmq) if (ANDROID) target_sources(lokinet-platform PRIVATE android/ifaddrs.c) @@ -242,7 +242,7 @@ if(WITH_HIVE) ) endif() -target_link_libraries(liblokinet PUBLIC cxxopts lokinet-platform lokinet-util lokinet-cryptography sqlite_orm ngtcp2_static) +target_link_libraries(liblokinet PUBLIC cxxopts oxenc::oxenc lokinet-platform lokinet-util lokinet-cryptography sqlite_orm ngtcp2_static oxenmq::oxenmq) target_link_libraries(liblokinet PRIVATE libunbound) pkg_check_modules(CRYPT libcrypt IMPORTED_TARGET) if(CRYPT_FOUND AND NOT CMAKE_CROSSCOMPILING) From d30fe95f2e9a06f9b6119741cfc1f67409873fa8 Mon Sep 17 00:00:00 2001 From: majestrate Date: Wed, 18 May 2022 12:16:46 -0400 Subject: [PATCH 158/202] use ::/0 makes us map all ranges not just ipv4 when exit mode is on by default. --- llarp/rpc/rpc_server.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/llarp/rpc/rpc_server.cpp b/llarp/rpc/rpc_server.cpp index 6c9fa866c..b85751a27 100644 --- a/llarp/rpc/rpc_server.cpp +++ b/llarp/rpc/rpc_server.cpp @@ -432,7 +432,7 @@ namespace llarp::rpc const auto range_itr = obj.find("range"); if (range_itr == obj.end()) { - range.FromString("0.0.0.0/0"); + range.FromString("::/0"); } else if (not range.FromString(range_itr->get())) { From 70b07bab4478e16380cf88410657c655edc00294 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 18 May 2022 17:06:32 -0400 Subject: [PATCH 159/202] clean up ip packet code --- llarp/handlers/exit.cpp | 17 +- llarp/handlers/exit.hpp | 11 +- llarp/handlers/tun.hpp | 8 - llarp/net/ip_packet.cpp | 1109 ++++++++++++++++++++------------------- llarp/net/ip_packet.hpp | 493 ++++++++--------- llarp/util/codel.hpp | 167 ------ 6 files changed, 789 insertions(+), 1016 deletions(-) delete mode 100644 llarp/util/codel.hpp diff --git a/llarp/handlers/exit.cpp b/llarp/handlers/exit.cpp index 7dc80f65a..73902ac04 100644 --- a/llarp/handlers/exit.cpp +++ b/llarp/handlers/exit.cpp @@ -23,8 +23,6 @@ namespace llarp , m_Name(std::move(name)) , m_LocalResolverAddr{"127.0.0.1:53"} , m_QUIC{std::make_shared(*this)} - , m_InetToNetwork(name + "_exit_rx", r->loop(), r->loop()) - { m_ShouldInitTun = true; m_QUIC = std::make_shared(*this); @@ -363,7 +361,11 @@ namespace llarp void ExitEndpoint::Flush() { - m_InetToNetwork.Process([&](Pkt_t& pkt) { + while (not m_InetToNetwork.empty()) + { + net::IPPacket pkt{m_InetToNetwork.top()}; + m_InetToNetwork.pop(); + PubKey pk; { auto itr = m_IPToKey.find(pkt.dstv6()); @@ -371,7 +373,7 @@ namespace llarp { // drop LogWarn(Name(), " dropping packet, has no session at ", pkt.dstv6()); - return; + continue; } pk = itr->second; } @@ -385,7 +387,7 @@ namespace llarp if (itr != m_SNodeSessions.end()) { itr->second->SendPacketToRemote(pkt.ConstBuffer(), service::ProtocolType::TrafficV4); - return; + continue; } } auto tryFlushingTraffic = [&](exit::Endpoint* const ep) -> bool { @@ -412,7 +414,8 @@ namespace llarp pk, " as we have no working endpoints"); } - }); + } + for (auto& [pubkey, endpoint] : m_ActiveExits) { if (!endpoint->Flush()) @@ -633,7 +636,7 @@ namespace llarp void ExitEndpoint::OnInetPacket(net::IPPacket pkt) { - m_InetToNetwork.Emplace(std::move(pkt)); + m_InetToNetwork.emplace(std::move(pkt)); } bool diff --git a/llarp/handlers/exit.hpp b/llarp/handlers/exit.hpp index d5053b8d2..5c2f3ec41 100644 --- a/llarp/handlers/exit.hpp +++ b/llarp/handlers/exit.hpp @@ -214,15 +214,8 @@ namespace llarp std::shared_ptr m_QUIC; - using Pkt_t = net::IPPacket; - using PacketQueue_t = util::CoDelQueue< - Pkt_t, - Pkt_t::GetTime, - Pkt_t::PutTime, - Pkt_t::CompareOrder, - Pkt_t::GetNow, - util::NullMutex, - util::NullLock>; + using PacketQueue_t = std:: + priority_queue, net::IPPacket::CompareOrder>; /// internet to llarp packet queue PacketQueue_t m_InetToNetwork; diff --git a/llarp/handlers/tun.hpp b/llarp/handlers/tun.hpp index fe93dfdae..74161754b 100644 --- a/llarp/handlers/tun.hpp +++ b/llarp/handlers/tun.hpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include @@ -174,13 +173,6 @@ namespace llarp ResetInternalState() override; protected: - using PacketQueue_t = llarp::util::CoDelQueue< - net::IPPacket, - net::IPPacket::GetTime, - net::IPPacket::PutTime, - net::IPPacket::CompareOrder, - net::IPPacket::GetNow>; - struct WritePacket { uint64_t seqno; diff --git a/llarp/net/ip_packet.cpp b/llarp/net/ip_packet.cpp index 5e287b6cd..d6e339ee5 100644 --- a/llarp/net/ip_packet.cpp +++ b/llarp/net/ip_packet.cpp @@ -13,642 +13,657 @@ #include #include -constexpr uint32_t ipv6_flowlabel_mask = 0b0000'0000'0000'1111'1111'1111'1111'1111; - -void -ipv6_header::FlowLabel(llarp::nuint32_t label) +namespace llarp::net { - // the ipv6 flow label is the last 20 bits in the first 32 bits of the header - preamble.flowlabel = - (htonl(ipv6_flowlabel_mask) & label.n) | (preamble.flowlabel & htonl(~ipv6_flowlabel_mask)); -} + constexpr uint32_t ipv6_flowlabel_mask = 0b0000'0000'0000'1111'1111'1111'1111'1111; -llarp::nuint32_t -ipv6_header::FlowLabel() const -{ - return llarp::nuint32_t{preamble.flowlabel & htonl(ipv6_flowlabel_mask)}; -} + template + struct ipv6_header_preamble + { + unsigned char pad_small : 4; + unsigned char version : 4; + uint8_t pad[3]; + }; -namespace llarp -{ - namespace net + template <> + struct ipv6_header_preamble { - std::string - IPProtocolName(IPProtocol proto) - { - if (const auto* ent = ::getprotobynumber(static_cast(proto))) - { - return ent->p_name; - } - throw std::invalid_argument{ - "cannot determine protocol name for ip proto '" + std::to_string(static_cast(proto)) - + "'"}; - } + unsigned char version : 4; + unsigned char pad_small : 4; + uint8_t pad[3]; + }; + + /// get 20 bit truncated flow label in network order + llarp::nuint32_t + ipv6_header::FlowLabel() const + { + return llarp::nuint32_t{preamble.flowlabel & htonl(ipv6_flowlabel_mask)}; + } + + /// put 20 bit truncated flow label network order + void + ipv6_header::FlowLabel(llarp::nuint32_t label) + { + // the ipv6 flow label is the last 20 bits in the first 32 bits of the header + preamble.flowlabel = + (htonl(ipv6_flowlabel_mask) & label.n) | (preamble.flowlabel & htonl(~ipv6_flowlabel_mask)); + }; - IPProtocol - ParseIPProtocol(std::string data) + std::string + IPProtocolName(IPProtocol proto) + { + if (const auto* ent = ::getprotobynumber(static_cast(proto))) { - if (const auto* ent = ::getprotobyname(data.c_str())) - { - return static_cast(ent->p_proto); - } - if (starts_with(data, "0x")) - { - if (const int intVal = std::stoi(data.substr(2), nullptr, 16); intVal > 0) - return static_cast(intVal); - } - throw std::invalid_argument{"no such ip protocol: '" + data + "'"}; + return ent->p_name; } + throw std::invalid_argument{ + "cannot determine protocol name for ip proto '" + std::to_string(static_cast(proto)) + + "'"}; + } - inline static uint32_t* - in6_uint32_ptr(in6_addr& addr) + IPProtocol + ParseIPProtocol(std::string data) + { + if (const auto* ent = ::getprotobyname(data.c_str())) { - return (uint32_t*)addr.s6_addr; + return static_cast(ent->p_proto); } - - inline static const uint32_t* - in6_uint32_ptr(const in6_addr& addr) + if (starts_with(data, "0x")) { - return (uint32_t*)addr.s6_addr; + if (const int intVal = std::stoi(data.substr(2), nullptr, 16); intVal > 0) + return static_cast(intVal); } + throw std::invalid_argument{"no such ip protocol: '" + data + "'"}; + } - huint128_t - IPPacket::srcv6() const - { - if (IsV6()) - return In6ToHUInt(HeaderV6()->srcaddr); + inline static uint32_t* + in6_uint32_ptr(in6_addr& addr) + { + return (uint32_t*)addr.s6_addr; + } - return ExpandV4(srcv4()); - } + inline static const uint32_t* + in6_uint32_ptr(const in6_addr& addr) + { + return (uint32_t*)addr.s6_addr; + } - huint128_t - IPPacket::dstv6() const - { - if (IsV6()) - return In6ToHUInt(HeaderV6()->dstaddr); + huint128_t + IPPacket::srcv6() const + { + if (IsV6()) + return In6ToHUInt(HeaderV6()->srcaddr); - return ExpandV4(dstv4()); - } + return ExpandV4(srcv4()); + } - bool - IPPacket::Load(const llarp_buffer_t& pkt) - { - if (pkt.sz > sizeof(buf) or pkt.sz == 0) - return false; - sz = pkt.sz; - std::copy_n(pkt.base, sz, buf); - return true; - } + huint128_t + IPPacket::dstv6() const + { + if (IsV6()) + return In6ToHUInt(HeaderV6()->dstaddr); - ManagedBuffer - IPPacket::ConstBuffer() const - { - const byte_t* ptr = buf; - llarp_buffer_t b(ptr, sz); - return ManagedBuffer(b); - } + return ExpandV4(dstv4()); + } - ManagedBuffer - IPPacket::Buffer() - { - byte_t* ptr = buf; - llarp_buffer_t b(ptr, sz); - return ManagedBuffer(b); - } + bool + IPPacket::Load(const llarp_buffer_t& pkt) + { + if (pkt.sz > sizeof(buf) or pkt.sz == 0) + return false; + sz = pkt.sz; + std::copy_n(pkt.base, sz, buf); + return true; + } + + ManagedBuffer + IPPacket::ConstBuffer() const + { + const byte_t* ptr = buf; + llarp_buffer_t b(ptr, sz); + return ManagedBuffer(b); + } - std::optional - IPPacket::DstPort() const - { - switch (IPProtocol{Header()->protocol}) - { - case IPProtocol::TCP: - case IPProtocol::UDP: - return nuint16_t{*reinterpret_cast(buf + (Header()->ihl * 4) + 2)}; - default: - return std::nullopt; - } - } + ManagedBuffer + IPPacket::Buffer() + { + byte_t* ptr = buf; + llarp_buffer_t b(ptr, sz); + return ManagedBuffer(b); + } - std::optional - IPPacket::SrcPort() const + std::optional + IPPacket::DstPort() const + { + switch (IPProtocol{Header()->protocol}) { - switch (IPProtocol{Header()->protocol}) - { - case IPProtocol::TCP: - case IPProtocol::UDP: - return nuint16_t{*reinterpret_cast(buf + (Header()->ihl * 4))}; - default: - return std::nullopt; - } + case IPProtocol::TCP: + case IPProtocol::UDP: + return nuint16_t{*reinterpret_cast(buf + (Header()->ihl * 4) + 2)}; + default: + return std::nullopt; } + } - huint32_t - IPPacket::srcv4() const + std::optional + IPPacket::SrcPort() const + { + switch (IPProtocol{Header()->protocol}) { - return huint32_t{ntohl(Header()->saddr)}; + case IPProtocol::TCP: + case IPProtocol::UDP: + return nuint16_t{*reinterpret_cast(buf + (Header()->ihl * 4))}; + default: + return std::nullopt; } + } - huint32_t - IPPacket::dstv4() const - { - return huint32_t{ntohl(Header()->daddr)}; - } + huint32_t + IPPacket::srcv4() const + { + return huint32_t{ntohl(Header()->saddr)}; + } - huint128_t - IPPacket::dst4to6() const - { - return ExpandV4(dstv4()); - } + huint32_t + IPPacket::dstv4() const + { + return huint32_t{ntohl(Header()->daddr)}; + } - huint128_t - IPPacket::src4to6() const - { - return ExpandV4(srcv4()); - } + huint128_t + IPPacket::dst4to6() const + { + return ExpandV4(dstv4()); + } - huint128_t - IPPacket::dst4to6Lan() const - { - return ExpandV4Lan(dstv4()); - } + huint128_t + IPPacket::src4to6() const + { + return ExpandV4(srcv4()); + } - huint128_t - IPPacket::src4to6Lan() const + huint128_t + IPPacket::dst4to6Lan() const + { + return ExpandV4Lan(dstv4()); + } + + huint128_t + IPPacket::src4to6Lan() const + { + return ExpandV4Lan(srcv4()); + } + + uint16_t + ipchksum(const byte_t* buf, size_t sz, uint32_t sum) + { + while (sz > 1) { - return ExpandV4Lan(srcv4()); + sum += *(const uint16_t*)buf; + sz -= sizeof(uint16_t); + buf += sizeof(uint16_t); } - - uint16_t - ipchksum(const byte_t* buf, size_t sz, uint32_t sum) + if (sz != 0) { - while (sz > 1) - { - sum += *(const uint16_t*)buf; - sz -= sizeof(uint16_t); - buf += sizeof(uint16_t); - } - if (sz != 0) - { - uint16_t x = 0; + uint16_t x = 0; - *(byte_t*)&x = *(const byte_t*)buf; - sum += x; - } + *(byte_t*)&x = *(const byte_t*)buf; + sum += x; + } - // only need to do it 2 times to be sure - // proof: 0xFFff + 0xFFff = 0x1FFfe -> 0xFFff - sum = (sum & 0xFFff) + (sum >> 16); - sum += sum >> 16; + // only need to do it 2 times to be sure + // proof: 0xFFff + 0xFFff = 0x1FFfe -> 0xFFff + sum = (sum & 0xFFff) + (sum >> 16); + sum += sum >> 16; - return uint16_t((~sum) & 0xFFff); - } + return uint16_t((~sum) & 0xFFff); + } #define ADD32CS(x) ((uint32_t)(x & 0xFFff) + (uint32_t)(x >> 16)) #define SUB32CS(x) ((uint32_t)((~x) & 0xFFff) + (uint32_t)((~x) >> 16)) - static nuint16_t - deltaIPv4Checksum( - nuint16_t old_sum, - nuint32_t old_src_ip, - nuint32_t old_dst_ip, - nuint32_t new_src_ip, - nuint32_t new_dst_ip) - { - uint32_t sum = uint32_t(old_sum.n) + ADD32CS(old_src_ip.n) + ADD32CS(old_dst_ip.n) - + SUB32CS(new_src_ip.n) + SUB32CS(new_dst_ip.n); - - // only need to do it 2 times to be sure - // proof: 0xFFff + 0xFFff = 0x1FFfe -> 0xFFff - sum = (sum & 0xFFff) + (sum >> 16); - sum += sum >> 16; - - return nuint16_t{uint16_t(sum & 0xFFff)}; - } - - static nuint16_t - deltaIPv6Checksum( - nuint16_t old_sum, - const uint32_t old_src_ip[4], - const uint32_t old_dst_ip[4], - const uint32_t new_src_ip[4], - const uint32_t new_dst_ip[4]) - { - /* we don't actually care in what way integers are arranged in memory - * internally */ - /* as long as uint16 pairs are swapped in correct direction, result will - * be correct (assuming there are no gaps in structure) */ - /* we represent 128bit stuff there as 4 32bit ints, that should be more or - * less correct */ - /* we could do 64bit ints too but then we couldn't reuse 32bit macros and - * that'd suck for 32bit cpus */ + static nuint16_t + deltaIPv4Checksum( + nuint16_t old_sum, + nuint32_t old_src_ip, + nuint32_t old_dst_ip, + nuint32_t new_src_ip, + nuint32_t new_dst_ip) + { + uint32_t sum = uint32_t(old_sum.n) + ADD32CS(old_src_ip.n) + ADD32CS(old_dst_ip.n) + + SUB32CS(new_src_ip.n) + SUB32CS(new_dst_ip.n); + + // only need to do it 2 times to be sure + // proof: 0xFFff + 0xFFff = 0x1FFfe -> 0xFFff + sum = (sum & 0xFFff) + (sum >> 16); + sum += sum >> 16; + + return nuint16_t{uint16_t(sum & 0xFFff)}; + } + + static nuint16_t + deltaIPv6Checksum( + nuint16_t old_sum, + const uint32_t old_src_ip[4], + const uint32_t old_dst_ip[4], + const uint32_t new_src_ip[4], + const uint32_t new_dst_ip[4]) + { + /* we don't actually care in what way integers are arranged in memory + * internally */ + /* as long as uint16 pairs are swapped in correct direction, result will + * be correct (assuming there are no gaps in structure) */ + /* we represent 128bit stuff there as 4 32bit ints, that should be more or + * less correct */ + /* we could do 64bit ints too but then we couldn't reuse 32bit macros and + * that'd suck for 32bit cpus */ #define ADDN128CS(x) (ADD32CS(x[0]) + ADD32CS(x[1]) + ADD32CS(x[2]) + ADD32CS(x[3])) #define SUBN128CS(x) (SUB32CS(x[0]) + SUB32CS(x[1]) + SUB32CS(x[2]) + SUB32CS(x[3])) - uint32_t sum = uint32_t(old_sum.n) + ADDN128CS(old_src_ip) + ADDN128CS(old_dst_ip) - + SUBN128CS(new_src_ip) + SUBN128CS(new_dst_ip); + uint32_t sum = uint32_t(old_sum.n) + ADDN128CS(old_src_ip) + ADDN128CS(old_dst_ip) + + SUBN128CS(new_src_ip) + SUBN128CS(new_dst_ip); #undef ADDN128CS #undef SUBN128CS - // only need to do it 2 times to be sure - // proof: 0xFFff + 0xFFff = 0x1FFfe -> 0xFFff - sum = (sum & 0xFFff) + (sum >> 16); - sum += sum >> 16; + // only need to do it 2 times to be sure + // proof: 0xFFff + 0xFFff = 0x1FFfe -> 0xFFff + sum = (sum & 0xFFff) + (sum >> 16); + sum += sum >> 16; - return nuint16_t{uint16_t(sum & 0xFFff)}; - } + return nuint16_t{uint16_t(sum & 0xFFff)}; + } #undef ADD32CS #undef SUB32CS - static void - deltaChecksumIPv4TCP( - byte_t* pld, - size_t psz, - size_t fragoff, - size_t chksumoff, - nuint32_t oSrcIP, - nuint32_t oDstIP, - nuint32_t nSrcIP, - nuint32_t nDstIP) - { - if (fragoff > chksumoff || psz < chksumoff - fragoff + 2) - return; - - auto check = (nuint16_t*)(pld + chksumoff - fragoff); - - *check = deltaIPv4Checksum(*check, oSrcIP, oDstIP, nSrcIP, nDstIP); - // usually, TCP checksum field cannot be 0xFFff, - // because one's complement addition cannot result in 0x0000, - // and there's inversion in the end; - // emulate that. - if (check->n == 0xFFff) - check->n = 0x0000; - } - - static void - deltaChecksumIPv6TCP( - byte_t* pld, - size_t psz, - size_t fragoff, - size_t chksumoff, - const uint32_t oSrcIP[4], - const uint32_t oDstIP[4], - const uint32_t nSrcIP[4], - const uint32_t nDstIP[4]) - { - if (fragoff > chksumoff || psz < chksumoff - fragoff + 2) - return; - - auto check = (nuint16_t*)(pld + chksumoff - fragoff); - - *check = deltaIPv6Checksum(*check, oSrcIP, oDstIP, nSrcIP, nDstIP); - // usually, TCP checksum field cannot be 0xFFff, - // because one's complement addition cannot result in 0x0000, - // and there's inversion in the end; - // emulate that. - if (check->n == 0xFFff) - check->n = 0x0000; - } + static void + deltaChecksumIPv4TCP( + byte_t* pld, + size_t psz, + size_t fragoff, + size_t chksumoff, + nuint32_t oSrcIP, + nuint32_t oDstIP, + nuint32_t nSrcIP, + nuint32_t nDstIP) + { + if (fragoff > chksumoff || psz < chksumoff - fragoff + 2) + return; + + auto check = (nuint16_t*)(pld + chksumoff - fragoff); + + *check = deltaIPv4Checksum(*check, oSrcIP, oDstIP, nSrcIP, nDstIP); + // usually, TCP checksum field cannot be 0xFFff, + // because one's complement addition cannot result in 0x0000, + // and there's inversion in the end; + // emulate that. + if (check->n == 0xFFff) + check->n = 0x0000; + } + + static void + deltaChecksumIPv6TCP( + byte_t* pld, + size_t psz, + size_t fragoff, + size_t chksumoff, + const uint32_t oSrcIP[4], + const uint32_t oDstIP[4], + const uint32_t nSrcIP[4], + const uint32_t nDstIP[4]) + { + if (fragoff > chksumoff || psz < chksumoff - fragoff + 2) + return; + + auto check = (nuint16_t*)(pld + chksumoff - fragoff); + + *check = deltaIPv6Checksum(*check, oSrcIP, oDstIP, nSrcIP, nDstIP); + // usually, TCP checksum field cannot be 0xFFff, + // because one's complement addition cannot result in 0x0000, + // and there's inversion in the end; + // emulate that. + if (check->n == 0xFFff) + check->n = 0x0000; + } + + static void + deltaChecksumIPv4UDP( + byte_t* pld, + size_t psz, + size_t fragoff, + nuint32_t oSrcIP, + nuint32_t oDstIP, + nuint32_t nSrcIP, + nuint32_t nDstIP) + { + if (fragoff > 6 || psz < 6 + 2) + return; + + auto check = (nuint16_t*)(pld + 6); + if (check->n == 0x0000) + return; // 0 is used to indicate "no checksum", don't change + + *check = deltaIPv4Checksum(*check, oSrcIP, oDstIP, nSrcIP, nDstIP); + // 0 is used to indicate "no checksum" + // 0xFFff and 0 are equivalent in one's complement math + // 0xFFff + 1 = 0x10000 -> 0x0001 (same as 0 + 1) + // infact it's impossible to get 0 with such addition, + // when starting from non-0 value. + // inside deltachksum we don't invert so it's safe to skip check there + // if(check->n == 0x0000) + // check->n = 0xFFff; + } + + static void + deltaChecksumIPv6UDP( + byte_t* pld, + size_t psz, + size_t fragoff, + const uint32_t oSrcIP[4], + const uint32_t oDstIP[4], + const uint32_t nSrcIP[4], + const uint32_t nDstIP[4]) + { + if (fragoff > 6 || psz < 6 + 2) + return; + + auto check = (nuint16_t*)(pld + 6); + // 0 is used to indicate "no checksum", don't change + // even tho this shouldn't happen for IPv6, handle it properly + // we actually should drop/log 0-checksum packets per spec + // but that should be done at upper level than this function + // it's better to do correct thing there regardless + // XXX or maybe we should change this function to be able to return error? + // either way that's not a priority + if (check->n == 0x0000) + return; + + *check = deltaIPv6Checksum(*check, oSrcIP, oDstIP, nSrcIP, nDstIP); + // 0 is used to indicate "no checksum" + // 0xFFff and 0 are equivalent in one's complement math + // 0xFFff + 1 = 0x10000 -> 0x0001 (same as 0 + 1) + // infact it's impossible to get 0 with such addition, + // when starting from non-0 value. + // inside deltachksum we don't invert so it's safe to skip check there + // if(check->n == 0x0000) + // check->n = 0xFFff; + } + + void + IPPacket::UpdateIPv4Address(nuint32_t nSrcIP, nuint32_t nDstIP) + { + llarp::LogDebug("set src=", nSrcIP, " dst=", nDstIP); - static void - deltaChecksumIPv4UDP( - byte_t* pld, - size_t psz, - size_t fragoff, - nuint32_t oSrcIP, - nuint32_t oDstIP, - nuint32_t nSrcIP, - nuint32_t nDstIP) - { - if (fragoff > 6 || psz < 6 + 2) - return; - - auto check = (nuint16_t*)(pld + 6); - if (check->n == 0x0000) - return; // 0 is used to indicate "no checksum", don't change - - *check = deltaIPv4Checksum(*check, oSrcIP, oDstIP, nSrcIP, nDstIP); - // 0 is used to indicate "no checksum" - // 0xFFff and 0 are equivalent in one's complement math - // 0xFFff + 1 = 0x10000 -> 0x0001 (same as 0 + 1) - // infact it's impossible to get 0 with such addition, - // when starting from non-0 value. - // inside deltachksum we don't invert so it's safe to skip check there - // if(check->n == 0x0000) - // check->n = 0xFFff; - } + auto hdr = Header(); - static void - deltaChecksumIPv6UDP( - byte_t* pld, - size_t psz, - size_t fragoff, - const uint32_t oSrcIP[4], - const uint32_t oDstIP[4], - const uint32_t nSrcIP[4], - const uint32_t nDstIP[4]) - { - if (fragoff > 6 || psz < 6 + 2) - return; - - auto check = (nuint16_t*)(pld + 6); - // 0 is used to indicate "no checksum", don't change - // even tho this shouldn't happen for IPv6, handle it properly - // we actually should drop/log 0-checksum packets per spec - // but that should be done at upper level than this function - // it's better to do correct thing there regardless - // XXX or maybe we should change this function to be able to return error? - // either way that's not a priority - if (check->n == 0x0000) - return; - - *check = deltaIPv6Checksum(*check, oSrcIP, oDstIP, nSrcIP, nDstIP); - // 0 is used to indicate "no checksum" - // 0xFFff and 0 are equivalent in one's complement math - // 0xFFff + 1 = 0x10000 -> 0x0001 (same as 0 + 1) - // infact it's impossible to get 0 with such addition, - // when starting from non-0 value. - // inside deltachksum we don't invert so it's safe to skip check there - // if(check->n == 0x0000) - // check->n = 0xFFff; - } + auto oSrcIP = nuint32_t{hdr->saddr}; + auto oDstIP = nuint32_t{hdr->daddr}; - void - IPPacket::UpdateIPv4Address(nuint32_t nSrcIP, nuint32_t nDstIP) + // L4 checksum + auto ihs = size_t(hdr->ihl * 4); + if (ihs <= sz) { - llarp::LogDebug("set src=", nSrcIP, " dst=", nDstIP); - - auto hdr = Header(); + auto pld = buf + ihs; + auto psz = sz - ihs; - auto oSrcIP = nuint32_t{hdr->saddr}; - auto oDstIP = nuint32_t{hdr->daddr}; + auto fragoff = size_t((ntohs(hdr->frag_off) & 0x1Fff) * 8); - // L4 checksum - auto ihs = size_t(hdr->ihl * 4); - if (ihs <= sz) + switch (hdr->protocol) { - auto pld = buf + ihs; - auto psz = sz - ihs; - - auto fragoff = size_t((ntohs(hdr->frag_off) & 0x1Fff) * 8); - - switch (hdr->protocol) - { - case 6: // TCP - deltaChecksumIPv4TCP(pld, psz, fragoff, 16, oSrcIP, oDstIP, nSrcIP, nDstIP); - break; - case 17: // UDP - case 136: // UDP-Lite - same checksum place, same 0->0xFFff condition - deltaChecksumIPv4UDP(pld, psz, fragoff, oSrcIP, oDstIP, nSrcIP, nDstIP); - break; - case 33: // DCCP - deltaChecksumIPv4TCP(pld, psz, fragoff, 6, oSrcIP, oDstIP, nSrcIP, nDstIP); - break; - } + case 6: // TCP + deltaChecksumIPv4TCP(pld, psz, fragoff, 16, oSrcIP, oDstIP, nSrcIP, nDstIP); + break; + case 17: // UDP + case 136: // UDP-Lite - same checksum place, same 0->0xFFff condition + deltaChecksumIPv4UDP(pld, psz, fragoff, oSrcIP, oDstIP, nSrcIP, nDstIP); + break; + case 33: // DCCP + deltaChecksumIPv4TCP(pld, psz, fragoff, 6, oSrcIP, oDstIP, nSrcIP, nDstIP); + break; } + } - // IPv4 checksum - auto v4chk = (nuint16_t*)&(hdr->check); - *v4chk = deltaIPv4Checksum(*v4chk, oSrcIP, oDstIP, nSrcIP, nDstIP); + // IPv4 checksum + auto v4chk = (nuint16_t*)&(hdr->check); + *v4chk = deltaIPv4Checksum(*v4chk, oSrcIP, oDstIP, nSrcIP, nDstIP); - // write new IP addresses - hdr->saddr = nSrcIP.n; - hdr->daddr = nDstIP.n; - } + // write new IP addresses + hdr->saddr = nSrcIP.n; + hdr->daddr = nDstIP.n; + } - void - IPPacket::UpdateIPv6Address(huint128_t src, huint128_t dst, std::optional flowlabel) - { - const size_t ihs = 4 + 4 + 16 + 16; + void + IPPacket::UpdateIPv6Address(huint128_t src, huint128_t dst, std::optional flowlabel) + { + const size_t ihs = 4 + 4 + 16 + 16; - // XXX should've been checked at upper level? - if (sz <= ihs) - return; + // XXX should've been checked at upper level? + if (sz <= ihs) + return; - auto hdr = HeaderV6(); - if (flowlabel.has_value()) - { - // set flow label if desired - hdr->FlowLabel(*flowlabel); - } + auto hdr = HeaderV6(); + if (flowlabel.has_value()) + { + // set flow label if desired + hdr->FlowLabel(*flowlabel); + } - const auto oldSrcIP = hdr->srcaddr; - const auto oldDstIP = hdr->dstaddr; - const uint32_t* oSrcIP = in6_uint32_ptr(oldSrcIP); - const uint32_t* oDstIP = in6_uint32_ptr(oldDstIP); + const auto oldSrcIP = hdr->srcaddr; + const auto oldDstIP = hdr->dstaddr; + const uint32_t* oSrcIP = in6_uint32_ptr(oldSrcIP); + const uint32_t* oDstIP = in6_uint32_ptr(oldDstIP); - // IPv6 address - hdr->srcaddr = HUIntToIn6(src); - hdr->dstaddr = HUIntToIn6(dst); - const uint32_t* nSrcIP = in6_uint32_ptr(hdr->srcaddr); - const uint32_t* nDstIP = in6_uint32_ptr(hdr->dstaddr); + // IPv6 address + hdr->srcaddr = HUIntToIn6(src); + hdr->dstaddr = HUIntToIn6(dst); + const uint32_t* nSrcIP = in6_uint32_ptr(hdr->srcaddr); + const uint32_t* nDstIP = in6_uint32_ptr(hdr->dstaddr); - // TODO IPv6 header options - auto pld = buf + ihs; - auto psz = sz - ihs; + // TODO IPv6 header options + auto pld = buf + ihs; + auto psz = sz - ihs; - size_t fragoff = 0; - auto nextproto = hdr->proto; - for (;;) + size_t fragoff = 0; + auto nextproto = hdr->proto; + for (;;) + { + switch (nextproto) { - switch (nextproto) + case 0: // Hop-by-Hop Options + case 43: // Routing Header + case 60: // Destination Options { - case 0: // Hop-by-Hop Options - case 43: // Routing Header - case 60: // Destination Options - { - nextproto = pld[0]; - auto addlen = (size_t(pld[1]) + 1) * 8; - if (psz < addlen) - return; - pld += addlen; - psz -= addlen; - break; - } - - case 44: // Fragment Header - /* - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Next Header | Reserved | Fragment Offset |Res|M| - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | Identification | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - nextproto = pld[0]; - fragoff = (uint16_t(pld[2]) << 8) | (uint16_t(pld[3]) & 0xFC); - if (psz < 8) - return; - pld += 8; - psz -= 8; - - // jump straight to payload processing - if (fragoff != 0) - goto endprotohdrs; - break; - - default: - goto endprotohdrs; + nextproto = pld[0]; + auto addlen = (size_t(pld[1]) + 1) * 8; + if (psz < addlen) + return; + pld += addlen; + psz -= addlen; + break; } - } - endprotohdrs: - switch (nextproto) - { - case 6: // TCP - deltaChecksumIPv6TCP(pld, psz, fragoff, 16, oSrcIP, oDstIP, nSrcIP, nDstIP); - break; - case 17: // UDP - case 136: // UDP-Lite - same checksum place, same 0->0xFFff condition - deltaChecksumIPv6UDP(pld, psz, fragoff, oSrcIP, oDstIP, nSrcIP, nDstIP); - break; - case 33: // DCCP - deltaChecksumIPv6TCP(pld, psz, fragoff, 6, oSrcIP, oDstIP, nSrcIP, nDstIP); + case 44: // Fragment Header + /* + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Next Header | Reserved | Fragment Offset |Res|M| + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Identification | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + nextproto = pld[0]; + fragoff = (uint16_t(pld[2]) << 8) | (uint16_t(pld[3]) & 0xFC); + if (psz < 8) + return; + pld += 8; + psz -= 8; + + // jump straight to payload processing + if (fragoff != 0) + goto endprotohdrs; break; + + default: + goto endprotohdrs; } } + endprotohdrs: - void - IPPacket::ZeroAddresses(std::optional flowlabel) + switch (nextproto) { - if (IsV4()) - { - UpdateIPv4Address({0}, {0}); - } - else if (IsV6()) - { - UpdateIPv6Address({0}, {0}, flowlabel); - } + case 6: // TCP + deltaChecksumIPv6TCP(pld, psz, fragoff, 16, oSrcIP, oDstIP, nSrcIP, nDstIP); + break; + case 17: // UDP + case 136: // UDP-Lite - same checksum place, same 0->0xFFff condition + deltaChecksumIPv6UDP(pld, psz, fragoff, oSrcIP, oDstIP, nSrcIP, nDstIP); + break; + case 33: // DCCP + deltaChecksumIPv6TCP(pld, psz, fragoff, 6, oSrcIP, oDstIP, nSrcIP, nDstIP); + break; } + } - void - IPPacket::ZeroSourceAddress(std::optional flowlabel) + void + IPPacket::ZeroAddresses(std::optional flowlabel) + { + if (IsV4()) { - if (IsV4()) - { - UpdateIPv4Address({0}, xhtonl(dstv4())); - } - else if (IsV6()) - { - UpdateIPv6Address({0}, dstv6(), flowlabel); - } + UpdateIPv4Address({0}, {0}); } - - std::optional - IPPacket::MakeICMPUnreachable() const + else if (IsV6()) { - if (IsV4()) - { - constexpr auto icmp_Header_size = 8; - constexpr auto ip_Header_size = 20; - net::IPPacket pkt{}; - auto* pkt_Header = pkt.Header(); - - pkt_Header->version = 4; - pkt_Header->ihl = 0x05; - pkt_Header->tos = 0; - pkt_Header->check = 0; - pkt_Header->tot_len = ntohs(icmp_Header_size + ip_Header_size); - pkt_Header->saddr = Header()->daddr; - pkt_Header->daddr = Header()->saddr; - pkt_Header->protocol = 1; // ICMP - pkt_Header->ttl = 1; - pkt_Header->frag_off = htons(0b0100000000000000); - // size pf ip header - const size_t l3_HeaderSize = Header()->ihl * 4; - // size of l4 packet to reflect back - const size_t l4_PacketSize = 8; - pkt_Header->tot_len += ntohs(l4_PacketSize + l3_HeaderSize); - - uint16_t* checksum; - uint8_t* itr = pkt.buf + (pkt_Header->ihl * 4); - uint8_t* icmp_begin = itr; // type 'destination unreachable' - *itr++ = 3; - // code 'Destination host unknown error' - *itr++ = 7; - // checksum + unused - oxenc::write_host_as_big(0, itr); - checksum = (uint16_t*)itr; - itr += 4; - // next hop mtu is ignored but let's put something here anyways just in case tm - oxenc::write_host_as_big(1500, itr); - itr += 2; - // copy ip header and first 8 bytes of datagram for icmp rject - std::copy_n(buf, l4_PacketSize + l3_HeaderSize, itr); - itr += l4_PacketSize + l3_HeaderSize; - // calculate checksum of ip header - pkt_Header->check = ipchksum(pkt.buf, pkt_Header->ihl * 4); - const auto icmp_size = std::distance(icmp_begin, itr); - // calculate icmp checksum - *checksum = ipchksum(icmp_begin, icmp_size); - pkt.sz = ntohs(pkt_Header->tot_len); - return pkt; - } - return std::nullopt; + UpdateIPv6Address({0}, {0}, flowlabel); } + } - std::optional> - IPPacket::L4Data() const + void + IPPacket::ZeroSourceAddress(std::optional flowlabel) + { + if (IsV4()) { - const auto* hdr = Header(); - size_t l4_HeaderSize = 0; - if (hdr->protocol == 0x11) - { - l4_HeaderSize = 8; - } - else - return std::nullopt; - - // check for invalid size - if (sz < (hdr->ihl * 4) + l4_HeaderSize) - return std::nullopt; + UpdateIPv4Address({0}, xhtonl(dstv4())); + } + else if (IsV6()) + { + UpdateIPv6Address({0}, dstv6(), flowlabel); + } + } - const uint8_t* ptr = buf + ((hdr->ihl * 4) + l4_HeaderSize); - return std::make_pair(reinterpret_cast(ptr), std::distance(ptr, buf + sz)); + std::optional + IPPacket::MakeICMPUnreachable() const + { + if (IsV4()) + { + constexpr auto icmp_Header_size = 8; + constexpr auto ip_Header_size = 20; + net::IPPacket pkt{}; + auto* pkt_Header = pkt.Header(); + + pkt_Header->version = 4; + pkt_Header->ihl = 0x05; + pkt_Header->tos = 0; + pkt_Header->check = 0; + pkt_Header->tot_len = ntohs(icmp_Header_size + ip_Header_size); + pkt_Header->saddr = Header()->daddr; + pkt_Header->daddr = Header()->saddr; + pkt_Header->protocol = 1; // ICMP + pkt_Header->ttl = 1; + pkt_Header->frag_off = htons(0b0100000000000000); + // size pf ip header + const size_t l3_HeaderSize = Header()->ihl * 4; + // size of l4 packet to reflect back + const size_t l4_PacketSize = 8; + pkt_Header->tot_len += ntohs(l4_PacketSize + l3_HeaderSize); + + uint16_t* checksum; + uint8_t* itr = pkt.buf + (pkt_Header->ihl * 4); + uint8_t* icmp_begin = itr; // type 'destination unreachable' + *itr++ = 3; + // code 'Destination host unknown error' + *itr++ = 7; + // checksum + unused + oxenc::write_host_as_big(0, itr); + checksum = (uint16_t*)itr; + itr += 4; + // next hop mtu is ignored but let's put something here anyways just in case tm + oxenc::write_host_as_big(1500, itr); + itr += 2; + // copy ip header and first 8 bytes of datagram for icmp rject + std::copy_n(buf, l4_PacketSize + l3_HeaderSize, itr); + itr += l4_PacketSize + l3_HeaderSize; + // calculate checksum of ip header + pkt_Header->check = ipchksum(pkt.buf, pkt_Header->ihl * 4); + const auto icmp_size = std::distance(icmp_begin, itr); + // calculate icmp checksum + *checksum = ipchksum(icmp_begin, icmp_size); + pkt.sz = ntohs(pkt_Header->tot_len); + return pkt; } + return std::nullopt; + } - IPPacket - IPPacket::UDP( - nuint32_t srcaddr, - nuint16_t srcport, - nuint32_t dstaddr, - nuint16_t dstport, - const llarp_buffer_t& buf) + std::optional> + IPPacket::L4Data() const + { + const auto* hdr = Header(); + size_t l4_HeaderSize = 0; + if (hdr->protocol == 0x11) { - net::IPPacket pkt; + l4_HeaderSize = 8; + } + else + return std::nullopt; - if (buf.sz + 28 > sizeof(pkt.buf)) - { - pkt.sz = 0; - return pkt; - } - auto* hdr = pkt.Header(); - pkt.buf[1] = 0; - hdr->version = 4; - hdr->ihl = 5; - hdr->tot_len = htons(buf.sz + 28); - hdr->protocol = 0x11; // udp - hdr->ttl = 64; - hdr->frag_off = htons(0b0100000000000000); - - hdr->saddr = srcaddr.n; - hdr->daddr = dstaddr.n; - - // make udp packet - uint8_t* ptr = pkt.buf + 20; - std::memcpy(ptr, &srcport.n, 2); - ptr += 2; - std::memcpy(ptr, &dstport.n, 2); - ptr += 2; - oxenc::write_host_as_big(static_cast(buf.sz + 8), ptr); - ptr += 2; - oxenc::write_host_as_big(uint16_t{0}, ptr); // checksum - ptr += 2; - std::copy_n(buf.base, buf.sz, ptr); - - hdr->check = 0; - hdr->check = net::ipchksum(pkt.buf, 20); - pkt.sz = 28 + buf.sz; + // check for invalid size + if (sz < (hdr->ihl * 4) + l4_HeaderSize) + return std::nullopt; + + const uint8_t* ptr = buf + ((hdr->ihl * 4) + l4_HeaderSize); + return std::make_pair(reinterpret_cast(ptr), std::distance(ptr, buf + sz)); + } + + IPPacket + IPPacket::UDP( + nuint32_t srcaddr, + nuint16_t srcport, + nuint32_t dstaddr, + nuint16_t dstport, + const llarp_buffer_t& buf) + { + net::IPPacket pkt; + + if (buf.sz + 28 > sizeof(pkt.buf)) + { + pkt.sz = 0; return pkt; } - - } // namespace net -} // namespace llarp + auto* hdr = pkt.Header(); + pkt.buf[1] = 0; + hdr->version = 4; + hdr->ihl = 5; + hdr->tot_len = htons(buf.sz + 28); + hdr->protocol = 0x11; // udp + hdr->ttl = 64; + hdr->frag_off = htons(0b0100000000000000); + + hdr->saddr = srcaddr.n; + hdr->daddr = dstaddr.n; + + // make udp packet + uint8_t* ptr = pkt.buf + 20; + std::memcpy(ptr, &srcport.n, 2); + ptr += 2; + std::memcpy(ptr, &dstport.n, 2); + ptr += 2; + oxenc::write_host_as_big(static_cast(buf.sz + 8), ptr); + ptr += 2; + oxenc::write_host_as_big(uint16_t{0}, ptr); // checksum + ptr += 2; + std::copy_n(buf.base, buf.sz, ptr); + + hdr->check = 0; + hdr->check = net::ipchksum(pkt.buf, 20); + pkt.sz = 28 + buf.sz; + return pkt; + } + +} // namespace llarp::net diff --git a/llarp/net/ip_packet.hpp b/llarp/net/ip_packet.hpp index b9e3c4ddd..f0cc63f94 100644 --- a/llarp/net/ip_packet.hpp +++ b/llarp/net/ip_packet.hpp @@ -4,324 +4,261 @@ #include "net.hpp" #include #include -#include // Guarantees __{LITTLE,BIG}_ENDIAN__ defines - -#ifndef _WIN32 -// unix, linux -#include // FreeBSD needs this for uchar for ip.h -#include -#include -// anything not win32 -struct ip_header -{ -#ifdef __LITTLE_ENDIAN__ - unsigned int ihl : 4; - unsigned int version : 4; -#else - unsigned int version : 4; - unsigned int ihl : 4; -#endif - -#if defined(__linux__) -#define ip_version version -#endif - uint8_t tos; - uint16_t tot_len; - uint16_t id; - uint16_t frag_off; - uint8_t ttl; - uint8_t protocol; - uint16_t check; - uint32_t saddr; - uint32_t daddr; -}; -#else -// windows nt -#include -typedef struct ip_hdr -{ - unsigned char ip_header_len : 4; // 4-bit header length (in 32-bit words) normally=5 - // (Means 20 Bytes may be 24 also) - unsigned char version : 4; // 4-bit IPv4 version - unsigned char ip_tos; // IP type of service - unsigned short ip_total_length; // Total length - unsigned short ip_id; // Unique identifier - - unsigned char ip_frag_offset : 5; // Fragment offset field - - unsigned char ip_more_fragment : 1; - unsigned char ip_dont_fragment : 1; - unsigned char ip_reserved_zero : 1; - - unsigned char ip_frag_offset1; // fragment offset - - unsigned char ip_ttl; // Time to live - unsigned char ip_protocol; // Protocol(TCP,UDP etc) - unsigned short ip_checksum; // IP checksum - unsigned int ip_srcaddr; // Source address - unsigned int ip_destaddr; // Source address -} IPV4_HDR; -#define ip_header IPV4_HDR -#define saddr ip_srcaddr -#define daddr ip_destaddr -#define check ip_checksum -#define ihl ip_header_len -#define protocol ip_protocol -#define frag_off ip_frag_offset -#define tos ip_tos -#define ttl ip_ttl -#define tot_len ip_total_length -#endif - -struct ipv6_header_preamble -{ - unsigned char version : 4; - unsigned char pad_small : 4; - uint8_t pad[3]; -}; - -struct ipv6_header -{ - union - { - ipv6_header_preamble preamble; - uint32_t flowlabel; - } preamble; - - uint16_t payload_len; - uint8_t proto; - uint8_t hoplimit; - in6_addr srcaddr; - in6_addr dstaddr; - - /// get 20 bit truncated flow label in network order - llarp::nuint32_t - FlowLabel() const; - - /// put 20 bit truncated flow label network order - void - FlowLabel(llarp::nuint32_t flowlabel); -}; - #include #include #include -namespace llarp +namespace llarp::net { - namespace net + template + struct ip_header_le { - /// "well known" ip protocols - /// TODO: extend this to non "well known values" - enum class IPProtocol : uint8_t - { - ICMP = 0x01, - IGMP = 0x02, - IPIP = 0x04, - TCP = 0x06, - UDP = 0x11, - GRE = 0x2F, - ICMP6 = 0x3A, - OSFP = 0x59, - PGM = 0x71, - }; - - /// get string representation of this protocol - /// throws std::invalid_argument if we don't know the name of this ip protocol - std::string - IPProtocolName(IPProtocol proto); + unsigned int ihl : 4; + unsigned int version : 4; + uint8_t tos; + uint16_t tot_len; + uint16_t id; + uint16_t frag_off; + uint8_t ttl; + uint8_t protocol; + uint16_t check; + uint32_t saddr; + uint32_t daddr; + }; + + template <> + struct ip_header_le + { + unsigned int version : 4; + unsigned int ihl : 4; + uint8_t tos; + uint16_t tot_len; + uint16_t id; + uint16_t frag_off; + uint8_t ttl; + uint8_t protocol; + uint16_t check; + uint32_t saddr; + uint32_t daddr; + }; + + using ip_header = ip_header_le; + + template + struct ipv6_header_preamble_le + { + unsigned char pad_small : 4; + unsigned char version : 4; + uint8_t pad[3]; + }; - /// parse a string to an ip protocol - /// throws std::invalid_argument if cannot be parsed - IPProtocol - ParseIPProtocol(std::string data); + template <> + struct ipv6_header_preamble_le + { + unsigned char version : 4; + unsigned char pad_small : 4; + uint8_t pad[3]; + }; - /// an Packet - struct IPPacket + struct ipv6_header + { + union { - static constexpr size_t MaxSize = 1500; - llarp_time_t timestamp; - size_t sz; - byte_t buf[MaxSize]; + ipv6_header_preamble_le preamble; + uint32_t flowlabel; + } preamble; + + uint16_t payload_len; + uint8_t proto; + uint8_t hoplimit; + in6_addr srcaddr; + in6_addr dstaddr; + llarp::nuint32_t + FlowLabel() const; + + /// put 20 bit truncated flow label network order + void + FlowLabel(llarp::nuint32_t label); + }; + + /// "well known" ip protocols + /// TODO: extend this to non "well known values" + enum class IPProtocol : uint8_t + { + ICMP = 0x01, + IGMP = 0x02, + IPIP = 0x04, + TCP = 0x06, + UDP = 0x11, + GRE = 0x2F, + ICMP6 = 0x3A, + OSFP = 0x59, + PGM = 0x71, + }; + + /// get string representation of this protocol + /// throws std::invalid_argument if we don't know the name of this ip protocol + std::string + IPProtocolName(IPProtocol proto); + + /// parse a string to an ip protocol + /// throws std::invalid_argument if cannot be parsed + IPProtocol + ParseIPProtocol(std::string data); + + /// an Packet + struct IPPacket + { + static constexpr size_t MaxSize = 1500; + llarp_time_t timestamp; + size_t sz; + byte_t buf[MaxSize]; - static IPPacket - UDP(nuint32_t srcaddr, - nuint16_t srcport, - nuint32_t dstaddr, - nuint16_t dstport, - const llarp_buffer_t& data); + static IPPacket + UDP(nuint32_t srcaddr, + nuint16_t srcport, + nuint32_t dstaddr, + nuint16_t dstport, + const llarp_buffer_t& data); - ManagedBuffer - Buffer(); + ManagedBuffer + Buffer(); - ManagedBuffer - ConstBuffer() const; + ManagedBuffer + ConstBuffer() const; - bool - Load(const llarp_buffer_t& buf); + bool + Load(const llarp_buffer_t& buf); - struct GetTime - { - llarp_time_t - operator()(const IPPacket& pkt) const - { - return pkt.timestamp; - } - }; - - struct PutTime - { - EventLoop_ptr loop; - PutTime(EventLoop_ptr evloop) : loop(std::move(evloop)) - {} - void - operator()(IPPacket& pkt) const - { - pkt.timestamp = loop->time_now(); - } - }; - - struct GetNow - { - EventLoop_ptr loop; - GetNow(EventLoop_ptr evloop) : loop(std::move(evloop)) - {} - llarp_time_t - operator()() const - { - return loop->time_now(); - } - }; - - struct CompareSize - { - bool - operator()(const IPPacket& left, const IPPacket& right) - { - return left.sz < right.sz; - } - }; - - struct CompareOrder - { - bool - operator()(const IPPacket& left, const IPPacket& right) - { - return left.timestamp < right.timestamp; - } - }; - - inline ip_header* - Header() + struct CompareSize + { + bool + operator()(const IPPacket& left, const IPPacket& right) { - return (ip_header*)&buf[0]; + return left.sz < right.sz; } + }; - inline const ip_header* - Header() const + struct CompareOrder + { + bool + operator()(const IPPacket& left, const IPPacket& right) { - return (ip_header*)&buf[0]; + return left.timestamp < right.timestamp; } + }; - inline ipv6_header* - HeaderV6() - { - return (ipv6_header*)&buf[0]; - } + inline ip_header* + Header() + { + return reinterpret_cast(&buf[0]); + } - inline const ipv6_header* - HeaderV6() const - { - return (ipv6_header*)&buf[0]; - } + inline const ip_header* + Header() const + { + return reinterpret_cast(&buf[0]); + } - inline int - Version() const - { - return Header()->version; - } + inline ipv6_header* + HeaderV6() + { + return reinterpret_cast(&buf[0]); + } - inline bool - IsV4() const - { - return Version() == 4; - } + inline const ipv6_header* + HeaderV6() const + { + return reinterpret_cast(&buf[0]); + } - inline bool - IsV6() const - { - return Version() == 6; - } + inline int + Version() const + { + return Header()->version; + } - inline service::ProtocolType - ServiceProtocol() const - { - if (IsV4()) - return service::ProtocolType::TrafficV4; - if (IsV6()) - return service::ProtocolType::TrafficV6; + inline bool + IsV4() const + { + return Version() == 4; + } - return service::ProtocolType::Control; - } + inline bool + IsV6() const + { + return Version() == 6; + } - huint128_t - srcv6() const; + inline service::ProtocolType + ServiceProtocol() const + { + if (IsV4()) + return service::ProtocolType::TrafficV4; + if (IsV6()) + return service::ProtocolType::TrafficV6; - huint128_t - dstv6() const; + return service::ProtocolType::Control; + } - huint32_t - srcv4() const; + huint128_t + srcv6() const; - huint32_t - dstv4() const; + huint128_t + dstv6() const; - huint128_t - src4to6() const; + huint32_t + srcv4() const; - huint128_t - dst4to6() const; + huint32_t + dstv4() const; - huint128_t - src4to6Lan() const; + huint128_t + src4to6() const; - huint128_t - dst4to6Lan() const; + huint128_t + dst4to6() const; - /// get destination port if applicable - std::optional - DstPort() const; + huint128_t + src4to6Lan() const; - /// get source port if applicable - std::optional - SrcPort() const; + huint128_t + dst4to6Lan() const; - /// get pointer and size of layer 4 data - std::optional> - L4Data() const; + /// get destination port if applicable + std::optional + DstPort() const; - void - UpdateIPv4Address(nuint32_t src, nuint32_t dst); + /// get source port if applicable + std::optional + SrcPort() const; - void - UpdateIPv6Address( - huint128_t src, huint128_t dst, std::optional flowlabel = std::nullopt); + /// get pointer and size of layer 4 data + std::optional> + L4Data() const; - /// set addresses to zero and recacluate checksums - void - ZeroAddresses(std::optional flowlabel = std::nullopt); + void + UpdateIPv4Address(nuint32_t src, nuint32_t dst); - /// zero out source address - void - ZeroSourceAddress(std::optional flowlabel = std::nullopt); + void + UpdateIPv6Address( + huint128_t src, huint128_t dst, std::optional flowlabel = std::nullopt); - /// make an icmp unreachable reply packet based of this ip packet - std::optional - MakeICMPUnreachable() const; - }; + /// set addresses to zero and recacluate checksums + void + ZeroAddresses(std::optional flowlabel = std::nullopt); + + /// zero out source address + void + ZeroSourceAddress(std::optional flowlabel = std::nullopt); + + /// make an icmp unreachable reply packet based of this ip packet + std::optional + MakeICMPUnreachable() const; + }; + + /// generate ip checksum + uint16_t + ipchksum(const byte_t* buf, size_t sz, uint32_t sum = 0); - /// generate ip checksum - uint16_t - ipchksum(const byte_t* buf, size_t sz, uint32_t sum = 0); - } // namespace net -} // namespace llarp +} // namespace llarp::net diff --git a/llarp/util/codel.hpp b/llarp/util/codel.hpp deleted file mode 100644 index a18be23c2..000000000 --- a/llarp/util/codel.hpp +++ /dev/null @@ -1,167 +0,0 @@ -#pragma once - -#include -#include "mem.hpp" -#include -#include "time.hpp" - -#include -#include -#include -#include -#include -#include - -namespace llarp -{ - namespace util - { - struct GetNowSyscall - { - llarp_time_t - operator()() const - { - return llarp::time_now_ms(); - } - }; - - template < - typename T, - typename GetTime, - typename PutTime, - typename Compare, - typename GetNow = GetNowSyscall, - typename Mutex_t = util::Mutex, - typename Lock_t = std::lock_guard, - size_t MaxSize = 1024> - struct CoDelQueue - { - CoDelQueue(std::string name, PutTime put, GetNow now) - : m_QueueIdx(0) - , m_name(std::move(name)) - , _putTime(std::move(put)) - , _getNow(std::move(now)) - {} - - size_t - Size() EXCLUDES(m_QueueMutex) - { - Lock_t lock(m_QueueMutex); - return m_QueueIdx; - } - - template - bool - EmplaceIf(std::function pred, Args&&... args) EXCLUDES(m_QueueMutex) - { - Lock_t lock(m_QueueMutex); - if (m_QueueIdx == MaxSize) - return false; - T* t = &m_Queue[m_QueueIdx]; - new (t) T(std::forward(args)...); - if (!pred(*t)) - { - t->~T(); - return false; - } - - _putTime(m_Queue[m_QueueIdx]); - if (firstPut == 0s) - firstPut = _getTime(m_Queue[m_QueueIdx]); - ++m_QueueIdx; - - return true; - } - - template - void - Emplace(Args&&... args) EXCLUDES(m_QueueMutex) - { - Lock_t lock(m_QueueMutex); - if (m_QueueIdx == MaxSize) - return; - T* t = &m_Queue[m_QueueIdx]; - new (t) T(std::forward(args)...); - _putTime(m_Queue[m_QueueIdx]); - if (firstPut == 0s) - firstPut = _getTime(m_Queue[m_QueueIdx]); - ++m_QueueIdx; - } - - template - void - Process(Visit v) - { - return Process(v, [](T&) -> bool { return false; }); - } - - template - void - Process(Visit visitor, Filter f) EXCLUDES(m_QueueMutex) - { - llarp_time_t lowest = std::numeric_limits::max(); - if (_getNow() < nextTickAt) - return; - // llarp::LogInfo("CoDelQueue::Process - start at ", start); - Lock_t lock(m_QueueMutex); - auto start = firstPut; - - if (m_QueueIdx == 1) - { - visitor(m_Queue[0]); - T* t = &m_Queue[0]; - t->~T(); - m_QueueIdx = 0; - firstPut = 0s; - return; - } - size_t idx = 0; - while (m_QueueIdx) - { - llarp::LogDebug(m_name, " - queue has ", m_QueueIdx); - T* item = &m_Queue[idx++]; - if (f(*item)) - break; - --m_QueueIdx; - const llarp_time_t dlt = start - _getTime(*item); - // llarp::LogInfo("CoDelQueue::Process - dlt ", dlt); - lowest = std::min(dlt, lowest); - if (m_QueueIdx == 0) - { - // llarp::LogInfo("CoDelQueue::Process - single item: lowest ", - // lowest, " dropMs: ", dropMs); - if (lowest > dropMs) - { - item->~T(); - nextTickInterval += initialIntervalMs / uint64_t(std::sqrt(++dropNum)); - firstPut = 0s; - nextTickAt = start + nextTickInterval; - return; - } - - nextTickInterval = initialIntervalMs; - dropNum = 0; - } - visitor(*item); - item->~T(); - } - firstPut = 0s; - nextTickAt = start + nextTickInterval; - } - - const llarp_time_t initialIntervalMs = 5ms; - const llarp_time_t dropMs = 100ms; - llarp_time_t firstPut = 0s; - size_t dropNum = 0; - llarp_time_t nextTickInterval = initialIntervalMs; - llarp_time_t nextTickAt = 0s; - Mutex_t m_QueueMutex; - size_t m_QueueIdx GUARDED_BY(m_QueueMutex); - std::array m_Queue GUARDED_BY(m_QueueMutex); - std::string m_name; - GetTime _getTime; - PutTime _putTime; - GetNow _getNow; - }; // namespace util - } // namespace util -} // namespace llarp From 7396eb7f3b6039ba1e4d2105b9ad6b333f0e9839 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 18 May 2022 17:40:55 -0400 Subject: [PATCH 160/202] bump oxenc submodule and force oxenc submodule on win32 --- contrib/windows.sh | 1 + external/oxen-encoding | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/contrib/windows.sh b/contrib/windows.sh index 5d6bf399c..32ec7aaac 100755 --- a/contrib/windows.sh +++ b/contrib/windows.sh @@ -23,6 +23,7 @@ cmake \ -DSTATIC_LINK=ON \ -DWITH_SYSTEMD=OFF \ -DFORCE_OXENMQ_SUBMODULE=ON \ + -DFORCE_OXENC_SUBMODULE=ON \ -DSUBMODULE_CHECK=OFF \ -DWITH_LTO=OFF \ -DCMAKE_BUILD_TYPE=Release \ diff --git a/external/oxen-encoding b/external/oxen-encoding index 02ded69fa..b48aef693 160000 --- a/external/oxen-encoding +++ b/external/oxen-encoding @@ -1 +1 @@ -Subproject commit 02ded69fa18640d1f2455f7a70c5d705d8895db3 +Subproject commit b48aef693b39a2408931c4ba25580648423c81a7 From f0867832e5fa1132582e2c1c9bf2e40293f05f80 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 18 May 2022 18:02:57 -0400 Subject: [PATCH 161/202] alignas --- llarp/net/ip_packet.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/llarp/net/ip_packet.hpp b/llarp/net/ip_packet.hpp index f0cc63f94..a9db47904 100644 --- a/llarp/net/ip_packet.hpp +++ b/llarp/net/ip_packet.hpp @@ -112,7 +112,8 @@ namespace llarp::net static constexpr size_t MaxSize = 1500; llarp_time_t timestamp; size_t sz; - byte_t buf[MaxSize]; + + alignas(ip_header) byte_t buf[MaxSize]; static IPPacket UDP(nuint32_t srcaddr, From 3fccb3ab0c12cbaa04ea289166895466e86dc77e Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 19 May 2022 09:41:43 -0400 Subject: [PATCH 162/202] fixup edge case on windows * add platform detection constexprs * add quark for platforms without native ipv6 like windows, exit mapping cannot work with ipv6 yet --- llarp/constants/platform.hpp | 65 ++++++++++++++++++++++++++++++++++++ llarp/rpc/rpc_server.cpp | 17 +++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 llarp/constants/platform.hpp diff --git a/llarp/constants/platform.hpp b/llarp/constants/platform.hpp new file mode 100644 index 000000000..eaaf14084 --- /dev/null +++ b/llarp/constants/platform.hpp @@ -0,0 +1,65 @@ +#pragma once + +/// namespace for platform feature detection constexprs +namespace llarp::platform +{ + /// are we linux ? + inline constexpr bool is_linux = +#ifdef __linux__ + true +#else + false +#endif + ; + + /// are we freebsd ? + inline constexpr bool is_freebsd = +#ifdef __FreeBSD__ + true +#else + false +#endif + ; + + /// are we windows ? + inline constexpr bool is_windows = +#ifdef _WIN32 + true +#else + false +#endif + ; + + /// are we an apple platform ? + inline constexpr bool is_apple = +#ifdef __apple__ + true +#else + false +#endif + ; + + /// are we an android platform ? + inline constexpr bool is_android = +#ifdef ANDROID + true +#else + false +#endif + ; + + /// are we an iphone ? + inline constexpr bool is_iphone = +#ifdef IOS + true +#else + false +#endif + ; + + /// are we a mobile phone ? + inline constexpr bool is_mobile = is_android or is_iphone; + + /// does this platform support native ipv6 ? + inline constexpr bool supports_ipv6 = not is_windows; +} // namespace llarp::platform diff --git a/llarp/rpc/rpc_server.cpp b/llarp/rpc/rpc_server.cpp index b85751a27..7bc2f3f42 100644 --- a/llarp/rpc/rpc_server.cpp +++ b/llarp/rpc/rpc_server.cpp @@ -1,5 +1,6 @@ #include "rpc_server.hpp" #include +#include #include #include #include @@ -432,13 +433,27 @@ namespace llarp::rpc const auto range_itr = obj.find("range"); if (range_itr == obj.end()) { - range.FromString("::/0"); + // platforms without ipv6 support will shit themselves + // here if we give them an exit mapping that is ipv6 + if constexpr (platform::supports_ipv6) + { + range.FromString("::/0"); + } + else + { + range.FromString("0.0.0.0/0"); + } } else if (not range.FromString(range_itr->get())) { reply(CreateJSONError("invalid ip range")); return; } + if (not platform::supports_ipv6 and not range.IsV4()) + { + reply(CreateJSONError("ipv6 ranges not supported on this platform")); + return; + } std::optional token; const auto token_itr = obj.find("token"); if (token_itr != obj.end()) From a61e9636b26544bf8d923e4c7728d27c4a97fc28 Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 19 May 2022 16:48:55 -0400 Subject: [PATCH 163/202] state machine fix for link layer if a pending inbound session did not complete a handshake after an unclean close from a previous session the remote udp endpoint would remain stuck mapped as authed and thus any further attempts from the remote would be silently dropped as it entered a stuck state in the state machine. this was happening as a small part of the state machine was hidden in the implementation details of iwp, but instead should be in the super type as it is logic exclusively outside the details which every dialect would have regardless of its details. this commit will unmap the udp endpoint every time it needs to in the link layer state machine, independat of the implementation details of the diact. --- llarp/iwp/linklayer.cpp | 15 --------------- llarp/iwp/linklayer.hpp | 7 ------- llarp/iwp/session.cpp | 3 +-- llarp/link/server.cpp | 12 +++++++++++- llarp/link/server.hpp | 7 +++++-- 5 files changed, 17 insertions(+), 27 deletions(-) diff --git a/llarp/iwp/linklayer.cpp b/llarp/iwp/linklayer.cpp index 7be2419fe..03e2259eb 100644 --- a/llarp/iwp/linklayer.cpp +++ b/llarp/iwp/linklayer.cpp @@ -83,21 +83,6 @@ namespace llarp::iwp } } - bool - LinkLayer::MapAddr(const RouterID& r, ILinkSession* s) - { - if (not ILinkLayer::MapAddr(r, s)) - return false; - m_AuthedAddrs.emplace(s->GetRemoteEndpoint(), r); - return true; - } - - void - LinkLayer::UnmapAddr(const SockAddr& addr) - { - m_AuthedAddrs.erase(addr); - } - std::shared_ptr LinkLayer::NewOutboundSession(const RouterContact& rc, const AddressInfo& ai) { diff --git a/llarp/iwp/linklayer.hpp b/llarp/iwp/linklayer.hpp index 52bfd34e6..6d58b046c 100644 --- a/llarp/iwp/linklayer.hpp +++ b/llarp/iwp/linklayer.hpp @@ -44,12 +44,6 @@ namespace llarp::iwp void RecvFrom(const SockAddr& from, ILinkSession::Packet_t pkt) override; - bool - MapAddr(const RouterID& pk, ILinkSession* s) override; - - void - UnmapAddr(const SockAddr& addr); - void WakeupPlaintext(); @@ -61,7 +55,6 @@ namespace llarp::iwp HandleWakeupPlaintext(); const std::shared_ptr m_Wakeup; - std::unordered_map m_AuthedAddrs; std::vector m_WakingUp; const bool m_Inbound; }; diff --git a/llarp/iwp/session.cpp b/llarp/iwp/session.cpp index 09ea9ec6b..e5ba94a5a 100644 --- a/llarp/iwp/session.cpp +++ b/llarp/iwp/session.cpp @@ -175,8 +175,7 @@ namespace llarp if (m_State == State::Closed) return; auto close_msg = CreatePacket(Command::eCLOS, 0, 16, 16); - if (m_State == State::Ready) - m_Parent->UnmapAddr(m_RemoteAddr); + m_Parent->UnmapAddr(m_RemoteAddr); m_State = State::Closed; if (m_SentClosed.test_and_set()) return; diff --git a/llarp/link/server.cpp b/llarp/link/server.cpp index ddc21da76..0abe80eaa 100644 --- a/llarp/link/server.cpp +++ b/llarp/link/server.cpp @@ -192,6 +192,7 @@ namespace llarp llarp::LogInfo("session to ", RouterID(itr->second->GetPubKey()), " timed out"); itr->second->Close(); closedSessions.emplace(itr->first); + UnmapAddr(itr->second->GetRemoteEndpoint()); itr = m_AuthedLinks.erase(itr); } } @@ -210,6 +211,7 @@ namespace llarp else { LogInfo("pending session at ", itr->first, " timed out"); + UnmapAddr(itr->second->GetRemoteEndpoint()); // defer call so we can acquire mutexes later closedPending.emplace_back(std::move(itr->second)); itr = m_Pending.erase(itr); @@ -234,6 +236,12 @@ namespace llarp } } + void + ILinkLayer::UnmapAddr(const SockAddr& addr) + { + m_AuthedAddrs.erase(addr); + } + bool ILinkLayer::MapAddr(const RouterID& pk, ILinkSession* s) { @@ -249,6 +257,7 @@ namespace llarp s->Close(); return false; } + m_AuthedAddrs.emplace(addr, pk); m_AuthedLinks.emplace(pk, itr->second); itr = m_Pending.erase(itr); m_Router->TriggerPump(); @@ -435,7 +444,8 @@ namespace llarp void ILinkLayer::SendTo_LL(const SockAddr& to, const llarp_buffer_t& pkt) { - m_udp->send(to, pkt); + if (not m_udp->send(to, pkt)) + LogError("could not send udp packet to ", to); } bool diff --git a/llarp/link/server.hpp b/llarp/link/server.hpp index a976dbbc2..8aa25928f 100644 --- a/llarp/link/server.hpp +++ b/llarp/link/server.hpp @@ -101,6 +101,9 @@ namespace llarp void ForEachSession(std::function visit) EXCLUDES(m_AuthedLinksMutex); + void + UnmapAddr(const SockAddr& addr); + void SendTo_LL(const SockAddr& to, const llarp_buffer_t& pkt); @@ -183,7 +186,7 @@ namespace llarp return false; } - virtual bool + bool MapAddr(const RouterID& pk, ILinkSession* s); void @@ -255,7 +258,7 @@ namespace llarp AuthedLinks m_AuthedLinks GUARDED_BY(m_AuthedLinksMutex); mutable DECLARE_LOCK(Mutex_t, m_PendingMutex, ACQUIRED_AFTER(m_AuthedLinksMutex)); Pending m_Pending GUARDED_BY(m_PendingMutex); - + std::unordered_map m_AuthedAddrs; std::unordered_map m_RecentlyClosed; private: From f16c2ecd43c591244c6f9ea66900b87e5dbb702e Mon Sep 17 00:00:00 2001 From: majestrate Date: Tue, 24 May 2022 12:56:37 -0400 Subject: [PATCH 164/202] use oxenc submodule in android --- contrib/android-configure.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/contrib/android-configure.sh b/contrib/android-configure.sh index c17929814..c800458cc 100755 --- a/contrib/android-configure.sh +++ b/contrib/android-configure.sh @@ -36,6 +36,7 @@ for abi in $build_abis; do -DSTATIC_LINK=ON \ -DWITH_SYSTEMD=OFF \ -DFORCE_OXENMQ_SUBMODULE=ON \ + -DFORCE_OXEN_SUBMODULE=ON \ -DSUBMODULE_CHECK=OFF \ -DWITH_LTO=OFF \ -DCMAKE_BUILD_TYPE=Release \ From 608dced827c92645ce74478378985400cb104a6f Mon Sep 17 00:00:00 2001 From: majestrate Date: Tue, 24 May 2022 12:56:55 -0400 Subject: [PATCH 165/202] typofix --- contrib/android-configure.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/android-configure.sh b/contrib/android-configure.sh index c800458cc..2130b07cf 100755 --- a/contrib/android-configure.sh +++ b/contrib/android-configure.sh @@ -36,7 +36,7 @@ for abi in $build_abis; do -DSTATIC_LINK=ON \ -DWITH_SYSTEMD=OFF \ -DFORCE_OXENMQ_SUBMODULE=ON \ - -DFORCE_OXEN_SUBMODULE=ON \ + -DFORCE_OXENC_SUBMODULE=ON \ -DSUBMODULE_CHECK=OFF \ -DWITH_LTO=OFF \ -DCMAKE_BUILD_TYPE=Release \ From 0d0295e2dc35e0fc3bd5f4154725badeafb1ce90 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 25 May 2022 16:42:10 -0400 Subject: [PATCH 166/202] remove unneeded submodule --- .gitmodules | 3 --- external/clang-format-hooks | 1 - 2 files changed, 4 deletions(-) delete mode 160000 external/clang-format-hooks diff --git a/.gitmodules b/.gitmodules index ddf50410d..60d5d9621 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,9 +17,6 @@ path = external/pybind11 url = https://github.com/pybind/pybind11 branch = stable -[submodule "external/clang-format-hooks"] - path = external/clang-format-hooks - url = https://github.com/barisione/clang-format-hooks/ [submodule "external/sqlite_orm"] path = external/sqlite_orm url = https://github.com/fnc12/sqlite_orm diff --git a/external/clang-format-hooks b/external/clang-format-hooks deleted file mode 160000 index ac35e705e..000000000 --- a/external/clang-format-hooks +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ac35e705ebf20187b7ff92c29d1411f6a4c8d522 From 0df26fe81cb6c08a1121b1a37703a61a02f7e770 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 2 May 2022 09:42:27 -0400 Subject: [PATCH 167/202] bump ci to make jammy debs --- .drone.jsonnet | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index ffc968653..1e2e3c59e 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -398,7 +398,7 @@ local docs_pipeline(name, image, extra_cmds=[], allow_fail=false) = { // Deb builds: deb_builder(docker_base + 'debian-sid-builder', 'sid', 'debian/sid'), deb_builder(docker_base + 'debian-bullseye-builder', 'bullseye', 'debian/bullseye'), - deb_builder(docker_base + 'ubuntu-impish-builder', 'impish', 'ubuntu/impish'), + deb_builder(docker_base + 'ubuntu-jammy-builder', 'jammy', 'ubuntu/jammy'), deb_builder(docker_base + 'ubuntu-focal-builder', 'focal', 'ubuntu/focal'), deb_builder(docker_base + 'debian-sid-builder', 'sid', 'debian/sid', arch='arm64'), From 994c6e1a2155f4e2d61de648b674309afecee994 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 3 May 2022 12:00:43 -0400 Subject: [PATCH 168/202] remove focal deb build from ci --- .drone.jsonnet | 1 - 1 file changed, 1 deletion(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index 1e2e3c59e..d7ead8a5c 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -399,7 +399,6 @@ local docs_pipeline(name, image, extra_cmds=[], allow_fail=false) = { deb_builder(docker_base + 'debian-sid-builder', 'sid', 'debian/sid'), deb_builder(docker_base + 'debian-bullseye-builder', 'bullseye', 'debian/bullseye'), deb_builder(docker_base + 'ubuntu-jammy-builder', 'jammy', 'ubuntu/jammy'), - deb_builder(docker_base + 'ubuntu-focal-builder', 'focal', 'ubuntu/focal'), deb_builder(docker_base + 'debian-sid-builder', 'sid', 'debian/sid', arch='arm64'), // Macos builds: From 706e34c08277aba1c8a440f85340ba0384b25cba Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 28 Apr 2022 18:28:55 -0400 Subject: [PATCH 169/202] connect to routers even if we are decomissioned --- llarp/router/router.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index d5a1994ad..f63160ee0 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -982,17 +982,17 @@ namespace llarp connectToNum = strictConnect; } - if (decom) + // complain about being deregistered + if (decom and now >= m_NextDecommissionWarn) { - // complain about being deregistered - if (now >= m_NextDecommissionWarn) - { - constexpr auto DecommissionWarnInterval = 30s; - LogError("We are running as a service node but we seem to be decommissioned"); - m_NextDecommissionWarn = now + DecommissionWarnInterval; - } + constexpr auto DecommissionWarnInterval = 30s; + LogError("We are running as a service node but we seem to be decommissioned"); + m_NextDecommissionWarn = now + DecommissionWarnInterval; } - else if (connected < connectToNum) + + // if we need more sessions to routers and we are not a service node kicked from the network + // we shall connect out to others + if (connected < connectToNum and not(isSvcNode and not SessionToRouterAllowed(pubkey()))) { size_t dlt = connectToNum - connected; LogDebug("connecting to ", dlt, " random routers to keep alive"); From 5f496259b7a36f9a8c194d493c3d78c1db8ac38c Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 3 May 2022 13:37:57 -0400 Subject: [PATCH 170/202] if we are decommissioned or deregistered, do not test other routers so we do not spam them. disambiguiate error message to distinguish between decomissioned and deregistered. --- llarp/router/router.cpp | 38 +++++++++++++++++++++++++++++++------- llarp/router/router.hpp | 8 ++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index f63160ee0..291ca7785 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -479,6 +479,25 @@ namespace llarp and _rcLookupHandler.IsGreylisted(pubkey()); } + bool + Router::LooksDeregistered() const + { + return IsServiceNode() and whitelistRouters and _rcLookupHandler.HaveReceivedWhitelist() + and not _rcLookupHandler.SessionIsAllowed(pubkey()); + } + + bool + Router::ShouldTestOtherRouters() const + { + if (not IsServiceNode()) + return false; + if (not whitelistRouters) + return true; + if (not _rcLookupHandler.HaveReceivedWhitelist()) + return false; + return _rcLookupHandler.PathIsAllowed(pubkey()); + } + bool Router::SessionToRouterAllowed(const RouterID& router) const { @@ -982,17 +1001,19 @@ namespace llarp connectToNum = strictConnect; } - // complain about being deregistered - if (decom and now >= m_NextDecommissionWarn) + if (auto dereg = LooksDeregistered(); (dereg or decom) and now >= m_NextDecommissionWarn) { + // complain about being deregistered constexpr auto DecommissionWarnInterval = 30s; - LogError("We are running as a service node but we seem to be decommissioned"); + LogError( + "We are running as a service node but we seem to be ", + dereg ? "deregistered" : "decommissioned"); m_NextDecommissionWarn = now + DecommissionWarnInterval; } // if we need more sessions to routers and we are not a service node kicked from the network // we shall connect out to others - if (connected < connectToNum and not(isSvcNode and not SessionToRouterAllowed(pubkey()))) + if (connected < connectToNum and not LooksDeregistered()) { size_t dlt = connectToNum - connected; LogDebug("connecting to ", dlt, " random routers to keep alive"); @@ -1013,7 +1034,8 @@ namespace llarp if (m_peerDb) { // TODO: throttle this? - // TODO: need to capture session stats when session terminates / is removed from link manager + // TODO: need to capture session stats when session terminates / is removed from link + // manager _linkManager.updatePeerDb(m_peerDb); if (m_peerDb->shouldFlush(now)) @@ -1293,8 +1315,10 @@ namespace llarp // dont run tests if we are not running or we are stopping if (not _running) return; - // dont run tests if we are decommissioned - if (LooksDecommissioned()) + // dont run tests if we think we should not test other routers + // this occurs when we are decomissions, deregistered or do not have the service node list + // yet when we expect to have one. + if (not ShouldTestOtherRouters()) return; auto tests = m_routerTesting.get_failing(); if (auto maybe = m_routerTesting.next_random(this)) diff --git a/llarp/router/router.hpp b/llarp/router/router.hpp index 0e56e4920..afe9ec163 100644 --- a/llarp/router/router.hpp +++ b/llarp/router/router.hpp @@ -197,6 +197,14 @@ namespace llarp bool LooksDecommissioned() const; + /// return true if we look like we are a deregistered service node + bool + LooksDeregistered() const; + + /// return true if we look like we are allowed and able to test other routers + bool + ShouldTestOtherRouters() const; + std::optional _ourAddress; EventLoop_ptr _loop; From 3c44a0640396ce16cda23a3d7b42a1067e6070a1 Mon Sep 17 00:00:00 2001 From: Jeff Date: Tue, 3 May 2022 16:05:22 -0400 Subject: [PATCH 171/202] publish our rc out to the network when we regenerate them --- llarp/router/i_gossiper.hpp | 4 ++++ llarp/router/rc_gossiper.cpp | 12 +++++++++--- llarp/router/rc_gossiper.hpp | 3 +++ llarp/router/router.cpp | 23 ++++++++++++++++------- llarp/util/decaying_hashset.hpp | 6 ++++++ 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/llarp/router/i_gossiper.hpp b/llarp/router/i_gossiper.hpp index 86954930f..c22431578 100644 --- a/llarp/router/i_gossiper.hpp +++ b/llarp/router/i_gossiper.hpp @@ -27,5 +27,9 @@ namespace llarp /// return true if that rc is owned by us virtual bool IsOurRC(const RouterContact& rc) const = 0; + + /// forget the replay filter entry given pubkey + virtual void + Forget(const RouterID& router) = 0; }; } // namespace llarp diff --git a/llarp/router/rc_gossiper.cpp b/llarp/router/rc_gossiper.cpp index 70493523b..a88089bb5 100644 --- a/llarp/router/rc_gossiper.cpp +++ b/llarp/router/rc_gossiper.cpp @@ -27,9 +27,7 @@ namespace llarp bool RCGossiper::ShouldGossipOurRC(Time_t now) const { - bool should = now >= (m_LastGossipedOurRC + GossipOurRCInterval); - LogWarn("ShouldGossipOurRC: ", should); - return should; + return now >= (m_LastGossipedOurRC + GossipOurRCInterval); } bool @@ -44,6 +42,14 @@ namespace llarp m_Filter.Decay(now); } + void + RCGossiper::Forget(const RouterID& pk) + { + m_Filter.Remove(pk); + if (m_OurRouterID == pk) + m_LastGossipedOurRC = 0s; + } + bool RCGossiper::GossipRC(const RouterContact& rc) { diff --git a/llarp/router/rc_gossiper.hpp b/llarp/router/rc_gossiper.hpp index 7b836b218..e3fb70d5b 100644 --- a/llarp/router/rc_gossiper.hpp +++ b/llarp/router/rc_gossiper.hpp @@ -29,6 +29,9 @@ namespace llarp void Init(ILinkManager*, const RouterID&, AbstractRouter*); + void + Forget(const RouterID& router) override; + private: RouterID m_OurRouterID; Time_t m_LastGossipedOurRC = 0s; diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index 291ca7785..3e6c62f1e 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -495,7 +495,7 @@ namespace llarp return true; if (not _rcLookupHandler.HaveReceivedWhitelist()) return false; - return _rcLookupHandler.PathIsAllowed(pubkey()); + return _rcLookupHandler.SessionIsAllowed(pubkey()); } bool @@ -908,15 +908,24 @@ namespace llarp const bool gotWhitelist = _rcLookupHandler.HaveReceivedWhitelist(); const bool isSvcNode = IsServiceNode(); const bool decom = LooksDecommissioned(); + bool shouldGossip = isSvcNode and whitelistRouters and gotWhitelist + and _rcLookupHandler.SessionIsAllowed(pubkey()); - if (_rc.ExpiresSoon(now, std::chrono::milliseconds(randint() % 10000)) - || (now - _rc.last_updated) > rcRegenInterval) + if (isSvcNode + and (_rc.ExpiresSoon(now, std::chrono::milliseconds(randint() % 10000)) or (now - _rc.last_updated) > rcRegenInterval)) { LogInfo("regenerating RC"); - if (!UpdateOurRC(false)) - LogError("Failed to update our RC"); + if (UpdateOurRC()) + { + // our rc changed so we should gossip it + shouldGossip = true; + // remove our replay entry so it goes out + _rcGossiper.Forget(pubkey()); + } + else + LogError("failed to update our RC"); } - else if (whitelistRouters and gotWhitelist and _rcLookupHandler.SessionIsAllowed(pubkey())) + if (shouldGossip) { // if we have the whitelist enabled, we have fetched the list and we are in either // the white or grey list, we want to gossip our RC @@ -1316,7 +1325,7 @@ namespace llarp if (not _running) return; // dont run tests if we think we should not test other routers - // this occurs when we are decomissions, deregistered or do not have the service node list + // this occurs when we are deregistered or do not have the service node list // yet when we expect to have one. if (not ShouldTestOtherRouters()) return; diff --git a/llarp/util/decaying_hashset.hpp b/llarp/util/decaying_hashset.hpp index 9618f4089..e7c0a0a72 100644 --- a/llarp/util/decaying_hashset.hpp +++ b/llarp/util/decaying_hashset.hpp @@ -72,6 +72,12 @@ namespace llarp m_CacheInterval = interval; } + void + Remove(const Val_t& val) + { + m_Values.erase(val); + } + private: template void From 18e1272c76dfba399109d0792d63d55cf17af810 Mon Sep 17 00:00:00 2001 From: Jeff Date: Wed, 4 May 2022 10:37:35 -0400 Subject: [PATCH 172/202] add gossip info to systemd status * adds next and last gossip datetimes * adds a few things for time points, like ostream operator overloads for time point --- llarp/router/i_gossiper.hpp | 12 +++++++++++- llarp/router/rc_gossiper.cpp | 16 ++++++++++++++++ llarp/router/rc_gossiper.hpp | 6 ++++++ llarp/router/router.cpp | 11 ++++++++++- llarp/util/time.cpp | 29 ++++++++++++++++++++--------- llarp/util/time.hpp | 3 +++ llarp/util/types.hpp | 3 +++ 7 files changed, 69 insertions(+), 11 deletions(-) diff --git a/llarp/router/i_gossiper.hpp b/llarp/router/i_gossiper.hpp index c22431578..c0d6fd51f 100644 --- a/llarp/router/i_gossiper.hpp +++ b/llarp/router/i_gossiper.hpp @@ -1,5 +1,7 @@ #pragma once #include +#include +#include namespace llarp { @@ -15,7 +17,7 @@ namespace llarp virtual bool GossipRC(const RouterContact& rc) = 0; - using Time_t = std::chrono::milliseconds; + using Time_t = Duration_t; virtual void Decay(Time_t now) = 0; @@ -31,5 +33,13 @@ namespace llarp /// forget the replay filter entry given pubkey virtual void Forget(const RouterID& router) = 0; + + /// returns the time point when we will send our next gossip at + virtual TimePoint_t + NextGossipAt() const = 0; + + /// returns the time point when we sent our last gossip at or nullopt if we never did + virtual std::optional + LastGossipAt() const = 0; }; } // namespace llarp diff --git a/llarp/router/rc_gossiper.cpp b/llarp/router/rc_gossiper.cpp index a88089bb5..bd9e99b7b 100644 --- a/llarp/router/rc_gossiper.cpp +++ b/llarp/router/rc_gossiper.cpp @@ -50,6 +50,22 @@ namespace llarp m_LastGossipedOurRC = 0s; } + TimePoint_t + RCGossiper::NextGossipAt() const + { + if (auto maybe = LastGossipAt()) + return *maybe + GossipOurRCInterval; + return DateClock_t::now(); + } + + std::optional + RCGossiper::LastGossipAt() const + { + if (m_LastGossipedOurRC == 0s) + return std::nullopt; + return DateClock_t::time_point{m_LastGossipedOurRC}; + } + bool RCGossiper::GossipRC(const RouterContact& rc) { diff --git a/llarp/router/rc_gossiper.hpp b/llarp/router/rc_gossiper.hpp index e3fb70d5b..948abad32 100644 --- a/llarp/router/rc_gossiper.hpp +++ b/llarp/router/rc_gossiper.hpp @@ -32,6 +32,12 @@ namespace llarp void Forget(const RouterID& router) override; + TimePoint_t + NextGossipAt() const override; + + std::optional + LastGossipAt() const override; + private: RouterID m_OurRouterID; Time_t m_LastGossipedOurRC = 0s; diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index 3e6c62f1e..43de835e5 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -869,7 +869,16 @@ namespace llarp ss << " snode | known/svc/clients: " << nodedb()->NumLoaded() << "/" << NumberOfConnectedRouters() << "/" << NumberOfConnectedClients() << " | " << pathContext().CurrentTransitPaths() << " active paths | " - << "block " << (m_lokidRpcClient ? m_lokidRpcClient->BlockHeight() : 0); + << "block " << (m_lokidRpcClient ? m_lokidRpcClient->BlockHeight() : 0) << " | gossip: " + << "(next/last) " << _rcGossiper.NextGossipAt() << "/"; + if (auto maybe = _rcGossiper.LastGossipAt()) + { + ss << *maybe; + } + else + { + ss << "never"; + } } else { diff --git a/llarp/util/time.cpp b/llarp/util/time.cpp index 4c9cde1f8..cd6a3b4fd 100644 --- a/llarp/util/time.cpp +++ b/llarp/util/time.cpp @@ -1,20 +1,24 @@ #include "time.hpp" #include +#include namespace llarp { - using Clock_t = std::chrono::system_clock; - - template - static Duration_t - time_since_epoch(std::chrono::time_point point) + namespace { - return std::chrono::duration_cast(point.time_since_epoch()); - } + using Clock_t = std::chrono::system_clock; + + template + static Duration_t + time_since_epoch(std::chrono::time_point point) + { + return std::chrono::duration_cast(point.time_since_epoch()); + } - const static auto started_at_system = Clock_t::now(); + const static auto started_at_system = Clock_t::now(); - const static auto started_at_steady = std::chrono::steady_clock::now(); + const static auto started_at_steady = std::chrono::steady_clock::now(); + } // namespace uint64_t ToMS(Duration_t ms) @@ -74,4 +78,11 @@ namespace llarp out.fill(old_fill); return out << "s"; } + + std::ostream& + operator<<(std::ostream& out, const TimePoint_t& tp) + { + auto t = TimePoint_t::clock::to_time_t(tp); + return out << std::put_time(std::localtime(&t), "%c %Z"); + } } // namespace llarp diff --git a/llarp/util/time.hpp b/llarp/util/time.hpp index d5f558f8c..0b3c20d3d 100644 --- a/llarp/util/time.hpp +++ b/llarp/util/time.hpp @@ -26,4 +26,7 @@ namespace llarp nlohmann::json to_json(const Duration_t& t); + std::ostream& + operator<<(std::ostream& out, const TimePoint_t& t); + } // namespace llarp diff --git a/llarp/util/types.hpp b/llarp/util/types.hpp index cd86b4d04..be6a0a286 100644 --- a/llarp/util/types.hpp +++ b/llarp/util/types.hpp @@ -14,6 +14,9 @@ namespace llarp /// convert to milliseconds uint64_t ToMS(Duration_t duration); + + using DateClock_t = std::chrono::system_clock; + using TimePoint_t = DateClock_t::time_point; } // namespace llarp using llarp_time_t = llarp::Duration_t; From 60ada470dbc52fd701ee4d70b8d2667e5c196b1c Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 20 May 2022 11:11:34 -0400 Subject: [PATCH 173/202] format systemd status as time deltas from now --- llarp/router/router.cpp | 5 +++-- llarp/util/time.hpp | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index 43de835e5..5c04b6799 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -870,10 +870,11 @@ namespace llarp << NumberOfConnectedRouters() << "/" << NumberOfConnectedClients() << " | " << pathContext().CurrentTransitPaths() << " active paths | " << "block " << (m_lokidRpcClient ? m_lokidRpcClient->BlockHeight() : 0) << " | gossip: " - << "(next/last) " << _rcGossiper.NextGossipAt() << "/"; + << "(next/last) " << time_delta{_rcGossiper.NextGossipAt()} + << " / "; if (auto maybe = _rcGossiper.LastGossipAt()) { - ss << *maybe; + ss << time_delta{*maybe}; } else { diff --git a/llarp/util/time.hpp b/llarp/util/time.hpp index 0b3c20d3d..abd725f05 100644 --- a/llarp/util/time.hpp +++ b/llarp/util/time.hpp @@ -29,4 +29,33 @@ namespace llarp std::ostream& operator<<(std::ostream& out, const TimePoint_t& t); + template + struct time_delta + { + const TimePoint_t at; + + std::ostream& + operator()(std::ostream& out) const + { + const auto dlt = std::chrono::duration_cast(TimePoint_t::clock::now() - at); + if (dlt > 0s) + return out << std::chrono::duration_cast(dlt) << " ago "; + else if (dlt < 0s) + return out << "in " << std::chrono::duration_cast(-dlt); + else + return out << "now"; + } + }; + + inline std::ostream& + operator<<(std::ostream& out, const time_delta& td) + { + return td(out); + } + + inline std::ostream& + operator<<(std::ostream& out, const time_delta& td) + { + return td(out); + } } // namespace llarp From 98b3860655a9ac06cebfcd90d2a1528d2dba3178 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 20 May 2022 13:10:04 -0400 Subject: [PATCH 174/202] set source ip on service nodes for outbound link to not use all interfaces --- llarp/config/config.cpp | 38 ++++++---------- llarp/config/config.hpp | 3 +- llarp/link/server.cpp | 43 +++++++++++++++--- llarp/link/server.hpp | 2 +- llarp/net/address_info.cpp | 6 +++ llarp/net/address_info.hpp | 6 +++ llarp/net/net.cpp | 80 +++++++++++++++++++++++++-------- llarp/net/net.hpp | 31 ++++++++++++- llarp/net/sock_addr.cpp | 8 ++++ llarp/net/sock_addr.hpp | 11 +++++ llarp/router/abstractrouter.hpp | 4 ++ llarp/router/router.cpp | 42 ++++++++++++----- llarp/router/router.hpp | 3 ++ pybind/llarp/config.cpp | 5 --- 14 files changed, 214 insertions(+), 68 deletions(-) diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp index 5e6a53115..6bff96112 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp @@ -126,32 +126,23 @@ namespace llarp "provided the public-port option must also be specified.", }, [this](std::string arg) { - if (not arg.empty()) - { - llarp::LogInfo("public ip ", arg, " size ", arg.size()); + if (arg.empty()) + return; + nuint32_t addr{}; + if (not addr.FromString(arg)) + throw std::invalid_argument{stringify(arg, " is not a valid IPv4 address")}; - if (arg.size() > 15) - throw std::invalid_argument(stringify("Not a valid IPv4 addr: ", arg)); + if (IsIPv4Bogon(addr)) + throw std::invalid_argument{ + stringify(addr, " looks like it is not a publicly routable ip address")}; - m_publicAddress.setAddress(arg); - } + m_PublicIP = addr; }); - conf.defineOption("router", "public-address", Hidden, [this](std::string arg) { - if (not arg.empty()) - { - llarp::LogWarn( - "*** WARNING: The config option [router]:public-address=", - arg, - " is deprecated, use public-ip=", - arg, - " instead to avoid this warning and avoid future configuration problems."); - - if (arg.size() > 15) - throw std::invalid_argument(stringify("Not a valid IPv4 addr: ", arg)); - - m_publicAddress.setAddress(arg); - } + conf.defineOption("router", "public-address", Hidden, [](std::string) { + throw std::invalid_argument{ + "[router]:public-address option no longer supported, use [router]:public-ip and " + "[router]:public-port instead"}; }); conf.defineOption( @@ -166,8 +157,7 @@ namespace llarp [this](int arg) { if (arg <= 0 || arg > std::numeric_limits::max()) throw std::invalid_argument("public-port must be >= 0 and <= 65536"); - - m_publicAddress.setPort(arg); + m_PublicPort = ToNet(huint16_t{static_cast(arg)}); }); conf.defineOption( diff --git a/llarp/config/config.hpp b/llarp/config/config.hpp index 09eb1febb..975a8ac99 100644 --- a/llarp/config/config.hpp +++ b/llarp/config/config.hpp @@ -54,7 +54,8 @@ namespace llarp bool m_blockBogons = false; - IpAddress m_publicAddress; + std::optional m_PublicIP; + nuint16_t m_PublicPort; int m_workerThreads = -1; int m_numNetThreads = -1; diff --git a/llarp/link/server.cpp b/llarp/link/server.cpp index 0abe80eaa..44ac8b4a3 100644 --- a/llarp/link/server.cpp +++ b/llarp/link/server.cpp @@ -8,6 +8,7 @@ #include #include #include +#include static constexpr auto LINK_LAYER_TICK_INTERVAL = 100ms; @@ -129,7 +130,7 @@ namespace llarp } bool - ILinkLayer::Configure(AbstractRouter* router, const std::string& ifname, int af, uint16_t port) + ILinkLayer::Configure(AbstractRouter* router, std::string ifname, int af, uint16_t port) { m_Router = router; m_udp = m_Router->loop()->make_udp( @@ -142,11 +143,42 @@ namespace llarp if (ifname == "*") { - if (!AllInterfaces(af, m_ourAddr)) + if (router->IsServiceNode()) + { + if (auto maybe = router->OurPublicIP()) + { + auto addr = var::visit([](auto&& addr) { return SockAddr{addr}; }, *maybe); + // service node outbound link + if (HasInterfaceAddress(addr.getIP())) + { + // we have our ip claimed on a local net interface + m_ourAddr = addr; + } + else if (auto maybe = net::AllInterfaces(addr)) + { + // we do not have our claimed ip, nat or something? + m_ourAddr = *maybe; + } + else + return false; // the ultimate failure case + } + else + return false; + } + else if (auto maybe = net::AllInterfaces(SockAddr{"0.0.0.0"})) + { + // client outbound link + m_ourAddr = *maybe; + } + else return false; } else { + if (ifname == "0.0.0.0" and not GetBestNetIF(ifname)) + throw std::invalid_argument{ + "0.0.0.0 provided and we cannot find a valid ip to use, please set one " + "explicitly instead in the bind section instead of 0.0.0.0"}; if (const auto maybe = GetInterfaceAddr(ifname, af)) { m_ourAddr = *maybe; @@ -157,10 +189,11 @@ namespace llarp { m_ourAddr = SockAddr{ifname + ":0"}; } - catch (const std::exception& e) + catch (const std::exception& ex) { - LogError(stringify("Could not use ifname ", ifname, " to configure ILinkLayer")); - throw e; + LogError( + stringify("Could not use ifname ", ifname, " to configure ILinkLayer: ", ex.what())); + throw ex; } } } diff --git a/llarp/link/server.hpp b/llarp/link/server.hpp index 8aa25928f..02cc17f83 100644 --- a/llarp/link/server.hpp +++ b/llarp/link/server.hpp @@ -108,7 +108,7 @@ namespace llarp SendTo_LL(const SockAddr& to, const llarp_buffer_t& pkt); virtual bool - Configure(AbstractRouter* loop, const std::string& ifname, int af, uint16_t port); + Configure(AbstractRouter* loop, std::string ifname, int af, uint16_t port); virtual std::shared_ptr NewOutboundSession(const RouterContact& rc, const AddressInfo& ai) = 0; diff --git a/llarp/net/address_info.cpp b/llarp/net/address_info.cpp index c4aad84fe..8daf8f8de 100644 --- a/llarp/net/address_info.cpp +++ b/llarp/net/address_info.cpp @@ -27,6 +27,12 @@ namespace llarp return lhs.rank < rhs.rank || lhs.ip < rhs.ip || lhs.port < rhs.port; } + std::variant + AddressInfo::IP() const + { + return SockAddr{ip}.getIP(); + } + bool AddressInfo::DecodeKey(const llarp_buffer_t& key, llarp_buffer_t* buf) { diff --git a/llarp/net/address_info.hpp b/llarp/net/address_info.hpp index 5851ad07c..a3bcd02e7 100644 --- a/llarp/net/address_info.hpp +++ b/llarp/net/address_info.hpp @@ -9,6 +9,8 @@ #include #include +#include + /** * address_info.hpp * @@ -47,6 +49,10 @@ namespace llarp void fromSockAddr(const SockAddr& address); + /// get this as an explicit v4 or explicit v6 + std::variant + IP() const; + std::ostream& print(std::ostream& stream, int level, int spaces) const; }; diff --git a/llarp/net/net.cpp b/llarp/net/net.cpp index fe2b883c1..83e62cff5 100644 --- a/llarp/net/net.cpp +++ b/llarp/net/net.cpp @@ -576,29 +576,56 @@ namespace llarp return addr.asIPv6(); } - bool - AllInterfaces(int af, SockAddr& result) + namespace net { - if (af == AF_INET) + namespace { - sockaddr_in addr; - addr.sin_family = AF_INET; - addr.sin_addr.s_addr = htonl(INADDR_ANY); - addr.sin_port = htons(0); - result = SockAddr{addr}; - return true; - } - if (af == AF_INET6) + SockAddr + All(int af) + { + if (af == AF_INET) + { + sockaddr_in addr{}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(0); + return SockAddr{addr}; + } + sockaddr_in6 addr6{}; + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons(0); + addr6.sin6_addr = IN6ADDR_ANY_INIT; + return SockAddr{addr6}; + } + } // namespace + + std::optional + AllInterfaces(SockAddr pub) { - sockaddr_in6 addr6; - addr6.sin6_family = AF_INET6; - addr6.sin6_port = htons(0); - addr6.sin6_addr = IN6ADDR_ANY_INIT; - result = SockAddr{addr6}; - return true; + std::optional found; + IterAllNetworkInterfaces([pub, &found](auto* ifa) { + if (found) + return; + if (auto ifa_addr = ifa->ifa_addr) + { + if (ifa_addr->sa_family != pub.Family()) + return; + + SockAddr addr{*ifa->ifa_addr}; + + if (addr == pub) + found = addr; + } + }); + + // 0.0.0.0 is used in our compat shim as our public ip so we check for that special case + const auto zero = IPRange::FromIPv4(0, 0, 0, 0, 8); + // when we cannot find an address but we are looking for 0.0.0.0 just default to the old style + if (not found and (pub.isIPv4() and zero.Contains(pub.asIPv4()))) + found = All(pub.Family()); + return found; } - return false; - } + } // namespace net #if !defined(TESTNET) static constexpr std::array bogonRanges_v6 = { @@ -692,5 +719,20 @@ namespace llarp return false; } #endif + bool + HasInterfaceAddress(std::variant ip) + { + bool found{false}; + IterAllNetworkInterfaces([ip, &found](const auto* iface) { + if (found or iface == nullptr) + return; + if (auto addr = iface->ifa_addr; + addr and (addr->sa_family == AF_INET or addr->sa_family == AF_INET6)) + { + found = SockAddr{*iface->ifa_addr}.getIP() == ip; + } + }); + return found; + } } // namespace llarp diff --git a/llarp/net/net.hpp b/llarp/net/net.hpp index 95141f3a9..7c40617bc 100644 --- a/llarp/net/net.hpp +++ b/llarp/net/net.hpp @@ -52,6 +52,12 @@ namespace llarp bool IsIPv4Bogon(const huint32_t& addr); + inline bool + IsIPv4Bogon(const nuint32_t& addr) + { + return IsIPv4Bogon(ToHost(addr)); + } + bool IsBogon(const in6_addr& addr); @@ -61,8 +67,25 @@ namespace llarp bool IsBogonRange(const in6_addr& host, const in6_addr& mask); - bool - AllInterfaces(int af, SockAddr& addr); + /// get a sock addr we can use for all interfaces given our public address + namespace net + { + std::optional + AllInterfaces(SockAddr pubaddr); + } + + /// compat shim + // TODO: remove me + inline bool + AllInterfaces(int af, SockAddr& addr) + { + if (auto maybe = net::AllInterfaces(SockAddr{af == AF_INET ? "0.0.0.0" : "::"})) + { + addr = *maybe; + return true; + } + return false; + } /// get first network interface with public address bool @@ -92,4 +115,8 @@ namespace llarp } #endif + /// return true if we have a network interface with this ip + bool + HasInterfaceAddress(std::variant ip); + } // namespace llarp diff --git a/llarp/net/sock_addr.cpp b/llarp/net/sock_addr.cpp index a09a88527..dbb1734a2 100644 --- a/llarp/net/sock_addr.cpp +++ b/llarp/net/sock_addr.cpp @@ -351,6 +351,14 @@ namespace llarp return a; } + std::variant + SockAddr::getIP() const + { + if (isIPv4()) + return getIPv4(); + return getIPv6(); + } + void SockAddr::setIPv4(nuint32_t ip) { diff --git a/llarp/net/sock_addr.hpp b/llarp/net/sock_addr.hpp index 3c25e8914..a51379b13 100644 --- a/llarp/net/sock_addr.hpp +++ b/llarp/net/sock_addr.hpp @@ -12,6 +12,7 @@ #include #include #include "net_int.hpp" +#include namespace llarp { @@ -81,6 +82,14 @@ namespace llarp std::string hostString() const; + inline int + Family() const + { + if (isIPv6()) + return AF_INET6; + return AF_INET; + } + /// Returns true if this is an empty SockAddr, defined by having no IP address set. An empty IP /// address with a valid port is still considered empty. /// @@ -133,6 +142,8 @@ namespace llarp getIPv6() const; nuint32_t getIPv4() const; + std::variant + getIP() const; /// in host order huint128_t diff --git a/llarp/router/abstractrouter.hpp b/llarp/router/abstractrouter.hpp index 3d0d1e747..a0e761b78 100644 --- a/llarp/router/abstractrouter.hpp +++ b/llarp/router/abstractrouter.hpp @@ -220,6 +220,10 @@ namespace llarp virtual const byte_t* pubkey() const = 0; + /// get what our real public ip is if we can know it + virtual std::optional> + OurPublicIP() const = 0; + /// connect to N random routers virtual void ConnectToRandomRouters(int N) = 0; diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index 5c04b6799..2b2a4131c 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -415,9 +415,6 @@ namespace llarp if (!FromConfig(conf)) throw std::runtime_error("FromConfig() failed"); - if (!InitOutboundLinks()) - throw std::runtime_error("InitOutboundLinks() failed"); - if (not EnsureIdentity()) throw std::runtime_error("EnsureIdentity() failed"); @@ -596,8 +593,8 @@ namespace llarp transport_keyfile = m_keyManager->m_transportKeyPath; ident_keyfile = m_keyManager->m_idKeyPath; - if (not conf.router.m_publicAddress.isEmpty()) - _ourAddress = conf.router.m_publicAddress.createSockAddr(); + if (auto maybe = conf.router.m_PublicIP) + _ourAddress = SockAddr{*maybe, conf.router.m_PublicPort}; RouterContact::BlockBogons = conf.router.m_blockBogons; @@ -728,14 +725,15 @@ namespace llarp if (inboundLinks.empty() and m_isServiceNode) { - const auto& publicAddr = conf.router.m_publicAddress; - if (publicAddr.isEmpty() or not publicAddr.hasPort()) + if (_ourAddress) { - throw std::runtime_error( - "service node enabled but could not find a public IP to bind to; you need to set the " - "public-ip= and public-port= options"); + inboundLinks.push_back(LinksConfig::LinkInfo{ + _ourAddress->hostString(), _ourAddress->Family(), _ourAddress->getPort()}); } - inboundLinks.push_back(LinksConfig::LinkInfo{"0.0.0.0", AF_INET, *publicAddr.getPort()}); + else + throw std::runtime_error{ + "service node enabled but could not find a public IP to bind to; you need to set the " + "public-ip= and public-port= options"}; } // create inbound links, if we are a service node @@ -1253,6 +1251,12 @@ namespace llarp return false; } + if (not InitOutboundLinks()) + { + LogError("failed to init outbound links"); + return false; + } + if (IsServiceNode()) { if (!SaveRC()) @@ -1563,6 +1567,22 @@ namespace llarp return ep and ep->HasExit(); } + std::optional> + Router::OurPublicIP() const + { + if (_ourAddress) + return _ourAddress->getIP(); + std::optional> found; + _linkManager.ForEachInboundLink([&found](const auto& link) { + if (found) + return; + AddressInfo ai; + if (link->GetOurAddressInfo(ai)) + found = ai.IP(); + }); + return found; + } + bool Router::InitOutboundLinks() { diff --git a/llarp/router/router.hpp b/llarp/router/router.hpp index afe9ec163..f8578c024 100644 --- a/llarp/router/router.hpp +++ b/llarp/router/router.hpp @@ -102,6 +102,9 @@ namespace llarp return _dht; } + std::optional> + OurPublicIP() const override; + util::StatusObject ExtractStatus() const override; diff --git a/pybind/llarp/config.cpp b/pybind/llarp/config.cpp index 0675f98e5..9083f213d 100644 --- a/pybind/llarp/config.cpp +++ b/pybind/llarp/config.cpp @@ -39,11 +39,6 @@ namespace llarp [](RouterConfig& self) { return self.m_dataDir.c_str(); }, [](RouterConfig& self, std::string dir) { self.m_dataDir = dir; }) .def_readwrite("blockBogons", &RouterConfig::m_blockBogons) - .def( - "overrideAddress", - [](RouterConfig& self, std::string addr) { - self.m_publicAddress = llarp::IpAddress(addr); - }) .def_readwrite("workerThreads", &RouterConfig::m_workerThreads) .def_readwrite("numNetThreads", &RouterConfig::m_numNetThreads) .def_readwrite("JobQueueSize", &RouterConfig::m_JobQueueSize); From 33a2226079614c6162572efe9a2d8f8a8dc0bbf7 Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 26 May 2022 10:57:39 -0400 Subject: [PATCH 175/202] footcannon prevention: check for invalid address family. throw if we pass in a bogus af value when getting a sockaddr for all interfaces --- llarp/net/net.cpp | 14 +++++++++----- llarp/util/str.hpp | 11 +++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/llarp/net/net.cpp b/llarp/net/net.cpp index 83e62cff5..0c73e3167 100644 --- a/llarp/net/net.cpp +++ b/llarp/net/net.cpp @@ -591,11 +591,15 @@ namespace llarp addr.sin_port = htons(0); return SockAddr{addr}; } - sockaddr_in6 addr6{}; - addr6.sin6_family = AF_INET6; - addr6.sin6_port = htons(0); - addr6.sin6_addr = IN6ADDR_ANY_INIT; - return SockAddr{addr6}; + if (af == AF_INET6) + { + sockaddr_in6 addr6{}; + addr6.sin6_family = AF_INET6; + addr6.sin6_port = htons(0); + addr6.sin6_addr = IN6ADDR_ANY_INIT; + return SockAddr{addr6}; + } + throw llarp::make_exception(af, " is not a valid address family"); } } // namespace diff --git a/llarp/util/str.hpp b/llarp/util/str.hpp index c90781c9f..1e564b6cf 100644 --- a/llarp/util/str.hpp +++ b/llarp/util/str.hpp @@ -35,6 +35,17 @@ namespace llarp return o.str(); } + /// util for constructing an exception with a message constructed from a set of whatever passed + /// into stringify + /// E must be derived from std::exception here + template + E + make_exception(T&&... stuff) + { + static_assert(std::is_base_of_v); + return E{stringify(std::forward(stuff)...)}; + } + using namespace std::literals; /// Returns true if the first string is equal to the second string, compared case-insensitively. From 1eba0f836ed1eb8a5080bd7c4e34ed48e1848be3 Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 26 May 2022 11:59:44 -0400 Subject: [PATCH 176/202] replace LLARP_PROTO_VERSION macro --- llarp/constants/proto.hpp | 16 ++++++---------- llarp/constants/version.cpp.in | 2 +- llarp/dht/message.hpp | 2 +- llarp/dht/messages/findintro.cpp | 4 ++-- llarp/dht/messages/gotrouter.cpp | 2 +- llarp/dht/messages/gotrouter.hpp | 2 +- llarp/dht/messages/pubintro.cpp | 2 +- llarp/exit/policy.hpp | 2 +- llarp/iwp/session.cpp | 9 ++++++--- llarp/messages/dht_immediate.cpp | 4 ++-- llarp/messages/discard.hpp | 2 +- llarp/messages/link_intro.cpp | 7 ++++--- llarp/messages/link_message.hpp | 2 +- llarp/messages/relay.cpp | 8 ++++---- llarp/messages/relay_commit.cpp | 12 +++++++----- llarp/messages/relay_status.cpp | 12 +++++++----- llarp/net/address_info.cpp | 4 ++-- llarp/net/address_info.hpp | 2 +- llarp/net/exit_info.hpp | 2 +- llarp/path/path.cpp | 2 +- llarp/path/pathbuilder.cpp | 2 +- llarp/pow.hpp | 2 +- llarp/profiling.hpp | 2 +- llarp/router/router.cpp | 2 +- llarp/router_contact.cpp | 2 +- llarp/router_contact.hpp | 2 +- llarp/router_version.hpp | 2 +- llarp/routing/dht_message.cpp | 2 +- llarp/routing/message.hpp | 2 +- llarp/routing/path_transfer_message.cpp | 2 +- llarp/service/async_key_exchange.cpp | 2 +- llarp/service/identity.hpp | 2 +- llarp/service/info.cpp | 2 +- llarp/service/info.hpp | 2 +- llarp/service/intro.hpp | 2 +- llarp/service/intro_set.hpp | 2 +- llarp/service/protocol.cpp | 2 +- llarp/service/protocol.hpp | 4 ++-- test/router/test_llarp_router_version.cpp | 6 +++--- .../test_llarp_routing_obtainexitmessage.cpp | 2 +- 40 files changed, 74 insertions(+), 70 deletions(-) diff --git a/llarp/constants/proto.hpp b/llarp/constants/proto.hpp index fe2bb03ec..0ed9b53f7 100644 --- a/llarp/constants/proto.hpp +++ b/llarp/constants/proto.hpp @@ -1,13 +1,9 @@ #pragma once -#ifndef LLARP_PROTO_VERSION -#define LLARP_PROTO_VERSION (0) -#endif +namespace llarp::constants +{ + /// current network wide protocol version + // TODO: enum class + constexpr auto proto_version = 0; -#ifndef LLARP_ETH_PROTO -#define LLARP_ETH_PROTO (0xD1CE) -#endif - -#ifndef LLARP_KEYFILE_VERSION -#define LLARP_KEYFILE_VERSION (1) -#endif +} // namespace llarp::constants diff --git a/llarp/constants/version.cpp.in b/llarp/constants/version.cpp.in index 67c3cfa9c..c21cdc8ee 100644 --- a/llarp/constants/version.cpp.in +++ b/llarp/constants/version.cpp.in @@ -16,7 +16,7 @@ namespace llarp { // clang-format off const std::array VERSION{{LLARP_VERSION_MAJOR, LLARP_VERSION_MINOR, LLARP_VERSION_PATCH}}; - const std::array ROUTER_VERSION{{LLARP_PROTO_VERSION, LLARP_VERSION_MAJOR, LLARP_VERSION_MINOR, LLARP_VERSION_PATCH}}; + const std::array ROUTER_VERSION{{llarp::constants::proto_version, LLARP_VERSION_MAJOR, LLARP_VERSION_MINOR, LLARP_VERSION_PATCH}}; const char* const VERSION_STR = LLARP_VERSION_STR; const char* const VERSION_TAG = "@VERSIONTAG@"; const char* const VERSION_FULL = LLARP_NAME "-" LLARP_VERSION_STR "-@VERSIONTAG@"; diff --git a/llarp/dht/message.hpp b/llarp/dht/message.hpp index 338c98c33..13fdd9229 100644 --- a/llarp/dht/message.hpp +++ b/llarp/dht/message.hpp @@ -34,7 +34,7 @@ namespace llarp Key_t From; PathID_t pathID; - uint64_t version = LLARP_PROTO_VERSION; + uint64_t version = llarp::constants::proto_version; }; IMessage::Ptr_t diff --git a/llarp/dht/messages/findintro.cpp b/llarp/dht/messages/findintro.cpp index 0e21523b7..a898b78c1 100644 --- a/llarp/dht/messages/findintro.cpp +++ b/llarp/dht/messages/findintro.cpp @@ -28,7 +28,7 @@ namespace llarp if (!BEncodeMaybeReadDictInt("T", txID, read, k, val)) return false; - if (!BEncodeMaybeVerifyVersion("V", version, LLARP_PROTO_VERSION, read, k, val)) + if (!BEncodeMaybeVerifyVersion("V", version, llarp::constants::proto_version, read, k, val)) return false; return read; @@ -66,7 +66,7 @@ namespace llarp if (!BEncodeWriteDictInt("T", txID, buf)) return false; // protocol version - if (!BEncodeWriteDictInt("V", LLARP_PROTO_VERSION, buf)) + if (!BEncodeWriteDictInt("V", llarp::constants::proto_version, buf)) return false; return bencode_end(buf); diff --git a/llarp/dht/messages/gotrouter.cpp b/llarp/dht/messages/gotrouter.cpp index 4ab90a7d4..9fcb56c31 100644 --- a/llarp/dht/messages/gotrouter.cpp +++ b/llarp/dht/messages/gotrouter.cpp @@ -73,7 +73,7 @@ namespace llarp return bencode_read_integer(val, &txid); } bool read = false; - if (!BEncodeMaybeVerifyVersion("V", version, LLARP_PROTO_VERSION, read, key, val)) + if (!BEncodeMaybeVerifyVersion("V", version, llarp::constants::proto_version, read, key, val)) return false; return read; diff --git a/llarp/dht/messages/gotrouter.hpp b/llarp/dht/messages/gotrouter.hpp index 259911313..55a74f98e 100644 --- a/llarp/dht/messages/gotrouter.hpp +++ b/llarp/dht/messages/gotrouter.hpp @@ -30,7 +30,7 @@ namespace llarp /// gossip message GotRouterMessage(const RouterContact rc) : IMessage({}), foundRCs({rc}), txid(0) { - version = LLARP_PROTO_VERSION; + version = llarp::constants::proto_version; } GotRouterMessage(const GotRouterMessage& other) diff --git a/llarp/dht/messages/pubintro.cpp b/llarp/dht/messages/pubintro.cpp index 07598f9cc..b36f9b342 100644 --- a/llarp/dht/messages/pubintro.cpp +++ b/llarp/dht/messages/pubintro.cpp @@ -195,7 +195,7 @@ namespace llarp return false; if (!BEncodeWriteDictInt("T", txID, buf)) return false; - if (!BEncodeWriteDictInt("V", LLARP_PROTO_VERSION, buf)) + if (!BEncodeWriteDictInt("V", llarp::constants::proto_version, buf)) return false; return bencode_end(buf); } diff --git a/llarp/exit/policy.hpp b/llarp/exit/policy.hpp index ed8e857fd..de41e1ebc 100644 --- a/llarp/exit/policy.hpp +++ b/llarp/exit/policy.hpp @@ -11,7 +11,7 @@ namespace llarp uint64_t proto = 0; uint64_t port = 0; uint64_t drop = 0; - uint64_t version = LLARP_PROTO_VERSION; + uint64_t version = llarp::constants::proto_version; bool BDecode(llarp_buffer_t* buf) diff --git a/llarp/iwp/session.cpp b/llarp/iwp/session.cpp index e5ba94a5a..9aa88d01a 100644 --- a/llarp/iwp/session.cpp +++ b/llarp/iwp/session.cpp @@ -24,7 +24,7 @@ namespace llarp } // randomize nounce CryptoManager::instance()->randbytes(pkt.data() + HMACSIZE, TUNNONCESIZE); - pkt[PacketOverhead] = LLARP_PROTO_VERSION; + pkt[PacketOverhead] = llarp::constants::proto_version; pkt[PacketOverhead + 1] = cmd; return pkt; } @@ -653,10 +653,13 @@ namespace llarp LogError("failed to decrypt session data from ", m_RemoteAddr); continue; } - if (pkt[PacketOverhead] != LLARP_PROTO_VERSION) + if (pkt[PacketOverhead] != llarp::constants::proto_version) { LogError( - "protocol version mismatch ", int(pkt[PacketOverhead]), " != ", LLARP_PROTO_VERSION); + "protocol version mismatch ", + int(pkt[PacketOverhead]), + " != ", + llarp::constants::proto_version); itr = msgs.erase(itr); continue; } diff --git a/llarp/messages/dht_immediate.cpp b/llarp/messages/dht_immediate.cpp index 1af4a9c47..2ab9f13e3 100644 --- a/llarp/messages/dht_immediate.cpp +++ b/llarp/messages/dht_immediate.cpp @@ -20,7 +20,7 @@ namespace llarp { if (!bencode_read_integer(buf, &version)) return false; - return version == LLARP_PROTO_VERSION; + return version == llarp::constants::proto_version; } // bad key return false; @@ -54,7 +54,7 @@ namespace llarp return false; // protocol version - if (!bencode_write_uint64_entry(buf, "v", 1, LLARP_PROTO_VERSION)) + if (!bencode_write_uint64_entry(buf, "v", 1, llarp::constants::proto_version)) return false; return bencode_end(buf); diff --git a/llarp/messages/discard.hpp b/llarp/messages/discard.hpp index 2466cb3d0..46d24d429 100644 --- a/llarp/messages/discard.hpp +++ b/llarp/messages/discard.hpp @@ -69,7 +69,7 @@ namespace llarp DataDiscardMessage(const PathID_t& dst, uint64_t s) : P(dst) { S = s; - version = LLARP_PROTO_VERSION; + version = llarp::constants::proto_version; } void diff --git a/llarp/messages/link_intro.cpp b/llarp/messages/link_intro.cpp index f334e2aba..9144baf20 100644 --- a/llarp/messages/link_intro.cpp +++ b/llarp/messages/link_intro.cpp @@ -43,9 +43,10 @@ namespace llarp { if (!bencode_read_integer(buf, &version)) return false; - if (version != LLARP_PROTO_VERSION) + if (version != llarp::constants::proto_version) { - llarp::LogWarn("llarp protocol version mismatch ", version, " != ", LLARP_PROTO_VERSION); + llarp::LogWarn( + "llarp protocol version mismatch ", version, " != ", llarp::constants::proto_version); return false; } llarp::LogDebug("LIM version ", version); @@ -86,7 +87,7 @@ namespace llarp if (!rc.BEncode(buf)) return false; - if (!bencode_write_uint64_entry(buf, "v", 1, LLARP_PROTO_VERSION)) + if (!bencode_write_uint64_entry(buf, "v", 1, llarp::constants::proto_version)) return false; if (!bencode_write_bytestring(buf, "z", 1)) diff --git a/llarp/messages/link_message.hpp b/llarp/messages/link_message.hpp index 9ed1cf2f9..af5af6413 100644 --- a/llarp/messages/link_message.hpp +++ b/llarp/messages/link_message.hpp @@ -17,7 +17,7 @@ namespace llarp { /// who did this message come from or is going to ILinkSession* session = nullptr; - uint64_t version = LLARP_PROTO_VERSION; + uint64_t version = llarp::constants::proto_version; PathID_t pathid; diff --git a/llarp/messages/relay.cpp b/llarp/messages/relay.cpp index fdbac1a1f..ace3d7df2 100644 --- a/llarp/messages/relay.cpp +++ b/llarp/messages/relay.cpp @@ -25,7 +25,7 @@ namespace llarp if (!BEncodeWriteDictEntry("p", pathid, buf)) return false; - if (!BEncodeWriteDictInt("v", LLARP_PROTO_VERSION, buf)) + if (!BEncodeWriteDictInt("v", llarp::constants::proto_version, buf)) return false; if (!BEncodeWriteDictEntry("x", X, buf)) return false; @@ -40,7 +40,7 @@ namespace llarp bool read = false; if (!BEncodeMaybeReadDictEntry("p", pathid, read, key, buf)) return false; - if (!BEncodeMaybeVerifyVersion("v", version, LLARP_PROTO_VERSION, read, key, buf)) + if (!BEncodeMaybeVerifyVersion("v", version, llarp::constants::proto_version, read, key, buf)) return false; if (!BEncodeMaybeReadDictEntry("x", X, read, key, buf)) return false; @@ -79,7 +79,7 @@ namespace llarp if (!BEncodeWriteDictEntry("p", pathid, buf)) return false; - if (!BEncodeWriteDictInt("v", LLARP_PROTO_VERSION, buf)) + if (!BEncodeWriteDictInt("v", llarp::constants::proto_version, buf)) return false; if (!BEncodeWriteDictEntry("x", X, buf)) return false; @@ -94,7 +94,7 @@ namespace llarp bool read = false; if (!BEncodeMaybeReadDictEntry("p", pathid, read, key, buf)) return false; - if (!BEncodeMaybeVerifyVersion("v", version, LLARP_PROTO_VERSION, read, key, buf)) + if (!BEncodeMaybeVerifyVersion("v", version, llarp::constants::proto_version, read, key, buf)) return false; if (!BEncodeMaybeReadDictEntry("x", X, read, key, buf)) return false; diff --git a/llarp/messages/relay_commit.cpp b/llarp/messages/relay_commit.cpp index 900971ecb..3dc67d7a5 100644 --- a/llarp/messages/relay_commit.cpp +++ b/llarp/messages/relay_commit.cpp @@ -29,7 +29,7 @@ namespace llarp return BEncodeReadArray(frames, buf); } bool read = false; - if (!BEncodeMaybeVerifyVersion("v", version, LLARP_PROTO_VERSION, read, key, buf)) + if (!BEncodeMaybeVerifyVersion("v", version, llarp::constants::proto_version, read, key, buf)) return false; return read; @@ -54,7 +54,7 @@ namespace llarp if (!BEncodeWriteDictArray("c", frames, buf)) return false; // version - if (!bencode_write_uint64_entry(buf, "v", 1, LLARP_PROTO_VERSION)) + if (!bencode_write_uint64_entry(buf, "v", 1, llarp::constants::proto_version)) return false; return bencode_end(buf); @@ -102,9 +102,10 @@ namespace llarp if (!BEncodeWriteDictEntry("u", *nextRC, buf)) return false; } - if (!bencode_write_uint64_entry(buf, "v", 1, LLARP_PROTO_VERSION)) + + if (not bencode_write_uint64_entry(buf, "v", 1, llarp::constants::proto_version)) return false; - if (work && !BEncodeWriteDictEntry("w", *work, buf)) + if (work and not BEncodeWriteDictEntry("w", *work, buf)) return false; return bencode_end(buf); @@ -135,7 +136,8 @@ namespace llarp nextRC = std::make_unique(); return nextRC->BDecode(buffer); } - if (!BEncodeMaybeVerifyVersion("v", version, LLARP_PROTO_VERSION, read, *key, buffer)) + if (!BEncodeMaybeVerifyVersion( + "v", version, llarp::constants::proto_version, read, *key, buffer)) return false; if (*key == "w") { diff --git a/llarp/messages/relay_status.cpp b/llarp/messages/relay_status.cpp index 0ae3358f0..67dcb3294 100644 --- a/llarp/messages/relay_status.cpp +++ b/llarp/messages/relay_status.cpp @@ -80,7 +80,7 @@ namespace llarp } else if (key == "v") { - if (!BEncodeMaybeVerifyVersion("v", version, LLARP_PROTO_VERSION, read, key, buf)) + if (!BEncodeMaybeVerifyVersion("v", version, llarp::constants::proto_version, read, key, buf)) { return false; } @@ -115,7 +115,7 @@ namespace llarp if (!BEncodeWriteDictInt("s", status, buf)) return false; // version - if (!bencode_write_uint64_entry(buf, "v", 1, LLARP_PROTO_VERSION)) + if (!bencode_write_uint64_entry(buf, "v", 1, llarp::constants::proto_version)) return false; return bencode_end(buf); @@ -190,7 +190,7 @@ namespace llarp LR_StatusRecord record; record.status = newStatus; - record.version = LLARP_PROTO_VERSION; + record.version = llarp::constants::proto_version; llarp_buffer_t buf(frame.data(), frame.size()); buf.cur = buf.base + EncryptedFrameOverheadSize; @@ -256,7 +256,8 @@ namespace llarp LR_StatusRecord::BEncode(llarp_buffer_t* buf) const { return bencode_start_dict(buf) && BEncodeWriteDictInt("s", status, buf) - && bencode_write_uint64_entry(buf, "v", 1, LLARP_PROTO_VERSION) && bencode_end(buf); + && bencode_write_uint64_entry(buf, "v", 1, llarp::constants::proto_version) + && bencode_end(buf); } bool @@ -269,7 +270,8 @@ namespace llarp if (!BEncodeMaybeReadDictInt("s", status, read, *key, buffer)) return false; - if (!BEncodeMaybeVerifyVersion("v", version, LLARP_PROTO_VERSION, read, *key, buffer)) + if (!BEncodeMaybeVerifyVersion( + "v", version, llarp::constants::proto_version, read, *key, buffer)) return false; return read; diff --git a/llarp/net/address_info.cpp b/llarp/net/address_info.cpp index 8daf8f8de..d4b8cd398 100644 --- a/llarp/net/address_info.cpp +++ b/llarp/net/address_info.cpp @@ -105,7 +105,7 @@ namespace llarp { if (!bencode_read_integer(buf, &i)) return false; - return i == LLARP_PROTO_VERSION; + return i == llarp::constants::proto_version; } // bad key @@ -149,7 +149,7 @@ namespace llarp return false; /** version */ - if (!bencode_write_uint64_entry(buff, "v", 1, LLARP_PROTO_VERSION)) + if (!bencode_write_uint64_entry(buff, "v", 1, llarp::constants::proto_version)) return false; /** end */ return bencode_end(buff); diff --git a/llarp/net/address_info.hpp b/llarp/net/address_info.hpp index a3bcd02e7..a66f77112 100644 --- a/llarp/net/address_info.hpp +++ b/llarp/net/address_info.hpp @@ -27,7 +27,7 @@ namespace llarp llarp::PubKey pubkey; in6_addr ip = {}; uint16_t port; - uint64_t version = LLARP_PROTO_VERSION; + uint64_t version = llarp::constants::proto_version; bool BDecode(llarp_buffer_t* buf) diff --git a/llarp/net/exit_info.hpp b/llarp/net/exit_info.hpp index 7265e181a..b3727c4f5 100644 --- a/llarp/net/exit_info.hpp +++ b/llarp/net/exit_info.hpp @@ -21,7 +21,7 @@ namespace llarp IpAddress ipAddress; IpAddress netmask; PubKey pubkey; - uint64_t version = LLARP_PROTO_VERSION; + uint64_t version = llarp::constants::proto_version; ExitInfo() = default; diff --git a/llarp/path/path.cpp b/llarp/path/path.cpp index c61d9098b..1e0540f0f 100644 --- a/llarp/path/path.cpp +++ b/llarp/path/path.cpp @@ -646,7 +646,7 @@ namespace llarp llarp_buffer_t buf(tmp); // should help prevent bad paths with uninitialized members // FIXME: Why would we get uninitialized IMessages? - if (msg.version != LLARP_PROTO_VERSION) + if (msg.version != llarp::constants::proto_version) return false; if (!msg.BEncode(&buf)) { diff --git a/llarp/path/pathbuilder.cpp b/llarp/path/pathbuilder.cpp index 2b7f4be39..c401965aa 100644 --- a/llarp/path/pathbuilder.cpp +++ b/llarp/path/pathbuilder.cpp @@ -68,7 +68,7 @@ namespace llarp } // build record record.lifetime = path::default_lifetime; - record.version = LLARP_PROTO_VERSION; + record.version = llarp::constants::proto_version; record.txid = hop.txID; record.rxid = hop.rxID; record.tunnelNonce = hop.nonce; diff --git a/llarp/pow.hpp b/llarp/pow.hpp index c1e947c1e..be075ca6c 100644 --- a/llarp/pow.hpp +++ b/llarp/pow.hpp @@ -12,7 +12,7 @@ namespace llarp llarp_time_t timestamp = 0s; llarp_time_t extendedLifetime = 0s; AlignedBuffer<32> nonce; - uint64_t version = LLARP_PROTO_VERSION; + uint64_t version = llarp::constants::proto_version; ~PoW(); diff --git a/llarp/profiling.hpp b/llarp/profiling.hpp index 03372d3fb..df26b6bcb 100644 --- a/llarp/profiling.hpp +++ b/llarp/profiling.hpp @@ -20,7 +20,7 @@ namespace llarp uint64_t pathTimeoutCount = 0; llarp_time_t lastUpdated = 0s; llarp_time_t lastDecay = 0s; - uint64_t version = LLARP_PROTO_VERSION; + uint64_t version = llarp::constants::proto_version; bool BEncode(llarp_buffer_t* buf) const; diff --git a/llarp/router/router.cpp b/llarp/router/router.cpp index 2b2a4131c..b6ca5c6ba 100644 --- a/llarp/router/router.cpp +++ b/llarp/router/router.cpp @@ -1210,7 +1210,7 @@ namespace llarp // set router version if service node if (IsServiceNode()) { - _rc.routerVersion = RouterVersion(llarp::VERSION, LLARP_PROTO_VERSION); + _rc.routerVersion = RouterVersion(llarp::VERSION, llarp::constants::proto_version); } _linkManager.ForEachInboundLink([&](LinkLayer_ptr link) { diff --git a/llarp/router_contact.cpp b/llarp/router_contact.cpp index dabbeae85..76dac45fa 100644 --- a/llarp/router_contact.cpp +++ b/llarp/router_contact.cpp @@ -221,7 +221,7 @@ namespace llarp routerVersion = std::optional{}; last_updated = 0s; srvRecords.clear(); - version = LLARP_PROTO_VERSION; + version = llarp::constants::proto_version; } util::StatusObject diff --git a/llarp/router_contact.hpp b/llarp/router_contact.hpp index ccac70d67..53f29bd38 100644 --- a/llarp/router_contact.hpp +++ b/llarp/router_contact.hpp @@ -101,7 +101,7 @@ namespace llarp llarp::AlignedBuffer nickname; llarp_time_t last_updated = 0s; - uint64_t version = LLARP_PROTO_VERSION; + uint64_t version = llarp::constants::proto_version; std::optional routerVersion; /// should we serialize the exit info? const static bool serializeExit = true; diff --git a/llarp/router_version.hpp b/llarp/router_version.hpp index 80632c328..0df69a7d1 100644 --- a/llarp/router_version.hpp +++ b/llarp/router_version.hpp @@ -57,7 +57,7 @@ namespace llarp private: Version_t m_Version = {{0, 0, 0}}; - int64_t m_ProtoVersion = LLARP_PROTO_VERSION; + int64_t m_ProtoVersion = llarp::constants::proto_version; }; inline std::ostream& diff --git a/llarp/routing/dht_message.cpp b/llarp/routing/dht_message.cpp index a581e5d73..510754049 100644 --- a/llarp/routing/dht_message.cpp +++ b/llarp/routing/dht_message.cpp @@ -39,7 +39,7 @@ namespace llarp return false; if (!BEncodeWriteDictInt("S", S, buf)) return false; - if (!BEncodeWriteDictInt("V", LLARP_PROTO_VERSION, buf)) + if (!BEncodeWriteDictInt("V", llarp::constants::proto_version, buf)) return false; return bencode_end(buf); diff --git a/llarp/routing/message.hpp b/llarp/routing/message.hpp index 8274b956b..6544792d5 100644 --- a/llarp/routing/message.hpp +++ b/llarp/routing/message.hpp @@ -16,7 +16,7 @@ namespace llarp { PathID_t from; uint64_t S{0}; - uint64_t version = LLARP_PROTO_VERSION; + uint64_t version = llarp::constants::proto_version; IMessage() = default; diff --git a/llarp/routing/path_transfer_message.cpp b/llarp/routing/path_transfer_message.cpp index 8667fbd52..ddb5ac82d 100644 --- a/llarp/routing/path_transfer_message.cpp +++ b/llarp/routing/path_transfer_message.cpp @@ -40,7 +40,7 @@ namespace llarp if (!BEncodeWriteDictEntry("T", T, buf)) return false; - if (!BEncodeWriteDictInt("V", LLARP_PROTO_VERSION, buf)) + if (!BEncodeWriteDictInt("V", llarp::constants::proto_version, buf)) return false; if (!BEncodeWriteDictEntry("Y", Y, buf)) return false; diff --git a/llarp/service/async_key_exchange.cpp b/llarp/service/async_key_exchange.cpp index 43a765a75..bf814daac 100644 --- a/llarp/service/async_key_exchange.cpp +++ b/llarp/service/async_key_exchange.cpp @@ -70,7 +70,7 @@ namespace llarp // set sender self->msg.sender = self->m_LocalIdentity.pub; // set version - self->msg.version = LLARP_PROTO_VERSION; + self->msg.version = llarp::constants::proto_version; // encrypt and sign if (frame->EncryptAndSign(self->msg, K, self->m_LocalIdentity)) self->loop->call([self, frame] { AsyncKeyExchange::Result(self, frame); }); diff --git a/llarp/service/identity.hpp b/llarp/service/identity.hpp index 7fd1b72a7..5278f1af6 100644 --- a/llarp/service/identity.hpp +++ b/llarp/service/identity.hpp @@ -22,7 +22,7 @@ namespace llarp SecretKey signkey; PrivateKey derivedSignKey; PQKeyPair pq; - uint64_t version = LLARP_PROTO_VERSION; + uint64_t version = llarp::constants::proto_version; VanityNonce vanity; // public service info diff --git a/llarp/service/info.cpp b/llarp/service/info.cpp index ad5191758..3e2d15fd4 100644 --- a/llarp/service/info.cpp +++ b/llarp/service/info.cpp @@ -56,7 +56,7 @@ namespace llarp return false; if (!BEncodeWriteDictEntry("s", signkey, buf)) return false; - if (!BEncodeWriteDictInt("v", LLARP_PROTO_VERSION, buf)) + if (!BEncodeWriteDictInt("v", llarp::constants::proto_version, buf)) return false; if (!vanity.IsZero()) { diff --git a/llarp/service/info.hpp b/llarp/service/info.hpp index cf9a5d757..1a56f13bf 100644 --- a/llarp/service/info.hpp +++ b/llarp/service/info.hpp @@ -20,7 +20,7 @@ namespace llarp public: VanityNonce vanity; - uint64_t version = LLARP_PROTO_VERSION; + uint64_t version = llarp::constants::proto_version; void RandomizeVanity() diff --git a/llarp/service/intro.hpp b/llarp/service/intro.hpp index 507d0ffc9..36ea34c2a 100644 --- a/llarp/service/intro.hpp +++ b/llarp/service/intro.hpp @@ -17,7 +17,7 @@ namespace llarp PathID_t pathID; llarp_time_t latency = 0s; llarp_time_t expiresAt = 0s; - uint64_t version = LLARP_PROTO_VERSION; + uint64_t version = llarp::constants::proto_version; util::StatusObject ExtractStatus() const; diff --git a/llarp/service/intro_set.hpp b/llarp/service/intro_set.hpp index 66e88fb14..c39a08404 100644 --- a/llarp/service/intro_set.hpp +++ b/llarp/service/intro_set.hpp @@ -49,7 +49,7 @@ namespace llarp std::optional exitTrafficPolicy; Signature signature; - uint64_t version = LLARP_PROTO_VERSION; + uint64_t version = llarp::constants::proto_version; bool OtherIsNewer(const IntroSet& other) const diff --git a/llarp/service/protocol.cpp b/llarp/service/protocol.cpp index 8ecf32ac8..0b9939c59 100644 --- a/llarp/service/protocol.cpp +++ b/llarp/service/protocol.cpp @@ -191,7 +191,7 @@ namespace llarp return false; if (!BEncodeMaybeReadDictEntry("T", T, read, key, val)) return false; - if (!BEncodeMaybeVerifyVersion("V", version, LLARP_PROTO_VERSION, read, key, val)) + if (!BEncodeMaybeVerifyVersion("V", version, llarp::constants::proto_version, read, key, val)) return false; if (!BEncodeMaybeReadDictEntry("Z", Z, read, key, val)) return false; diff --git a/llarp/service/protocol.hpp b/llarp/service/protocol.hpp index 1776abba4..4286ce221 100644 --- a/llarp/service/protocol.hpp +++ b/llarp/service/protocol.hpp @@ -45,7 +45,7 @@ namespace llarp Endpoint* handler = nullptr; ConvoTag tag; uint64_t seqno = 0; - uint64_t version = LLARP_PROTO_VERSION; + uint64_t version = llarp::constants::proto_version; /// encode metainfo for lmq endpoint auth std::vector @@ -155,7 +155,7 @@ namespace llarp N.Zero(); Z.Zero(); R = 0; - version = LLARP_PROTO_VERSION; + version = llarp::constants::proto_version; } bool diff --git a/test/router/test_llarp_router_version.cpp b/test/router/test_llarp_router_version.cpp index 6230b2161..57e436347 100644 --- a/test/router/test_llarp_router_version.cpp +++ b/test/router/test_llarp_router_version.cpp @@ -24,14 +24,14 @@ TEST_CASE("Compatibility when protocol unequal", "[RouterVersion]") TEST_CASE("Empty compatibility", "[RouterVersion]") { - llarp::RouterVersion v1({0, 0, 1}, LLARP_PROTO_VERSION); + llarp::RouterVersion v1({0, 0, 1}, llarp::constants::proto_version); CHECK_FALSE(v1.IsCompatableWith(llarp::emptyRouterVersion)); } TEST_CASE("IsEmpty", "[RouterVersion]") { - llarp::RouterVersion notEmpty({0, 0, 1}, LLARP_PROTO_VERSION); + llarp::RouterVersion notEmpty({0, 0, 1}, llarp::constants::proto_version); CHECK_FALSE(notEmpty.IsEmpty()); CHECK(llarp::emptyRouterVersion.IsEmpty()); @@ -39,7 +39,7 @@ TEST_CASE("IsEmpty", "[RouterVersion]") TEST_CASE("Clear", "[RouterVersion]") { - llarp::RouterVersion version({0, 0, 1}, LLARP_PROTO_VERSION); + llarp::RouterVersion version({0, 0, 1}, llarp::constants::proto_version); CHECK_FALSE(version.IsEmpty()); version.Clear(); diff --git a/test/routing/test_llarp_routing_obtainexitmessage.cpp b/test/routing/test_llarp_routing_obtainexitmessage.cpp index 8a5c2ca2b..fca9842e6 100644 --- a/test/routing/test_llarp_routing_obtainexitmessage.cpp +++ b/test/routing/test_llarp_routing_obtainexitmessage.cpp @@ -28,6 +28,6 @@ TEST_CASE_METHOD(LlarpTest<>, "Sign-verify") CHECK(msg.Sign(alice)); CHECK(msg.Verify()); CHECK(msg.I == PubKey{seckey_topublic(alice)}); - CHECK(msg.version == LLARP_PROTO_VERSION); + CHECK(msg.version == llarp::constants::proto_version); CHECK_FALSE(msg.Z.IsZero()); } From f05c2ebc71d9ceca9bd1b1a524c84c89d36c54b4 Mon Sep 17 00:00:00 2001 From: Jeff Date: Thu, 26 May 2022 12:25:55 -0400 Subject: [PATCH 177/202] macro removals clean up version cmake stuff clean up generated cpp version stuff make all the windows rc stuff get generated by cmake bump release motto message properly inject release motto into version --- CMakeLists.txt | 9 +---- cmake/Version.cmake | 43 +++++++++++++++++++---- daemon/CMakeLists.txt | 2 +- llarp/constants/version.cpp.in | 23 ++++-------- llarp/constants/version.h | 24 ------------- llarp/win32/{resource.h => resource.h.in} | 2 ++ llarp/win32/{version.rc => version.rc.in} | 28 ++++++--------- 7 files changed, 57 insertions(+), 74 deletions(-) delete mode 100644 llarp/constants/version.h rename llarp/win32/{resource.h => resource.h.in} (74%) rename llarp/win32/{version.rc => version.rc.in} (64%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a36f117e..007daf6e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,14 +35,7 @@ if(APPLE) set(LOKINET_APPLE_BUILD 0) endif() -set(RELEASE_MOTTO "A Series of Tubes" CACHE STRING "Release motto") - -add_definitions(-DLLARP_VERSION_MAJOR=${lokinet_VERSION_MAJOR}) -add_definitions(-DLLARP_VERSION_MINOR=${lokinet_VERSION_MINOR}) -add_definitions(-DLLARP_VERSION_PATCH=${lokinet_VERSION_PATCH}) -if(RELEASE_MOTTO AND CMAKE_BUILD_TYPE MATCHES "[Rr][Ee][Ll][Ee][Aa][Ss][Ee]") - add_definitions(-DLLARP_RELEASE_MOTTO="${RELEASE_MOTTO}") -endif() +set(RELEASE_MOTTO "The Current Thing" CACHE STRING "Release motto") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") diff --git a/cmake/Version.cmake b/cmake/Version.cmake index 45037a081..c967d53e3 100644 --- a/cmake/Version.cmake +++ b/cmake/Version.cmake @@ -1,9 +1,9 @@ +# 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. -find_package(Git QUIET) +set(VERSIONTAG "${GIT_VERSION}") set(GIT_INDEX_FILE "${PROJECT_SOURCE_DIR}/.git/index") if(EXISTS ${GIT_INDEX_FILE} AND ( GIT_FOUND OR Git_FOUND) ) message(STATUS "Found Git: ${GIT_EXECUTABLE}") - add_custom_command( OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp" COMMAND "${CMAKE_COMMAND}" @@ -12,11 +12,40 @@ if(EXISTS ${GIT_INDEX_FILE} AND ( GIT_FOUND OR Git_FOUND) ) "-D" "DEST=${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp" "-P" "${CMAKE_CURRENT_LIST_DIR}/GenVersion.cmake" DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/constants/version.cpp.in" - "${GIT_INDEX_FILE}") + "${GIT_INDEX_FILE}") + if(WIN32) + add_custom_command( + OUTPUT "${CMAKE_BINARY_DIR}/version.rc" + COMMAND "${CMAKE_COMMAND}" + "-D" "GIT=${GIT_EXECUTABLE}" + "-D" "SRC=${CMAKE_CURRENT_SOURCE_DIR}/constants/version.cpp.in" + "-D" "DEST=${CMAKE_BINARY_DIR}/version.rc" + "-P" "${CMAKE_CURRENT_LIST_DIR}/GenVersion.cmake" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/win32/version.rc.in" + "${GIT_INDEX_FILE}") + + add_custom_command( + OUTPUT "${CMAKE_BINARY_DIR}/lokinet_resource.h" + COMMAND "${CMAKE_COMMAND}" + "-D" "GIT=${GIT_EXECUTABLE}" + "-D" "SRC=${CMAKE_CURRENT_SOURCE_DIR}/win32/resource.h.in" + "-D" "DEST=${CMAKE_BINARY_DIR}/lokinet_resource.h" + "-P" "${CMAKE_CURRENT_LIST_DIR}/GenVersion.cmake" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/win32/resource.h.in" + "${GIT_INDEX_FILE}") + endif() else() - message(STATUS "Git was not found! Setting version to to nogit") - set(VERSIONTAG "nogit") - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/constants/version.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp") + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/constants/version.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp" @ONLY) + if(WIN32) + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/win32/version.rc.in" "${CMAKE_BINARY_DIR}/version.rc" @ONLY) + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/win32/resource.h.in" "${CMAKE_BINARY_DIR}/lokinet_resource.h" @ONLY) + endif() endif() -add_custom_target(genversion DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp") +add_custom_target(genversion_cpp DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp") +if(WIN32) + add_custom_target(genversion_rc DEPENDS "${CMAKE_BINARY_DIR}/version.rc" "${CMAKE_BINARY_DIR}/lokinet_resource.h") +else() + add_custom_target(genversion_rc) +endif() +add_custom_target(genversion DEPENDS genversion_cpp genversion_rc) diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt index a565bf96f..a65ed660a 100644 --- a/daemon/CMakeLists.txt +++ b/daemon/CMakeLists.txt @@ -49,7 +49,7 @@ endif() foreach(exe ${exetargets}) if(WIN32 AND NOT MSVC_VERSION) - target_sources(${exe} PRIVATE ../llarp/win32/version.rc) + target_sources(${exe} PRIVATE ${CMAKE_BINARY_DIR}/version.rc) target_link_libraries(${exe} PRIVATE -static-libstdc++ -static-libgcc --static -Wl,--pic-executable,-e,mainCRTStartup,--subsystem,console:5.00) target_link_libraries(${exe} PRIVATE ws2_32 iphlpapi) elseif(CMAKE_SYSTEM_NAME MATCHES "FreeBSD") diff --git a/llarp/constants/version.cpp.in b/llarp/constants/version.cpp.in index c21cdc8ee..57156338d 100644 --- a/llarp/constants/version.cpp.in +++ b/llarp/constants/version.cpp.in @@ -1,27 +1,16 @@ #include -#include #include -// clang-format off -#define LLARP_STRINGIFY2(val) #val -#define LLARP_STRINGIFY(val) LLARP_STRINGIFY2(val) - -#define LLARP_VERSION_STR \ - LLARP_STRINGIFY(LLARP_VERSION_MAJOR) \ - "." LLARP_STRINGIFY(LLARP_VERSION_MINOR) "." LLARP_STRINGIFY( \ - LLARP_VERSION_PATCH) -#define LLARP_VERSION_FULL LLARP_VERSION_STR "-@VERSIONTAG@" - namespace llarp { // clang-format off - const std::array VERSION{{LLARP_VERSION_MAJOR, LLARP_VERSION_MINOR, LLARP_VERSION_PATCH}}; - const std::array ROUTER_VERSION{{llarp::constants::proto_version, LLARP_VERSION_MAJOR, LLARP_VERSION_MINOR, LLARP_VERSION_PATCH}}; - const char* const VERSION_STR = LLARP_VERSION_STR; + const std::array VERSION{{@lokinet_VERSION_MAJOR@, @lokinet_VERSION_MINOR@, @lokinet_VERSION_PATCH@}}; + const std::array ROUTER_VERSION{{llarp::constants::proto_version, @lokinet_VERSION_MAJOR@, @lokinet_VERSION_MINOR@, @lokinet_VERSION_PATCH@}}; + const char* const VERSION_STR = "@lokinet_VERSION_MAJOR@.@lokinet_VERSION_MINOR@.@lokinet_VERSION_PATCH@"; const char* const VERSION_TAG = "@VERSIONTAG@"; - const char* const VERSION_FULL = LLARP_NAME "-" LLARP_VERSION_STR "-@VERSIONTAG@"; + const char* const VERSION_FULL = "lokinet-@lokinet_VERSION_MAJOR@.@lokinet_VERSION_MINOR@.@lokinet_VERSION_PATCH@-@VERSIONTAG@"; - const char* const RELEASE_MOTTO = LLARP_RELEASE_MOTTO; - const char* const DEFAULT_NETID = LLARP_DEFAULT_NETID; + const char* const RELEASE_MOTTO = "@RELEASE_MOTTO@"; + const char* const DEFAULT_NETID = "lokinet"; // clang-format on } // namespace llarp diff --git a/llarp/constants/version.h b/llarp/constants/version.h deleted file mode 100644 index 30893701e..000000000 --- a/llarp/constants/version.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -// Don't include this file directly but rather go through version.hpp instead. -// This is only here so version.cpp.in and the weird archaic windows build -// recipies can use the version. - -#define LLARP_NAME "lokinet" - -#define LLARP_DEFAULT_NETID "lokinet" - -#ifndef LLARP_RELEASE_MOTTO -#define LLARP_RELEASE_MOTTO "(dev build)" -#endif - -#if defined(_WIN32) && defined(RC_INVOKED) -#define LLARP_VERSION LLARP_VERSION_MAJOR, LLARP_VERSION_MINOR, LLARP_VERSION_PATCH, 0 - -#define MAKE_TRIPLET(X, Y, Z) TRIPLET_CAT(X, ., Y, ., Z) -#define TRIPLET_CAT(X, D1, Y, D2, Z) X##D1##Y##D2##Z - -#define LLARP_VERSION_TRIPLET \ - MAKE_TRIPLET(LLARP_VERSION_MAJOR, LLARP_VERSION_MINOR, LLARP_VERSION_PATCH) - -#endif diff --git a/llarp/win32/resource.h b/llarp/win32/resource.h.in similarity index 74% rename from llarp/win32/resource.h rename to llarp/win32/resource.h.in index d90e55303..477e93b9b 100644 --- a/llarp/win32/resource.h +++ b/llarp/win32/resource.h.in @@ -13,3 +13,5 @@ #define _APS_NEXT_SYMED_VALUE 101 #endif #endif +// clang-format off +#define lokinet_VERSION @lokinet_VERSION_MAJOR@, @lokinet_VERSION_MINOR@, @lokinet_VERSION_PATCH@, 0 diff --git a/llarp/win32/version.rc b/llarp/win32/version.rc.in similarity index 64% rename from llarp/win32/version.rc rename to llarp/win32/version.rc.in index 8b024dccd..23b4e996d 100644 --- a/llarp/win32/version.rc +++ b/llarp/win32/version.rc.in @@ -6,19 +6,13 @@ // // Microsoft Visual C++ generated resource script. // -#include "resource.h" -#include +#include "lokinet_resource.h" #ifdef __GNUC__ // make windows rc accept this #include #endif ///////////////////////////////////////////////////////////////////////////// // English (United States) resources -#define STRINGIZER(version) #version - -#define VERSION_STRING(version, codename, revision) \ - STRINGIZER(version) "-release [" STRINGIZER(codename) "] (rev-" STRINGIZER(revision) ")" - #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) #ifdef _WIN32 LANGUAGE 1033,1 @@ -56,8 +50,8 @@ END // VS_VERSION_INFO VERSIONINFO - FILEVERSION LLARP_VERSION - PRODUCTVERSION LLARP_VERSION + FILEVERSION lokinet_VERSION + PRODUCTVERSION lokinet_VERSION FILEFLAGSMASK 0x17L #ifdef _DEBUG FILEFLAGS 0x3L @@ -72,15 +66,15 @@ BEGIN BEGIN BLOCK "040904b0" BEGIN - VALUE "Comments", "includes relay/exit functionality, such code is highly experimental on non-Linux targets" - VALUE "CompanyName", "Loki Foundation" - VALUE "FileDescription", "LokiNET daemon for Microsoft® Windows® NT™" - VALUE "FileVersion", VERSION_STRING(LLARP_VERSION_TRIPLET, LLARP_RELEASE_MOTTO, VERSIONTAG) - VALUE "InternalName", "llarpd" - 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." - VALUE "OriginalFilename", "llarpd.exe" + VALUE "Comments", "This comment has invoked its 5th ammendment constitutional right to remain silent" + VALUE "CompanyName", "OPTF" + VALUE "FileDescription", "LokiNET daemon for Windows" + VALUE "FileVersion", "@lokinet_VERSION@" + VALUE "InternalName", "lokinet" + 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." + VALUE "OriginalFilename", "lokinet.exe" VALUE "ProductName", "LokiNET for Windows" - VALUE "ProductVersion", VERSION_STRING(LLARP_VERSION_TRIPLET, LLARP_RELEASE_MOTTO, VERSIONTAG) + VALUE "ProductVersion", "@lokinet_VERSION@" END END BLOCK "VarFileInfo" From e480e36f3de45ee50bfe504cf32e26ec5c52cc48 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 27 May 2022 10:14:31 -0400 Subject: [PATCH 178/202] generate windows rc for each executable target so we can set the executable name right for each one --- cmake/Version.cmake | 41 +++++++++++++++++---------------------- daemon/CMakeLists.txt | 2 +- llarp/win32/resource.h.in | 17 ---------------- llarp/win32/version.rc.in | 7 +++++-- 4 files changed, 24 insertions(+), 43 deletions(-) delete mode 100644 llarp/win32/resource.h.in diff --git a/cmake/Version.cmake b/cmake/Version.cmake index c967d53e3..79cc10250 100644 --- a/cmake/Version.cmake +++ b/cmake/Version.cmake @@ -12,39 +12,34 @@ if(EXISTS ${GIT_INDEX_FILE} AND ( GIT_FOUND OR Git_FOUND) ) "-D" "DEST=${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp" "-P" "${CMAKE_CURRENT_LIST_DIR}/GenVersion.cmake" DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/constants/version.cpp.in" - "${GIT_INDEX_FILE}") + "${GIT_INDEX_FILE}") if(WIN32) - add_custom_command( - OUTPUT "${CMAKE_BINARY_DIR}/version.rc" - COMMAND "${CMAKE_COMMAND}" - "-D" "GIT=${GIT_EXECUTABLE}" - "-D" "SRC=${CMAKE_CURRENT_SOURCE_DIR}/constants/version.cpp.in" - "-D" "DEST=${CMAKE_BINARY_DIR}/version.rc" - "-P" "${CMAKE_CURRENT_LIST_DIR}/GenVersion.cmake" - DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/win32/version.rc.in" - "${GIT_INDEX_FILE}") - - add_custom_command( - OUTPUT "${CMAKE_BINARY_DIR}/lokinet_resource.h" - COMMAND "${CMAKE_COMMAND}" - "-D" "GIT=${GIT_EXECUTABLE}" - "-D" "SRC=${CMAKE_CURRENT_SOURCE_DIR}/win32/resource.h.in" - "-D" "DEST=${CMAKE_BINARY_DIR}/lokinet_resource.h" - "-P" "${CMAKE_CURRENT_LIST_DIR}/GenVersion.cmake" - DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/win32/resource.h.in" - "${GIT_INDEX_FILE}") + foreach(exe IN ITEMS lokinet lokinet-vpn lokinet-bootstrap) + set(lokinet_EXE_NAME "${exe}.exe") + add_custom_command( + OUTPUT "${CMAKE_BINARY_DIR}/${exe}.rc" + COMMAND "${CMAKE_COMMAND}" + "-D" "GIT=${GIT_EXECUTABLE}" + "-D" "SRC=${CMAKE_CURRENT_SOURCE_DIR}/win32/version.rc.in" + "-D" "DEST=${CMAKE_BINARY_DIR}/${exe}.rc" + "-P" "${CMAKE_CURRENT_LIST_DIR}/GenVersion.cmake" + DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/win32/version.rc.in" + "${GIT_INDEX_FILE}") + endforeach() endif() else() configure_file("${CMAKE_CURRENT_SOURCE_DIR}/constants/version.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp" @ONLY) if(WIN32) - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/win32/version.rc.in" "${CMAKE_BINARY_DIR}/version.rc" @ONLY) - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/win32/resource.h.in" "${CMAKE_BINARY_DIR}/lokinet_resource.h" @ONLY) + foreach(exe IN ITEMS lokinet lokinet-vpn lokinet-bootstrap) + set(lokinet_EXE_NAME "${exe}.exe") + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/win32/version.rc.in" "${CMAKE_BINARY_DIR}/${exe}.rc" @ONLY) + endforeach() endif() endif() add_custom_target(genversion_cpp DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp") if(WIN32) - add_custom_target(genversion_rc DEPENDS "${CMAKE_BINARY_DIR}/version.rc" "${CMAKE_BINARY_DIR}/lokinet_resource.h") + add_custom_target(genversion_rc DEPENDS "${CMAKE_BINARY_DIR}/lokinet.rc" "${CMAKE_BINARY_DIR}/lokinet-vpn.rc" "${CMAKE_BINARY_DIR}/lokinet-bootstrap.rc") else() add_custom_target(genversion_rc) endif() diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt index a65ed660a..233c436ce 100644 --- a/daemon/CMakeLists.txt +++ b/daemon/CMakeLists.txt @@ -49,7 +49,7 @@ endif() foreach(exe ${exetargets}) if(WIN32 AND NOT MSVC_VERSION) - target_sources(${exe} PRIVATE ${CMAKE_BINARY_DIR}/version.rc) + target_sources(${exe} PRIVATE ${CMAKE_BINARY_DIR}/${exe}.rc) target_link_libraries(${exe} PRIVATE -static-libstdc++ -static-libgcc --static -Wl,--pic-executable,-e,mainCRTStartup,--subsystem,console:5.00) target_link_libraries(${exe} PRIVATE ws2_32 iphlpapi) elseif(CMAKE_SYSTEM_NAME MATCHES "FreeBSD") diff --git a/llarp/win32/resource.h.in b/llarp/win32/resource.h.in deleted file mode 100644 index 477e93b9b..000000000 --- a/llarp/win32/resource.h.in +++ /dev/null @@ -1,17 +0,0 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by version.rc -// - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 101 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1000 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif -// clang-format off -#define lokinet_VERSION @lokinet_VERSION_MAJOR@, @lokinet_VERSION_MINOR@, @lokinet_VERSION_PATCH@, 0 diff --git a/llarp/win32/version.rc.in b/llarp/win32/version.rc.in index 23b4e996d..d0021caa3 100644 --- a/llarp/win32/version.rc.in +++ b/llarp/win32/version.rc.in @@ -6,7 +6,10 @@ // // Microsoft Visual C++ generated resource script. // -#include "lokinet_resource.h" + +// clang-format off +#define lokinet_VERSION @lokinet_VERSION_MAJOR@, @lokinet_VERSION_MINOR@, @lokinet_VERSION_PATCH@, 0 + #ifdef __GNUC__ // make windows rc accept this #include #endif @@ -72,7 +75,7 @@ BEGIN VALUE "FileVersion", "@lokinet_VERSION@" VALUE "InternalName", "lokinet" 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." - VALUE "OriginalFilename", "lokinet.exe" + VALUE "OriginalFilename", "@lokinet_EXE_NAME@" VALUE "ProductName", "LokiNET for Windows" VALUE "ProductVersion", "@lokinet_VERSION@" END From 065e52ab803818d47fa85ef3ffa3ea4bf354ceb3 Mon Sep 17 00:00:00 2001 From: Jeff Date: Fri, 27 May 2022 11:33:44 -0400 Subject: [PATCH 179/202] move windows nsis parts for cpack into external files --- cmake/win32_installer_deps.cmake | 28 ++++++++++++++++++++-------- win32-setup/extra_create_icons.nsis | 1 + win32-setup/extra_delete_icons.nsis | 1 + win32-setup/extra_install.nsis | 7 +++++++ win32-setup/extra_preinstall.nsis | 6 ++++++ win32-setup/extra_uninstall.nsis | 5 +++++ 6 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 win32-setup/extra_create_icons.nsis create mode 100644 win32-setup/extra_delete_icons.nsis create mode 100644 win32-setup/extra_install.nsis create mode 100644 win32-setup/extra_preinstall.nsis create mode 100644 win32-setup/extra_uninstall.nsis diff --git a/cmake/win32_installer_deps.cmake b/cmake/win32_installer_deps.cmake index 81ea1eac2..825eda90f 100644 --- a/cmake/win32_installer_deps.cmake +++ b/cmake/win32_installer_deps.cmake @@ -27,14 +27,26 @@ set(CPACK_PACKAGE_INSTALL_DIRECTORY "Lokinet") set(CPACK_NSIS_MUI_ICON "${CMAKE_SOURCE_DIR}/win32-setup/lokinet.ico") set(CPACK_NSIS_DEFINES "RequestExecutionLevel admin") set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON) -set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "ifFileExists $INSTDIR\\\\bin\\\\tuntap-install.exe 0 +2\\nExecWait '$INSTDIR\\\\bin\\\\tuntap-install.exe /S'\\nExecWait '$INSTDIR\\\\bin\\\\lokinet.exe --install'\\nExecWait 'sc failure lokinet reset= 60 actions= restart/1000'\\nExecWait '$INSTDIR\\\\bin\\\\lokinet.exe -g C:\\\\ProgramData\\\\lokinet\\\\lokinet.ini'\\nCopyFiles '$INSTDIR\\\\share\\\\bootstrap.signed' C:\\\\ProgramData\\\\lokinet\\\\bootstrap.signed\\n") -set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS "ExecWait 'net stop lokinet'\\nExecWait 'taskkill /f /t /im lokinet-gui.exe'\\nExecWait '$INSTDIR\\\\bin\\\\lokinet.exe --remove'\\nRMDir /r /REBOOTOK C:\\\\ProgramData\\\\lokinet") -set(CPACK_NSIS_CREATE_ICONS_EXTRA - "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\Lokinet.lnk' '$INSTDIR\\\\share\\\\gui\\\\lokinet-gui.exe'" -) -set(CPACK_NSIS_DELETE_ICONS_EXTRA - "Delete '$SMPROGRAMS\\\\$START_MENU\\\\Lokinet.lnk'" -) + + +function(read_nsis_file filename outvar) + file(STRINGS "${filename}" _outvar) + list(TRANSFORM _outvar REPLACE "\\\\" "\\\\\\\\") + list(JOIN _outvar "\\n" out) + set(${outvar} ${out} PARENT_SCOPE) +endfunction() + +read_nsis_file("${CMAKE_SOURCE_DIR}/win32-setup/extra_preinstall.nsis" _extra_preinstall) +read_nsis_file("${CMAKE_SOURCE_DIR}/win32-setup/extra_install.nsis" _extra_install) +read_nsis_file("${CMAKE_SOURCE_DIR}/win32-setup/extra_uninstall.nsis" _extra_uninstall) +read_nsis_file("${CMAKE_SOURCE_DIR}/win32-setup/extra_create_icons.nsis" _extra_create_icons) +read_nsis_file("${CMAKE_SOURCE_DIR}/win32-setup/extra_delete_icons.nsis" _extra_delete_icons) + +set(CPACK_NSIS_EXTRA_PREINSTALL_COMMANDS "${_extra_preinstall}") +set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS "${_extra_install}") +set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS "${_extra_uninstall}") +set(CPACK_NSIS_CREATE_ICONS_EXTRA "${_extra_create_icons}") +set(CPACK_NSIS_DELETE_ICONS_EXTRA "${_extra_delete_icons}") get_cmake_property(CPACK_COMPONENTS_ALL COMPONENTS) list(REMOVE_ITEM CPACK_COMPONENTS_ALL "Unspecified") diff --git a/win32-setup/extra_create_icons.nsis b/win32-setup/extra_create_icons.nsis new file mode 100644 index 000000000..75cc048f1 --- /dev/null +++ b/win32-setup/extra_create_icons.nsis @@ -0,0 +1 @@ +CreateShortCut '$SMPROGRAMS\$STARTMENU_FOLDER\Lokinet.lnk' '$INSTDIR\share\gui\lokinet-gui.exe' diff --git a/win32-setup/extra_delete_icons.nsis b/win32-setup/extra_delete_icons.nsis new file mode 100644 index 000000000..75cc048f1 --- /dev/null +++ b/win32-setup/extra_delete_icons.nsis @@ -0,0 +1 @@ +CreateShortCut '$SMPROGRAMS\$STARTMENU_FOLDER\Lokinet.lnk' '$INSTDIR\share\gui\lokinet-gui.exe' diff --git a/win32-setup/extra_install.nsis b/win32-setup/extra_install.nsis new file mode 100644 index 000000000..ec07fb540 --- /dev/null +++ b/win32-setup/extra_install.nsis @@ -0,0 +1,7 @@ +ifFileExists $INSTDIR\bin\tuntap-install.exe 0 +2 +ExecWait '$INSTDIR\bin\tuntap-install.exe /S' +ExecWait '$INSTDIR\bin\lokinet.exe --install' +ExecWait 'sc failure lokinet reset= 60 actions= restart/1000' +ExecWait '$INSTDIR\bin\lokinet.exe -g C:\ProgramData\lokinet\lokinet.ini' +CopyFiles '$INSTDIR\share\bootstrap.signed' C:\ProgramData\lokinet\bootstrap.signed + diff --git a/win32-setup/extra_preinstall.nsis b/win32-setup/extra_preinstall.nsis new file mode 100644 index 000000000..de3904f6f --- /dev/null +++ b/win32-setup/extra_preinstall.nsis @@ -0,0 +1,6 @@ +IfFileExists $INSTDIR\bin\lokinet.exe 0 +3 +ExecWait 'net stop lokinet' +ExecWait '$INSTDIR\bin\lokinet.exe --remove' + +IfFileExists $INSTDIR\share\gui\lokinet.exe 0 +2 +ExecWait 'taskkill /f /t /im lokinet-gui.exe' diff --git a/win32-setup/extra_uninstall.nsis b/win32-setup/extra_uninstall.nsis new file mode 100644 index 000000000..ea8664219 --- /dev/null +++ b/win32-setup/extra_uninstall.nsis @@ -0,0 +1,5 @@ +ExecWait 'net stop lokinet' +ExecWait 'taskkill /f /t /im lokinet-gui.exe' +ExecWait '$INSTDIR\bin\lokinet.exe --remove' +RMDir /r /REBOOTOK C:\ProgramData\lokinet + From 1dfc2e883b001641f5054a2bfd6940453d6658f9 Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 30 May 2022 10:29:23 -0400 Subject: [PATCH 180/202] bump verions of all deps bump submodules Fix dynamic version generation - GIT_FOUND OR Git_FOUND wasn't available because we hadn't done a find_package(Git) yet. - required version variables weren't being passed through to the cmake script --- CMakeLists.txt | 4 ++-- cmake/GenVersion.cmake | 2 +- cmake/StaticBuild.cmake | 20 ++++++++++---------- cmake/Version.cmake | 14 ++++++++++---- external/oxen-encoding | 2 +- external/oxen-mq | 2 +- 6 files changed, 25 insertions(+), 19 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 007daf6e9..d8bb2bfae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -177,7 +177,7 @@ endif() option(FORCE_OXENC_SUBMODULE "force using oxen-encoding submodule" OFF) if(NOT FORCE_OXENC_SUBMODULE) - pkg_check_modules(OXENC liboxenc>=1.0.2 IMPORTED_TARGET) + pkg_check_modules(OXENC liboxenc>=1.0.3 IMPORTED_TARGET) endif() if(OXENC_FOUND) @@ -197,7 +197,7 @@ endif() option(FORCE_OXENMQ_SUBMODULE "force using oxenmq submodule" OFF) if(NOT FORCE_OXENMQ_SUBMODULE) - pkg_check_modules(OXENMQ liboxenmq>=1.2.4 IMPORTED_TARGET) + pkg_check_modules(OXENMQ liboxenmq>=1.2.12 IMPORTED_TARGET) endif() if(OXENMQ_FOUND) add_library(oxenmq::oxenmq ALIAS PkgConfig::OXENMQ) diff --git a/cmake/GenVersion.cmake b/cmake/GenVersion.cmake index 21aeab9df..9cda8b8ae 100644 --- a/cmake/GenVersion.cmake +++ b/cmake/GenVersion.cmake @@ -57,4 +57,4 @@ else() endif() endif() -configure_file("${SRC}" "${DEST}") +configure_file("${SRC}" "${DEST}" @ONLY) diff --git a/cmake/StaticBuild.cmake b/cmake/StaticBuild.cmake index c92c733b9..3ddbe9757 100644 --- a/cmake/StaticBuild.cmake +++ b/cmake/StaticBuild.cmake @@ -5,31 +5,31 @@ set(LOCAL_MIRROR "" CACHE STRING "local mirror path/URL for lib downloads") -set(OPENSSL_VERSION 1.1.1m CACHE STRING "openssl version") +set(OPENSSL_VERSION 1.1.1o CACHE STRING "openssl version") set(OPENSSL_MIRROR ${LOCAL_MIRROR} https://www.openssl.org/source CACHE STRING "openssl download mirror(s)") set(OPENSSL_SOURCE openssl-${OPENSSL_VERSION}.tar.gz) -set(OPENSSL_HASH SHA256=f89199be8b23ca45fc7cb9f1d8d3ee67312318286ad030f5316aca6462db6c96 +set(OPENSSL_HASH SHA256=9384a2b0570dd80358841464677115df785edb941c71211f75076d72fe6b438f CACHE STRING "openssl source hash") -set(EXPAT_VERSION 2.4.4 CACHE STRING "expat version") +set(EXPAT_VERSION 2.4.8 CACHE STRING "expat version") string(REPLACE "." "_" EXPAT_TAG "R_${EXPAT_VERSION}") set(EXPAT_MIRROR ${LOCAL_MIRROR} https://github.com/libexpat/libexpat/releases/download/${EXPAT_TAG} CACHE STRING "expat download mirror(s)") set(EXPAT_SOURCE expat-${EXPAT_VERSION}.tar.xz) -set(EXPAT_HASH SHA256=b5d25d6e373351c2ed19b562b4732d01d2589ac8c8e9e7962d8df1207cc311b8 +set(EXPAT_HASH SHA256=f79b8f904b749e3e0d20afeadecf8249c55b2e32d4ebb089ae378df479dcaf25 CACHE STRING "expat source hash") -set(UNBOUND_VERSION 1.14.0 CACHE STRING "unbound version") +set(UNBOUND_VERSION 1.15.0 CACHE STRING "unbound version") set(UNBOUND_MIRROR ${LOCAL_MIRROR} https://nlnetlabs.nl/downloads/unbound CACHE STRING "unbound download mirror(s)") set(UNBOUND_SOURCE unbound-${UNBOUND_VERSION}.tar.gz) -set(UNBOUND_HASH SHA256=6ef91cbf02d5299eab39328c0857393de7b4885a2fe7233ddfe3c124ff5a89c8 +set(UNBOUND_HASH SHA256=a480dc6c8937447b98d161fe911ffc76cfaffa2da18788781314e81339f1126f CACHE STRING "unbound source hash") -set(SQLITE3_VERSION 3370200 CACHE STRING "sqlite3 version") +set(SQLITE3_VERSION 3380500 CACHE STRING "sqlite3 version") set(SQLITE3_MIRROR ${LOCAL_MIRROR} https://www.sqlite.org/2022 CACHE STRING "sqlite3 download mirror(s)") set(SQLITE3_SOURCE sqlite-autoconf-${SQLITE3_VERSION}.tar.gz) -set(SQLITE3_HASH SHA3_256=3764f471d188ef4e7a70a120f6cb80014dc50bb5fa53406b566508390a32e745 +set(SQLITE3_HASH SHA3_256=ab649fea76f49a6ec7f907f001d87b8bd76dec0679c783e3992284c5a882a98c CACHE STRING "sqlite3 source hash") set(SODIUM_VERSION 1.0.18 CACHE STRING "libsodium version") @@ -62,11 +62,11 @@ set(ZLIB_SOURCE zlib-${ZLIB_VERSION}.tar.gz) set(ZLIB_HASH SHA256=91844808532e5ce316b3c010929493c0244f3d37593afd6de04f71821d5136d9 CACHE STRING "zlib source hash") -set(CURL_VERSION 7.81.0 CACHE STRING "curl version") +set(CURL_VERSION 7.83.1 CACHE STRING "curl version") set(CURL_MIRROR ${LOCAL_MIRROR} https://curl.haxx.se/download https://curl.askapache.com CACHE STRING "curl mirror(s)") set(CURL_SOURCE curl-${CURL_VERSION}.tar.xz) -set(CURL_HASH SHA256=a067b688d1645183febc31309ec1f3cdce9213d02136b6a6de3d50f69c95a7d3 +set(CURL_HASH SHA256=2cb9c2356e7263a1272fd1435ef7cdebf2cd21400ec287b068396deb705c22c4 CACHE STRING "curl source hash") include(ExternalProject) diff --git a/cmake/Version.cmake b/cmake/Version.cmake index 79cc10250..6a9a8e466 100644 --- a/cmake/Version.cmake +++ b/cmake/Version.cmake @@ -2,12 +2,18 @@ set(VERSIONTAG "${GIT_VERSION}") set(GIT_INDEX_FILE "${PROJECT_SOURCE_DIR}/.git/index") -if(EXISTS ${GIT_INDEX_FILE} AND ( GIT_FOUND OR Git_FOUND) ) +find_package(Git) +if(EXISTS "${GIT_INDEX_FILE}" AND ( GIT_FOUND OR Git_FOUND) ) message(STATUS "Found Git: ${GIT_EXECUTABLE}") + set(genversion_args "-DGIT=${GIT_EXECUTABLE}") + foreach(v lokinet_VERSION lokinet_VERSION_MAJOR lokinet_VERSION_MINOR lokinet_VERSION_PATCH RELEASE_MOTTO) + list(APPEND genversion_args "-D${v}=${${v}}") + endforeach() + add_custom_command( OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp" COMMAND "${CMAKE_COMMAND}" - "-D" "GIT=${GIT_EXECUTABLE}" + ${genversion_args} "-D" "SRC=${CMAKE_CURRENT_SOURCE_DIR}/constants/version.cpp.in" "-D" "DEST=${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp" "-P" "${CMAKE_CURRENT_LIST_DIR}/GenVersion.cmake" @@ -15,11 +21,11 @@ if(EXISTS ${GIT_INDEX_FILE} AND ( GIT_FOUND OR Git_FOUND) ) "${GIT_INDEX_FILE}") if(WIN32) foreach(exe IN ITEMS lokinet lokinet-vpn lokinet-bootstrap) - set(lokinet_EXE_NAME "${exe}.exe") add_custom_command( OUTPUT "${CMAKE_BINARY_DIR}/${exe}.rc" COMMAND "${CMAKE_COMMAND}" - "-D" "GIT=${GIT_EXECUTABLE}" + ${genversion_args} + "-D" "lokinet_EXE_NAME=${exe}.exe" "-D" "SRC=${CMAKE_CURRENT_SOURCE_DIR}/win32/version.rc.in" "-D" "DEST=${CMAKE_BINARY_DIR}/${exe}.rc" "-P" "${CMAKE_CURRENT_LIST_DIR}/GenVersion.cmake" diff --git a/external/oxen-encoding b/external/oxen-encoding index b48aef693..79193e58f 160000 --- a/external/oxen-encoding +++ b/external/oxen-encoding @@ -1 +1 @@ -Subproject commit b48aef693b39a2408931c4ba25580648423c81a7 +Subproject commit 79193e58fb26624d40cd2e95156f78160f2b9b3e diff --git a/external/oxen-mq b/external/oxen-mq index 5c72a57ec..eadb37c76 160000 --- a/external/oxen-mq +++ b/external/oxen-mq @@ -1 +1 @@ -Subproject commit 5c72a57eca120750ecf557ce5a668fb38242956b +Subproject commit eadb37c7654150bef18497773718f15ef843734a From 6a2114fa1d6e123e3451d335a343d9917db13288 Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 30 May 2022 14:59:12 -0300 Subject: [PATCH 181/202] Don't be dynamic for win32 rc crap --- cmake/Version.cmake | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/cmake/Version.cmake b/cmake/Version.cmake index 6a9a8e466..a017995eb 100644 --- a/cmake/Version.cmake +++ b/cmake/Version.cmake @@ -19,28 +19,17 @@ if(EXISTS "${GIT_INDEX_FILE}" AND ( GIT_FOUND OR Git_FOUND) ) "-P" "${CMAKE_CURRENT_LIST_DIR}/GenVersion.cmake" DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/constants/version.cpp.in" "${GIT_INDEX_FILE}") - if(WIN32) - foreach(exe IN ITEMS lokinet lokinet-vpn lokinet-bootstrap) - add_custom_command( - OUTPUT "${CMAKE_BINARY_DIR}/${exe}.rc" - COMMAND "${CMAKE_COMMAND}" - ${genversion_args} - "-D" "lokinet_EXE_NAME=${exe}.exe" - "-D" "SRC=${CMAKE_CURRENT_SOURCE_DIR}/win32/version.rc.in" - "-D" "DEST=${CMAKE_BINARY_DIR}/${exe}.rc" - "-P" "${CMAKE_CURRENT_LIST_DIR}/GenVersion.cmake" - DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/win32/version.rc.in" - "${GIT_INDEX_FILE}") - endforeach() - endif() else() configure_file("${CMAKE_CURRENT_SOURCE_DIR}/constants/version.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp" @ONLY) - if(WIN32) - foreach(exe IN ITEMS lokinet lokinet-vpn lokinet-bootstrap) - set(lokinet_EXE_NAME "${exe}.exe") - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/win32/version.rc.in" "${CMAKE_BINARY_DIR}/${exe}.rc" @ONLY) - endforeach() - endif() +endif() + + +if(WIN32) + foreach(exe IN ITEMS lokinet lokinet-vpn lokinet-bootstrap) + set(lokinet_EXE_NAME "${exe}.exe") + configure_file("${CMAKE_CURRENT_SOURCE_DIR}/win32/version.rc.in" "${CMAKE_BINARY_DIR}/${exe}.rc" @ONLY) + set_property(SOURCE "${CMAKE_BINARY_DIR}/${exe}.rc" PROPERTY GENERATED 1) + endforeach() endif() add_custom_target(genversion_cpp DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp") From 73a49f54736b69770114f6c404a28d94c9429cce Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 30 May 2022 14:34:54 -0400 Subject: [PATCH 182/202] do not build liblokinet in windows, exe gets too big --- contrib/windows.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/windows.sh b/contrib/windows.sh index 32ec7aaac..7520936e9 100755 --- a/contrib/windows.sh +++ b/contrib/windows.sh @@ -17,7 +17,7 @@ cmake \ -DBUILD_PACKAGE=ON \ -DBUILD_SHARED_LIBS=OFF \ -DBUILD_TESTING=OFF \ - -DBUILD_LIBLOKINET=ON \ + -DBUILD_LIBLOKINET=OFF \ -DWITH_TESTS=OFF \ -DNATIVE_BUILD=OFF \ -DSTATIC_LINK=ON \ From 34c3b8d0deafa777fa40e2f7a06689a9c66d26aa Mon Sep 17 00:00:00 2001 From: Jason Rhinelander Date: Mon, 30 May 2022 17:06:27 -0300 Subject: [PATCH 183/202] Add oxen repo to bionic for updated cmake version Also rename the arg from loki_repo to oxen_repo --- .drone.jsonnet | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index d7ead8a5c..87aaf71ff 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -40,7 +40,7 @@ local debian_pipeline(name, extra_cmds=[], jobs=6, tests=true, - loki_repo=false, + oxen_repo=false, allow_fail=false) = { kind: 'pipeline', type: 'docker', @@ -61,7 +61,7 @@ local debian_pipeline(name, apt_get_quiet + ' update', apt_get_quiet + ' install -y eatmydata', ] + ( - if loki_repo then [ + if oxen_repo then [ 'eatmydata ' + apt_get_quiet + ' install --no-install-recommends -y lsb-release', 'cp contrib/deb.oxen.io.gpg /etc/apt/trusted.gpg.d', 'echo deb http://deb.oxen.io $$(lsb_release -sc) main >/etc/apt/sources.list.d/oxen.list', @@ -180,7 +180,7 @@ local linux_cross_pipeline(name, }; // Builds a snapshot .deb on a debian-like system by merging into the debian/* or ubuntu/* branch -local deb_builder(image, distro, distro_branch, arch='amd64', loki_repo=true) = { +local deb_builder(image, distro, distro_branch, arch='amd64', oxen_repo=true) = { kind: 'pipeline', type: 'docker', name: 'DEB (' + distro + (if arch == 'amd64' then '' else '/' + arch) + ')', @@ -197,7 +197,7 @@ local deb_builder(image, distro, distro_branch, arch='amd64', loki_repo=true) = commands: [ 'echo "Building on ${DRONE_STAGE_MACHINE}"', 'echo "man-db man-db/auto-update boolean false" | debconf-set-selections', - ] + (if loki_repo then [ + ] + (if oxen_repo then [ 'cp contrib/deb.oxen.io.gpg /etc/apt/trusted.gpg.d', 'echo deb http://deb.oxen.io $${distro} main >/etc/apt/sources.list.d/oxen.list', ] else []) + [ @@ -338,7 +338,7 @@ local docs_pipeline(name, image, extra_cmds=[], allow_fail=false) = { docker_base + 'ubuntu-bionic', deps=['g++-8'] + default_deps_nocxx, cmake_extra='-DCMAKE_C_COMPILER=gcc-8 -DCMAKE_CXX_COMPILER=g++-8', - loki_repo=true), + oxen_repo=true), // ARM builds (ARM64 and armhf) debian_pipeline('Debian sid (ARM64)', docker_base + 'debian-sid', arch='arm64', jobs=4), @@ -366,6 +366,7 @@ local docs_pipeline(name, image, extra_cmds=[], allow_fail=false) = { deps=['g++-8', 'python3-dev', 'automake', 'libtool'], lto=true, tests=false, + oxen_repo=true, cmake_extra='-DBUILD_STATIC_DEPS=ON -DBUILD_SHARED_LIBS=OFF -DSTATIC_LINK=ON ' + '-DCMAKE_C_COMPILER=gcc-8 -DCMAKE_CXX_COMPILER=g++-8 ' + '-DCMAKE_CXX_FLAGS="-march=x86-64 -mtune=haswell" ' + From 2576b87c5de995cc66ae47e49d2e70f2fa7de16b Mon Sep 17 00:00:00 2001 From: Jeff Date: Mon, 30 May 2022 09:07:56 -0400 Subject: [PATCH 184/202] version bump to 0.9.9 --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d8bb2bfae..bf5e6cabd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ endif() project(lokinet - VERSION 0.9.8 + VERSION 0.9.9 DESCRIPTION "lokinet - IP packet onion router" LANGUAGES ${LANGS}) @@ -35,7 +35,7 @@ if(APPLE) set(LOKINET_APPLE_BUILD 0) endif() -set(RELEASE_MOTTO "The Current Thing" CACHE STRING "Release motto") +set(RELEASE_MOTTO "Gluten Free Edition" CACHE STRING "Release motto") list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake") From 55866f8b81a34d5955b67237c854efba9095ee40 Mon Sep 17 00:00:00 2001 From: necro-nemsis Date: Sat, 18 Jun 2022 12:45:07 -0400 Subject: [PATCH 185/202] Merge v0.9.9 --- SPECS/lokinet.spec | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/SPECS/lokinet.spec b/SPECS/lokinet.spec index 65cd62b15..8b69ea1b9 100644 --- a/SPECS/lokinet.spec +++ b/SPECS/lokinet.spec @@ -1,5 +1,5 @@ Name: lokinet -Version: 0.9.8 +Version: 0.9.9 Release: 1%{?dist} Summary: Lokinet anonymous, decentralized overlay network @@ -156,6 +156,9 @@ fi %systemd_postun lokinet.service %changelog +* Sat Jun 18 2022 Technical Tumbleweed - 0.9.8-1 +- 0.9.9 release + * Wed Nov 17 2021 Technical Tumbleweed - 0.9.8-1 - bump version From cdad3e7f093c4b0c69f73580e4fbacc24067db96 Mon Sep 17 00:00:00 2001 From: necro-nemsis Date: Sun, 19 Jun 2022 06:47:01 -0400 Subject: [PATCH 186/202] Change default port to 953 --- llarp/config/config.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp index 6bff96112..9c89bce6f 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp @@ -760,7 +760,10 @@ namespace llarp // can bind to other 127.* IPs to avoid conflicting with something else that may be listening on // 127.0.0.1:53. #ifdef __linux__ - constexpr Default DefaultDNSBind{"127.3.2.1:53"}; + // Fedora's systemd-resolved seems unable to connect to 127.3.2.1 for unknown reasons, + // however since systemd-resolved is perfectly happy with a different port so listen on + // localhost:953 as a workaround. + constexpr Default DefaultDNSBind{"127.3.2.1:953"}; #else constexpr Default DefaultDNSBind{"127.0.0.1:53"}; #endif From 34874019838533555ce9bfa15b269624d40b7473 Mon Sep 17 00:00:00 2001 From: necro-nemesis Date: Sun, 19 Jun 2022 07:03:08 -0400 Subject: [PATCH 187/202] update default-dns patch --- SOURCES/default-dns.patch | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/SOURCES/default-dns.patch b/SOURCES/default-dns.patch index b646e8a54..2232811b7 100644 --- a/SOURCES/default-dns.patch +++ b/SOURCES/default-dns.patch @@ -1,8 +1,17 @@ +From cdad3e7f093c4b0c69f73580e4fbacc24067db96 Mon Sep 17 00:00:00 2001 +From: necro-nemsis +Date: Sun, 19 Jun 2022 06:47:01 -0400 +Subject: [PATCH] Change default port to 953 + +--- + llarp/config/config.cpp | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) + diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp -index 78d152602..8b07b0cec 100644 +index 6bff9611..9c89bce6 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp -@@ -703,7 +703,10 @@ namespace llarp +@@ -760,7 +760,10 @@ namespace llarp // can bind to other 127.* IPs to avoid conflicting with something else that may be listening on // 127.0.0.1:53. #ifdef __linux__ @@ -10,7 +19,10 @@ index 78d152602..8b07b0cec 100644 + // Fedora's systemd-resolved seems unable to connect to 127.3.2.1 for unknown reasons, + // however since systemd-resolved is perfectly happy with a different port so listen on + // localhost:953 as a workaround. -+ constexpr Default DefaultDNSBind{"127.0.0.1:953"}; ++ constexpr Default DefaultDNSBind{"127.3.2.1:953"}; #else constexpr Default DefaultDNSBind{"127.0.0.1:53"}; #endif +-- +2.30.2 + From f3ed998bb1ba9e501f753cedf1d9c2136ae4a59d Mon Sep 17 00:00:00 2001 From: necro-nemsis Date: Sun, 19 Jun 2022 07:14:24 -0400 Subject: [PATCH 188/202] Disable version-as-rpm patch --- SPECS/lokinet.spec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SPECS/lokinet.spec b/SPECS/lokinet.spec index 8b69ea1b9..76268ed05 100644 --- a/SPECS/lokinet.spec +++ b/SPECS/lokinet.spec @@ -23,10 +23,10 @@ BuildRequires: jemalloc-devel BuildRequires: libsqlite3x-devel # Puts the rpm version instead of the git tag in the version string: -Patch1: version-as-rpm-version.patch +# Patch1: version-as-rpm-version.patch # Changes the default dns listener to 127.0.0.1:953 because Fedora's systemd-resolved doesn't like # talking to 127.3.2.1:53 for unknown reasons. -Patch2: default-dns.patch +Patch1: default-dns.patch Requires: lokinet-bin = %{version}-%{release} %{?systemd_requires} From 67e80929e1bb5860699d0a73b2e3265e5a4e8b63 Mon Sep 17 00:00:00 2001 From: necro-nemsis Date: Sun, 19 Jun 2022 07:31:19 -0400 Subject: [PATCH 189/202] Comment regarding lokinet/cmake/Version.cmake --- SPECS/lokinet.spec | 1 + 1 file changed, 1 insertion(+) diff --git a/SPECS/lokinet.spec b/SPECS/lokinet.spec index 76268ed05..ccabef0bd 100644 --- a/SPECS/lokinet.spec +++ b/SPECS/lokinet.spec @@ -24,6 +24,7 @@ BuildRequires: libsqlite3x-devel # Puts the rpm version instead of the git tag in the version string: # Patch1: version-as-rpm-version.patch +# Sun Jun 19 2022 Request Jason details to changes lokinet/cmake/Version.cmake # Changes the default dns listener to 127.0.0.1:953 because Fedora's systemd-resolved doesn't like # talking to 127.3.2.1:53 for unknown reasons. Patch1: default-dns.patch From 0439bde6d414027a3cf24760e12263dfcdecd727 Mon Sep 17 00:00:00 2001 From: necro-nemesis Date: Tue, 28 Jun 2022 21:39:19 -0400 Subject: [PATCH 190/202] Update lokinet.spec --- SPECS/lokinet.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPECS/lokinet.spec b/SPECS/lokinet.spec index ccabef0bd..f4461ee60 100644 --- a/SPECS/lokinet.spec +++ b/SPECS/lokinet.spec @@ -85,7 +85,7 @@ export CFLAGS="%{optflags} -march=armv6 -mtune=cortex-a53 -mfloat-abi=hard -mfpu %endif %undefine __cmake_in_source_build -%cmake -DNATIVE_BUILD=OFF -DUSE_AVX2=OFF -DWITH_TESTS=OFF %{cmake_extra_args} -DCMAKE_BUILD_TYPE=Release -DGIT_VERSION="%{release}" -DWITH_SETCAP=OFF -DSUBMODULE_CHECK=OFF -DBUILD_SHARED_LIBS=OFF -DBUILD_LIBLOKINET=OFF +%cmake -DFORCE_OXENC_SUBMODULE=ON -DFORCE_OXENMQ_SUBMODULE=ON -DNATIVE_BUILD=OFF -DUSE_AVX2=OFF -DWITH_TESTS=OFF %{cmake_extra_args} -DCMAKE_BUILD_TYPE=Release -DLOKINET_VERSIONTAG=%{release} -DWITH_SETCAP=OFF -DSUBMODULE_CHECK=OFF -DBUILD_SHARED_LIBS=OFF -DBUILD_LIBLOKINET=OFF %cmake_build %install From 6548ac498157bcb11a8af7208c4b006c6b92b2d5 Mon Sep 17 00:00:00 2001 From: necro-nemesis Date: Tue, 28 Jun 2022 21:44:16 -0400 Subject: [PATCH 191/202] Update lokinet.spec --- SPECS/lokinet.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPECS/lokinet.spec b/SPECS/lokinet.spec index f4461ee60..3d0751d23 100644 --- a/SPECS/lokinet.spec +++ b/SPECS/lokinet.spec @@ -27,7 +27,7 @@ BuildRequires: libsqlite3x-devel # Sun Jun 19 2022 Request Jason details to changes lokinet/cmake/Version.cmake # Changes the default dns listener to 127.0.0.1:953 because Fedora's systemd-resolved doesn't like # talking to 127.3.2.1:53 for unknown reasons. -Patch1: default-dns.patch +# Patch1: default-dns.patch Requires: lokinet-bin = %{version}-%{release} %{?systemd_requires} From f61515dae53a104ed257395690d0ecad54c2a409 Mon Sep 17 00:00:00 2001 From: necro-nemesis Date: Tue, 28 Jun 2022 21:51:32 -0400 Subject: [PATCH 192/202] Update lokinet.spec --- SPECS/lokinet.spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPECS/lokinet.spec b/SPECS/lokinet.spec index 3d0751d23..b8f1b9546 100644 --- a/SPECS/lokinet.spec +++ b/SPECS/lokinet.spec @@ -101,7 +101,7 @@ install -Dm644 SOURCES/bootstrap.signed $RPM_BUILD_ROOT%{_sharedstatedir}/lokine %files -%license LICENSE.txt +%license LICENSE %doc readme.* %{_datadir}/polkit-1/rules.d/50-lokinet.rules %{_unitdir}/lokinet.service From fe7657c8fbee0bf5bb0792686a748772619f8fb9 Mon Sep 17 00:00:00 2001 From: necro-nemesis Date: Tue, 28 Jun 2022 22:30:05 -0400 Subject: [PATCH 193/202] backup patch --- SOURCES/{default-dns.patch => default-dns.patch.bak} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename SOURCES/{default-dns.patch => default-dns.patch.bak} (100%) diff --git a/SOURCES/default-dns.patch b/SOURCES/default-dns.patch.bak similarity index 100% rename from SOURCES/default-dns.patch rename to SOURCES/default-dns.patch.bak From cbf7e3180643dffba9161a60f5b49fcd94e9c2be Mon Sep 17 00:00:00 2001 From: necro-nemesis Date: Tue, 28 Jun 2022 22:31:54 -0400 Subject: [PATCH 194/202] Create default-dns.patch from jammy patch --- SOURCES/default-dns.patch | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 SOURCES/default-dns.patch diff --git a/SOURCES/default-dns.patch b/SOURCES/default-dns.patch new file mode 100644 index 000000000..dc742a296 --- /dev/null +++ b/SOURCES/default-dns.patch @@ -0,0 +1,26 @@ +From: Jason Rhinelander +Date: Wed, 18 May 2022 12:47:59 -0300 +Subject: Change default DNS to 127.0.0.1:953 + +This is a needed workaround for a systemd resolved bug 23010, until +0.9.10 comes out (which will have a better fix to listen on both +127.3.2.1 + 127.0.0.1:random-high-port). +--- + llarp/config/config.cpp | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp +index 6bff961..a01f560 100644 +--- a/llarp/config/config.cpp ++++ b/llarp/config/config.cpp +@@ -760,7 +760,9 @@ namespace llarp + // can bind to other 127.* IPs to avoid conflicting with something else that may be listening on + // 127.0.0.1:53. + #ifdef __linux__ +- constexpr Default DefaultDNSBind{"127.3.2.1:53"}; ++ // Workaround for systemd bug #23010 that breaks 127.0.0.0/8 resolvers on anything other than ++ // 127.0.0.1. (This is temporary, until lokinet PR #1888 is merged in 0.9.10). ++ constexpr Default DefaultDNSBind{"127.0.0.1:953"}; + #else + constexpr Default DefaultDNSBind{"127.0.0.1:53"}; + #endif From 7c5c30f32b138e4840cd5f48cdbe982df080de86 Mon Sep 17 00:00:00 2001 From: necro-nemesis Date: Tue, 28 Jun 2022 22:34:00 -0400 Subject: [PATCH 195/202] include default-dns.patch --- SPECS/lokinet.spec | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/SPECS/lokinet.spec b/SPECS/lokinet.spec index b8f1b9546..2f6924d9a 100644 --- a/SPECS/lokinet.spec +++ b/SPECS/lokinet.spec @@ -22,12 +22,9 @@ BuildRequires: libcurl-devel BuildRequires: jemalloc-devel BuildRequires: libsqlite3x-devel -# Puts the rpm version instead of the git tag in the version string: -# Patch1: version-as-rpm-version.patch -# Sun Jun 19 2022 Request Jason details to changes lokinet/cmake/Version.cmake # Changes the default dns listener to 127.0.0.1:953 because Fedora's systemd-resolved doesn't like # talking to 127.3.2.1:53 for unknown reasons. -# Patch1: default-dns.patch +Patch1: default-dns.patch Requires: lokinet-bin = %{version}-%{release} %{?systemd_requires} From 11040080a94138305fbd23b8e94170b9b0cf8eaa Mon Sep 17 00:00:00 2001 From: necro-nemsis Date: Tue, 28 Jun 2022 22:52:32 -0400 Subject: [PATCH 196/202] Revert "Change default port to 953" This reverts commit cdad3e7f093c4b0c69f73580e4fbacc24067db96. --- llarp/config/config.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp index 9c89bce6f..6bff96112 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp @@ -760,10 +760,7 @@ namespace llarp // can bind to other 127.* IPs to avoid conflicting with something else that may be listening on // 127.0.0.1:53. #ifdef __linux__ - // Fedora's systemd-resolved seems unable to connect to 127.3.2.1 for unknown reasons, - // however since systemd-resolved is perfectly happy with a different port so listen on - // localhost:953 as a workaround. - constexpr Default DefaultDNSBind{"127.3.2.1:953"}; + constexpr Default DefaultDNSBind{"127.3.2.1:53"}; #else constexpr Default DefaultDNSBind{"127.0.0.1:53"}; #endif From 042e227f959d6c94809b8b99e39330dc63497f24 Mon Sep 17 00:00:00 2001 From: necro-nemsis Date: Wed, 29 Jun 2022 18:07:00 -0400 Subject: [PATCH 197/202] Update patch and SPEC --- SOURCES/default-dns.patch | 28 +++++++++++++++------------- SOURCES/default-dns.patch.bak | 28 ---------------------------- SOURCES/default-dns.patch.jason | 26 ++++++++++++++++++++++++++ SPECS/lokinet.spec | 7 +++++-- 4 files changed, 46 insertions(+), 43 deletions(-) delete mode 100644 SOURCES/default-dns.patch.bak create mode 100644 SOURCES/default-dns.patch.jason diff --git a/SOURCES/default-dns.patch b/SOURCES/default-dns.patch index dc742a296..2232811b7 100644 --- a/SOURCES/default-dns.patch +++ b/SOURCES/default-dns.patch @@ -1,26 +1,28 @@ -From: Jason Rhinelander -Date: Wed, 18 May 2022 12:47:59 -0300 -Subject: Change default DNS to 127.0.0.1:953 +From cdad3e7f093c4b0c69f73580e4fbacc24067db96 Mon Sep 17 00:00:00 2001 +From: necro-nemsis +Date: Sun, 19 Jun 2022 06:47:01 -0400 +Subject: [PATCH] Change default port to 953 -This is a needed workaround for a systemd resolved bug 23010, until -0.9.10 comes out (which will have a better fix to listen on both -127.3.2.1 + 127.0.0.1:random-high-port). --- - llarp/config/config.cpp | 4 +++- - 1 file changed, 3 insertions(+), 1 deletion(-) + llarp/config/config.cpp | 5 ++++- + 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp -index 6bff961..a01f560 100644 +index 6bff9611..9c89bce6 100644 --- a/llarp/config/config.cpp +++ b/llarp/config/config.cpp -@@ -760,7 +760,9 @@ namespace llarp +@@ -760,7 +760,10 @@ namespace llarp // can bind to other 127.* IPs to avoid conflicting with something else that may be listening on // 127.0.0.1:53. #ifdef __linux__ - constexpr Default DefaultDNSBind{"127.3.2.1:53"}; -+ // Workaround for systemd bug #23010 that breaks 127.0.0.0/8 resolvers on anything other than -+ // 127.0.0.1. (This is temporary, until lokinet PR #1888 is merged in 0.9.10). -+ constexpr Default DefaultDNSBind{"127.0.0.1:953"}; ++ // Fedora's systemd-resolved seems unable to connect to 127.3.2.1 for unknown reasons, ++ // however since systemd-resolved is perfectly happy with a different port so listen on ++ // localhost:953 as a workaround. ++ constexpr Default DefaultDNSBind{"127.3.2.1:953"}; #else constexpr Default DefaultDNSBind{"127.0.0.1:53"}; #endif +-- +2.30.2 + diff --git a/SOURCES/default-dns.patch.bak b/SOURCES/default-dns.patch.bak deleted file mode 100644 index 2232811b7..000000000 --- a/SOURCES/default-dns.patch.bak +++ /dev/null @@ -1,28 +0,0 @@ -From cdad3e7f093c4b0c69f73580e4fbacc24067db96 Mon Sep 17 00:00:00 2001 -From: necro-nemsis -Date: Sun, 19 Jun 2022 06:47:01 -0400 -Subject: [PATCH] Change default port to 953 - ---- - llarp/config/config.cpp | 5 ++++- - 1 file changed, 4 insertions(+), 1 deletion(-) - -diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp -index 6bff9611..9c89bce6 100644 ---- a/llarp/config/config.cpp -+++ b/llarp/config/config.cpp -@@ -760,7 +760,10 @@ namespace llarp - // can bind to other 127.* IPs to avoid conflicting with something else that may be listening on - // 127.0.0.1:53. - #ifdef __linux__ -- constexpr Default DefaultDNSBind{"127.3.2.1:53"}; -+ // Fedora's systemd-resolved seems unable to connect to 127.3.2.1 for unknown reasons, -+ // however since systemd-resolved is perfectly happy with a different port so listen on -+ // localhost:953 as a workaround. -+ constexpr Default DefaultDNSBind{"127.3.2.1:953"}; - #else - constexpr Default DefaultDNSBind{"127.0.0.1:53"}; - #endif --- -2.30.2 - diff --git a/SOURCES/default-dns.patch.jason b/SOURCES/default-dns.patch.jason new file mode 100644 index 000000000..dc742a296 --- /dev/null +++ b/SOURCES/default-dns.patch.jason @@ -0,0 +1,26 @@ +From: Jason Rhinelander +Date: Wed, 18 May 2022 12:47:59 -0300 +Subject: Change default DNS to 127.0.0.1:953 + +This is a needed workaround for a systemd resolved bug 23010, until +0.9.10 comes out (which will have a better fix to listen on both +127.3.2.1 + 127.0.0.1:random-high-port). +--- + llarp/config/config.cpp | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp +index 6bff961..a01f560 100644 +--- a/llarp/config/config.cpp ++++ b/llarp/config/config.cpp +@@ -760,7 +760,9 @@ namespace llarp + // can bind to other 127.* IPs to avoid conflicting with something else that may be listening on + // 127.0.0.1:53. + #ifdef __linux__ +- constexpr Default DefaultDNSBind{"127.3.2.1:53"}; ++ // Workaround for systemd bug #23010 that breaks 127.0.0.0/8 resolvers on anything other than ++ // 127.0.0.1. (This is temporary, until lokinet PR #1888 is merged in 0.9.10). ++ constexpr Default DefaultDNSBind{"127.0.0.1:953"}; + #else + constexpr Default DefaultDNSBind{"127.0.0.1:53"}; + #endif diff --git a/SPECS/lokinet.spec b/SPECS/lokinet.spec index 2f6924d9a..9d92ec44f 100644 --- a/SPECS/lokinet.spec +++ b/SPECS/lokinet.spec @@ -154,8 +154,11 @@ fi %systemd_postun lokinet.service %changelog -* Sat Jun 18 2022 Technical Tumbleweed - 0.9.8-1 -- 0.9.9 release +* Wed Jun 29 2022 Technical Tumbleweed - 0.9.9-1 +- bump version +- cmake flags for no system library search +- update port patch +- remove version patch * Wed Nov 17 2021 Technical Tumbleweed - 0.9.8-1 - bump version From dc7f635e20de235f860a9dd08a4dc7ba59b53150 Mon Sep 17 00:00:00 2001 From: necro-nemsis Date: Wed, 29 Jun 2022 18:15:57 -0400 Subject: [PATCH 198/202] remove dead patches --- SOURCES/default-dns.patch.jason | 26 -------------------------- SOURCES/version-as-rpm-version.patch | 28 ---------------------------- 2 files changed, 54 deletions(-) delete mode 100644 SOURCES/default-dns.patch.jason delete mode 100644 SOURCES/version-as-rpm-version.patch diff --git a/SOURCES/default-dns.patch.jason b/SOURCES/default-dns.patch.jason deleted file mode 100644 index dc742a296..000000000 --- a/SOURCES/default-dns.patch.jason +++ /dev/null @@ -1,26 +0,0 @@ -From: Jason Rhinelander -Date: Wed, 18 May 2022 12:47:59 -0300 -Subject: Change default DNS to 127.0.0.1:953 - -This is a needed workaround for a systemd resolved bug 23010, until -0.9.10 comes out (which will have a better fix to listen on both -127.3.2.1 + 127.0.0.1:random-high-port). ---- - llarp/config/config.cpp | 4 +++- - 1 file changed, 3 insertions(+), 1 deletion(-) - -diff --git a/llarp/config/config.cpp b/llarp/config/config.cpp -index 6bff961..a01f560 100644 ---- a/llarp/config/config.cpp -+++ b/llarp/config/config.cpp -@@ -760,7 +760,9 @@ namespace llarp - // can bind to other 127.* IPs to avoid conflicting with something else that may be listening on - // 127.0.0.1:53. - #ifdef __linux__ -- constexpr Default DefaultDNSBind{"127.3.2.1:53"}; -+ // Workaround for systemd bug #23010 that breaks 127.0.0.0/8 resolvers on anything other than -+ // 127.0.0.1. (This is temporary, until lokinet PR #1888 is merged in 0.9.10). -+ constexpr Default DefaultDNSBind{"127.0.0.1:953"}; - #else - constexpr Default DefaultDNSBind{"127.0.0.1:53"}; - #endif diff --git a/SOURCES/version-as-rpm-version.patch b/SOURCES/version-as-rpm-version.patch deleted file mode 100644 index 868783b3b..000000000 --- a/SOURCES/version-as-rpm-version.patch +++ /dev/null @@ -1,28 +0,0 @@ -From: Jason Rhinelander -Date: Fri, 13 Dec 2019 17:23:41 -0400 -Subject: Pass debian version as GIT_VERSION - ---- - cmake/Version.cmake | 5 +++++ - 1 file changed, 5 insertions(+) - -diff --git a/cmake/Version.cmake b/cmake/Version.cmake -index 45037a0..d9fdaef 100644 ---- a/cmake/Version.cmake -+++ b/cmake/Version.cmake -@@ -1,4 +1,8 @@ - -+if(GIT_VERSION) -+ set(VERSIONTAG "${GIT_VERSION}") -+ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/constants/version.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp") -+else() - find_package(Git QUIET) - set(GIT_INDEX_FILE "${PROJECT_SOURCE_DIR}/.git/index") - if(EXISTS ${GIT_INDEX_FILE} AND ( GIT_FOUND OR Git_FOUND) ) -@@ -18,5 +22,6 @@ else() - set(VERSIONTAG "nogit") - configure_file("${CMAKE_CURRENT_SOURCE_DIR}/constants/version.cpp.in" "${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp") - endif() -+endif() - - add_custom_target(genversion DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/constants/version.cpp") From c64a3316b10553b0576ac82528f092315d7dd90d Mon Sep 17 00:00:00 2001 From: necro-nemesis Date: Thu, 30 Jun 2022 03:25:55 -0400 Subject: [PATCH 199/202] Update lokinet.spec --- SPECS/lokinet.spec | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/SPECS/lokinet.spec b/SPECS/lokinet.spec index 97a2b4f72..9d92ec44f 100644 --- a/SPECS/lokinet.spec +++ b/SPECS/lokinet.spec @@ -1,9 +1,5 @@ Name: lokinet -<<<<<<< HEAD -Version: 0.9.8 -======= Version: 0.9.9 ->>>>>>> fedora/34 Release: 1%{?dist} Summary: Lokinet anonymous, decentralized overlay network @@ -158,11 +154,6 @@ fi %systemd_postun lokinet.service %changelog -<<<<<<< HEAD -* Wed Nov 17 2021 Technical Tumbleweed - 0.9.8-1 -- bump version -- bumped to build Sat Nov 20 2021 -======= * Wed Jun 29 2022 Technical Tumbleweed - 0.9.9-1 - bump version - cmake flags for no system library search @@ -171,7 +162,6 @@ fi * Wed Nov 17 2021 Technical Tumbleweed - 0.9.8-1 - bump version ->>>>>>> fedora/34 * Wed Oct 20 2021 Technical Tumbleweed - 0.9.7-1 - bump version From 391494ed11c97d52a0a3fbdf3e188be6a50a0285 Mon Sep 17 00:00:00 2001 From: necro-nemsis Date: Fri, 1 Jul 2022 09:07:33 -0400 Subject: [PATCH 200/202] Change patch address to localhost. --- SOURCES/default-dns.patch | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/SOURCES/default-dns.patch b/SOURCES/default-dns.patch index 2232811b7..32b7cc014 100644 --- a/SOURCES/default-dns.patch +++ b/SOURCES/default-dns.patch @@ -19,10 +19,9 @@ index 6bff9611..9c89bce6 100644 + // Fedora's systemd-resolved seems unable to connect to 127.3.2.1 for unknown reasons, + // however since systemd-resolved is perfectly happy with a different port so listen on + // localhost:953 as a workaround. -+ constexpr Default DefaultDNSBind{"127.3.2.1:953"}; ++ constexpr Default DefaultDNSBind{"127.0.0.1:953"}; #else constexpr Default DefaultDNSBind{"127.0.0.1:53"}; #endif --- +-- 2.30.2 - From 38a5d6e634ed38f5e03e8d56284fb0c90e108906 Mon Sep 17 00:00:00 2001 From: necro-nemsis Date: Fri, 1 Jul 2022 10:04:56 -0400 Subject: [PATCH 201/202] remove oxenmq-devel dependency --- SPECS/lokinet.spec | 1 - 1 file changed, 1 deletion(-) diff --git a/SPECS/lokinet.spec b/SPECS/lokinet.spec index 9d92ec44f..e9a0c0a97 100644 --- a/SPECS/lokinet.spec +++ b/SPECS/lokinet.spec @@ -13,7 +13,6 @@ BuildRequires: cmake BuildRequires: gcc-c++ BuildRequires: pkgconfig BuildRequires: libuv-devel -BuildRequires: oxenmq-devel BuildRequires: unbound-devel BuildRequires: libsodium-devel BuildRequires: systemd-devel From 1dab255c21a5e8939401b70d192a626765e52d0e Mon Sep 17 00:00:00 2001 From: necro-nemsis Date: Fri, 1 Jul 2022 11:21:04 -0400 Subject: [PATCH 202/202] Build on F35. --- .drone.jsonnet | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.drone.jsonnet b/.drone.jsonnet index b1c6222a6..97fd42718 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -1,6 +1,6 @@ -local distro = "fedora-36"; -local distro_name = 'Fedora 36'; -local distro_docker = 'fedora:36'; +local distro = "fedora-35"; +local distro_name = 'Fedora 35'; +local distro_docker = 'fedora:35'; local submodules = { name: 'submodules',